@Nonnull public JobEvent create() { return ObjectFactory.getInstance().createJobEvent(this); } }
@Nonnull private Duration computeDuration() { return Duration.between(jobEvent.getStartDateTime(), jobEvent.getEndDateTime()); }
@Nonnull public Customer create() { final Customer customer = ObjectFactory.getInstance().createCustomer(this); callback.register(customer); return customer; } }
@Nonnull protected Aggregate aggregatePresentationModel() final Money budget = project.getBudget(); final Money earnings = project.getEarnings(); final Money invoicedEarnings = project.getInvoicedEarnings(); .with("Client", (Displayable) () -> ((CustomerSpi)project.getCustomer()).getName()) .with("Status", (Displayable) () -> project.getStatus().name()) .with("#", (Displayable) () -> project.getNumber()) .with("Name", (Displayable) () -> project.getName()) .with("Start Date", (Displayable) () -> DATE_FORMATTER.format(project.getStartDate()), new DefaultStyleable("right-aligned")) .with("Due Date", (Displayable) () -> DATE_FORMATTER.format(project.getEndDate()), new DefaultStyleable("right-aligned")) .with("Notes", (Displayable) () -> project.getNotes()) .with("Budget", (Displayable) () -> MONEY_FORMATTER.format(budget), new DefaultStyleable("right-aligned", budget.isEqualTo(Money.ZERO) ? "alerted" : "")) .with("Earnings", (Displayable) () -> MONEY_FORMATTER.format(earnings), new DefaultStyleable("right-aligned", earnings.greaterThan(budget) ? "alerted" : "", earnings.isEqualTo(budget) ? "green" : "")) .with("Time", (Displayable) () -> DURATION_FORMATTER.format(project.getDuration()), new DefaultStyleable("right-aligned")) .with("Invoiced", (Displayable) () -> MONEY_FORMATTER.format(invoicedEarnings), new DefaultStyleable("right-aligned", invoicedEarnings.greaterThan(earnings) ? "alerted" : "",
private void makeReport (final @Nonnull Writer w) { final PrintWriter pw = new PrintWriter(w); System.err.println("CREATE REPORT " + project); pw.printf(SEPARATOR + "\n"); pw.printf(PATTERN, "Date", "Description", "Time", "Cost"); pw.printf(SEPARATOR + "\n"); // TODO: quick and dirty - refactor with visitor, lambdas final List<JobEventSpi> jobEvents = new ArrayList<>(); addAll(jobEvents, project.findChildren().results()); jobEvents.stream().sorted(comparing(JobEventSpi::getDateTime)) .forEach(event -> pw.printf(PATTERN2, DATE_FORMATTER.format(event.getDateTime()), event.getName(), DURATION_FORMATTER.format(event.getDuration()), MONEY_FORMATTER.format(event.getEarnings()))); pw.printf(SEPARATOR + "\n"); pw.printf(PATTERN3, "", "", DURATION_FORMATTER.format(project.getDuration()), MONEY_FORMATTER.format(project.getEarnings())); // FIXME: rename getAmount() -> getBudget() // FIXME: introduce getBudgetDuration() final Duration duration = Duration.ofHours((long)project.getBudget().divided(project.getHourlyRate())); pw.printf("BUDGET: %s\n", MONEY_FORMATTER.format(project.getBudget())); pw.printf("HOURLY RATE: %s\n", MONEY_FORMATTER.format(project.getHourlyRate())); pw.printf("DURATION: %s\n", DURATION_FORMATTER.format(duration)); pw.printf("REMAINING BUDGET: %s\n", MONEY_FORMATTER.format(project.getBudget().subtract(project.getEarnings()))); pw.printf("REMAINING TIME: %s\n", DURATION_FORMATTER.format(duration.minus(project.getDuration()))); pw.flush(); }
@Override @Nonnull protected AggregatePresentationModelBuilder aggregateBuilder() { return super.aggregateBuilder() .with(DATE, (Displayable) () -> DATETIME_FORMATTER.format(jobEvent.getStartDateTime())) .with(TIME, (Displayable) () -> DURATION_FORMATTER.format(computeDuration()), STYLE_RIGHT_ALIGNED) .with(HOURLY_RATE, (Displayable) () -> MONEY_FORMATTER.format(jobEvent.getHourlyRate()), STYLE_RIGHT_ALIGNED); }
@Nonnull protected AggregatePresentationModelBuilder aggregateBuilder() { // FIXME: uses the column header names, should be an internal id instead return AggregatePresentationModelBuilder.newInstance() .with(JOB_EVENT, (Displayable) () -> jobEvent.getName()) .with(NOTES, (Displayable) () -> jobEvent.getDescription()) // FIXME: this is dynamically computed, can be slow - should be also cached .with(AMOUNT, (Displayable) () -> MONEY_FORMATTER.format(jobEvent.getEarnings()), STYLE_RIGHT_ALIGNED); }
@CheckForNull private static Object safeGet (final @Nonnull Field field, final @Nonnull Object object) { try { Object value = field.get(object); if (value instanceof Customer) { value = ((CustomerSpi)value).getName(); } else if (value instanceof Project) { value = ((ProjectSpi)value).getName(); } return value; } catch (IllegalArgumentException | IllegalAccessException e) { throw new RuntimeException(e); } } }
@Override @Nonnull protected AggregatePresentationModelBuilder aggregateBuilder() { return super.aggregateBuilder() .with(DATE, (Displayable) () -> DATE_FORMATTER.format(jobEvent.getDateTime().toLocalDate())) .with(HOURLY_RATE, new DefaultDisplayable("")) .with(TIME, (Displayable) () -> DURATION_FORMATTER.format(jobEvent.getDuration()), STYLE_RIGHT_ALIGNED); }
@Override @Nonnull protected AggregatePresentationModelBuilder aggregateBuilder() { return super.aggregateBuilder() .with(DATE, (Displayable) () -> DATE_FORMATTER.format(jobEvent.getDate())) .with(TIME, new DefaultDisplayable("")) .with(HOURLY_RATE, new DefaultDisplayable("")) .with(AMOUNT , (Displayable) () -> MONEY_FORMATTER.format(jobEvent.getEarnings()), new DefaultStyleable("right-aligned"), new RedStyleForNegativeMoney(jobEvent::getEarnings)); }
@Override public PresentationModel createPresentationModel (final @Nonnull Object... instanceRoles) { final List<Object> temp = new ArrayList<>(); temp.addAll(Arrays.asList(instanceRoles)); temp.add((Displayable) () -> customer.getName()); return new DefaultPresentationModel(customer, temp.toArray()); } }
List<TimedJobEventSpi> timedEventsSubList = temp; final TimedJobEventSpi lastEvent = timedEventsSubList.get(eventsSubList.size() - 1); final LocalDate lastDate = lastEvent.getStartDateTime().toLocalDate(); final double earnings = timedEventsSubList.stream() .collect(Collectors.summingDouble(ev -> ev.getEarnings().getAmount().doubleValue())); final double taxRate = 0.20d;
@Nonnull public Project create() { final Project project = ObjectFactory.getInstance().createProject(this); callback.register(project); return project; } }
@Nonnull public Invoice create() { // TODO // if (!jobEvents.stream().allMatch(jobEvent -> jobEvent.getProject() == project)) // { // // FIXME: better diagnostics // throw new IllegalArgumentException("Illegal project for jobEvent"); // } final Invoice invoice = ObjectFactory.getInstance().createInvoice(this); callback.register(invoice); return invoice; } }
@Override @Nonnull public PresentationModel createPresentationModel (final @Nonnull Object... instanceRoles) { final Styleable styleable = new DefaultStyleable(getStyles()); return jobEvent.findChildren() .stream() .map(jobEvent -> (JobEventSpi)jobEvent) .sorted(comparing(JobEventSpi::getDateTime)) .map(jobEvent -> jobEvent.as(Presentable).createPresentationModel()) .collect(toCompositePresentationModel(aggregateBuilder().create(), styleable)); // FIXME: use SimpleCompositePresentable? }
/******************************************************************************************************************* * * Retrieves the hourly rates - if missing from the project description, tries to recover it from the first * meaningful job event. * ******************************************************************************************************************/ @Nonnull private Money getHourlyRate(final ConfigurationDecorator projectConfig, final List<JobEvent> jobEvents) throws NotFoundException { Money hourlyRate = projectConfig.getMoney("projectRate"); if ((hourlyRate.compareTo(Money.ZERO) == 0) && !jobEvents.isEmpty()) // don't use equals() - see http://stackoverflow.com/questions/6787142/bigdecimal-equals-versus-compareto { JobEvent event = jobEvents.get(0); while ((event instanceof JobEventGroup) && ((JobEventGroup)event).findChildren().count() > 0) { event = ((JobEventGroup)event).findChildren().firstResult(); } if (event instanceof TimedJobEventSpi) { hourlyRate = ((TimedJobEventSpi)event).getHourlyRate(); } } return hourlyRate; }
@Test(dataProvider = "projects", dataProviderClass = ScenarioFactory.class) public void must_properly_generate_report (final @Nonnull String scenarioName, final @Nonnull ProjectSpi project) throws IOException { final Path expectedResultsFolder = Paths.get("src/test/resources/expected-results"); final Path testFolder = Paths.get("target/test-results"); Files.createDirectories(testFolder); final String name = scenarioName + "-" + project.getName() + ".txt"; final Path actualResult = testFolder.resolve(name); final Path expectedResult = expectedResultsFolder.resolve(name); final HourlyReport report = new DefaultHourlyReportGenerator(project).createReport(); try (final PrintWriter pw = new PrintWriter(actualResult.toFile())) { pw.print(report.asString()); } FileComparisonUtils.assertSameContents(expectedResult.toFile(), actualResult.toFile()); } }
/******************************************************************************************************************* * * Reacts to the notification that a {@link Project} has been selected by populating the presentation with * its job events. * * @param event the notification event * ******************************************************************************************************************/ @VisibleForTesting void onProjectSelectedEvent (final @Nonnull @ListensTo ProjectSelectedEvent event) { log.info("onProjectSelectedEvent({})", event); presentation.populate(event.getProject().findChildren() .stream() .map(jobEvent -> (JobEventSpi)jobEvent) .sorted(comparing(JobEventSpi::getDateTime)) .map(jobEvent -> jobEvent.as(Presentable).createPresentationModel()) .collect(toCompositePresentationModel())); } }