diff --git a/README.md b/README.md index 383243c..369dec8 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,10 @@ Not every piece of software is equally important... Not every piece will decide about company / product success or can cause not reversible negative business consequences like materialise brand risk or money loses. On the other hand scalability or non functional requirements are different for different activities in software. + To accommodate to those differences, separate architectural patterns are applied: -![Command Query CRUD Responsibility Segregation](https://github.com/michal-michaluk/factory/raw/master/command-query-crud.png) +![Command Query CRUD Responsibility Segregation](command-query-crud.png) **Simple Create Read Update Delete functionality** are exposed with leverage of CRUD framework. @@ -16,6 +17,11 @@ Goals of that approach: - fast respond to typical changes (ex. „please add another 2 fields on UI”), - exposure of high quality API. +Examples in code: +- CRUD-able document [ProductDescription](product-management-adapters/src/main/java/pl/com/bottega/factory/product/management/ProductDescription.java) +- persistence of document [ProductDescriptionEntity](product-management-adapters/src/main/java/pl/com/bottega/factory/product/management/ProductDescriptionEntity.java) +- CRUD exposed as DAO and REST endpoint [ProductDescriptionDao](product-management-adapters/src/main/java/pl/com/bottega/factory/product/management/ProductDescriptionDao.java) + **Complex Commands (business processing)** expressed in Domain Model which is embedded in hexagonal architecture. Goals of that approach: @@ -25,6 +31,25 @@ caused by technological choices or transport models from external services / con - make the core business of application technology agnostic, enabling continues technology migration and keeping long living projects up to date with fast evolving frameworks and libraries. +Examples of Domain Model in code: +- aggregate [ProductDemand](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/ProductDemand.java) +- entity [DailyDemand](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/DailyDemand.java) +- value object [Adjustment](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/Adjustment.java) +- policy [ReviewPolicy](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/ReviewPolicy.java) +- domain event [DemandedLevelsChanged](shared-kernel-model/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandedLevelsChanged.java) + +Examples of Ports in code: +- application service (primary port) [DemandService](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandService.java) +- repository (secondary port) [ProductDemandRepository](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/ProductDemandRepository.java) +- domain events handling (secondary port) [DemandEvents](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEvents.java) + +Examples of Adapters in code: +- REST endpoint for complex command (driving adapter) + - command resource [DemandAdjustmentDao](demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/command/DemandAdjustmentDao.java) + - command handler [CommandsHandler](demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/command/CommandsHandler.java) +- repository implementation (driven adapter) [ProductDemandORMRepository](demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/ProductDemandORMRepository.java) +- events propagation (driven adapter) [DemandEventsPropagation](app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEventsPropagation.java) + **Complex Query** implemented as direct and simple as possible by: - fetching persistent read model expected by consumer, the read model is a projection of past domain event, - read model composed at query execution time build directly from persistent form of Domain Model, @@ -34,15 +59,20 @@ Additional complex calculations or projections can be partially delegated to the Goals of that approach: - encapsulation of the Domain Model complexity by providing (simpler) consumer driven or published language API, -- freeing Domain the Model from exposing data for reads making the Domain Model simpler, +- freeing the Domain Model from exposing data for reads making the Domain Model simpler, - improves reads performance and enable horizontal scalability. +Examples in code: +- projection of domain events to persistent read model [DeliveryForecastProjection](demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/delivery/planning/projection/DeliveryForecastProjection.java) +- REST endpoint for persistent read model [DeliveryForecastDao](demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/delivery/planning/projection/DeliveryForecastDao.java) +- read model composed at query execution time [StockForecastQuery](app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/StockForecastQuery.java) +- REST resource processor for NOT persistent read model [StockForecastResourceProcessor](app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/ressource/StockForecastResourceProcessor.java) ## Hexagonal Architecture Only the most valuable part of that enterprise software is embedded in hexagonal architecture - complex business processing modeled in form of the Domain Model. -![Domain Model embedded in hexagonal architecture](https://github.com/michal-michaluk/factory/raw/master/hexagon.png) +![Domain Model embedded in hexagonal architecture](hexagon.png) **Application Services** - providing entry point to Domain Model functionality, Application Services are ports for Primary / Driving Adapters like RESTfull endpoints. @@ -69,13 +99,13 @@ with **Model Exploration Whirlpool** and build **Ubiquitous Language** with your Adding infrastructure and technology later is easy thanks to Hexagonal Architecture. Simply starting from ZERO business knowledge through initial domain and opportunity exploration with **Big Picture Event Storming**: -![Big Picture Event Storming](https://github.com/michal-michaluk/factory/raw/master/es-big-picture-original.jpg) +![Big Picture Event Storming](es-big-picture-original.jpg) after cleaning and trimming initial model to most valuable and needed areas: -![Big Picture Event Storming](https://github.com/michal-michaluk/factory/raw/master/es-big-picture-cleaned.jpg) +![Big Picture Event Storming](es-big-picture-cleaned.jpg) Deep dive in **Demand Forecasting** sub-domain with **Design Level Event Storming**: -![Design Level Event Storming - Demand Forecasting](https://github.com/michal-michaluk/factory/raw/master/es-design-demand-forecasting.jpg) +![Design Level Event Storming - Demand Forecasting](es-design-demand-forecasting.jpg) is excellent canvas to cooperative exploration of: - impacted and required actors, diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/delivery/planning/definition/DeliveryPlannerDefinitionEventsMapping.java b/app-monolith/src/main/java/pl/com/bottega/factory/delivery/planning/definition/DeliveryPlannerDefinitionEventsPropagation.java similarity index 74% rename from app-monolith/src/main/java/pl/com/bottega/factory/delivery/planning/definition/DeliveryPlannerDefinitionEventsMapping.java rename to app-monolith/src/main/java/pl/com/bottega/factory/delivery/planning/definition/DeliveryPlannerDefinitionEventsPropagation.java index 2167f36..80cab0f 100644 --- a/app-monolith/src/main/java/pl/com/bottega/factory/delivery/planning/definition/DeliveryPlannerDefinitionEventsMapping.java +++ b/app-monolith/src/main/java/pl/com/bottega/factory/delivery/planning/definition/DeliveryPlannerDefinitionEventsPropagation.java @@ -10,13 +10,13 @@ import pl.com.bottega.factory.delivery.planning.projection.DeliveryForecastProje @Component @AllArgsConstructor @RepositoryEventHandler -public class DeliveryPlannerDefinitionEventsMapping { +public class DeliveryPlannerDefinitionEventsPropagation { private DeliveryForecastProjection projection; @HandleAfterSave @HandleAfterCreate - public void handle(DeliveryPlannerDefinitionEntity entity) { - projection.handleDeliveryPlannerDefinitionChange(entity.getRefNo()); + public void handleCreateAndUpdate(DeliveryPlannerDefinitionEntity entity) { + projection.applyDeliveryPlannerDefinitionChange(entity.getRefNo()); } } diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEventsMapping.java b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEventsPropagation.java similarity index 88% rename from app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEventsMapping.java rename to app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEventsPropagation.java index 959fc3e..bfa6228 100644 --- a/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEventsMapping.java +++ b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEventsPropagation.java @@ -16,7 +16,7 @@ import java.util.stream.Collectors; @Lazy @Component @AllArgsConstructor -class DemandEventsMapping implements DemandEvents { +class DemandEventsPropagation implements DemandEvents { private final CurrentDemandProjection demandProjection; private final DeliveryForecastProjection deliveryProjection; @@ -26,8 +26,8 @@ class DemandEventsMapping implements DemandEvents { @Override public void emit(DemandedLevelsChanged event) { - demandProjection.persistCurrentDemands(event); - deliveryProjection.persistDeliveryForecasts(event); + demandProjection.applyDemandedLevelsChanged(event); + deliveryProjection.applyDemandedLevelsChanged(event); shortagePrediction.predictShortages(event); } diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/product/management/ProductDescriptionEventsMapping.java b/app-monolith/src/main/java/pl/com/bottega/factory/product/management/ProductDescriptionEventsPropagation.java similarity index 70% rename from app-monolith/src/main/java/pl/com/bottega/factory/product/management/ProductDescriptionEventsMapping.java rename to app-monolith/src/main/java/pl/com/bottega/factory/product/management/ProductDescriptionEventsPropagation.java index 5e0af21..0c3d82f 100644 --- a/app-monolith/src/main/java/pl/com/bottega/factory/product/management/ProductDescriptionEventsMapping.java +++ b/app-monolith/src/main/java/pl/com/bottega/factory/product/management/ProductDescriptionEventsPropagation.java @@ -2,6 +2,7 @@ package pl.com.bottega.factory.product.management; import lombok.AllArgsConstructor; import org.springframework.data.rest.core.annotation.HandleAfterCreate; +import org.springframework.data.rest.core.annotation.HandleAfterDelete; import org.springframework.data.rest.core.annotation.RepositoryEventHandler; import org.springframework.stereotype.Component; import pl.com.bottega.factory.demand.forecasting.DemandService; @@ -11,14 +12,19 @@ import pl.com.bottega.factory.stock.forecast.ressource.StockForecastEntity; @Component @AllArgsConstructor @RepositoryEventHandler -public class ProductDescriptionEventsMapping { +public class ProductDescriptionEventsPropagation { private final DemandService demandService; private final StockForecastDao stockForecasts; @HandleAfterCreate - public void handle(ProductDescriptionEntity entity) { + public void handleCreate(ProductDescriptionEntity entity) { demandService.init(entity.getRefNo()); stockForecasts.save(new StockForecastEntity(entity.getRefNo())); } + + @HandleAfterDelete + public void handleDelete(ProductDescriptionEntity entity) { + stockForecasts.delete(entity.getRefNo()); + } } diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/shortages/prediction/monitoring/ShortageEventsMapping.java b/app-monolith/src/main/java/pl/com/bottega/factory/shortages/prediction/monitoring/ShortageEventsPropagation.java similarity index 90% rename from app-monolith/src/main/java/pl/com/bottega/factory/shortages/prediction/monitoring/ShortageEventsMapping.java rename to app-monolith/src/main/java/pl/com/bottega/factory/shortages/prediction/monitoring/ShortageEventsPropagation.java index f9fb3f4..ea8eaf6 100644 --- a/app-monolith/src/main/java/pl/com/bottega/factory/shortages/prediction/monitoring/ShortageEventsMapping.java +++ b/app-monolith/src/main/java/pl/com/bottega/factory/shortages/prediction/monitoring/ShortageEventsPropagation.java @@ -8,7 +8,7 @@ import pl.com.bottega.factory.shortages.prediction.notification.NotificationOfSh @Lazy @Component @AllArgsConstructor -class ShortageEventsMapping implements ShortageEvents { +class ShortageEventsPropagation implements ShortageEvents { private final NotificationOfShortage notification; diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/StockForecastQuery.java b/app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/StockForecastQuery.java index a66faa4..41c0491 100644 --- a/app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/StockForecastQuery.java +++ b/app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/StockForecastQuery.java @@ -31,7 +31,7 @@ public class StockForecastQuery { public StockForecast get(RefNoId refNo) { Stock stock = stocks.forRefNo(refNo); LocalDate today = LocalDate.now(clock); - return build(refNo, today, stock, + return build(today, stock, this.demands .findByRefNoAndDateGreaterThanEqual(refNo.getRefNo(), today).stream() .collect(toMap( @@ -47,7 +47,7 @@ public class StockForecastQuery { ); } - private StockForecast build(RefNoId refNo, LocalDate today, + private StockForecast build(LocalDate today, Stock stock, Map demands, Map outputs) { diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/ressource/StaticAccess.java b/app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/ressource/StaticAccess.java deleted file mode 100644 index 30c05aa..0000000 --- a/app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/ressource/StaticAccess.java +++ /dev/null @@ -1,20 +0,0 @@ -package pl.com.bottega.factory.stock.forecast.ressource; - -import org.springframework.stereotype.Component; -import pl.com.bottega.factory.product.management.RefNoId; -import pl.com.bottega.factory.stock.forecast.StockForecast; -import pl.com.bottega.factory.stock.forecast.StockForecastQuery; - -@Component -class StaticAccess { - - private static StockForecastQuery query; - - StaticAccess(StockForecastQuery query) { - StaticAccess.query = query; - } - - static StockForecast calculateQuery(String refNo) { - return query.get(new RefNoId(refNo)); - } -} diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/ressource/StockForecastDao.java b/app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/ressource/StockForecastDao.java index 77e4904..217c7ae 100644 --- a/app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/ressource/StockForecastDao.java +++ b/app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/ressource/StockForecastDao.java @@ -1,6 +1,7 @@ package pl.com.bottega.factory.stock.forecast.ressource; import org.springframework.data.rest.core.annotation.RepositoryRestResource; +import org.springframework.data.rest.core.config.Projection; import org.springframework.stereotype.Repository; import pl.com.bottega.tools.ProjectionRepository; @@ -8,7 +9,12 @@ import pl.com.bottega.tools.ProjectionRepository; @RepositoryRestResource(path = "stock-forecasts", collectionResourceRel = "stock-forecasts", itemResourceRel = "stock-forecast", - excerptProjection = StockForecastEntity.CollectionItem.class) + excerptProjection = StockForecastDao.CollectionItem.class) public interface StockForecastDao extends ProjectionRepository { + @Projection(types = {StockForecastEntity.class}) + interface CollectionItem { + String getRefNo(); + } + } diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/ressource/StockForecastEntity.java b/app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/ressource/StockForecastEntity.java index e1042b3..99222d6 100644 --- a/app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/ressource/StockForecastEntity.java +++ b/app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/ressource/StockForecastEntity.java @@ -2,7 +2,7 @@ package pl.com.bottega.factory.stock.forecast.ressource; import lombok.Getter; import lombok.NoArgsConstructor; -import org.springframework.data.rest.core.config.Projection; +import lombok.Setter; import pl.com.bottega.factory.stock.forecast.StockForecast; import javax.persistence.Entity; @@ -18,17 +18,10 @@ public class StockForecastEntity implements Serializable { @Id private String refNo; + @Setter + private transient StockForecast stockForecast; public StockForecastEntity(String refNo) { this.refNo = refNo; } - - public StockForecast getStockForecast() { - return StaticAccess.calculateQuery(refNo); - } - - @Projection(types = {StockForecastEntity.class}) - interface CollectionItem { - String getRefNo(); - } } diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/ressource/StockForecastResourceProcessor.java b/app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/ressource/StockForecastResourceProcessor.java new file mode 100644 index 0000000..8cf64b3 --- /dev/null +++ b/app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/ressource/StockForecastResourceProcessor.java @@ -0,0 +1,23 @@ +package pl.com.bottega.factory.stock.forecast.ressource; + +import lombok.AllArgsConstructor; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.ResourceProcessor; +import org.springframework.stereotype.Component; +import pl.com.bottega.factory.product.management.RefNoId; +import pl.com.bottega.factory.stock.forecast.StockForecastQuery; + +@Component +@AllArgsConstructor +class StockForecastResourceProcessor implements ResourceProcessor> { + + private final StockForecastQuery query; + + @Override + public Resource process(Resource resource) { + resource.getContent().setStockForecast( + query.get(new RefNoId(resource.getContent().getRefNo())) + ); + return resource; + } +} diff --git a/command-query-crud.png b/command-query-crud.png index e18ff2d..31b8030 100644 Binary files a/command-query-crud.png and b/command-query-crud.png differ diff --git a/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/delivery/planning/projection/DeliveryForecastProjection.java b/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/delivery/planning/projection/DeliveryForecastProjection.java index c558a34..bba52eb 100644 --- a/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/delivery/planning/projection/DeliveryForecastProjection.java +++ b/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/delivery/planning/projection/DeliveryForecastProjection.java @@ -22,7 +22,7 @@ public class DeliveryForecastProjection { private final CurrentDemandDao demandDao; private final DeliveryAutoPlannerORMRepository planners; - public void persistDeliveryForecasts(DemandedLevelsChanged event) { + public void applyDemandedLevelsChanged(DemandedLevelsChanged event) { DeliveryAutoPlanner planner = planners.get(event.getRefNo().getRefNo()); event.getResults().keySet() .forEach(daily -> forecastDao.deleteByRefNoAndDate( @@ -42,7 +42,7 @@ public class DeliveryForecastProjection { ); } - public void handleDeliveryPlannerDefinitionChange(String refNo) { + public void applyDeliveryPlannerDefinitionChange(String refNo) { List demands = demandDao.findByRefNoAndDateGreaterThanEqual(refNo, LocalDate.now(clock)); DeliveryAutoPlanner planner = planners.get(refNo); forecastDao.deleteByRefNo(refNo); diff --git a/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/command/Handler.java b/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/command/CommandsHandler.java similarity index 98% rename from demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/command/Handler.java rename to demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/command/CommandsHandler.java index 7513106..308d660 100644 --- a/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/command/Handler.java +++ b/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/command/CommandsHandler.java @@ -17,7 +17,7 @@ import java.time.LocalDate; @Component @AllArgsConstructor @RepositoryEventHandler -public class Handler { +public class CommandsHandler { private final DemandService service; private final DemandAdjustmentDao adjustments; diff --git a/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/projection/CurrentDemandProjection.java b/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/projection/CurrentDemandProjection.java index af441cf..ea6599a 100644 --- a/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/projection/CurrentDemandProjection.java +++ b/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/projection/CurrentDemandProjection.java @@ -10,7 +10,7 @@ public class CurrentDemandProjection { private final CurrentDemandDao demandDao; - public void persistCurrentDemands(DemandedLevelsChanged event) { + public void applyDemandedLevelsChanged(DemandedLevelsChanged event) { event.getResults().forEach((daily, change) -> { demandDao.deleteByRefNoAndDate( daily.getRefNo(), diff --git a/demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/AdjustDemand.java b/demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/AdjustDemand.java index cc73e66..c54bce8 100644 --- a/demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/AdjustDemand.java +++ b/demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/AdjustDemand.java @@ -1,6 +1,6 @@ package pl.com.bottega.factory.demand.forecasting; -import lombok.Value; +import lombok.AllArgsConstructor; import pl.com.bottega.factory.demand.forecasting.DailyDemand.Result; import java.time.LocalDate; @@ -11,12 +11,12 @@ import java.util.Optional; import java.util.function.BiFunction; import java.util.stream.Collectors; -@Value +@AllArgsConstructor public class AdjustDemand { - String refNo; - Map adjustments; + private final String refNo; + private final Map adjustments; - public List forEachStartingFrom(LocalDate date, BiFunction f) { + List forEachStartingFrom(LocalDate date, BiFunction f) { return adjustments.entrySet().stream() .filter(e -> !e.getKey().isBefore(date)) .map(e -> f.apply(e.getKey(), e.getValue())) @@ -24,6 +24,11 @@ public class AdjustDemand { } public Optional latestAdjustment() { - return adjustments.keySet().stream().max(Comparator.naturalOrder()); + return adjustments.keySet().stream() + .max(Comparator.naturalOrder()); + } + + public String getRefNo() { + return refNo; } } diff --git a/demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/Document.java b/demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/Document.java index d7553c5..0a4b670 100644 --- a/demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/Document.java +++ b/demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/Document.java @@ -1,33 +1,30 @@ package pl.com.bottega.factory.demand.forecasting; -import lombok.Value; +import lombok.AllArgsConstructor; import java.time.Instant; import java.time.LocalDate; -import java.util.Collections; import java.util.List; import java.util.SortedMap; import java.util.function.BiFunction; import java.util.stream.Collectors; -@Value +@AllArgsConstructor public class Document { - Instant created; - String refNo; - SortedMap demands; + private final Instant created; + private final String refNo; + private final SortedMap demands; - public Document(Instant created, String refNo, SortedMap demands) { - this.created = created; - this.refNo = refNo; - this.demands = Collections.unmodifiableSortedMap(demands); - } - - public List forEachStartingFrom(LocalDate date, BiFunction f) { + List forEachStartingFrom(LocalDate date, BiFunction f) { return demands.entrySet().stream() .filter(e -> !e.getKey().isBefore(date)) .map(e -> f.apply(e.getKey(), e.getValue())) .collect(Collectors.toList()); } + + public String getRefNo() { + return refNo; + } } diff --git a/hexagon.png b/hexagon.png index 207984f..3cca9b7 100644 Binary files a/hexagon.png and b/hexagon.png differ