├── .codecov.yml ├── .gitattributes ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── MIGRATION.md ├── README.md ├── adapter-commons ├── build.gradle └── src │ └── main │ ├── java │ └── io │ │ └── dddbyexamples │ │ └── tools │ │ ├── CommandRepository.java │ │ ├── JsonConverter.java │ │ ├── ProjectionRepository.java │ │ └── TechnicalId.java │ └── resources │ ├── application-test.properties │ └── schema │ └── commons.yml ├── app-monolith ├── Dockerfile ├── build.gradle └── src │ ├── assembly │ └── stub.xml │ ├── main │ ├── java │ │ └── io │ │ │ └── dddbyexamples │ │ │ └── factory │ │ │ ├── AppConfiguration.java │ │ │ ├── delivery │ │ │ └── planning │ │ │ │ └── definition │ │ │ │ └── DeliveryPlannerDefinitionEventsPropagation.java │ │ │ ├── demand │ │ │ └── forecasting │ │ │ │ └── DemandEventsPropagation.java │ │ │ ├── product │ │ │ └── management │ │ │ │ └── ProductDescriptionEventsPropagation.java │ │ │ ├── shortages │ │ │ └── prediction │ │ │ │ ├── calculation │ │ │ │ └── ForecastORMRepository.java │ │ │ │ └── monitoring │ │ │ │ └── ShortageEventsPropagation.java │ │ │ ├── stock │ │ │ └── forecast │ │ │ │ ├── StockForecast.java │ │ │ │ ├── StockForecastQuery.java │ │ │ │ └── ressource │ │ │ │ ├── StockForecastDao.java │ │ │ │ ├── StockForecastEntity.java │ │ │ │ └── StockForecastResourceProcessor.java │ │ │ └── warehouse │ │ │ └── WarehouseService.java │ └── resources │ │ ├── application-cloud.properties │ │ ├── application-docker.properties │ │ ├── application.properties │ │ └── schema │ │ └── db.changelog.yml │ └── test │ ├── groovy │ └── io │ │ └── dddbyexamples │ │ └── factory │ │ ├── ProductTrait.groovy │ │ └── integration │ │ ├── CallOffDocumentIntegrationSpec.groovy │ │ ├── DemandAdjustmentIntegrationSpec.groovy │ │ └── ShortageIntegrationSpec.groovy │ └── resources │ ├── application-test.properties │ ├── curl.txt │ └── examples │ ├── delivery-definitions.json │ ├── delivery-forecasts.json │ ├── demand-adjustments.json │ ├── demand-forecasts.json │ ├── product-descriptions.json │ ├── production-outputs-daily.json │ ├── production-outputs.json │ ├── shortages.json │ └── stock-forecasts.json ├── build.gradle ├── command-query-crud.png ├── demand-forecasting-adapters ├── build.gradle └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── dddbyexamples │ │ │ └── factory │ │ │ ├── delivery │ │ │ └── planning │ │ │ │ ├── DeliveryAutoPlannerORMRepository.java │ │ │ │ ├── definition │ │ │ │ ├── DeliveryPlannerDefinition.java │ │ │ │ ├── DeliveryPlannerDefinitionDao.java │ │ │ │ └── DeliveryPlannerDefinitionEntity.java │ │ │ │ └── projection │ │ │ │ ├── DeliveryForecastDao.java │ │ │ │ ├── DeliveryForecastEntity.java │ │ │ │ └── DeliveryForecastProjection.java │ │ │ └── demand │ │ │ └── forecasting │ │ │ ├── DemandForecastingConfiguration.java │ │ │ ├── DemandValue.java │ │ │ ├── ProductDemandORMRepository.java │ │ │ ├── command │ │ │ ├── CommandsHandler.java │ │ │ ├── DemandAdjustmentDao.java │ │ │ ├── DemandAdjustmentEntity.java │ │ │ ├── RequiredReviewDao.java │ │ │ └── RequiredReviewEntity.java │ │ │ ├── persistence │ │ │ ├── DemandDao.java │ │ │ ├── DemandEntity.java │ │ │ ├── DocumentDao.java │ │ │ ├── DocumentEntity.java │ │ │ ├── ProductDemandDao.java │ │ │ └── ProductDemandEntity.java │ │ │ └── projection │ │ │ ├── CurrentDemandDao.java │ │ │ ├── CurrentDemandEntity.java │ │ │ └── CurrentDemandProjection.java │ └── resources │ │ └── schema │ │ ├── db.changelog.yml │ │ ├── delivery-planning.yml │ │ └── demand-forecasting.yml │ └── test │ └── groovy │ └── io │ └── dddbyexamples │ └── factory │ ├── ForecastingAdaptersConfiguration.groovy │ ├── delivery │ └── planning │ │ └── DeliveryPlannerDefinitionSpec.groovy │ └── demand │ └── forecasting │ └── ProductDemandORMRepositorySpec.groovy ├── demand-forecasting-model ├── build.gradle └── src │ ├── main │ └── java │ │ └── io │ │ └── dddbyexamples │ │ └── factory │ │ ├── delivery │ │ └── planning │ │ │ ├── DeliveriesSuggestion.java │ │ │ ├── Delivery.java │ │ │ └── DeliveryAutoPlanner.java │ │ └── demand │ │ └── forecasting │ │ ├── AdjustDemand.java │ │ ├── Adjustment.java │ │ ├── ApplyReviewDecision.java │ │ ├── DailyDemand.java │ │ ├── DemandEvents.java │ │ ├── DemandService.java │ │ ├── Demands.java │ │ ├── Document.java │ │ ├── ProductDemand.java │ │ ├── ProductDemandRepository.java │ │ ├── ReviewDecision.java │ │ └── ReviewPolicy.java │ └── test │ ├── groovy │ └── io │ │ └── dddbyexamples │ │ └── factory │ │ ├── delivery │ │ └── planning │ │ │ ├── DeliveriesSuggestionSpec.groovy │ │ │ └── DeliveryAutoPlannerSpec.groovy │ │ └── demand │ │ └── forecasting │ │ ├── DailyDemandBuilder.groovy │ │ ├── DemandAdjustmentSpec.groovy │ │ ├── DemandServiceSpec.groovy │ │ ├── DemandsFake.groovy │ │ ├── DocumentProcessingSpec.groovy │ │ ├── KeepingDailyDemandsSpec.groovy │ │ ├── ProductDemandBuilder.groovy │ │ ├── ProductDemandTrait.groovy │ │ ├── ReviewPolicySpec.groovy │ │ └── ReviewProcessingSpec.groovy │ └── resources │ └── scenarios │ ├── DemandAdjustements.feature │ ├── ProductionPlanning.feature │ └── domain-stories.txt ├── docker-compose.yml ├── es-big-picture-cleaned.jpg ├── es-big-picture-original.jpg ├── es-design-demand-forecasting.jpg ├── gradle.properties ├── gradle ├── pipeline.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── hexagon.png ├── lombok.config ├── manifest.yml ├── product-management-adapters ├── build.gradle └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── dddbyexamples │ │ │ └── factory │ │ │ └── product │ │ │ └── management │ │ │ ├── ProductDescription.java │ │ │ ├── ProductDescriptionDao.java │ │ │ └── ProductDescriptionEntity.java │ └── resources │ │ └── schema │ │ ├── db.changelog.yml │ │ └── product-management.yml │ └── test │ └── groovy │ └── io │ └── dddbyexamples │ └── factory │ ├── ProductionManagementAdaptersConfiguration.groovy │ └── product │ └── management │ └── ProductDescriptionPersistenceSpec.groovy ├── production-planning-adapters ├── build.gradle └── src │ └── main │ ├── java │ └── io │ │ └── dddbyexamples │ │ └── factory │ │ └── production │ │ └── planning │ │ └── projection │ │ ├── ProductionDailyOutputDao.java │ │ ├── ProductionDailyOutputEntity.java │ │ ├── ProductionOutputDao.java │ │ └── ProductionOutputEntity.java │ └── resources │ └── schema │ ├── db.changelog.yml │ └── production-planning.yml ├── sc-pipelines.yml ├── settings.gradle ├── shared-kernel-model ├── build.gradle └── src │ └── main │ └── java │ └── io │ └── dddbyexamples │ └── factory │ ├── demand │ └── forecasting │ │ ├── DailyId.java │ │ ├── Demand.java │ │ ├── DemandedLevelsChanged.java │ │ └── ReviewRequired.java │ └── product │ └── management │ └── RefNoId.java ├── shortages-prediction-adapters ├── build.gradle └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── dddbyexamples │ │ │ └── factory │ │ │ └── shortages │ │ │ └── prediction │ │ │ ├── monitoring │ │ │ ├── MonitoringConfiguration.java │ │ │ ├── ShortagePredictionProcessORMRepository.java │ │ │ └── persistence │ │ │ │ ├── ShortagesDao.java │ │ │ │ └── ShortagesEntity.java │ │ │ └── notification │ │ │ └── NotificationConfiguration.java │ └── resources │ │ └── schema │ │ ├── db.changelog.yml │ │ └── shortages-prediction.yml │ └── test │ └── groovy │ ├── e2e │ └── E2eSpec.groovy │ ├── io │ └── dddbyexamples │ │ └── factory │ │ ├── PredictionAdaptersConfiguration.groovy │ │ └── shortages │ │ └── prediction │ │ └── monitoring │ │ ├── ShortagePredictionProcessORMRepositorySpec.groovy │ │ └── persistence │ │ ├── BaseClass.groovy │ │ ├── Config.groovy │ │ └── ShortagesDaoSpec.groovy │ └── smoke │ └── SmokeSpec.groovy └── shortages-prediction-model ├── build.gradle └── src ├── main └── java │ └── io │ └── dddbyexamples │ └── factory │ └── shortages │ └── prediction │ ├── ConfigurationParams.java │ ├── Shortage.java │ ├── calculation │ ├── DeliveriesForecast.java │ ├── ProductionForecast.java │ ├── ProductionOutputs.java │ ├── ShortageForecast.java │ ├── ShortageForecasts.java │ └── Stock.java │ ├── monitoring │ ├── NewShortage.java │ ├── ShortageDiffPolicy.java │ ├── ShortageEvents.java │ ├── ShortagePredictionProcess.java │ ├── ShortagePredictionProcessRepository.java │ ├── ShortagePredictionService.java │ └── ShortageSolved.java │ └── notification │ ├── NotificationOfShortage.java │ ├── Notifications.java │ ├── QualityTasks.java │ └── RecoveryTaskPriorityChangePolicy.java └── test └── groovy └── io └── dddbyexamples └── factory └── shortages └── prediction ├── calculation ├── ShortageCalculationAlgorithmSpec.groovy ├── ShortageCalculationExamplesSpec.groovy ├── ShortagesCalculationAssembler.groovy ├── ShortagesCalculationTrait.groovy └── TimeGrammar.groovy ├── monitoring ├── InMemoryConfigurationParams.groovy ├── ShortageDiffPolicySpec.groovy ├── ShortagePredictionProcessSpec.groovy ├── ShortagePredictionProcessTrait.groovy └── ShortagePredictionServiceSpec.groovy └── notification ├── NotificationOfShortageSpec.groovy └── RecoveryTaskPriorityChangePolicySpec.groovy /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | precision: 2 3 | round: down 4 | range: 70...100 5 | 6 | status: off 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | 2 | # Set default behaviour, in case users don't have core.autocrlf set. 3 | * text=auto 4 | 5 | # Denote all files that are truly binary and should not be modified. 6 | *.png binary 7 | *.jpg binary 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ## travis-ci 3 | !.travis.yml 4 | !.codecov.yml 5 | 6 | ## maven 7 | target/ 8 | target-test/ 9 | !.mvn 10 | 11 | 12 | ## linux 13 | .* 14 | !.git* 15 | 16 | 17 | ## windows 18 | # Windows image file caches 19 | Thumbs.db 20 | ehthumbs.db 21 | # Folder config file 22 | Desktop.ini 23 | # Recycle Bin used on file shares 24 | $RECYCLE.BIN/ 25 | 26 | 27 | ## macOS 28 | .DS_Store 29 | .AppleDouble 30 | .LSOverride 31 | Icon 32 | # Thumbnails 33 | ._* 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | 39 | ## eclipse 40 | *.pydevproject 41 | .project 42 | .metadata 43 | *.tmp 44 | *.bak 45 | *.swp 46 | *~.nib 47 | local.properties 48 | .classpath 49 | .settings/ 50 | .loadpath 51 | # External tool builders 52 | .externalToolBuilders/ 53 | # Locally stored "Eclipse launch configurations" 54 | *.launch 55 | # CDT-specific 56 | .cproject 57 | # PDT-specific 58 | .buildpath 59 | 60 | 61 | ## intelij 62 | *.iml 63 | *.ipr 64 | *.iws 65 | .idea/ 66 | 67 | 68 | ## netbeans 69 | nbactions.xml 70 | nb-configuration.xml 71 | 72 | 73 | ## emacs 74 | *~ 75 | \#*\# 76 | /.emacs.desktop 77 | /.emacs.desktop.lock 78 | .elc 79 | auto-save-list 80 | tramp 81 | .\#* 82 | # Org-mode 83 | .org-id-locations 84 | *_archive 85 | 86 | 87 | ## vim 88 | .*.s[a-w][a-z] 89 | *.un~ 90 | Session.vim 91 | .netrwhist 92 | 93 | build 94 | out/ 95 | build/ 96 | .gradle 97 | *.log 98 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - openjdk8 5 | - openjdk9 6 | - openjdk10 7 | 8 | after_success: 9 | - bash <(curl -s https://codecov.io/bash) || echo "Codecov did not collect coverage reports" 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at factory.ddd@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Michał Michaluk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MIGRATION.md: -------------------------------------------------------------------------------- 1 | # Migration to Spring Cloud Pipelines 2 | 3 | - Added Maven wrapper 4 | 5 | ```bash 6 | $ mvn -N io.takari:maven:wrapper 7 | $ git add . 8 | $ git add -f .mvn 9 | $ git commit -m "Added maven wrapper" 10 | ``` 11 | 12 | - Updated `manifest.yml` 13 | - Added `sc-pipelines.yml` 14 | - provided which module is the main one 15 | - added database as a required service 16 | - Created cloud foundry spaces 17 | 18 | ```bash 19 | $ cf login -o ... -a ... 20 | $ cf create-space sc-pipelines-test-app-monolith 21 | $ cf create-space sc-pipelines-stage-app-monolith 22 | $ cf create-space sc-pipelines-prod 23 | ``` 24 | 25 | - Added `` section for all projects 26 | - Added contract tests (`shortages-prediction-adapters/src/test/groovy/io/dddbyexamples/factory/shortages/prediction/monitoring/persistence/ShortagesDaoTest.groovy`) 27 | - Added stub jar generation in `app-monolith` (in the output stubs jar each module has its own folder) 28 | - Added `` for all types of tests for all projects 29 | - that way we can control the whole project from root 30 | - Added base class for rollback tests (for `shortages-prediction-adapters`) 31 | - Configured rollback tests via `sc-contract` plugin under `apicompatibility` profile (for `shortages-prediction-adapters`) 32 | - Added `smoke` tests (just pining health initially) (initially only for `shortages-prediction-adapters` but could be added for more) 33 | - Added `e2e` tests (just pining health initially) (initially only for `shortages-prediction-adapters` but could be added for more) 34 | - Added `ServiceConfiguration` for `cloud` profile (TODO: Verify why that's needed) 35 | - Created the `app-monolith-db` database service for production 36 | - The repo name is `app-monolith` 37 | - it matches the name of the artifact id of module that produces the fat jar 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /adapter-commons/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | compile("org.springframework.boot:spring-boot-starter-data-jpa") 3 | compile("org.springframework.boot:spring-boot-starter-data-rest") 4 | compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.6") 5 | compile("com.fasterxml.jackson.module:jackson-module-parameter-names:2.9.6") 6 | compile("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.9.6") 7 | compile("org.liquibase:liquibase-core:3.6.1") 8 | compile("javax.xml.bind:jaxb-api:2.3.0") 9 | runtime("org.postgresql:postgresql:42.2.2") 10 | 11 | testCompile("org.springframework.boot:spring-boot-starter-test") 12 | } 13 | -------------------------------------------------------------------------------- /adapter-commons/src/main/java/io/dddbyexamples/tools/CommandRepository.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.tools; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import org.springframework.data.rest.core.annotation.RestResource; 5 | 6 | import java.io.Serializable; 7 | 8 | public interface CommandRepository extends CrudRepository { 9 | 10 | @Override 11 | @RestResource(exported = false) 12 | void deleteById(ID id); 13 | 14 | @Override 15 | @RestResource(exported = false) 16 | void delete(T entity); 17 | 18 | @Override 19 | @RestResource(exported = false) 20 | void deleteAll(Iterable entities); 21 | 22 | @Override 23 | @RestResource(exported = false) 24 | void deleteAll(); 25 | } 26 | -------------------------------------------------------------------------------- /adapter-commons/src/main/java/io/dddbyexamples/tools/JsonConverter.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.tools; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 4 | import com.fasterxml.jackson.annotation.JsonCreator; 5 | import com.fasterxml.jackson.annotation.PropertyAccessor; 6 | import com.fasterxml.jackson.core.JsonProcessingException; 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import com.fasterxml.jackson.databind.SerializationFeature; 9 | import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; 10 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 11 | import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; 12 | 13 | import javax.persistence.AttributeConverter; 14 | import java.io.IOException; 15 | 16 | public abstract class JsonConverter implements AttributeConverter { 17 | 18 | private static final ObjectMapper objectMapper = new ObjectMapper() 19 | .setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE) 20 | .setVisibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.NONE) 21 | .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) 22 | .setVisibility(PropertyAccessor.CREATOR, JsonAutoDetect.Visibility.ANY) 23 | .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) 24 | .enable(SerializationFeature.WRITE_DATES_WITH_ZONE_ID) 25 | .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) 26 | .registerModule(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES)) 27 | .registerModule(new Jdk8Module()) 28 | .registerModule(new JavaTimeModule()); 29 | 30 | private final Class type; 31 | 32 | public JsonConverter(Class type) { 33 | this.type = type; 34 | } 35 | 36 | @Override 37 | public String convertToDatabaseColumn(T object) { 38 | try { 39 | return objectMapper.writeValueAsString(object); 40 | } catch (JsonProcessingException ex) { 41 | throw new RuntimeException(ex); 42 | } 43 | } 44 | 45 | @Override 46 | public T convertToEntityAttribute(String data) { 47 | try { 48 | return objectMapper.readValue(data, type); 49 | } catch (IOException ex) { 50 | throw new RuntimeException(ex); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /adapter-commons/src/main/java/io/dddbyexamples/tools/ProjectionRepository.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.tools; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import org.springframework.data.rest.core.annotation.RestResource; 5 | 6 | import java.io.Serializable; 7 | 8 | public interface ProjectionRepository extends CrudRepository { 9 | 10 | @Override 11 | @RestResource(exported = false) 12 | S save(S entity); 13 | 14 | @Override 15 | @RestResource(exported = false) 16 | Iterable saveAll(Iterable entities); 17 | 18 | @Override 19 | @RestResource(exported = false) 20 | void deleteById(ID id); 21 | 22 | @Override 23 | @RestResource(exported = false) 24 | void delete(T entity); 25 | 26 | @Override 27 | @RestResource(exported = false) 28 | void deleteAll(Iterable entities); 29 | 30 | @Override 31 | @RestResource(exported = false) 32 | void deleteAll(); 33 | } 34 | -------------------------------------------------------------------------------- /adapter-commons/src/main/java/io/dddbyexamples/tools/TechnicalId.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.tools; 2 | 3 | import java.util.Optional; 4 | 5 | public interface TechnicalId { 6 | 7 | Long getId(); 8 | 9 | default boolean isPersisted() { 10 | return getId() != null; 11 | } 12 | 13 | static Optional get(Object id) { 14 | return isPersisted(id) ? Optional.of(((TechnicalId) id).getId()) : Optional.empty(); 15 | } 16 | 17 | static boolean isPersisted(Object id) { 18 | return (id instanceof TechnicalId) && ((TechnicalId) id).isPersisted(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /adapter-commons/src/main/resources/application-test.properties: -------------------------------------------------------------------------------- 1 | spring.main.banner-mode=off 2 | spring.jpa.database=default 3 | spring.jpa.generate-ddl=false 4 | 5 | spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_ON_EXIT=FALSE 6 | spring.datasource.username=sa 7 | spring.datasource.password=sa 8 | spring.datasource.driver-class-name=org.h2.Driver 9 | spring.liquibase.change-log=classpath:/schema/db.changelog.yml 10 | 11 | logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n 12 | logging.level.org.hibernate.SQL=debug 13 | logging.level.=error 14 | -------------------------------------------------------------------------------- /adapter-commons/src/main/resources/schema/commons.yml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | 3 | - changeSet: 4 | id: 1.hibernate.init 5 | author: Michal Michaluk 6 | changes: 7 | - createSequence: 8 | sequenceName: hibernate_sequence 9 | startValue: 1 10 | incrementBy: 1 11 | cacheSize: 1 12 | 13 | - changeSet: 14 | id: 2.postgres.json 15 | author: Michal Michaluk 16 | dbms: postgresql 17 | failOnError: false 18 | changes: 19 | - sql: CREATE CAST (VARCHAR AS JSONB) WITH INOUT AS ASSIGNMENT 20 | rollback: 21 | - sql: DROP CAST (VARCHAR AS JSONB) 22 | -------------------------------------------------------------------------------- /app-monolith/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8u171-jdk-alpine3.7 2 | MAINTAINER Michał Michaluk 3 | 4 | EXPOSE 8080 5 | COPY build/libs/app.jar app.jar 6 | ENTRYPOINT ["java", "-jar","/app.jar", "--spring.profiles.active=docker"] 7 | -------------------------------------------------------------------------------- /app-monolith/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'groovy' 2 | 3 | dependencies { 4 | compile(project(":demand-forecasting-adapters")) 5 | compile(project(":shortages-prediction-adapters")) 6 | compile(project(":product-management-adapters")) 7 | compile(project(":production-planning-adapters")) 8 | compile(project(":adapter-commons")) 9 | 10 | compile("org.springframework.boot:spring-boot-starter-data-jpa") 11 | compile("org.springframework.boot:spring-boot-starter-web") 12 | compile("org.springframework.boot:spring-boot-starter-data-rest") 13 | compile("org.springframework.data:spring-data-rest-hal-browser") 14 | compile("org.springframework.boot:spring-boot-starter-cloud-connectors") 15 | 16 | runtime("org.cloudfoundry:auto-reconfiguration:1.12.0.RELEASE") 17 | 18 | testCompile("org.springframework.boot:spring-boot-starter-test") 19 | testCompile("org.spockframework:spock-spring:1.1-groovy-2.4") 20 | testCompile("com.h2database:h2:1.4.197") 21 | } 22 | 23 | [bootJar, bootRun]*.enabled = true 24 | jar.enabled = false 25 | bootJar.archiveName='app.jar' 26 | 27 | task stubsJar(type: Jar) { 28 | classifier = "stubs" 29 | into("META-INF/${project.rootProject.ext.projectGroupId}/${project.rootProject.ext.projectArtifactId}/${project.rootProject.ext.projectVersion}/shortages-prediction-adapters/mappings") { 30 | include('**/*.*') 31 | from(project(":shortages-prediction-adapters").projectDir.absolutePath + "/target/generated-snippets/stubs") 32 | } 33 | into("META-INF/${project.rootProject.ext.projectGroupId}/${project.rootProject.ext.projectArtifactId}/${project.rootProject.ext.projectVersion}/shortages-prediction-adapters/contracts") { 34 | include('**/*.groovy') 35 | from(project(":shortages-prediction-adapters").projectDir.absolutePath + "/target/generated-snippets/contracts") 36 | } 37 | } 38 | 39 | // we need the tests to pass to build the stub jar 40 | stubsJar.dependsOn(test) 41 | stubsJar.dependsOn(project(":shortages-prediction-adapters").test) 42 | 43 | artifacts { 44 | archives stubsJar 45 | } 46 | 47 | jar.dependsOn(stubsJar) 48 | 49 | publishing { 50 | publications { 51 | stubs(MavenPublication) { 52 | artifactId project.name 53 | artifact stubsJar 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app-monolith/src/assembly/stub.xml: -------------------------------------------------------------------------------- 1 | 5 | stubs 6 | 7 | jar 8 | 9 | false 10 | 11 | 12 | 13 | 14 | ${project.basedir}/../shortages-prediction-adapters/target/generated-snippets/stubs 15 | META-INF/${project.groupId}/${project.artifactId}/${project.version}/shortages-prediction-adapters/mappings 16 | 17 | **/* 18 | 19 | 20 | 21 | ${project.basedir}/../shortages-prediction-adapters/target/generated-snippets/contracts 22 | META-INF/${project.groupId}/${project.artifactId}/${project.version}/shortages-prediction-adapters/contracts 23 | 24 | **/*.groovy 25 | 26 | 27 | 28 | 29 | ${project.basedir}/src/assembly 30 | META-INF/ 31 | 32 | **/* 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app-monolith/src/main/java/io/dddbyexamples/factory/AppConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.autoconfigure.domain.EntityScan; 6 | import org.springframework.cloud.config.java.ServiceScan; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.Profile; 10 | import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters; 11 | import org.springframework.scheduling.annotation.EnableScheduling; 12 | import io.dddbyexamples.factory.shortages.prediction.calculation.Stock; 13 | import io.dddbyexamples.factory.warehouse.WarehouseService; 14 | 15 | import java.time.Clock; 16 | 17 | @SpringBootApplication 18 | @EnableScheduling 19 | @EntityScan( 20 | basePackageClasses = {AppConfiguration.class, Jsr310JpaConverters.class} 21 | ) 22 | public class AppConfiguration { 23 | 24 | public static void main(String[] args) { 25 | SpringApplication.run(AppConfiguration.class, args); 26 | } 27 | 28 | @Bean 29 | public WarehouseService warehouseService() { 30 | // mocked facade for external service 31 | return refNo -> new Stock(1200, 700); 32 | } 33 | 34 | @Bean 35 | public Clock clock() { 36 | return Clock.systemDefaultZone(); 37 | } 38 | 39 | @Configuration 40 | @ServiceScan 41 | @Profile("cloud") 42 | public class ServiceConfiguration { 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app-monolith/src/main/java/io/dddbyexamples/factory/delivery/planning/definition/DeliveryPlannerDefinitionEventsPropagation.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.delivery.planning.definition; 2 | 3 | import lombok.AllArgsConstructor; 4 | import org.springframework.data.rest.core.annotation.HandleAfterCreate; 5 | import org.springframework.data.rest.core.annotation.HandleAfterSave; 6 | import org.springframework.data.rest.core.annotation.RepositoryEventHandler; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.transaction.annotation.Transactional; 9 | import io.dddbyexamples.factory.delivery.planning.projection.DeliveryForecastProjection; 10 | 11 | @Component 12 | @Transactional 13 | @AllArgsConstructor 14 | @RepositoryEventHandler 15 | public class DeliveryPlannerDefinitionEventsPropagation { 16 | 17 | private DeliveryForecastProjection projection; 18 | 19 | @HandleAfterSave 20 | @HandleAfterCreate 21 | public void handleCreateAndUpdate(DeliveryPlannerDefinitionEntity entity) { 22 | projection.applyDeliveryPlannerDefinitionChange(entity.getRefNo()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app-monolith/src/main/java/io/dddbyexamples/factory/demand/forecasting/DemandEventsPropagation.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting; 2 | 3 | import io.dddbyexamples.factory.delivery.planning.projection.DeliveryForecastProjection; 4 | import io.dddbyexamples.factory.demand.forecasting.command.RequiredReviewDao; 5 | import io.dddbyexamples.factory.demand.forecasting.command.RequiredReviewEntity; 6 | import io.dddbyexamples.factory.demand.forecasting.projection.CurrentDemandProjection; 7 | import io.dddbyexamples.factory.shortages.prediction.monitoring.ShortagePredictionService; 8 | import lombok.AllArgsConstructor; 9 | import org.springframework.context.annotation.Lazy; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.time.Clock; 13 | import java.time.Instant; 14 | import java.util.stream.Collectors; 15 | 16 | @Lazy 17 | @Component 18 | @AllArgsConstructor 19 | class DemandEventsPropagation implements DemandEvents { 20 | 21 | private final CurrentDemandProjection demandProjection; 22 | private final DeliveryForecastProjection deliveryProjection; 23 | private final ShortagePredictionService shortagePrediction; 24 | private final RequiredReviewDao demandReviews; 25 | private final Clock clock; 26 | 27 | @Override 28 | public void emit(DemandedLevelsChanged event) { 29 | demandProjection.applyDemandedLevelsChanged(event); 30 | deliveryProjection.applyDemandedLevelsChanged(event); 31 | shortagePrediction.predictShortages(event); 32 | } 33 | 34 | @Override 35 | public void emit(ReviewRequired event) { 36 | Instant timestamp = Instant.now(clock); 37 | demandReviews.saveAll(event.getReviews().stream() 38 | .map(r -> new RequiredReviewEntity(timestamp, r)) 39 | .collect(Collectors.toList()) 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app-monolith/src/main/java/io/dddbyexamples/factory/product/management/ProductDescriptionEventsPropagation.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.product.management; 2 | 3 | import lombok.AllArgsConstructor; 4 | import org.springframework.data.rest.core.annotation.HandleAfterCreate; 5 | import org.springframework.data.rest.core.annotation.HandleAfterDelete; 6 | import org.springframework.data.rest.core.annotation.RepositoryEventHandler; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.transaction.annotation.Transactional; 9 | import io.dddbyexamples.factory.demand.forecasting.DemandService; 10 | import io.dddbyexamples.factory.stock.forecast.ressource.StockForecastDao; 11 | import io.dddbyexamples.factory.stock.forecast.ressource.StockForecastEntity; 12 | 13 | @Component 14 | @Transactional 15 | @AllArgsConstructor 16 | @RepositoryEventHandler 17 | public class ProductDescriptionEventsPropagation { 18 | 19 | private final DemandService demandService; 20 | private final StockForecastDao stockForecasts; 21 | 22 | @HandleAfterCreate 23 | public void handleCreate(ProductDescriptionEntity entity) { 24 | demandService.initNewProduct(entity.getRefNo()); 25 | stockForecasts.save(new StockForecastEntity(entity.getRefNo())); 26 | } 27 | 28 | @HandleAfterDelete 29 | public void handleDelete(ProductDescriptionEntity entity) { 30 | stockForecasts.deleteById(entity.getRefNo()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app-monolith/src/main/java/io/dddbyexamples/factory/shortages/prediction/calculation/ForecastORMRepository.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.calculation; 2 | 3 | import io.dddbyexamples.factory.production.planning.projection.ProductionOutputDao; 4 | import lombok.AllArgsConstructor; 5 | import org.springframework.stereotype.Component; 6 | import io.dddbyexamples.factory.delivery.planning.projection.DeliveryForecastDao; 7 | import io.dddbyexamples.factory.delivery.planning.projection.DeliveryForecastEntity; 8 | import io.dddbyexamples.factory.product.management.RefNoId; 9 | import io.dddbyexamples.factory.shortages.prediction.calculation.ProductionForecast.Item; 10 | import io.dddbyexamples.factory.warehouse.WarehouseService; 11 | 12 | import java.time.Clock; 13 | import java.time.LocalDateTime; 14 | import java.time.temporal.ChronoUnit; 15 | import java.util.Map; 16 | import java.util.SortedSet; 17 | import java.util.TreeSet; 18 | import java.util.stream.Collectors; 19 | 20 | import static java.util.stream.Collectors.toMap; 21 | 22 | @Component 23 | @AllArgsConstructor 24 | class ForecastORMRepository implements ShortageForecasts { 25 | 26 | private final WarehouseService stocks; 27 | private final DeliveryForecastDao deliveries; 28 | private final ProductionOutputDao outputs; 29 | private final Clock clock; 30 | 31 | @Override 32 | public ShortageForecast get(RefNoId refNo, int daysAhead) { 33 | Stock stock = stocks.forRefNo(refNo); 34 | LocalDateTime time = LocalDateTime.now(clock); 35 | LocalDateTime max = time.plusDays(daysAhead).truncatedTo(ChronoUnit.DAYS); 36 | 37 | Map deliveries = this.deliveries 38 | .findByRefNoAndTimeBetween(refNo.getRefNo(), time, max).stream() 39 | .collect(toMap( 40 | DeliveryForecastEntity::getTime, 41 | DeliveryForecastEntity::getLevel 42 | )); 43 | SortedSet deliveryTimes = new TreeSet<>(deliveries.keySet()); 44 | 45 | DeliveriesForecast demand = new DeliveriesForecast(deliveries); 46 | 47 | ProductionOutputs outputs = new ProductionForecast( 48 | this.outputs.findByRefNoAndEndGreaterThanAndStartLessThan(refNo.getRefNo(), time, max).stream() 49 | .map(e -> new Item( 50 | e.getStart(), 51 | e.getDuration(), 52 | e.getPartsPerMinute())) 53 | .collect(Collectors.toList()) 54 | ).outputsInTimes(time, deliveryTimes); 55 | 56 | return new ShortageForecast(refNo.getRefNo(), time, deliveryTimes, stock, outputs, demand); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app-monolith/src/main/java/io/dddbyexamples/factory/shortages/prediction/monitoring/ShortageEventsPropagation.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.monitoring; 2 | 3 | import lombok.AllArgsConstructor; 4 | import org.springframework.context.annotation.Lazy; 5 | import org.springframework.stereotype.Component; 6 | import io.dddbyexamples.factory.shortages.prediction.notification.NotificationOfShortage; 7 | 8 | @Lazy 9 | @Component 10 | @AllArgsConstructor 11 | class ShortageEventsPropagation implements ShortageEvents { 12 | 13 | private final NotificationOfShortage notification; 14 | 15 | @Override 16 | public void emit(NewShortage event) { 17 | notification.notifyAbout(event); 18 | } 19 | 20 | @Override 21 | public void emit(ShortageSolved event) { 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app-monolith/src/main/java/io/dddbyexamples/factory/stock/forecast/StockForecast.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.stock.forecast; 2 | 3 | import lombok.Builder; 4 | import lombok.Singular; 5 | import lombok.Value; 6 | 7 | import java.time.LocalDate; 8 | import java.util.List; 9 | 10 | @Value 11 | @Builder 12 | public class StockForecast { 13 | 14 | @Singular 15 | List forecasts; 16 | 17 | @Value 18 | static class DailyForecast { 19 | LocalDate date; 20 | long stock; 21 | long withLocked; 22 | long demand; 23 | long output; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app-monolith/src/main/java/io/dddbyexamples/factory/stock/forecast/StockForecastQuery.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.stock.forecast; 2 | 3 | import io.dddbyexamples.factory.demand.forecasting.projection.CurrentDemandDao; 4 | import io.dddbyexamples.factory.demand.forecasting.projection.CurrentDemandEntity; 5 | import io.dddbyexamples.factory.production.planning.projection.ProductionDailyOutputDao; 6 | import io.dddbyexamples.factory.production.planning.projection.ProductionDailyOutputEntity; 7 | import io.dddbyexamples.factory.shortages.prediction.calculation.Stock; 8 | import lombok.AllArgsConstructor; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.transaction.annotation.Transactional; 11 | import io.dddbyexamples.factory.product.management.RefNoId; 12 | import io.dddbyexamples.factory.stock.forecast.StockForecast.StockForecastBuilder; 13 | import io.dddbyexamples.factory.warehouse.WarehouseService; 14 | 15 | import java.time.Clock; 16 | import java.time.LocalDate; 17 | import java.util.Map; 18 | 19 | import static java.util.stream.Collectors.toMap; 20 | 21 | @Component 22 | @Transactional(readOnly = true) 23 | @AllArgsConstructor 24 | public class StockForecastQuery { 25 | 26 | private final WarehouseService stocks; 27 | private final CurrentDemandDao demands; 28 | private final ProductionDailyOutputDao outputs; 29 | private final Clock clock; 30 | 31 | public StockForecast get(RefNoId refNo) { 32 | Stock stock = stocks.forRefNo(refNo); 33 | LocalDate today = LocalDate.now(clock); 34 | return build(today, stock, 35 | this.demands 36 | .findByRefNoAndDateGreaterThanEqual(refNo.getRefNo(), today).stream() 37 | .collect(toMap( 38 | CurrentDemandEntity::getDate, 39 | CurrentDemandEntity::getLevel 40 | )), 41 | this.outputs 42 | .findByRefNoAndDateGreaterThanEqual(refNo.getRefNo(), today).stream() 43 | .collect(toMap( 44 | ProductionDailyOutputEntity::getDate, 45 | ProductionDailyOutputEntity::getOutput 46 | )) 47 | ); 48 | } 49 | 50 | private StockForecast build(LocalDate today, 51 | Stock stock, 52 | Map demands, 53 | Map outputs) { 54 | LocalDate stopAtDay = today.plusDays(15); 55 | long level = stock.getLevel(); 56 | StockForecastBuilder builder = StockForecast.builder(); 57 | for (LocalDate date = today; date.isBefore(stopAtDay); date = date.plusDays(1)) { 58 | long withLocked = level + stock.getLocked(); 59 | long demand = demands.getOrDefault(date, 0L); 60 | long output = outputs.getOrDefault(date, 0L); 61 | builder.forecast( 62 | new StockForecast.DailyForecast( 63 | date, 64 | level, 65 | withLocked, 66 | demand, 67 | output 68 | ) 69 | ); 70 | level = level - demand + output; 71 | } 72 | return builder.build(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app-monolith/src/main/java/io/dddbyexamples/factory/stock/forecast/ressource/StockForecastDao.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.stock.forecast.ressource; 2 | 3 | import io.dddbyexamples.tools.ProjectionRepository; 4 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 5 | import org.springframework.data.rest.core.config.Projection; 6 | import org.springframework.stereotype.Repository; 7 | 8 | @Repository 9 | @RepositoryRestResource(path = "stock-forecasts", 10 | collectionResourceRel = "stock-forecasts", 11 | itemResourceRel = "stock-forecast", 12 | excerptProjection = StockForecastDao.CollectionItem.class) 13 | public interface StockForecastDao extends ProjectionRepository { 14 | 15 | @Projection(types = {StockForecastEntity.class}) 16 | interface CollectionItem { 17 | String getRefNo(); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app-monolith/src/main/java/io/dddbyexamples/factory/stock/forecast/ressource/StockForecastEntity.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.stock.forecast.ressource; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | import io.dddbyexamples.factory.stock.forecast.StockForecast; 7 | 8 | import javax.persistence.Entity; 9 | import javax.persistence.Id; 10 | import javax.persistence.Table; 11 | import java.io.Serializable; 12 | 13 | @Entity(name = "StockForecast") 14 | @Table(schema = "shortages_prediction") 15 | @Getter 16 | @NoArgsConstructor 17 | public class StockForecastEntity implements Serializable { 18 | 19 | @Id 20 | private String refNo; 21 | @Setter 22 | private transient StockForecast stockForecast; 23 | 24 | public StockForecastEntity(String refNo) { 25 | this.refNo = refNo; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app-monolith/src/main/java/io/dddbyexamples/factory/stock/forecast/ressource/StockForecastResourceProcessor.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.stock.forecast.ressource; 2 | 3 | import lombok.AllArgsConstructor; 4 | import org.springframework.hateoas.Resource; 5 | import org.springframework.hateoas.ResourceProcessor; 6 | import org.springframework.stereotype.Component; 7 | import io.dddbyexamples.factory.product.management.RefNoId; 8 | import io.dddbyexamples.factory.stock.forecast.StockForecastQuery; 9 | 10 | @Component 11 | @AllArgsConstructor 12 | class StockForecastResourceProcessor implements ResourceProcessor> { 13 | 14 | private final StockForecastQuery query; 15 | 16 | @Override 17 | public Resource process(Resource resource) { 18 | resource.getContent().setStockForecast( 19 | query.get(new RefNoId(resource.getContent().getRefNo())) 20 | ); 21 | return resource; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app-monolith/src/main/java/io/dddbyexamples/factory/warehouse/WarehouseService.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.warehouse; 2 | 3 | import io.dddbyexamples.factory.shortages.prediction.calculation.Stock; 4 | import io.dddbyexamples.factory.product.management.RefNoId; 5 | 6 | public interface WarehouseService { 7 | Stock forRefNo(RefNoId refNo); 8 | } 9 | -------------------------------------------------------------------------------- /app-monolith/src/main/resources/application-cloud.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:postgresql://postgresql:5432/postgres 2 | spring.datasource.username=postgres 3 | spring.datasource.password=postgres 4 | # no money == small pool size ;) 5 | spring.datasource.hikari.maximum-pool-size=2 -------------------------------------------------------------------------------- /app-monolith/src/main/resources/application-docker.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:postgresql://database:5432/postgres 2 | spring.datasource.username=postgres 3 | spring.datasource.password=postgres 4 | spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true 5 | -------------------------------------------------------------------------------- /app-monolith/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.main.banner-mode=off 2 | spring.jpa.database=default 3 | spring.jpa.generate-ddl=false 4 | spring.jpa.hibernate.ddl-auto=none 5 | spring.datasource.url=jdbc:postgresql://localhost:5432/postgres 6 | spring.datasource.username=postgres 7 | spring.datasource.password=postgres 8 | spring.datasource.driver-class-name=org.postgresql.Driver 9 | spring.liquibase.change-log=classpath:/schema/db.changelog.yml 10 | logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n 11 | 12 | -------------------------------------------------------------------------------- /app-monolith/src/main/resources/schema/db.changelog.yml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - include: 3 | file: /schema/commons.yml 4 | - include: 5 | file: /schema/delivery-planning.yml 6 | - include: 7 | file: /schema/demand-forecasting.yml 8 | - include: 9 | file: /schema/product-management.yml 10 | - include: 11 | file: /schema/production-planning.yml 12 | - include: 13 | file: /schema/shortages-prediction.yml 14 | -------------------------------------------------------------------------------- /app-monolith/src/test/groovy/io/dddbyexamples/factory/ProductTrait.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory 2 | 3 | import io.dddbyexamples.factory.demand.forecasting.AdjustDemand 4 | import io.dddbyexamples.factory.demand.forecasting.Adjustment 5 | import io.dddbyexamples.factory.demand.forecasting.Demand 6 | import io.dddbyexamples.factory.demand.forecasting.Document 7 | import io.dddbyexamples.factory.demand.forecasting.command.DemandAdjustmentEntity 8 | import io.dddbyexamples.factory.demand.forecasting.persistence.DocumentEntity 9 | import io.dddbyexamples.factory.product.management.ProductDescription 10 | import io.dddbyexamples.factory.product.management.ProductDescriptionEntity 11 | 12 | import java.time.Instant 13 | import java.time.LocalDate 14 | import java.time.OffsetTime 15 | import java.time.ZoneOffset 16 | 17 | trait ProductTrait { 18 | 19 | DocumentEntity documentFor(String refNo, LocalDate date, long ... levels) { 20 | Document document = document(refNo, date, levels) 21 | return new DocumentEntity("uri", "storedUri", document) 22 | } 23 | 24 | ProductDescriptionEntity productDescription(String refNo) { 25 | ProductDescription desc = new ProductDescription("461952398951", ["PROWAD.POJ.NA JARZ.ESSENT"]) 26 | return new ProductDescriptionEntity(refNo, desc) 27 | } 28 | 29 | Document document(String refNo, LocalDate date, long ... levels) { 30 | Instant created = date.atTime(OffsetTime.of(8, 0, 0, 0, ZoneOffset.UTC)).toInstant() 31 | SortedMap results = new TreeMap<>() 32 | for (long level : levels) { 33 | results.put(date, Demand.of(level)) 34 | date = date.plusDays(1) 35 | } 36 | return new Document(created, refNo, results) 37 | } 38 | 39 | DemandAdjustmentEntity strongAdjustment(String refNo, Map adjustments) { 40 | return new DemandAdjustmentEntity(refNo, "agent", new AdjustDemand(refNo, adjustments)) 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app-monolith/src/test/resources/application-test.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_ON_EXIT=FALSE 2 | spring.datasource.username=sa 3 | spring.datasource.password=sa 4 | spring.datasource.driver-class-name=org.h2.Driver 5 | -------------------------------------------------------------------------------- /app-monolith/src/test/resources/curl.txt: -------------------------------------------------------------------------------- 1 | 2 | curl http://localhost:8080 3 | 4 | # define new product 5 | curl -X POST -H "Content-Type: application/json" \ 6 | -d @app-monolith/src/test/resources/examples/product-descriptions.json \ 7 | http://localhost:8080/product-descriptions 8 | 9 | # define non dummy preferred delivery hours 10 | curl -X POST -H "Content-Type: application/json" \ 11 | -d @app-monolith/src/test/resources/examples/delivery-definitions.json \ 12 | http://localhost:8080/delivery-definitions 13 | 14 | # define customer demands for our product 15 | curl -X POST -H "Content-Type: application/json" \ 16 | -d @app-monolith/src/test/resources/examples/demand-adjustments.json \ 17 | http://localhost:8080/demand-adjustments 18 | 19 | # check demands 20 | curl http://localhost:8080/demand-forecasts 21 | # check approximated deliveries 22 | curl http://localhost:8080/delivery-forecasts 23 | # check stock levels prediction 24 | curl http://localhost:8080/stock-forecasts 25 | # check potential shortages on delivery times 26 | curl http://localhost:8080/shortages 27 | 28 | -------------------------------------------------------------------------------- /app-monolith/src/test/resources/examples/delivery-definitions.json: -------------------------------------------------------------------------------- 1 | { 2 | "refNo": "3009002", 3 | "definition": { 4 | "definitions": { 5 | "AtDayStart": { 6 | "06:00": 1.0 7 | }, 8 | "TillDayEnd": { 9 | "22:00": 1.0 10 | }, 11 | "Twice": { 12 | "16:00": 0.5, 13 | "20:00": 0.5 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app-monolith/src/test/resources/examples/delivery-forecasts.json: -------------------------------------------------------------------------------- 1 | { 2 | "refNo": "3009000", 3 | "date": "2017-12-10", 4 | "time": "2017-12-10T18:00:00.000", 5 | "level": "3000" 6 | } 7 | -------------------------------------------------------------------------------- /app-monolith/src/test/resources/examples/demand-adjustments.json: -------------------------------------------------------------------------------- 1 | { 2 | "note": "Bo demo robie", 3 | "customerRepresentative": "Kolega z logistyki od klienta", 4 | "adjustment": { 5 | "refNo": "3009002", 6 | "adjustments": { 7 | "2018-03-10": { 8 | "demand": { 9 | "level": 2000, 10 | "schema": "AtDayStart" 11 | }, 12 | "strong": true 13 | }, 14 | "2018-03-11": { 15 | "demand": { 16 | "level": 2500, 17 | "schema": "TillDayEnd" 18 | }, 19 | "strong": false 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app-monolith/src/test/resources/examples/demand-forecasts.json: -------------------------------------------------------------------------------- 1 | { 2 | "refNo": "3009000", 3 | "date": "2017-12-10", 4 | "level": "2800", 5 | "schema": "TillDayEnd" 6 | } 7 | -------------------------------------------------------------------------------- /app-monolith/src/test/resources/examples/product-descriptions.json: -------------------------------------------------------------------------------- 1 | { 2 | "refNo": "3009002", 3 | "description": { 4 | "matNum": "461952398959", 5 | "names": [ 6 | "NOWA PROWAD.POJ.NA JARZ.ESSENT" 7 | ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app-monolith/src/test/resources/examples/production-outputs-daily.json: -------------------------------------------------------------------------------- 1 | { 2 | "refNo": "3009000", 3 | "date": "2017-12-10", 4 | "output": "1200" 5 | } 6 | -------------------------------------------------------------------------------- /app-monolith/src/test/resources/examples/production-outputs.json: -------------------------------------------------------------------------------- 1 | { 2 | "refNo": "3009000", 3 | "start": "2017-12-10T06:15:00.000", 4 | "duration": "DT120M", 5 | "partsPerMinute": "10", 6 | "total": "1200" 7 | } 8 | -------------------------------------------------------------------------------- /app-monolith/src/test/resources/examples/shortages.json: -------------------------------------------------------------------------------- 1 | { 2 | "refNo": "3009000", 3 | "shortages": { 4 | "refNo": "3009000", 5 | "lockedParts": 0, 6 | "found": "2017-12-10T18:21:07.293", 7 | "shortages": { 8 | "2018-12-11T00:00": 2800 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app-monolith/src/test/resources/examples/stock-forecasts.json: -------------------------------------------------------------------------------- 1 | { 2 | "refNo": "3009000", 3 | "description": { 4 | "matNum": "461952398951", 5 | "names": [ 6 | "PROWAD.POJ.NA JARZ.ESSENT" 7 | ] 8 | }, 9 | "forecasts": [ 10 | { 11 | "date": "2017-12-10", 12 | "stock": 1200, 13 | "withLocked": 1900, 14 | "demand": 0, 15 | "output": 0 16 | }, 17 | { 18 | "date": "2017-12-11", 19 | "stock": 1200, 20 | "withLocked": 1900, 21 | "demand": 2800, 22 | "output": 0 23 | }, 24 | { 25 | "date": "2017-12-12", 26 | "stock": -1600, 27 | "withLocked": -900, 28 | "demand": 0, 29 | "output": 0 30 | }, 31 | { 32 | "date": "2017-12-13", 33 | "stock": -1600, 34 | "withLocked": -900, 35 | "demand": 0, 36 | "output": 0 37 | }, 38 | { 39 | "date": "2017-12-14", 40 | "stock": -1600, 41 | "withLocked": -900, 42 | "demand": 0, 43 | "output": 0 44 | }, 45 | { 46 | "date": "2017-12-15", 47 | "stock": -1600, 48 | "withLocked": -900, 49 | "demand": 0, 50 | "output": 0 51 | }, 52 | { 53 | "date": "2017-12-16", 54 | "stock": -1600, 55 | "withLocked": -900, 56 | "demand": 0, 57 | "output": 0 58 | }, 59 | { 60 | "date": "2017-12-17", 61 | "stock": -1600, 62 | "withLocked": -900, 63 | "demand": 0, 64 | "output": 0 65 | }, 66 | { 67 | "date": "2017-12-18", 68 | "stock": -1600, 69 | "withLocked": -900, 70 | "demand": 0, 71 | "output": 0 72 | }, 73 | { 74 | "date": "2017-12-19", 75 | "stock": -1600, 76 | "withLocked": -900, 77 | "demand": 0, 78 | "output": 0 79 | }, 80 | { 81 | "date": "2017-12-20", 82 | "stock": -1600, 83 | "withLocked": -900, 84 | "demand": 0, 85 | "output": 0 86 | }, 87 | { 88 | "date": "2017-12-21", 89 | "stock": -1600, 90 | "withLocked": -900, 91 | "demand": 0, 92 | "output": 0 93 | }, 94 | { 95 | "date": "2017-12-22", 96 | "stock": -1600, 97 | "withLocked": -900, 98 | "demand": 0, 99 | "output": 0 100 | }, 101 | { 102 | "date": "2017-12-23", 103 | "stock": -1600, 104 | "withLocked": -900, 105 | "demand": 0, 106 | "output": 0 107 | }, 108 | { 109 | "date": "2017-12-24", 110 | "stock": -1600, 111 | "withLocked": -900, 112 | "demand": 0, 113 | "output": 0 114 | } 115 | ] 116 | } 117 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | mavenLocal() 5 | maven { url "https://repo.spring.io/snapshot" } 6 | maven { url "https://repo.spring.io/milestone" } 7 | maven { url "https://repo.spring.io/release" } 8 | } 9 | dependencies { 10 | classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.2.RELEASE" 11 | // TODO: Snapshots are used only for testing purposes 12 | classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:2.0.0.BUILD-SNAPSHOT" 13 | } 14 | } 15 | 16 | allprojects { 17 | apply plugin: 'java' 18 | apply plugin: 'org.springframework.boot' 19 | apply plugin: 'io.spring.dependency-management' 20 | apply plugin: 'maven-publish' 21 | 22 | group = 'pl.com.dddbyexamples' 23 | version = getProp('newVersion') ?: '1.0-SNAPSHOT' 24 | 25 | apply from: project.rootDir.absolutePath + '/gradle/pipeline.gradle' 26 | 27 | bootJar.enabled = false 28 | jar.enabled = true 29 | bootRun.enabled = false 30 | 31 | compileJava.options.compilerArgs << '-parameters' 32 | compileTestJava.options.compilerArgs << '-parameters' 33 | 34 | dependencies { 35 | compileOnly('org.projectlombok:lombok:1.18.0') 36 | annotationProcessor('org.projectlombok:lombok:1.18.0') 37 | 38 | testCompile("org.spockframework:spock-core:1.1-groovy-2.4") 39 | testCompile("net.bytebuddy:byte-buddy:1.8.12") 40 | } 41 | 42 | dependencyManagement { 43 | imports { 44 | mavenBom "org.springframework.cloud:spring-cloud-dependencies:${BOM_VERSION}" 45 | } 46 | } 47 | 48 | repositories { 49 | mavenCentral() 50 | mavenLocal() 51 | if (getProp("M2_LOCAL")) { 52 | maven { 53 | url getProp("M2_LOCAL") 54 | } 55 | } 56 | maven { url "https://repo.spring.io/snapshot" } 57 | maven { url "https://repo.spring.io/milestone" } 58 | maven { url "https://repo.spring.io/release" } 59 | } 60 | 61 | publishing { 62 | repositories { 63 | maven { 64 | url getProp('REPO_WITH_BINARIES_FOR_UPLOAD') ?: 65 | getProp('REPO_WITH_BINARIES') ?: 'http://localhost:8081/artifactory/libs-release-local' 66 | credentials { 67 | username getProp('M2_SETTINGS_REPO_USERNAME') ?: 'admin' 68 | password getProp('M2_SETTINGS_REPO_PASSWORD') ?: 'password' 69 | } 70 | } 71 | } 72 | publications { 73 | mavenJava(MavenPublication) { 74 | artifactId project.name 75 | from components.java 76 | } 77 | } 78 | } 79 | 80 | tasks.withType(Test) { 81 | testLogging { 82 | events "started", "passed", "skipped", "failed" 83 | } 84 | } 85 | } 86 | 87 | ext { 88 | projectGroupId = project.group 89 | // In a multi-module env we can specify which project is the one that produces the fat-jar 90 | projectArtifactId = "app-monolith" 91 | projectVersion = project.version 92 | } 93 | 94 | apply plugin: "java" 95 | [bootJar, bootRun]*.enabled = false 96 | 97 | task wrapper(type: Wrapper) { 98 | gradleVersion = '4.8' 99 | } 100 | 101 | String getProp(String propName) { 102 | return hasProperty(propName) ? 103 | (getProperty(propName) ?: System.properties[propName]) : System.properties[propName] ?: 104 | System.getenv(propName) 105 | } 106 | -------------------------------------------------------------------------------- /command-query-crud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddd-by-examples/factory/95c751ccefb879e02ecc959c712caa31f4cd9bcf/command-query-crud.png -------------------------------------------------------------------------------- /demand-forecasting-adapters/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'groovy' 2 | 3 | dependencies { 4 | compile(project(":demand-forecasting-model")) 5 | compile(project(":adapter-commons")) 6 | 7 | testCompile("org.springframework.boot:spring-boot-starter-test") 8 | testCompile("org.spockframework:spock-spring:1.1-groovy-2.4") 9 | testCompile("com.h2database:h2:1.4.194") 10 | } 11 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/delivery/planning/DeliveryAutoPlannerORMRepository.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.delivery.planning; 2 | 3 | import io.dddbyexamples.factory.delivery.planning.definition.DeliveryPlannerDefinition; 4 | import lombok.AllArgsConstructor; 5 | import org.springframework.stereotype.Component; 6 | import io.dddbyexamples.factory.delivery.planning.definition.DeliveryPlannerDefinitionDao; 7 | import io.dddbyexamples.factory.delivery.planning.definition.DeliveryPlannerDefinitionEntity; 8 | 9 | import java.util.Collections; 10 | 11 | @Component 12 | @AllArgsConstructor 13 | public class DeliveryAutoPlannerORMRepository { 14 | 15 | DeliveryPlannerDefinitionDao dao; 16 | 17 | public DeliveryAutoPlanner get(String refNo) { 18 | return new DeliveryAutoPlanner(refNo, 19 | dao.findById(refNo) 20 | .map(DeliveryPlannerDefinitionEntity::getDefinition) 21 | .filter(def -> !def.isDisabled()) 22 | .map(x -> x.map(DeliveriesSuggestion::timesAndFractions)) 23 | .orElse(Collections.emptyMap())); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/delivery/planning/definition/DeliveryPlannerDefinition.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.delivery.planning.definition; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Singular; 6 | import lombok.Value; 7 | import io.dddbyexamples.factory.demand.forecasting.Demand; 8 | 9 | import java.time.LocalTime; 10 | import java.util.Collections; 11 | import java.util.Map; 12 | import java.util.function.Function; 13 | import java.util.stream.Collectors; 14 | 15 | @Builder 16 | @Value 17 | @AllArgsConstructor 18 | public class DeliveryPlannerDefinition { 19 | @Singular 20 | private final Map> definitions; 21 | private final boolean disabled; 22 | 23 | public static Map of(LocalTime time, Double fraction) { 24 | return Collections.singletonMap(time, fraction); 25 | } 26 | 27 | public Map map(Function, T> timesAndFractions) { 28 | return definitions.entrySet().stream() 29 | .collect(Collectors.toMap( 30 | Map.Entry::getKey, 31 | e -> timesAndFractions.apply(e.getValue()) 32 | )); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/delivery/planning/definition/DeliveryPlannerDefinitionDao.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.delivery.planning.definition; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | @RepositoryRestResource( 9 | path = "delivery-definitions", 10 | collectionResourceRel = "delivery-definitions", 11 | itemResourceRel = "delivery-definition") 12 | public interface DeliveryPlannerDefinitionDao extends JpaRepository { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/delivery/planning/definition/DeliveryPlannerDefinitionEntity.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.delivery.planning.definition; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import io.dddbyexamples.tools.JsonConverter; 6 | 7 | import javax.persistence.Convert; 8 | import javax.persistence.Entity; 9 | import javax.persistence.Id; 10 | import javax.persistence.Table; 11 | import java.io.Serializable; 12 | 13 | @Entity(name = "DeliveryPlannerDefinition") 14 | @Table(schema = "delivery_planning") 15 | @Getter 16 | @NoArgsConstructor 17 | public class DeliveryPlannerDefinitionEntity implements Serializable { 18 | 19 | @Id 20 | private String refNo; 21 | @Convert(converter = DefinitionAsJson.class) 22 | private DeliveryPlannerDefinition definition; 23 | 24 | public DeliveryPlannerDefinitionEntity(String refNo, DeliveryPlannerDefinition definition) { 25 | this.refNo = refNo; 26 | this.definition = definition; 27 | } 28 | 29 | public static class DefinitionAsJson extends JsonConverter { 30 | public DefinitionAsJson() { 31 | super(DeliveryPlannerDefinition.class); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/delivery/planning/projection/DeliveryForecastDao.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.delivery.planning.projection; 2 | 3 | import io.dddbyexamples.tools.ProjectionRepository; 4 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 5 | import org.springframework.data.rest.core.annotation.RestResource; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.time.LocalDate; 9 | import java.time.LocalDateTime; 10 | import java.util.List; 11 | 12 | @Repository 13 | @RepositoryRestResource(path = "delivery-forecasts", 14 | collectionResourceRel = "delivery-forecasts", 15 | itemResourceRel = "delivery-forecast") 16 | public interface DeliveryForecastDao extends ProjectionRepository { 17 | 18 | @RestResource(path = "refNos", rel = "refNos") 19 | List findByRefNoAndTimeBetween(String refNo, LocalDateTime from, LocalDateTime to); 20 | 21 | @RestResource(exported = false) 22 | void deleteByRefNoAndDate(String refNo, LocalDate date); 23 | 24 | @RestResource(exported = false) 25 | void deleteByRefNo(String refNo); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/delivery/planning/projection/DeliveryForecastEntity.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.delivery.planning.projection; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | 6 | import javax.persistence.Entity; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.Id; 9 | import javax.persistence.Table; 10 | import java.io.Serializable; 11 | import java.time.LocalDate; 12 | import java.time.LocalDateTime; 13 | 14 | @Entity(name = "DeliveryForecast") 15 | @Table(schema = "delivery_planning") 16 | @Getter 17 | @NoArgsConstructor 18 | public class DeliveryForecastEntity implements Serializable { 19 | 20 | @Id 21 | @GeneratedValue 22 | private Long id; 23 | private String refNo; 24 | private LocalDate date; 25 | private LocalDateTime time; 26 | private long level; 27 | 28 | DeliveryForecastEntity(String refNo, LocalDateTime time, long level) { 29 | this.refNo = refNo; 30 | this.date = time.toLocalDate(); 31 | this.time = time; 32 | this.level = level; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/delivery/planning/projection/DeliveryForecastProjection.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.delivery.planning.projection; 2 | 3 | import io.dddbyexamples.factory.delivery.planning.DeliveryAutoPlanner; 4 | import io.dddbyexamples.factory.delivery.planning.DeliveryAutoPlannerORMRepository; 5 | import io.dddbyexamples.factory.demand.forecasting.DemandedLevelsChanged; 6 | import io.dddbyexamples.factory.demand.forecasting.projection.CurrentDemandDao; 7 | import lombok.AllArgsConstructor; 8 | import org.springframework.stereotype.Component; 9 | import io.dddbyexamples.factory.demand.forecasting.Demand; 10 | import io.dddbyexamples.factory.demand.forecasting.projection.CurrentDemandEntity; 11 | 12 | import java.time.Clock; 13 | import java.time.LocalDate; 14 | import java.util.List; 15 | 16 | @Component 17 | @AllArgsConstructor 18 | public class DeliveryForecastProjection { 19 | 20 | private final Clock clock; 21 | private final DeliveryForecastDao forecastDao; 22 | private final CurrentDemandDao demandDao; 23 | private final DeliveryAutoPlannerORMRepository planners; 24 | 25 | public void applyDemandedLevelsChanged(DemandedLevelsChanged event) { 26 | DeliveryAutoPlanner planner = planners.get(event.getRefNo().getRefNo()); 27 | event.getResults().keySet() 28 | .forEach(daily -> forecastDao.deleteByRefNoAndDate( 29 | daily.getRefNo(), 30 | daily.getDate()) 31 | ); 32 | event.getResults().entrySet().stream() 33 | .flatMap(entry -> planner.propose( 34 | entry.getKey().getDate(), 35 | entry.getValue().getCurrent())) 36 | .forEach(delivery -> 37 | forecastDao.save(new DeliveryForecastEntity( 38 | delivery.getRefNo(), 39 | delivery.getTime(), 40 | delivery.getLevel()) 41 | ) 42 | ); 43 | } 44 | 45 | public void applyDeliveryPlannerDefinitionChange(String refNo) { 46 | List demands = demandDao.findByRefNoAndDateGreaterThanEqual(refNo, LocalDate.now(clock)); 47 | DeliveryAutoPlanner planner = planners.get(refNo); 48 | forecastDao.deleteByRefNo(refNo); 49 | demands.stream() 50 | .flatMap(entry -> planner.propose( 51 | entry.getDate(), 52 | Demand.of(entry.getLevel(), entry.getSchema()))) 53 | .forEach(delivery -> 54 | forecastDao.save(new DeliveryForecastEntity( 55 | delivery.getRefNo(), 56 | delivery.getTime(), 57 | delivery.getLevel()) 58 | ) 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/demand/forecasting/DemandForecastingConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | @Configuration 8 | class DemandForecastingConfiguration { 9 | 10 | @Autowired 11 | private ProductDemandRepository repository; 12 | 13 | @Bean 14 | DemandService demandService() { 15 | return new DemandService(repository); 16 | } 17 | 18 | @Bean 19 | ReviewPolicy reviewPolicy() { 20 | return ReviewPolicy.BASIC; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/demand/forecasting/DemandValue.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting; 2 | 3 | import lombok.Value; 4 | 5 | @Value 6 | public class DemandValue { 7 | Demand documented; 8 | Adjustment adjustment; 9 | } 10 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/demand/forecasting/command/CommandsHandler.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting.command; 2 | 3 | import io.dddbyexamples.factory.demand.forecasting.persistence.DocumentEntity; 4 | import lombok.AllArgsConstructor; 5 | import org.springframework.boot.context.event.ApplicationReadyEvent; 6 | import org.springframework.context.event.EventListener; 7 | import org.springframework.data.rest.core.annotation.HandleAfterCreate; 8 | import org.springframework.data.rest.core.annotation.HandleBeforeCreate; 9 | import org.springframework.data.rest.core.annotation.HandleBeforeSave; 10 | import org.springframework.data.rest.core.annotation.RepositoryEventHandler; 11 | import org.springframework.scheduling.annotation.Scheduled; 12 | import org.springframework.stereotype.Component; 13 | import org.springframework.transaction.annotation.Transactional; 14 | import io.dddbyexamples.factory.demand.forecasting.DemandService; 15 | 16 | import java.time.Clock; 17 | import java.time.LocalDate; 18 | 19 | @Component 20 | @Transactional 21 | @AllArgsConstructor 22 | @RepositoryEventHandler 23 | public class CommandsHandler { 24 | 25 | private final DemandService service; 26 | private final DemandAdjustmentDao adjustments; 27 | private final RequiredReviewDao reviews; 28 | private final Clock clock; 29 | 30 | @HandleBeforeCreate 31 | @HandleBeforeSave 32 | public void adjust(DemandAdjustmentEntity adjustment) { 33 | LocalDate latest = adjustment.getAdjustment() 34 | .latestAdjustment() 35 | .orElse(LocalDate.now(clock)); 36 | adjustment.setCleanAfter(latest.plusDays(7)); 37 | service.adjust(adjustment.getAdjustment()); 38 | } 39 | 40 | @HandleAfterCreate 41 | public void process(DocumentEntity document) { 42 | service.process(document.getDocument()); 43 | } 44 | 45 | @HandleBeforeSave 46 | public void review(RequiredReviewEntity review) { 47 | if (review.decisionTaken()) { 48 | review.setCleanAfter(LocalDate.now(clock).plusDays(7)); 49 | service.review(review.getReviewDecision()); 50 | } 51 | } 52 | 53 | @Scheduled(cron = "0 0 12 * * ?") 54 | @EventListener(ApplicationReadyEvent.class) 55 | public void clean() { 56 | adjustments.deleteByCleanAfterGreaterThanEqual(LocalDate.now(clock)); 57 | reviews.deleteByCleanAfterGreaterThanEqual(LocalDate.now(clock)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/demand/forecasting/command/DemandAdjustmentDao.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting.command; 2 | 3 | import io.dddbyexamples.tools.CommandRepository; 4 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 5 | import org.springframework.data.rest.core.annotation.RestResource; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.time.LocalDate; 9 | 10 | @Repository 11 | @RepositoryRestResource(path = "demand-adjustments", 12 | collectionResourceRel = "demand-adjustments", 13 | itemResourceRel = "demand-adjustment") 14 | public interface DemandAdjustmentDao extends CommandRepository { 15 | @RestResource(exported = false) 16 | void deleteByCleanAfterGreaterThanEqual(LocalDate date); 17 | } 18 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/demand/forecasting/command/DemandAdjustmentEntity.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting.command; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | import io.dddbyexamples.factory.demand.forecasting.AdjustDemand; 7 | import io.dddbyexamples.tools.JsonConverter; 8 | 9 | import javax.persistence.*; 10 | import java.io.Serializable; 11 | import java.time.LocalDate; 12 | 13 | @Entity(name = "DemandAdjustment") 14 | @Table(schema = "demand_forecasting") 15 | @Getter 16 | @NoArgsConstructor 17 | public class DemandAdjustmentEntity implements Serializable { 18 | 19 | @Id 20 | @GeneratedValue 21 | private Long id; 22 | private String note; 23 | private String customerRepresentative; 24 | @Convert(converter = AdjustDemandAsJson.class) 25 | private AdjustDemand adjustment; 26 | 27 | @Setter 28 | private LocalDate cleanAfter; 29 | 30 | public DemandAdjustmentEntity(String note, String customerRepresentative, AdjustDemand adjustment) { 31 | this.note = note; 32 | this.customerRepresentative = customerRepresentative; 33 | this.adjustment = adjustment; 34 | } 35 | 36 | public static class AdjustDemandAsJson extends JsonConverter { 37 | public AdjustDemandAsJson() { 38 | super(AdjustDemand.class); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/demand/forecasting/command/RequiredReviewDao.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting.command; 2 | 3 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 4 | import org.springframework.data.rest.core.annotation.RestResource; 5 | import org.springframework.stereotype.Repository; 6 | import io.dddbyexamples.tools.CommandRepository; 7 | 8 | import java.time.LocalDate; 9 | import java.util.List; 10 | 11 | @Repository 12 | @RepositoryRestResource( 13 | path = "required-reviews", 14 | collectionResourceRel = "required-reviews", 15 | itemResourceRel = "required-review") 16 | public interface RequiredReviewDao extends CommandRepository { 17 | @RestResource(path = "refNos", rel = "refNos") 18 | List findByRefNoAndDecisionIsNull(String refNo); 19 | 20 | @RestResource(exported = false) 21 | void deleteByCleanAfterGreaterThanEqual(LocalDate date); 22 | } 23 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/demand/forecasting/command/RequiredReviewEntity.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting.command; 2 | 3 | import io.dddbyexamples.factory.demand.forecasting.ApplyReviewDecision; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | import io.dddbyexamples.factory.demand.forecasting.ReviewDecision; 8 | import io.dddbyexamples.factory.demand.forecasting.ReviewRequired.ToReview; 9 | import io.dddbyexamples.tools.JsonConverter; 10 | 11 | import javax.persistence.*; 12 | import java.io.Serializable; 13 | import java.time.Instant; 14 | import java.time.LocalDate; 15 | 16 | @Entity(name = "RequiredReview") 17 | @Table(schema = "demand_forecasting") 18 | @Getter 19 | @NoArgsConstructor 20 | public class RequiredReviewEntity implements Serializable { 21 | 22 | @Id 23 | @GeneratedValue 24 | private Long id; 25 | private String refNo; 26 | private LocalDate date; 27 | private Instant timestamp; 28 | @Convert(converter = ReviewAsJson.class) 29 | private ToReview review; 30 | 31 | @Enumerated(EnumType.STRING) 32 | private ReviewDecision decision; 33 | @Setter 34 | private LocalDate cleanAfter; 35 | 36 | public RequiredReviewEntity(Instant timestamp, ToReview review) { 37 | this.timestamp = timestamp; 38 | this.refNo = review.getId().getRefNo(); 39 | this.date = review.getId().getDate(); 40 | this.review = review; 41 | } 42 | 43 | public boolean decisionTaken() { 44 | return decision != null; 45 | } 46 | 47 | public ApplyReviewDecision getReviewDecision() { 48 | return new ApplyReviewDecision(review, decision); 49 | } 50 | 51 | public static class ReviewAsJson extends JsonConverter { 52 | public ReviewAsJson() { 53 | super(ToReview.class); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/demand/forecasting/persistence/DemandDao.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting.persistence; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.rest.core.annotation.RestResource; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.time.LocalDate; 8 | import java.util.List; 9 | 10 | @Repository 11 | @RestResource(exported = false) 12 | public interface DemandDao extends JpaRepository { 13 | List findByRefNoAndDateGreaterThanEqual(String refNo, LocalDate now); 14 | } 15 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/demand/forecasting/persistence/DemandEntity.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting.persistence; 2 | 3 | import io.dddbyexamples.factory.demand.forecasting.DailyId; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | import io.dddbyexamples.factory.demand.forecasting.DemandValue; 8 | import io.dddbyexamples.tools.JsonConverter; 9 | import io.dddbyexamples.tools.TechnicalId; 10 | 11 | import javax.persistence.*; 12 | import java.io.Serializable; 13 | import java.time.LocalDate; 14 | 15 | @Entity(name = "Demand") 16 | @Table(schema = "demand_forecasting") 17 | @Getter 18 | @NoArgsConstructor 19 | public class DemandEntity implements Serializable { 20 | 21 | @Id 22 | @GeneratedValue 23 | private Long id; 24 | private String refNo; 25 | private LocalDate date; 26 | @Setter 27 | @Convert(converter = DemandAsJson.class) 28 | private DemandValue value; 29 | 30 | public DemandEntity(String refNo, LocalDate date) { 31 | this.refNo = refNo; 32 | this.date = date; 33 | this.value = new DemandValue(null, null); 34 | } 35 | 36 | public DemandEntityId createId() { 37 | return new DemandEntityId(refNo, date, id); 38 | } 39 | 40 | public static class DemandAsJson extends JsonConverter { 41 | public DemandAsJson() { 42 | super(DemandValue.class); 43 | } 44 | } 45 | 46 | @Getter 47 | public static class DemandEntityId extends DailyId implements TechnicalId { 48 | 49 | private Long id; 50 | 51 | public DemandEntityId(String refNo, LocalDate date) { 52 | super(refNo, date); 53 | } 54 | 55 | DemandEntityId(String refNo, LocalDate date, Long id) { 56 | super(refNo, date); 57 | this.id = id; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/demand/forecasting/persistence/DocumentDao.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting.persistence; 2 | 3 | import io.dddbyexamples.tools.CommandRepository; 4 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository("documentDao") 8 | @RepositoryRestResource(path = "demand-documents", 9 | collectionResourceRel = "demand-documents", 10 | itemResourceRel = "demand-document") 11 | public interface DocumentDao extends CommandRepository { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/demand/forecasting/persistence/DocumentEntity.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting.persistence; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | import org.springframework.data.annotation.LastModifiedDate; 7 | import io.dddbyexamples.factory.demand.forecasting.Document; 8 | import io.dddbyexamples.tools.JsonConverter; 9 | 10 | import javax.persistence.*; 11 | import java.io.Serializable; 12 | import java.time.Instant; 13 | import java.time.LocalDate; 14 | 15 | @Entity(name = "Document") 16 | @Table(schema = "demand_forecasting") 17 | @Getter 18 | @NoArgsConstructor 19 | public class DocumentEntity implements Serializable { 20 | 21 | @Id 22 | @GeneratedValue 23 | private Long id; 24 | private String refNo; 25 | @LastModifiedDate 26 | private Instant saved; 27 | private String originalUri; 28 | private String storedUri; 29 | @Setter 30 | @Convert(converter = DocumentAsJson.class) 31 | private Document document; 32 | 33 | @Setter 34 | private LocalDate cleanAfter; 35 | 36 | public DocumentEntity(String originalUri, String storedUri, Document document) { 37 | this.saved = Instant.now(); 38 | this.originalUri = originalUri; 39 | this.storedUri = storedUri; 40 | this.document = document; 41 | this.refNo = document.getRefNo(); 42 | } 43 | 44 | public static class DocumentAsJson extends JsonConverter { 45 | public DocumentAsJson() { 46 | super(Document.class); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/demand/forecasting/persistence/ProductDemandDao.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting.persistence; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.rest.core.annotation.RestResource; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.util.Optional; 8 | 9 | @Repository 10 | @RestResource(exported = false) 11 | public interface ProductDemandDao extends JpaRepository { 12 | Optional findByRefNo(String refNo); 13 | } 14 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/demand/forecasting/persistence/ProductDemandEntity.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting.persistence; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import io.dddbyexamples.factory.product.management.RefNoId; 6 | import io.dddbyexamples.tools.TechnicalId; 7 | 8 | import javax.persistence.*; 9 | import java.io.Serializable; 10 | 11 | @Entity(name = "ProductDemand") 12 | @Table(schema = "demand_forecasting") 13 | @NoArgsConstructor 14 | public class ProductDemandEntity implements Serializable { 15 | 16 | @Id 17 | @GeneratedValue 18 | private Long id; 19 | @Version 20 | private Long version; 21 | private String refNo; 22 | 23 | public ProductDemandEntity(String refNo) { 24 | this.refNo = refNo; 25 | } 26 | 27 | public ProductDemandEntityId createId() { 28 | return new ProductDemandEntityId(refNo, id); 29 | } 30 | 31 | @Getter 32 | static class ProductDemandEntityId extends RefNoId implements TechnicalId { 33 | 34 | private Long id; 35 | 36 | ProductDemandEntityId(String refNo, long id) { 37 | super(refNo); 38 | this.id = id; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/demand/forecasting/projection/CurrentDemandDao.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting.projection; 2 | 3 | import org.springframework.data.repository.query.Param; 4 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 5 | import org.springframework.data.rest.core.annotation.RestResource; 6 | import org.springframework.format.annotation.DateTimeFormat; 7 | import org.springframework.stereotype.Repository; 8 | import io.dddbyexamples.tools.ProjectionRepository; 9 | 10 | import java.time.LocalDate; 11 | import java.util.List; 12 | import java.util.Optional; 13 | 14 | @Repository 15 | @RepositoryRestResource(path = "demand-forecasts", 16 | collectionResourceRel = "demand-forecasts", 17 | itemResourceRel = "demand-forecast") 18 | public interface CurrentDemandDao extends ProjectionRepository { 19 | @Deprecated 20 | @RestResource(path = "refNos", rel = "refNos") 21 | List findByRefNoAndDateGreaterThanEqual(@Param("refNo") String refNo, @Param("date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date); 22 | 23 | @RestResource(path = "byDate") 24 | List findByDate(LocalDate date); 25 | 26 | @RestResource(exported = false) 27 | Optional findByRefNoAndDate(String refNo, LocalDate date); 28 | } 29 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/demand/forecasting/projection/CurrentDemandEntity.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting.projection; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import io.dddbyexamples.factory.demand.forecasting.Demand; 6 | 7 | import javax.persistence.*; 8 | import java.io.Serializable; 9 | import java.time.LocalDate; 10 | 11 | @Entity(name = "CurrentDemand") 12 | @Table(schema = "demand_forecasting") 13 | @Getter 14 | @NoArgsConstructor 15 | public class CurrentDemandEntity implements Serializable { 16 | 17 | @Id 18 | @GeneratedValue 19 | private Long id; 20 | private String refNo; 21 | private LocalDate date; 22 | private long level; 23 | @Enumerated(EnumType.STRING) 24 | private Demand.Schema schema; 25 | 26 | CurrentDemandEntity(String refNo, LocalDate date, long level, Demand.Schema schema) { 27 | this.refNo = refNo; 28 | this.date = date; 29 | this.level = level; 30 | this.schema = schema; 31 | } 32 | 33 | void changeLevelTo(long level, Demand.Schema schema) { 34 | this.level = level; 35 | this.schema = schema; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/java/io/dddbyexamples/factory/demand/forecasting/projection/CurrentDemandProjection.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting.projection; 2 | 3 | import io.dddbyexamples.factory.demand.forecasting.DailyId; 4 | import io.dddbyexamples.factory.demand.forecasting.DemandedLevelsChanged; 5 | import lombok.AllArgsConstructor; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | @AllArgsConstructor 10 | public class CurrentDemandProjection { 11 | 12 | private final CurrentDemandDao demandDao; 13 | 14 | public void applyDemandedLevelsChanged(DemandedLevelsChanged event) { 15 | event.getResults().forEach(this::createOrUpdateDemand); 16 | } 17 | 18 | private void createOrUpdateDemand(DailyId daily, DemandedLevelsChanged.Change change) { 19 | CurrentDemandEntity currentDemandEntity = demandDao.findByRefNoAndDate( 20 | daily.getRefNo(), 21 | daily.getDate()) 22 | .orElseGet(() -> new CurrentDemandEntity( 23 | daily.getRefNo(), 24 | daily.getDate(), 25 | change.getCurrent().getLevel(), 26 | change.getCurrent().getSchema())); 27 | currentDemandEntity.changeLevelTo(change.getCurrent().getLevel(), change.getCurrent().getSchema()); 28 | demandDao.save(currentDemandEntity); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/resources/schema/db.changelog.yml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - include: 3 | file: /schema/commons.yml 4 | - include: 5 | file: /schema/delivery-planning.yml 6 | - include: 7 | file: /schema/demand-forecasting.yml 8 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/main/resources/schema/delivery-planning.yml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - property: 3 | name: json 4 | value: clob 5 | dbms: h2 6 | - property: 7 | name: json 8 | value: jsonb 9 | dbms: postgresql 10 | 11 | - changeSet: 12 | id: 0.delivery-planning.schema 13 | author: Michal Michaluk 14 | 15 | changes: 16 | - sql: CREATE SCHEMA delivery_planning 17 | rolback: 18 | - sql: DROP SCHEMA delivery_planning 19 | 20 | - changeSet: 21 | id: 1.delivery-planning.init 22 | author: Michal Michaluk 23 | 24 | changes: 25 | - createTable: 26 | schemaName: delivery_planning 27 | tableName: delivery_forecast 28 | columns: 29 | - column: 30 | name: id 31 | type: serial 32 | autoIncrement: true 33 | constraints: 34 | primaryKey: true 35 | primaryKeyName: delivery_forecast_pkey 36 | - column: 37 | name: ref_no 38 | type: varchar(64) 39 | constraints: 40 | nullable: false 41 | - column: 42 | name: time 43 | type: timestamp 44 | constraints: 45 | nullable: false 46 | - column: 47 | name: date 48 | type: timestamp 49 | constraints: 50 | nullable: false 51 | - column: 52 | name: level 53 | type: bigint 54 | constraints: 55 | nullable: false 56 | 57 | - createTable: 58 | schemaName: delivery_planning 59 | tableName: delivery_planner_definition 60 | columns: 61 | - column: 62 | name: ref_no 63 | type: varchar(64) 64 | constraints: 65 | primaryKey: true 66 | primaryKeyName: delivery_planner_definition_pkey 67 | - column: 68 | name: definition 69 | type: ${json} 70 | constraints: 71 | nullable: false 72 | 73 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/test/groovy/io/dddbyexamples/factory/ForecastingAdaptersConfiguration.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory; 2 | 3 | import io.dddbyexamples.factory.demand.forecasting.DemandEvents; 4 | import io.dddbyexamples.factory.demand.forecasting.DemandedLevelsChanged; 5 | import io.dddbyexamples.factory.demand.forecasting.ReviewRequired; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.boot.autoconfigure.domain.EntityScan; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters; 10 | import org.springframework.scheduling.annotation.EnableScheduling; 11 | 12 | import java.time.Clock; 13 | 14 | @SpringBootApplication 15 | @EnableScheduling 16 | @EntityScan( 17 | basePackageClasses = [ForecastingAdaptersConfiguration.class, Jsr310JpaConverters.class] 18 | ) 19 | public class ForecastingAdaptersConfiguration { 20 | @Bean 21 | public Clock clock() { 22 | return Clock.systemDefaultZone(); 23 | } 24 | 25 | @Bean 26 | public DemandEventsFake DemandEvents() { 27 | return new DemandEventsFake(); 28 | } 29 | 30 | private class DemandEventsFake implements DemandEvents { 31 | @Override 32 | public void emit(DemandedLevelsChanged event) { 33 | 34 | } 35 | 36 | @Override 37 | public void emit(ReviewRequired event) { 38 | 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /demand-forecasting-adapters/src/test/groovy/io/dddbyexamples/factory/delivery/planning/DeliveryPlannerDefinitionSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.delivery.planning 2 | 3 | import org.springframework.beans.factory.annotation.Autowired 4 | import org.springframework.boot.test.context.SpringBootTest 5 | import io.dddbyexamples.factory.delivery.planning.definition.DeliveryPlannerDefinition 6 | import io.dddbyexamples.factory.delivery.planning.definition.DeliveryPlannerDefinitionDao 7 | import io.dddbyexamples.factory.delivery.planning.definition.DeliveryPlannerDefinitionEntity 8 | import org.springframework.test.context.ActiveProfiles 9 | import org.springframework.test.context.TestPropertySource 10 | import spock.lang.Specification 11 | 12 | import static java.time.LocalTime.of as time 13 | import static io.dddbyexamples.factory.delivery.planning.definition.DeliveryPlannerDefinition.of 14 | import static io.dddbyexamples.factory.demand.forecasting.Demand.Schema.* 15 | 16 | @ActiveProfiles("test") 17 | @SpringBootTest 18 | class DeliveryPlannerDefinitionSpec extends Specification { 19 | 20 | @Autowired 21 | DeliveryPlannerDefinitionDao dao 22 | 23 | void setup() { 24 | dao.deleteAllInBatch() 25 | } 26 | 27 | def "verify access to DeliveryPlannerDefinition data"() { 28 | given: 29 | def definition = DeliveryPlannerDefinition.builder() 30 | .definition(AtDayStart, of(time(06, 00), 1.0d)) 31 | .definition(TillDayEnd, of(time(22, 00), 1.0d)) 32 | .definition(Twice, [(time(16, 00)): 0.5d, (time(20, 00)): 0.5d]) 33 | .build() 34 | 35 | dao.save(new DeliveryPlannerDefinitionEntity("3009000", definition)) 36 | 37 | when: 38 | def entities = dao.findAll() 39 | 40 | then: 41 | entities.size() == 1 42 | entities.get(0).definition == definition 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /demand-forecasting-model/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'groovy' 2 | apply plugin: "jacoco" 3 | 4 | dependencies { 5 | compile(project(":shared-kernel-model")) 6 | } 7 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/main/java/io/dddbyexamples/factory/delivery/planning/DeliveriesSuggestion.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.delivery.planning; 2 | 3 | import io.dddbyexamples.factory.demand.forecasting.Demand; 4 | 5 | import java.time.LocalDate; 6 | import java.time.LocalTime; 7 | import java.util.Map; 8 | import java.util.stream.Stream; 9 | 10 | interface DeliveriesSuggestion { 11 | 12 | DeliveriesSuggestion DUMMY = (refNo, date, demand) -> 13 | Stream.of(new Delivery(refNo, date.atStartOfDay(), demand.getLevel())); 14 | 15 | static DeliveriesSuggestion timesAndFractions(Map timesAndFractions) { 16 | return (refNo, date, demand) -> 17 | timesAndFractions.entrySet().stream() 18 | .map(e -> new Delivery( 19 | refNo, 20 | date.atTime(e.getKey()), ((long) ((double) demand.getLevel() * e.getValue()))) 21 | ); 22 | } 23 | 24 | Stream deliveriesFor(String refNo, LocalDate date, Demand demand); 25 | } 26 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/main/java/io/dddbyexamples/factory/delivery/planning/Delivery.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.delivery.planning; 2 | 3 | import lombok.Value; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | @Value 8 | public class Delivery { 9 | String refNo; 10 | LocalDateTime time; 11 | long level; 12 | } 13 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/main/java/io/dddbyexamples/factory/delivery/planning/DeliveryAutoPlanner.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.delivery.planning; 2 | 3 | import lombok.AllArgsConstructor; 4 | import io.dddbyexamples.factory.demand.forecasting.Demand; 5 | 6 | import java.time.LocalDate; 7 | import java.util.Map; 8 | import java.util.stream.Stream; 9 | 10 | @AllArgsConstructor 11 | public class DeliveryAutoPlanner { 12 | private String refNo; 13 | private Map policies; 14 | 15 | public Stream propose(LocalDate date, Demand demand) { 16 | return policies.getOrDefault(demand.getSchema(), DeliveriesSuggestion.DUMMY) 17 | .deliveriesFor(refNo, date, demand); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/main/java/io/dddbyexamples/factory/demand/forecasting/AdjustDemand.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting; 2 | 3 | import lombok.Value; 4 | import io.dddbyexamples.factory.demand.forecasting.DailyDemand.Result; 5 | 6 | import java.time.LocalDate; 7 | import java.util.Comparator; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Optional; 11 | import java.util.function.BiFunction; 12 | import java.util.stream.Collectors; 13 | 14 | @Value 15 | public class AdjustDemand { 16 | private final String refNo; 17 | private final Map adjustments; 18 | 19 | List forEachStartingFrom(LocalDate date, BiFunction f) { 20 | return adjustments.entrySet().stream() 21 | .filter(e -> !e.getKey().isBefore(date)) 22 | .map(e -> f.apply(e.getKey(), e.getValue())) 23 | .collect(Collectors.toList()); 24 | } 25 | 26 | public Optional latestAdjustment() { 27 | return adjustments.keySet().stream() 28 | .max(Comparator.naturalOrder()); 29 | } 30 | 31 | public String getRefNo() { 32 | return refNo; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/main/java/io/dddbyexamples/factory/demand/forecasting/Adjustment.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting; 2 | 3 | import lombok.Value; 4 | 5 | @Value 6 | public class Adjustment { 7 | 8 | Demand demand; 9 | boolean strong; 10 | 11 | static Adjustment strong(Demand demand) { 12 | return new Adjustment(demand, true); 13 | } 14 | 15 | static Adjustment weak(Demand demand) { 16 | return new Adjustment(demand, false); 17 | } 18 | 19 | static boolean isStrong(Adjustment adjustment) { 20 | return adjustment != null && adjustment.strong; 21 | } 22 | 23 | static boolean isNotStrong(Adjustment adjustment) { 24 | return !isStrong(adjustment); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/main/java/io/dddbyexamples/factory/demand/forecasting/ApplyReviewDecision.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting; 2 | 3 | import lombok.Value; 4 | 5 | import java.util.Collections; 6 | 7 | @Value 8 | public class ApplyReviewDecision { 9 | ReviewRequired.ToReview review; 10 | ReviewDecision decision; 11 | 12 | boolean requireAdjustment() { 13 | return decision != ReviewDecision.IGNORE; 14 | } 15 | 16 | AdjustDemand toAdjustment() { 17 | return new AdjustDemand(review.getRefNo(), 18 | Collections.singletonMap( 19 | review.getDate(), 20 | Adjustment.weak(decision.toAdjustment(review)) 21 | ) 22 | ); 23 | } 24 | 25 | public String getRefNo() { 26 | return review.getRefNo(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/main/java/io/dddbyexamples/factory/demand/forecasting/DemandEvents.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting; 2 | 3 | public interface DemandEvents { 4 | void emit(DemandedLevelsChanged event); 5 | 6 | void emit(ReviewRequired event); 7 | } 8 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/main/java/io/dddbyexamples/factory/demand/forecasting/DemandService.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting; 2 | 3 | import lombok.AllArgsConstructor; 4 | 5 | @AllArgsConstructor 6 | public class DemandService { 7 | 8 | private final ProductDemandRepository repository; 9 | 10 | public void initNewProduct(String refNo) { 11 | repository.initNewProduct(refNo); 12 | } 13 | 14 | public void process(Document document) { 15 | ProductDemand model = repository.get(document.getRefNo()); 16 | model.process(document); 17 | repository.save(model); 18 | } 19 | 20 | public void adjust(AdjustDemand adjustDemand) { 21 | ProductDemand model = repository.get(adjustDemand.getRefNo()); 22 | model.adjust(adjustDemand); 23 | repository.save(model); 24 | } 25 | 26 | public void review(ApplyReviewDecision reviewDecision) { 27 | ProductDemand model = repository.get(reviewDecision.getRefNo()); 28 | model.review(reviewDecision); 29 | repository.save(model); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/main/java/io/dddbyexamples/factory/demand/forecasting/Demands.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting; 2 | 3 | import java.time.LocalDate; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.function.Function; 7 | 8 | class Demands implements ProductDemand.Demands { 9 | final Map fetched = new HashMap<>(); 10 | Function fetch; 11 | 12 | @Override 13 | public DailyDemand get(LocalDate date) { 14 | return fetched.computeIfAbsent(date, fetch); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/main/java/io/dddbyexamples/factory/demand/forecasting/Document.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting; 2 | 3 | import lombok.Value; 4 | 5 | import java.time.Instant; 6 | import java.time.LocalDate; 7 | import java.util.List; 8 | import java.util.SortedMap; 9 | import java.util.function.BiFunction; 10 | import java.util.stream.Collectors; 11 | 12 | @Value 13 | public class Document { 14 | 15 | private final Instant created; 16 | private final String refNo; 17 | private final SortedMap demands; 18 | 19 | List forEachStartingFrom(LocalDate date, BiFunction f) { 20 | return demands.entrySet().stream() 21 | .filter(e -> !e.getKey().isBefore(date)) 22 | .map(e -> f.apply(e.getKey(), e.getValue())) 23 | .collect(Collectors.toList()); 24 | } 25 | 26 | public String getRefNo() { 27 | return refNo; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/main/java/io/dddbyexamples/factory/demand/forecasting/ProductDemand.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting; 2 | 3 | import lombok.AllArgsConstructor; 4 | import io.dddbyexamples.factory.product.management.RefNoId; 5 | 6 | import java.time.Clock; 7 | import java.time.LocalDate; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | @AllArgsConstructor 12 | class ProductDemand { 13 | 14 | final RefNoId id; 15 | final List updates; 16 | private final Demands demands; 17 | 18 | private final Clock clock; 19 | private final DemandEvents events; 20 | 21 | interface Demands { 22 | DailyDemand get(LocalDate date); 23 | } 24 | 25 | void adjust(AdjustDemand adjustDemand) { 26 | LocalDate today = LocalDate.now(clock); 27 | 28 | List results = adjustDemand 29 | .forEachStartingFrom(today, this::adjustDaily); 30 | updates.addAll(DailyDemand.Result.updates(results)); 31 | 32 | Map changes = DailyDemand.Result.levelChanges(results); 33 | 34 | if (!changes.isEmpty()) { 35 | events.emit(new DemandedLevelsChanged(id, changes)); 36 | } 37 | } 38 | 39 | void process(Document document) { 40 | LocalDate today = LocalDate.now(clock); 41 | 42 | List results = document 43 | .forEachStartingFrom(today, this::updateDaily); 44 | updates.addAll(DailyDemand.Result.updates(results)); 45 | 46 | Map changes = DailyDemand.Result.levelChanges(results); 47 | 48 | if (!changes.isEmpty()) { 49 | events.emit(new DemandedLevelsChanged(id, changes)); 50 | } 51 | 52 | List reviews = DailyDemand.Result.reviews(results); 53 | 54 | if (!reviews.isEmpty()) { 55 | events.emit(new ReviewRequired(id, reviews)); 56 | } 57 | } 58 | 59 | void review(ApplyReviewDecision reviewDecision) { 60 | if (reviewDecision.requireAdjustment()) { 61 | adjust(reviewDecision.toAdjustment()); 62 | } 63 | } 64 | 65 | private DailyDemand.Result adjustDaily(LocalDate date, Adjustment adjustment) { 66 | DailyDemand demand = demands.get(date); 67 | return demand.adjust(adjustment); 68 | } 69 | 70 | private DailyDemand.Result updateDaily(LocalDate date, Demand demand) { 71 | DailyDemand daily = demands.get(date); 72 | return daily.update(demand); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/main/java/io/dddbyexamples/factory/demand/forecasting/ProductDemandRepository.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting; 2 | 3 | interface ProductDemandRepository { 4 | ProductDemand get(String refNo); 5 | 6 | void save(ProductDemand model); 7 | 8 | void initNewProduct(String refNo); 9 | } 10 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/main/java/io/dddbyexamples/factory/demand/forecasting/ReviewDecision.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting; 2 | 3 | import lombok.AllArgsConstructor; 4 | import io.dddbyexamples.factory.demand.forecasting.ReviewRequired.ToReview; 5 | 6 | import java.util.function.Function; 7 | 8 | @AllArgsConstructor 9 | public enum ReviewDecision { 10 | IGNORE(r -> null), 11 | PICK_PREVIOUS(ToReview::getPreviousDocumented), 12 | MAKE_ADJUSTMENT_WEAK(ToReview::getAdjustment), 13 | PICK_NEW(ToReview::getNewDocumented); 14 | 15 | private final Function pick; 16 | 17 | public Demand toAdjustment(ToReview review) { 18 | if (this == ReviewDecision.IGNORE) { 19 | throw new IllegalStateException("can't convert " + this + " to adjustment"); 20 | } 21 | return pick.apply(review); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/main/java/io/dddbyexamples/factory/demand/forecasting/ReviewPolicy.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting; 2 | 3 | public interface ReviewPolicy { 4 | 5 | ReviewPolicy BASIC = (previousDocumented, adjustment, newDocumented) -> 6 | Adjustment.isStrong(adjustment) 7 | && !newDocumented.equals(previousDocumented) 8 | && !newDocumented.equals(adjustment.getDemand()); 9 | 10 | boolean reviewNeeded( 11 | Demand previousDocumented, 12 | Adjustment adjustment, 13 | Demand newDocumented 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/test/groovy/io/dddbyexamples/factory/delivery/planning/DeliveriesSuggestionSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.delivery.planning 2 | 3 | import io.dddbyexamples.factory.demand.forecasting.Demand 4 | import spock.lang.PendingFeature 5 | import spock.lang.Specification 6 | 7 | import java.time.LocalDate 8 | import java.time.LocalDateTime 9 | import java.time.LocalTime 10 | import java.util.stream.Collectors 11 | 12 | import static io.dddbyexamples.factory.demand.forecasting.Demand.Schema.AtDayStart 13 | 14 | class DeliveriesSuggestionSpec extends Specification { 15 | 16 | def final refNo = "3009000" 17 | def final date = LocalDate.now() 18 | def final midnight = LocalTime.of(0, 0) 19 | def final sevenAM = LocalTime.of(7, 0) 20 | def final nineAM = LocalTime.of(9, 0) 21 | 22 | def "Times and fractions - all at once"() { 23 | given: 24 | def suggestion = DeliveriesSuggestion.timesAndFractions([(sevenAM): 1.0d]) 25 | 26 | when: 27 | def deliveries = suggestion.deliveriesFor(refNo, date, atDayStart(2000)) 28 | .collect(Collectors.toList()) 29 | 30 | then: 31 | deliveries == [deliveryAt(date.atTime(sevenAM), 2000)] 32 | } 33 | 34 | def "Times and fractions - half twice"() { 35 | given: 36 | def suggestion = DeliveriesSuggestion.timesAndFractions([ 37 | (sevenAM): 0.5d, 38 | (nineAM): 0.5d, 39 | ]) 40 | 41 | when: 42 | def deliveries = suggestion.deliveriesFor(refNo, date, atDayStart(2000)) 43 | .collect(Collectors.toList()) 44 | 45 | then: 46 | deliveries == [ 47 | deliveryAt(date.atTime(sevenAM), 1000), 48 | deliveryAt(date.atTime(nineAM), 1000), 49 | ] 50 | } 51 | 52 | def "Times and fractions - not represented number"() { 53 | given: 54 | def suggestion = DeliveriesSuggestion.timesAndFractions([ 55 | (midnight): 1/3d, 56 | (sevenAM): 1/3d, 57 | (nineAM): 1/3d, 58 | ]) 59 | 60 | when: 61 | def deliveries = suggestion.deliveriesFor(refNo, date, atDayStart(2000)) 62 | .collect(Collectors.toList()) 63 | 64 | then: 65 | deliveries == [ 66 | deliveryAt(date.atTime(midnight), 666), 67 | deliveryAt(date.atTime(sevenAM), 666), 68 | deliveryAt(date.atTime(nineAM), 666), 69 | ] 70 | } 71 | 72 | @PendingFeature 73 | def "Times and fractions - do not lost parts on not represented number"() { 74 | given: 75 | def suggestion = DeliveriesSuggestion.timesAndFractions([ 76 | (midnight): 1/3d, 77 | (sevenAM): 1/3d, 78 | (nineAM): 1/3d, 79 | ]) 80 | 81 | when: 82 | def deliveries = suggestion.deliveriesFor(refNo, date, atDayStart(2000)) 83 | .collect(Collectors.toList()) 84 | 85 | then: 86 | deliveries == [ 87 | deliveryAt(date.atTime(midnight), 666), 88 | deliveryAt(date.atTime(sevenAM), 666), 89 | deliveryAt(date.atTime(nineAM), 668), 90 | ] 91 | } 92 | 93 | def deliveryAt(LocalDateTime time, Integer level) { 94 | new Delivery(refNo, time, level) 95 | } 96 | 97 | def atDayStart(long level) { 98 | Demand.of(level, AtDayStart) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/test/groovy/io/dddbyexamples/factory/delivery/planning/DeliveryAutoPlannerSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.delivery.planning 2 | 3 | import io.dddbyexamples.factory.demand.forecasting.Demand 4 | import spock.lang.Specification 5 | 6 | import java.time.LocalDate 7 | import java.time.LocalDateTime 8 | import java.time.LocalTime 9 | import java.util.stream.Collectors 10 | 11 | import static DeliveriesSuggestion.timesAndFractions 12 | import static io.dddbyexamples.factory.demand.forecasting.Demand.Schema.AtDayStart 13 | 14 | class DeliveryAutoPlannerSpec extends Specification { 15 | 16 | def final refNo = "3009000" 17 | def final date = LocalDate.now() 18 | def final midnight = LocalTime.of(0, 0) 19 | def final sevenAM = LocalTime.of(7, 0) 20 | 21 | def "Picks dummy suggestion"() { 22 | given: 23 | def planner = deliveryDefinitions([:]) 24 | 25 | when: 26 | def deliveries = planner.propose(date, atDayStart(2000)) 27 | .collect(Collectors.toList()) 28 | 29 | then: 30 | deliveries == [deliveryAt(date.atTime(midnight), 2000)] 31 | } 32 | 33 | def "Suggests according to defined policy"() { 34 | given: 35 | def planner = deliveryDefinitions([ 36 | (AtDayStart): timesAndFractions([(sevenAM): 1.0d]) 37 | ]) 38 | 39 | when: 40 | def deliveries = planner.propose(date, atDayStart(2000)) 41 | .collect(Collectors.toList()) 42 | 43 | then: 44 | deliveries == [deliveryAt(date.atTime(sevenAM), 2000)] 45 | } 46 | 47 | def deliveryDefinitions(Map suggestions) { 48 | new DeliveryAutoPlanner(refNo, suggestions) 49 | } 50 | 51 | def deliveryAt(LocalDateTime time, Integer level) { 52 | new Delivery(refNo, time, level) 53 | } 54 | 55 | def atDayStart(long level) { 56 | Demand.of(level, AtDayStart) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/test/groovy/io/dddbyexamples/factory/demand/forecasting/DailyDemandBuilder.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting 2 | 3 | import java.time.Clock 4 | import java.time.Instant 5 | import java.time.LocalDate 6 | import java.time.ZoneId 7 | 8 | class DailyDemandBuilder { 9 | 10 | Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()) 11 | ReviewPolicy policy = ReviewPolicy.BASIC 12 | 13 | String refNo = "3009000" 14 | private LocalDate date = LocalDate.now(clock) 15 | private Demand base 16 | private Adjustment adjustment 17 | 18 | DailyDemand build() { 19 | new DailyDemand(new DailyId(refNo, date), policy, base, adjustment) 20 | } 21 | 22 | DailyDemandBuilder reset() { 23 | nothingDemanded() 24 | noAdjustments() 25 | } 26 | 27 | Object asType(Class clazz) { 28 | clazz == DailyDemand ? build() : super.asType(clazz) 29 | } 30 | 31 | DailyDemandBuilder nextDate() { 32 | date.plusDays(1) 33 | this 34 | } 35 | 36 | DailyDemandBuilder date(LocalDate date) { 37 | this.date = date 38 | this 39 | } 40 | 41 | DailyDemandBuilder nothingDemanded() { 42 | base = null 43 | this 44 | } 45 | 46 | DailyDemandBuilder noAdjustments() { 47 | adjustment = null 48 | this 49 | } 50 | 51 | DailyDemandBuilder demandedLevels(long level) { 52 | base = Demand.of(level) 53 | this 54 | } 55 | 56 | DailyDemandBuilder demandedLevels(Demand level) { 57 | base = level 58 | this 59 | } 60 | 61 | DailyDemandBuilder adjustedTo(long level) { 62 | adjustment = new Adjustment(Demand.of(level), false) 63 | this 64 | } 65 | 66 | DailyDemandBuilder stronglyAdjustedTo(long level) { 67 | adjustment = new Adjustment(Demand.of(level), true) 68 | this 69 | } 70 | 71 | Demand newCallOffDemand(long level) { 72 | Demand.of(level) 73 | } 74 | 75 | Adjustment adjustDemandTo(long level) { 76 | new Adjustment(Demand.of(level), false) 77 | } 78 | 79 | DemandedLevelsChanged.Change levelChanged(long previous, long current) { 80 | new DemandedLevelsChanged.Change(Demand.of(previous), Demand.of(current)) 81 | } 82 | 83 | ReviewRequired.ToReview reviewRequest(long previousDocumented, long adjustment, long newDocumented) { 84 | new ReviewRequired.ToReview( 85 | new DailyId(refNo, date), 86 | Demand.of(previousDocumented), 87 | Demand.of(adjustment), 88 | Demand.of(newDocumented) 89 | ) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/test/groovy/io/dddbyexamples/factory/demand/forecasting/DemandAdjustmentSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting 2 | 3 | import spock.lang.Specification 4 | 5 | import java.time.LocalDate 6 | 7 | class DemandAdjustmentSpec extends Specification implements ProductDemandTrait { 8 | 9 | def events = Mock(DemandEvents) 10 | 11 | void setup() { 12 | builder = new ProductDemandBuilder(events: events) 13 | } 14 | 15 | def "Adjusted demands should be stored"() { 16 | given: 17 | def today = LocalDate.now(builder.clock) 18 | def demand = demanded(2800, 0) 19 | def adjustments = adjustments([(today): 1000]) 20 | 21 | when: 22 | demand.adjust(adjustments) 23 | 24 | then: 25 | 1 * events.emit(levelChanged([2800, 1000])) 26 | } 27 | 28 | def "Adjustment of future demands is possible"() { 29 | given: 30 | def today = LocalDate.now(builder.clock) 31 | def demand = demanded(2800) 32 | def adjustments = adjustments([(today.plusDays(1)): 1000]) 33 | 34 | when: 35 | demand.adjust(adjustments) 36 | 37 | then: 38 | 1 * events.emit(levelChanged(notChanged(), [0, 1000])) 39 | } 40 | 41 | def "Adjustment without changes should not generate event"() { 42 | given: 43 | def today = LocalDate.now(builder.clock) 44 | def demand = demanded(2800, 1000) 45 | def adjustments = adjustments([(today): 2800, (today.plusDays(1)): 1000]) 46 | 47 | when: 48 | demand.adjust(adjustments) 49 | 50 | then: 51 | 0 * events.emit(_ as DemandedLevelsChanged) 52 | } 53 | 54 | def "Should skip past demands adjustments"() { 55 | given: 56 | def pastDate = LocalDate.now(builder.clock).minusDays(2) 57 | def demand = demanded(2800, 0) 58 | def adjustments = adjustments([(pastDate): 1000]) 59 | 60 | when: 61 | demand.adjust(adjustments) 62 | 63 | then: 64 | 0 * events.emit(_ as DemandedLevelsChanged) 65 | } 66 | 67 | def "Adjustment should be idempotent"() { 68 | given: 69 | def today = LocalDate.now(builder.clock) 70 | def demand = demanded(2800, 0) 71 | def adjustments = adjustments((today): 2000, (today.plusDays(1)): 3500) 72 | 73 | when: 74 | demand.adjust(adjustments) 75 | 76 | then: 77 | 1 * events.emit(levelChanged([2800, 2000], [0, 3500])) 78 | 79 | when: 80 | demand.adjust(adjustments) 81 | 82 | then: 83 | 0 * events.emit(_ as DemandedLevelsChanged) 84 | } 85 | } -------------------------------------------------------------------------------- /demand-forecasting-model/src/test/groovy/io/dddbyexamples/factory/demand/forecasting/DemandServiceSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting 2 | 3 | import spock.lang.Specification 4 | 5 | import java.time.LocalDate 6 | 7 | import static ReviewDecision.PICK_NEW 8 | 9 | class DemandServiceSpec extends Specification implements ProductDemandTrait { 10 | 11 | def events = Mock(DemandEvents) 12 | def repo = Mock(ProductDemandRepository) 13 | def service = new DemandService(repo) 14 | 15 | void setup() { 16 | builder = new ProductDemandBuilder(events: events) 17 | } 18 | 19 | def "Repository interactions while product demand initialization"() { 20 | given: 21 | def refNo = "3009000" 22 | 23 | when: 24 | service.initNewProduct(refNo) 25 | 26 | then: 27 | 1 * repo.initNewProduct(refNo) 28 | } 29 | 30 | def "Repository interactions while document processing"() { 31 | given: 32 | def today = LocalDate.now(builder.clock) 33 | def demand = demanded(2800, 0) 34 | def document = document(today, 2000, 3500) 35 | repo.get(document.refNo) >> demand 36 | 37 | when: 38 | service.process(document) 39 | 40 | then: 41 | 1 * repo.save(demand) 42 | } 43 | 44 | def "Repository interactions while demand adjustments"() { 45 | given: 46 | def today = LocalDate.now(builder.clock) 47 | def demand = demanded(2800, 0) 48 | def adjustments = adjustments([(today): 1000]) 49 | repo.get(adjustments.refNo) >> demand 50 | 51 | when: 52 | service.adjust(adjustments) 53 | 54 | then: 55 | 1 * repo.save(demand) 56 | } 57 | 58 | def "Repository interactions while review processing"() { 59 | given: 60 | def today = LocalDate.now(builder.clock) 61 | def demand = demand(2800) 62 | .stronglyAdjusted((today): 3500) 63 | .build() 64 | def review = reviewDecision(review(today, 0, 3500, 2800), PICK_NEW) 65 | repo.get(review.refNo) >> demand 66 | 67 | when: 68 | service.review(review) 69 | 70 | then: 71 | 1 * repo.save(demand) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/test/groovy/io/dddbyexamples/factory/demand/forecasting/DemandsFake.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting 2 | 3 | import java.time.Clock 4 | import java.time.LocalDate 5 | 6 | class DemandsFake extends Demands { 7 | 8 | DailyDemandBuilder builder 9 | 10 | DemandsFake(String refNo, Clock clock) { 11 | this.builder = new DailyDemandBuilder(refNo: refNo, clock: clock) 12 | fetch = { date -> nothingDemanded(date) } 13 | } 14 | 15 | DailyDemand demanded(LocalDate date, long level) { 16 | def demand = builder.date(date) 17 | .demandedLevels(level) 18 | .noAdjustments() 19 | .build() 20 | fetched.put(date, demand) 21 | demand 22 | } 23 | 24 | DailyDemand adjusted(LocalDate date, long level) { 25 | def demand = builder.date(date) 26 | .demandedLevels(fetched.get(date)?.level) 27 | .adjustedTo(level) 28 | .build() 29 | 30 | fetched.put(date, demand) 31 | demand 32 | } 33 | 34 | DailyDemand stronglyAdjusted(LocalDate date, long level) { 35 | def demand = builder.date(date) 36 | .demandedLevels(fetched.get(date)?.level) 37 | .stronglyAdjustedTo(level) 38 | .build() 39 | 40 | fetched.put(date, demand) 41 | demand 42 | } 43 | 44 | private DailyDemand nothingDemanded(LocalDate date) { 45 | def demand = builder.reset() 46 | .date(date) 47 | .build() 48 | demand 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/test/groovy/io/dddbyexamples/factory/demand/forecasting/DocumentProcessingSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting 2 | 3 | import spock.lang.Specification 4 | 5 | import java.time.LocalDate 6 | 7 | class DocumentProcessingSpec extends Specification implements ProductDemandTrait { 8 | 9 | def events = Mock(DemandEvents) 10 | 11 | void setup() { 12 | builder = new ProductDemandBuilder(events: events) 13 | } 14 | 15 | def "Updated demands should be stored"() { 16 | given: 17 | def today = LocalDate.now(builder.clock) 18 | def demand = demanded(2800, 0) 19 | def document = document(today, 2000, 3500) 20 | 21 | when: 22 | demand.process(document) 23 | 24 | then: 25 | 1 * events.emit(levelChanged([2800, 2000], [0, 3500])) 26 | } 27 | 28 | def "Demands for dates not present in system should be stored "() { 29 | given: 30 | def today = LocalDate.now(builder.clock) 31 | def demand = demanded(1000) 32 | def document = document(today, 1000, 3500, 1000) 33 | 34 | when: 35 | demand.process(document) 36 | 37 | then: 38 | 1 * events.emit(levelChanged(notChanged(), [0, 3500], [0, 1000])) 39 | } 40 | 41 | def "Document without changes should not generate event"() { 42 | given: 43 | def today = LocalDate.now(builder.clock) 44 | def demand = demanded(2800, 0) 45 | def document = document(today, 2800, 0) 46 | 47 | when: 48 | demand.process(document) 49 | 50 | then: 51 | 0 * events.emit(_ as DemandedLevelsChanged) 52 | } 53 | 54 | def "Should skip past demands from document"() { 55 | given: 56 | def pastDate = LocalDate.now(builder.clock).minusDays(2) 57 | def demand = demanded(0, 0) 58 | def document = document(pastDate, 2800, 2800, 3500, 1000) 59 | 60 | when: 61 | demand.process(document) 62 | 63 | then: 64 | 1 * events.emit(levelChanged([0, 3500], [0, 1000])) 65 | } 66 | 67 | def "Document processing should be idempotent"() { 68 | given: 69 | def today = LocalDate.now(builder.clock) 70 | def demand = demanded(2800, 0) 71 | def document = document(today, 2000, 3500) 72 | 73 | when: 74 | demand.process(document) 75 | 76 | then: 77 | 1 * events.emit(levelChanged([2800, 2000], [0, 3500])) 78 | 79 | when: 80 | demand.process(document) 81 | 82 | then: 83 | 0 * events.emit(_ as DemandedLevelsChanged) 84 | } 85 | } -------------------------------------------------------------------------------- /demand-forecasting-model/src/test/groovy/io/dddbyexamples/factory/demand/forecasting/KeepingDailyDemandsSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting 2 | 3 | import spock.lang.Specification 4 | 5 | class KeepingDailyDemandsSpec extends Specification { 6 | 7 | def builder = new DailyDemandBuilder() 8 | 9 | def "Adjusted demands should be stored"() { 10 | given: 11 | def demand = demand() 12 | .demandedLevels(2800) 13 | .noAdjustments().build() 14 | 15 | when: 16 | def res = demand.adjust(adjustDemandTo(3500)) 17 | 18 | then: 19 | demand.getLevel() == Demand.of(3500) 20 | res.levelChange == levelChanged(2800, 3500) 21 | res.updated != null 22 | } 23 | 24 | def "Adjusted demands should be stored when there is no demand for product"() { 25 | given: 26 | def demand = demand() 27 | .nothingDemanded() 28 | .noAdjustments().build() 29 | 30 | when: 31 | def res = demand.adjust(adjustDemandTo(3500)) 32 | 33 | then: 34 | demand.getLevel() == Demand.of(3500) 35 | res.levelChange == levelChanged(0, 3500) 36 | res.updated != null 37 | } 38 | 39 | def "In standard case documented demands overrides adjustments"() { 40 | given: 41 | def demand = demand() 42 | .demandedLevels(2800) 43 | .adjustedTo(3500).build() 44 | 45 | when: 46 | def res = demand.update(newCallOffDemand(4000)) 47 | 48 | then: 49 | demand.getLevel() == Demand.of(4000) 50 | res.levelChange == levelChanged(3500, 4000) 51 | res.updated != null 52 | } 53 | 54 | def "Strong adjustment is kept even after processing of document"() { 55 | given: 56 | def demand = demand() 57 | .demandedLevels(2800) 58 | .stronglyAdjustedTo(3500).build() 59 | 60 | when: 61 | def res = demand.update(newCallOffDemand(2800)) 62 | 63 | then: 64 | demand.getLevel() == Demand.of(3500) 65 | res.levelChange == null 66 | res.updated == null 67 | } 68 | 69 | def "Document update ignored by strong adjustment should rise warning"() { 70 | given: 71 | def demand = demand() 72 | .demandedLevels(2800) 73 | .stronglyAdjustedTo(3500).build() 74 | 75 | when: 76 | def res = demand.update(newCallOffDemand(5000)) 77 | 78 | then: 79 | demand.getLevel() == Demand.of(3500) 80 | res.toReview == reviewRequest(2800, 3500, 5000) 81 | res.levelChange == null 82 | res.updated != null 83 | } 84 | 85 | DailyDemandBuilder demand() { 86 | builder 87 | } 88 | 89 | Demand newCallOffDemand(long level) { 90 | builder.newCallOffDemand(level) 91 | } 92 | 93 | Adjustment adjustDemandTo(long level) { 94 | builder.adjustDemandTo(level) 95 | } 96 | 97 | def levelChanged(long previous, long current) { 98 | builder.levelChanged(previous, current) 99 | } 100 | 101 | def reviewRequest(long previousDocumented, long adjustment, long newDocumented) { 102 | builder.reviewRequest(previousDocumented, adjustment, newDocumented) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/test/groovy/io/dddbyexamples/factory/demand/forecasting/ProductDemandBuilder.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting 2 | 3 | import io.dddbyexamples.factory.product.management.RefNoId 4 | 5 | import java.time.* 6 | 7 | import static ReviewRequired.ToReview 8 | 9 | class ProductDemandBuilder { 10 | 11 | def refNo = "3009000" 12 | def demands = new DemandsFake(refNo, clock) 13 | def clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()) 14 | DemandEvents events 15 | 16 | def demand(long ... levels) { 17 | def date = LocalDate.now(clock) 18 | for (long level : levels) { 19 | demands.demanded(date, level) 20 | date = date.plusDays(1) 21 | } 22 | this 23 | } 24 | 25 | def adjusted(Map adjustments) { 26 | adjustments.each { date, level -> 27 | demands.adjusted(date, level) 28 | } 29 | this 30 | } 31 | 32 | def stronglyAdjusted(Map adjustments) { 33 | adjustments.each { date, level -> 34 | demands.stronglyAdjusted(date, level) 35 | } 36 | this 37 | } 38 | 39 | def build() { 40 | new ProductDemand(new RefNoId(refNo), new ArrayList<>(), demands, clock, events) 41 | } 42 | 43 | def document(LocalDate date, long ... levels) { 44 | def created = date.atTime(OffsetTime.of(8, 0, 0, 0, ZoneOffset.UTC)).toInstant() 45 | SortedMap results = new TreeMap<>() 46 | for (def level : levels) { 47 | results.put(date, Demand.of(level)) 48 | date = date.plusDays(1) 49 | } 50 | new Document(created, refNo, results) 51 | } 52 | 53 | def adjustDemand(Map adjustments) { 54 | Map results = new HashMap<>() 55 | adjustments.forEach { date, level -> 56 | results.put(date, Adjustment.weak(Demand.of(level))) 57 | } 58 | new AdjustDemand(refNo, results) 59 | } 60 | 61 | def levelChanged(List... changes) { 62 | def date = LocalDate.now(clock) 63 | Map results = new HashMap<>() 64 | for (def change : changes) { 65 | if (change.size() == 2) { 66 | results.put(new DailyId(refNo, date), new DemandedLevelsChanged.Change( 67 | Demand.of(change[0]), 68 | Demand.of(change[1]))) 69 | } else if (!change.empty) throw new IllegalAccessException() 70 | date = date.plusDays(1) 71 | } 72 | new DemandedLevelsChanged(new RefNoId(refNo), results) 73 | } 74 | 75 | ReviewRequired reviewRequest(ToReview... reviews) { 76 | new ReviewRequired(new RefNoId(refNo), reviews as List) 77 | } 78 | 79 | ToReview review(LocalDate date, 80 | long previousDocumented, 81 | long strongAdjustment, 82 | long newDocumented) { 83 | new ToReview( 84 | new DailyId(refNo, date), 85 | Demand.of(previousDocumented), 86 | Demand.of(strongAdjustment), 87 | Demand.of(newDocumented)) 88 | } 89 | 90 | ApplyReviewDecision reviewDecision(ToReview review, 91 | ReviewDecision decision) { 92 | new ApplyReviewDecision(review, decision) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/test/groovy/io/dddbyexamples/factory/demand/forecasting/ProductDemandTrait.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting 2 | 3 | import java.time.LocalDate 4 | 5 | import static ReviewRequired.ToReview 6 | 7 | trait ProductDemandTrait { 8 | 9 | ProductDemandBuilder builder 10 | 11 | ProductDemand demanded(long ... levels) { 12 | builder.demand(levels).build() 13 | } 14 | 15 | ProductDemandBuilder demand(long ... levels) { 16 | builder.demand(levels) 17 | } 18 | 19 | Document document(LocalDate date, long ... levels) { 20 | builder.document(date, levels) 21 | } 22 | 23 | AdjustDemand adjustments(Map map) { 24 | builder.adjustDemand(map) 25 | } 26 | 27 | DemandedLevelsChanged levelChanged(List... changes) { 28 | builder.levelChanged(changes) 29 | } 30 | 31 | List notChanged() { 32 | [] 33 | } 34 | 35 | ReviewRequired reviewRequest(ToReview... reviews) { 36 | builder.reviewRequest(reviews) 37 | } 38 | 39 | ToReview review( 40 | LocalDate date, 41 | long previousDocumented, 42 | long strongAdjustment, 43 | long newDocumented) { 44 | return builder.review(date, previousDocumented, strongAdjustment, newDocumented) 45 | } 46 | 47 | ApplyReviewDecision reviewDecision(ToReview review, ReviewDecision decision) { 48 | builder.reviewDecision(review, decision) 49 | } 50 | } -------------------------------------------------------------------------------- /demand-forecasting-model/src/test/groovy/io/dddbyexamples/factory/demand/forecasting/ReviewPolicySpec.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting 2 | 3 | import spock.lang.Specification 4 | 5 | import static Adjustment.strong 6 | import static Adjustment.weak 7 | import static Demand.of 8 | 9 | class ReviewPolicySpec extends Specification { 10 | 11 | def "'basic review policy' requires review only after strong adjustment \ 12 | when new document doesn't match neither previous document nor adjustment"() { 13 | given: 14 | def policy = ReviewPolicy.BASIC 15 | 16 | expect: 17 | policy.reviewNeeded( 18 | previousDocument, 19 | adjustment, 20 | newDocument 21 | ) == review 22 | 23 | where: 24 | previousDocument | adjustment | newDocument || review 25 | of(1000) | strong(of(1000)) | of(1000) || notNeeded() 26 | of(1000) | strong(of(2000)) | of(2000) || notNeeded() 27 | of(1000) | strong(of(2000)) | of(1000) || notNeeded() 28 | of(1000) | strong(of(2000)) | of(1500) || needed() 29 | of(1000) | strong(of(2000)) | of(0) || needed() 30 | of(1000) | weak(of(1000)) | of(1000) || notNeeded() 31 | of(1000) | weak(of(2000)) | of(2000) || notNeeded() 32 | of(1000) | weak(of(2000)) | of(1000) || notNeeded() 33 | of(1000) | weak(of(2000)) | of(1500) || notNeeded() 34 | of(1000) | weak(of(2000)) | of(0) || notNeeded() 35 | } 36 | 37 | private static boolean needed() { 38 | true 39 | } 40 | 41 | private static boolean notNeeded() { 42 | false 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /demand-forecasting-model/src/test/groovy/io/dddbyexamples/factory/demand/forecasting/ReviewProcessingSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting 2 | 3 | import spock.lang.Specification 4 | 5 | import java.time.LocalDate 6 | 7 | import static ReviewDecision.* 8 | 9 | class ReviewProcessingSpec extends Specification implements ProductDemandTrait { 10 | 11 | def events = Mock(DemandEvents) 12 | 13 | void setup() { 14 | builder = new ProductDemandBuilder(events: events) 15 | } 16 | 17 | def "Review requested"() { 18 | given: 19 | def today = LocalDate.now(builder.clock) 20 | def tomorrow = today.plusDays(1) 21 | def demand = demand(0, 0) 22 | .stronglyAdjusted((tomorrow): 3500) 23 | .build() 24 | 25 | when: 26 | demand.process(document(today, 0, 2800)) 27 | 28 | then: 29 | 1 * events.emit(reviewRequest(review(tomorrow, 0, 3500, 2800))) 30 | } 31 | 32 | def "decision to 'ignore'"() { 33 | given: 34 | def today = LocalDate.now(builder.clock) 35 | def tomorrow = today.plusDays(1) 36 | def demand = demand(0, 2800) 37 | .stronglyAdjusted((tomorrow): 3500) 38 | .build() 39 | 40 | when: 41 | demand.review(reviewDecision( 42 | review(tomorrow, 0, 3500, 2800), 43 | IGNORE 44 | )) 45 | 46 | then: 47 | 0 * events.emit(_ as DemandedLevelsChanged) 48 | } 49 | 50 | def "decision to 'pick new'"() { 51 | given: 52 | def today = LocalDate.now(builder.clock) 53 | def tomorrow = today.plusDays(1) 54 | def demand = demand(0, 2800) 55 | .stronglyAdjusted((tomorrow): 3500) 56 | .build() 57 | 58 | when: 59 | demand.review(reviewDecision( 60 | review(tomorrow, 0, 3500, 2800), 61 | PICK_NEW 62 | )) 63 | 64 | then: 65 | 1 * events.emit(levelChanged([], [3500, 2800])) 66 | } 67 | 68 | def "decision to 'pick previous'"() { 69 | given: 70 | def today = LocalDate.now(builder.clock) 71 | def tomorrow = today.plusDays(1) 72 | def demand = demand(0, 2800) 73 | .stronglyAdjusted((tomorrow): 3500) 74 | .build() 75 | 76 | when: 77 | demand.review(reviewDecision( 78 | review(tomorrow, 0, 3500, 2800), 79 | PICK_PREVIOUS 80 | )) 81 | 82 | then: 83 | 1 * events.emit(levelChanged([], [3500, 0])) 84 | } 85 | 86 | def "decision to 'make adjustment weak'"() { 87 | given: 88 | def today = LocalDate.now(builder.clock) 89 | def tomorrow = today.plusDays(1) 90 | def demand = demand(0, 2800) 91 | .stronglyAdjusted((tomorrow): 3500) 92 | .build() 93 | 94 | when: 95 | demand.review(reviewDecision( 96 | review(tomorrow, 0, 3500, 2800), 97 | MAKE_ADJUSTMENT_WEAK 98 | )) 99 | 100 | then: 101 | 0 * events.emit(_ as DemandedLevelsChanged) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | backend: 5 | build: 6 | context: ./app-monolith/ 7 | ports: 8 | - "8080:8080" 9 | links: 10 | - database 11 | database: 12 | image: postgres:10 13 | ports: 14 | - "5432:5432" 15 | environment: 16 | - POSTGRES_USER=postgres 17 | - POSTGRES_PASSWORD=postgres 18 | - POSTGRES_DB=postgres 19 | volumes: 20 | - data:/var/lib/postgresql/data 21 | 22 | volumes: 23 | data: 24 | driver: local 25 | -------------------------------------------------------------------------------- /es-big-picture-cleaned.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddd-by-examples/factory/95c751ccefb879e02ecc959c712caa31f4cd9bcf/es-big-picture-cleaned.jpg -------------------------------------------------------------------------------- /es-big-picture-original.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddd-by-examples/factory/95c751ccefb879e02ecc959c712caa31f4cd9bcf/es-big-picture-original.jpg -------------------------------------------------------------------------------- /es-design-demand-forecasting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddd-by-examples/factory/95c751ccefb879e02ecc959c712caa31f4cd9bcf/es-design-demand-forecasting.jpg -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.daemon=false 2 | BOM_VERSION=Finchley.BUILD-SNAPSHOT 3 | -------------------------------------------------------------------------------- /gradle/pipeline.gradle: -------------------------------------------------------------------------------- 1 | test { 2 | description = "Task to run unit and integration tests" 3 | testLogging { 4 | exceptionFormat = 'full' 5 | } 6 | jvmArgs = systemPropsFromGradle() 7 | exclude 'smoke/**' 8 | exclude 'e2e/**' 9 | } 10 | 11 | task smoke(type: Test) { 12 | description = "Task to run smoke tests" 13 | testLogging { 14 | exceptionFormat = 'full' 15 | } 16 | jvmArgs = systemPropsFromGradle() 17 | include 'smoke/**' 18 | } 19 | 20 | task apiCompatibility(type: Test) { 21 | description = "Task to run api compatbility tests" 22 | testLogging { 23 | exceptionFormat = 'full' 24 | } 25 | jvmArgs = systemPropsFromGradle() 26 | include '**/contracttests/**' 27 | } 28 | 29 | task e2e(type: Test) { 30 | description = "Task to run end to end tests" 31 | testLogging { 32 | exceptionFormat = 'full' 33 | } 34 | jvmArgs = systemPropsFromGradle() 35 | include 'e2e/**' 36 | } 37 | 38 | task deploy(dependsOn: 'publish') { 39 | description = "Abstraction over publishing artifacts to Artifactory / Nexus" 40 | } 41 | 42 | task groupId { 43 | doLast { 44 | println projectGroupId 45 | } 46 | } 47 | groupId.description = "Task to retrieve Group ID" 48 | 49 | task artifactId { 50 | doLast { 51 | println projectArtifactId 52 | } 53 | } 54 | artifactId.description = "Task to retrieve Artifact ID" 55 | 56 | task currentVersion { 57 | doLast { 58 | println projectVersion 59 | } 60 | } 61 | currentVersion.description = "Task to retrieve version" 62 | 63 | task stubIds { 64 | doLast { 65 | println stubrunnerIds 66 | } 67 | } 68 | stubIds.description = "Task to retrieve Stub Runner IDS" 69 | 70 | [test, apiCompatibility, smoke, e2e, deploy, groupId, artifactId, currentVersion, stubIds].each { 71 | it.group = "Pipeline" 72 | } 73 | 74 | private List systemPropsFromGradle() { 75 | return project.gradle.startParameter.systemPropertiesArgs.entrySet().collect { "-D${it.key}=${it.value}" } 76 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddd-by-examples/factory/95c751ccefb879e02ecc959c712caa31f4cd9bcf/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /hexagon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddd-by-examples/factory/95c751ccefb879e02ecc959c712caa31f4cd9bcf/hexagon.png -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | config.stopBubbling=true 2 | lombok.addLombokGeneratedAnnotation=true 3 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | applications: 2 | - name: dddbyexamples-app-monolith 3 | timeout: 120 4 | services: 5 | - app-monolith-db 6 | env: 7 | JAVA_OPTS: -Djava.security.egd=file:///dev/urandom 8 | TRUST_CERTS: api.run.pivotal.io 9 | -------------------------------------------------------------------------------- /product-management-adapters/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'groovy' 2 | 3 | dependencies { 4 | compile(project(":adapter-commons")) 5 | 6 | testCompile("org.springframework.boot:spring-boot-starter-test") 7 | testCompile("org.spockframework:spock-spring:1.1-groovy-2.4") 8 | testCompile("com.h2database:h2:1.4.194") 9 | } 10 | -------------------------------------------------------------------------------- /product-management-adapters/src/main/java/io/dddbyexamples/factory/product/management/ProductDescription.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.product.management; 2 | 3 | import lombok.Value; 4 | 5 | import java.util.List; 6 | 7 | @Value 8 | public class ProductDescription { 9 | String matNum; 10 | List names; 11 | } 12 | -------------------------------------------------------------------------------- /product-management-adapters/src/main/java/io/dddbyexamples/factory/product/management/ProductDescriptionDao.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.product.management; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | @RepositoryRestResource( 9 | path = "product-descriptions", 10 | collectionResourceRel = "product-descriptions", 11 | itemResourceRel = "product-description") 12 | public interface ProductDescriptionDao extends JpaRepository { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /product-management-adapters/src/main/java/io/dddbyexamples/factory/product/management/ProductDescriptionEntity.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.product.management; 2 | 3 | import io.dddbyexamples.tools.JsonConverter; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | import javax.persistence.Convert; 8 | import javax.persistence.Entity; 9 | import javax.persistence.Id; 10 | import javax.persistence.Table; 11 | import java.io.Serializable; 12 | 13 | @Entity(name = "ProductDescription") 14 | @Table(schema = "product_management") 15 | @Getter 16 | @NoArgsConstructor 17 | public class ProductDescriptionEntity implements Serializable { 18 | 19 | @Id 20 | private String refNo; 21 | @Convert(converter = DescriptionAsJson.class) 22 | private ProductDescription description; 23 | 24 | public ProductDescriptionEntity(String refNo, ProductDescription description) { 25 | this.refNo = refNo; 26 | this.description = description; 27 | } 28 | 29 | public static class DescriptionAsJson extends JsonConverter { 30 | public DescriptionAsJson() { 31 | super(ProductDescription.class); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /product-management-adapters/src/main/resources/schema/db.changelog.yml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - include: 3 | file: /schema/commons.yml 4 | - include: 5 | file: /schema/product-management.yml 6 | -------------------------------------------------------------------------------- /product-management-adapters/src/main/resources/schema/product-management.yml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - property: 3 | name: json 4 | value: clob 5 | dbms: h2 6 | - property: 7 | name: json 8 | value: jsonb 9 | dbms: postgresql 10 | 11 | 12 | - changeSet: 13 | id: 0.product-management.schema 14 | author: Michal Michaluk 15 | 16 | changes: 17 | - sql: CREATE SCHEMA product_management 18 | rolback: 19 | - sql: DROP SCHEMA product_management 20 | 21 | - changeSet: 22 | id: 1.product-management.init 23 | author: Michal Michaluk 24 | 25 | changes: 26 | - createTable: 27 | schemaName: product_management 28 | tableName: product_description 29 | columns: 30 | - column: 31 | name: ref_no 32 | type: varchar(64) 33 | constraints: 34 | primaryKey: true 35 | primaryKeyName: product_description_pkey 36 | - column: 37 | name: description 38 | type: ${json} 39 | constraints: 40 | nullable: false 41 | -------------------------------------------------------------------------------- /product-management-adapters/src/test/groovy/io/dddbyexamples/factory/ProductionManagementAdaptersConfiguration.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | import org.springframework.boot.autoconfigure.domain.EntityScan; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters; 7 | import org.springframework.scheduling.annotation.EnableScheduling; 8 | 9 | import java.time.Clock; 10 | 11 | @SpringBootApplication 12 | @EnableScheduling 13 | @EntityScan( 14 | basePackageClasses = [ProductionManagementAdaptersConfiguration.class, Jsr310JpaConverters.class] 15 | ) 16 | public class ProductionManagementAdaptersConfiguration { 17 | @Bean 18 | public Clock clock() { 19 | return Clock.systemDefaultZone(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /product-management-adapters/src/test/groovy/io/dddbyexamples/factory/product/management/ProductDescriptionPersistenceSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.product.management 2 | 3 | import org.springframework.test.context.ActiveProfiles 4 | import spock.lang.Ignore 5 | 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.springframework.boot.test.context.SpringBootTest 8 | import spock.lang.Specification 9 | 10 | import static java.util.Collections.singletonList 11 | 12 | @SpringBootTest 13 | @ActiveProfiles("test") 14 | class ProductDescriptionPersistenceSpec extends Specification { 15 | 16 | @Autowired 17 | ProductDescriptionDao dao 18 | 19 | void setup() { 20 | dao.deleteAllInBatch() 21 | } 22 | 23 | def "verify access to ProductDescription data"() { 24 | given: 25 | dao.save(new ProductDescriptionEntity("3009000", 26 | new ProductDescription("461952398951", singletonList("PROWAD.POJ.NA JARZ.ESSENT")))) 27 | 28 | when: 29 | def entities = dao.findAll() 30 | 31 | then: 32 | entities.size() == 1 33 | entities.get(0).description == 34 | new ProductDescription("461952398951", singletonList("PROWAD.POJ.NA JARZ.ESSENT")) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /production-planning-adapters/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'groovy' 2 | 3 | dependencies { 4 | compile(project(":adapter-commons")) 5 | 6 | testCompile("org.springframework.boot:spring-boot-starter-test") 7 | testCompile("org.spockframework:spock-spring:1.1-groovy-2.4") 8 | testCompile("com.h2database:h2:1.4.194") 9 | } 10 | -------------------------------------------------------------------------------- /production-planning-adapters/src/main/java/io/dddbyexamples/factory/production/planning/projection/ProductionDailyOutputDao.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.production.planning.projection; 2 | 3 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 4 | import org.springframework.data.rest.core.annotation.RestResource; 5 | import org.springframework.stereotype.Repository; 6 | import io.dddbyexamples.tools.ProjectionRepository; 7 | 8 | import java.time.LocalDate; 9 | import java.util.List; 10 | 11 | @Repository 12 | @RepositoryRestResource(path = "production-outputs-daily", 13 | collectionResourceRel = "production-outputs-daily", 14 | itemResourceRel = "production-output-daily") 15 | public interface ProductionDailyOutputDao extends ProjectionRepository { 16 | @RestResource(path = "refNos", rel = "refNos") 17 | List findByRefNoAndDateGreaterThanEqual(String refNo, LocalDate date); 18 | } 19 | -------------------------------------------------------------------------------- /production-planning-adapters/src/main/java/io/dddbyexamples/factory/production/planning/projection/ProductionDailyOutputEntity.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.production.planning.projection; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | 6 | import javax.persistence.Entity; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.Id; 9 | import javax.persistence.Table; 10 | import java.io.Serializable; 11 | import java.time.LocalDate; 12 | 13 | @Entity(name = "ProductionDailyOutput") 14 | @Table(schema = "production_planning") 15 | @Getter 16 | @NoArgsConstructor 17 | public class ProductionDailyOutputEntity implements Serializable { 18 | 19 | @Id 20 | @GeneratedValue 21 | private Long id; 22 | private String refNo; 23 | private LocalDate date; 24 | private long output; 25 | 26 | ProductionDailyOutputEntity(String refNo, LocalDate date, long output) { 27 | this.refNo = refNo; 28 | this.date = date; 29 | this.output = output; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /production-planning-adapters/src/main/java/io/dddbyexamples/factory/production/planning/projection/ProductionOutputDao.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.production.planning.projection; 2 | 3 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 4 | import org.springframework.data.rest.core.annotation.RestResource; 5 | import org.springframework.stereotype.Repository; 6 | import io.dddbyexamples.tools.ProjectionRepository; 7 | 8 | import java.time.LocalDateTime; 9 | import java.util.List; 10 | 11 | @Repository 12 | @RepositoryRestResource(path = "production-outputs", 13 | collectionResourceRel = "production-outputs", 14 | itemResourceRel = "production-output") 15 | public interface ProductionOutputDao extends ProjectionRepository { 16 | @RestResource(path = "refNos", rel = "refNos") 17 | List findByRefNoAndEndGreaterThanAndStartLessThan(String refNo, LocalDateTime from, LocalDateTime to); 18 | } 19 | -------------------------------------------------------------------------------- /production-planning-adapters/src/main/java/io/dddbyexamples/factory/production/planning/projection/ProductionOutputEntity.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.production.planning.projection; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | 6 | import javax.persistence.Entity; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.Id; 9 | import javax.persistence.Table; 10 | import java.io.Serializable; 11 | import java.time.Duration; 12 | import java.time.LocalDateTime; 13 | 14 | @Entity(name = "ProductionOutput") 15 | @Table(schema = "production_planning") 16 | @Getter 17 | @NoArgsConstructor 18 | public class ProductionOutputEntity implements Serializable { 19 | 20 | @Id 21 | @GeneratedValue 22 | private Long id; 23 | private String refNo; 24 | private LocalDateTime start; 25 | private long duration; 26 | private LocalDateTime end; 27 | private int partsPerMinute; 28 | private long total; 29 | 30 | ProductionOutputEntity(String refNo, 31 | LocalDateTime start, Duration duration, 32 | int partsPerMinute, 33 | long total) { 34 | this.refNo = refNo; 35 | this.start = start; 36 | this.duration = duration.getSeconds(); 37 | this.end = start.plus(duration); 38 | this.partsPerMinute = partsPerMinute; 39 | this.total = total; 40 | } 41 | 42 | public Duration getDuration() { 43 | return Duration.ofSeconds(duration); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /production-planning-adapters/src/main/resources/schema/db.changelog.yml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - include: 3 | file: /schema/commons.yml 4 | - include: 5 | file: /schema/production-planning.yml 6 | -------------------------------------------------------------------------------- /production-planning-adapters/src/main/resources/schema/production-planning.yml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - property: 3 | name: json 4 | value: clob 5 | dbms: h2 6 | - property: 7 | name: json 8 | value: jsonb 9 | dbms: postgresql 10 | 11 | - changeSet: 12 | id: 0.production-planning.schema 13 | author: Michal Michaluk 14 | 15 | changes: 16 | - sql: CREATE SCHEMA production_planning 17 | rolback: 18 | - sql: DROP SCHEMA production_planning 19 | 20 | - changeSet: 21 | id: 1.production-planning.init 22 | author: Michal Michaluk 23 | 24 | changes: 25 | - createTable: 26 | schemaName: production_planning 27 | tableName: production_daily_output 28 | columns: 29 | - column: 30 | name: id 31 | type: serial 32 | autoIncrement: true 33 | constraints: 34 | primaryKey: true 35 | primaryKeyName: production_daily_output_pkey 36 | - column: 37 | name: ref_no 38 | type: varchar(64) 39 | constraints: 40 | nullable: false 41 | - column: 42 | name: date 43 | type: timestamp 44 | constraints: 45 | nullable: false 46 | - column: 47 | name: output 48 | type: bigint 49 | constraints: 50 | nullable: false 51 | 52 | - addUniqueConstraint: 53 | schemaName: production_planning 54 | tableName: production_daily_output 55 | columnNames: ref_no, date 56 | constraintName: production_daily_output_ref_no_date_key 57 | 58 | - createTable: 59 | schemaName: production_planning 60 | tableName: production_output 61 | columns: 62 | - column: 63 | name: id 64 | type: serial 65 | autoIncrement: true 66 | constraints: 67 | primaryKey: true 68 | primaryKeyName: production_output_pkey 69 | - column: 70 | name: ref_no 71 | type: varchar(64) 72 | constraints: 73 | nullable: false 74 | - column: 75 | name: start 76 | type: timestamp 77 | constraints: 78 | nullable: false 79 | - column: 80 | name: end 81 | type: timestamp 82 | constraints: 83 | nullable: false 84 | - column: 85 | name: duration 86 | type: bigint 87 | constraints: 88 | nullable: false 89 | - column: 90 | name: parts_per_minute 91 | type: integer 92 | constraints: 93 | nullable: false 94 | - column: 95 | name: total 96 | type: bigint 97 | constraints: 98 | nullable: false 99 | -------------------------------------------------------------------------------- /sc-pipelines.yml: -------------------------------------------------------------------------------- 1 | build: 2 | main_module: app-monolith 3 | test: 4 | # list of required services 5 | services: 6 | - name: app-monolith-db 7 | type: broker 8 | broker: elephantsql 9 | plan: turtle 10 | useExisting: true 11 | stage: 12 | # list of required services 13 | services: 14 | - name: app-monolith-db 15 | type: broker 16 | broker: elephantsql 17 | plan: turtle 18 | useExisting: true 19 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'factory' 2 | include "app-monolith" 3 | include "adapter-commons" 4 | include "shared-kernel-model" 5 | include "demand-forecasting-model" 6 | include "demand-forecasting-adapters" 7 | include "shortages-prediction-model" 8 | include "shortages-prediction-adapters" 9 | include "product-management-adapters" 10 | include "production-planning-adapters" 11 | -------------------------------------------------------------------------------- /shared-kernel-model/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'groovy' 2 | apply plugin: "jacoco" 3 | 4 | dependencies { 5 | } 6 | -------------------------------------------------------------------------------- /shared-kernel-model/src/main/java/io/dddbyexamples/factory/demand/forecasting/DailyId.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import lombok.ToString; 7 | 8 | import java.time.LocalDate; 9 | 10 | @Getter 11 | @AllArgsConstructor 12 | @EqualsAndHashCode 13 | @ToString 14 | public class DailyId { 15 | private final String refNo; 16 | private final LocalDate date; 17 | } 18 | -------------------------------------------------------------------------------- /shared-kernel-model/src/main/java/io/dddbyexamples/factory/demand/forecasting/Demand.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting; 2 | 3 | import lombok.Value; 4 | 5 | @Value 6 | public class Demand { 7 | private static final Demand NONE = of(0); 8 | 9 | long level; 10 | Schema schema; 11 | 12 | public enum Schema { 13 | AtDayStart, Till12, TillDayEnd, Twice 14 | } 15 | 16 | public static Demand of(long level) { 17 | return new Demand(level, Schema.TillDayEnd); 18 | } 19 | 20 | public static Demand of(long level, Schema schema) { 21 | return new Demand(level, schema); 22 | } 23 | 24 | static Demand nothingDemanded() { 25 | return NONE; 26 | } 27 | 28 | Demand nullIfNone() { 29 | return NONE == this ? null : this; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /shared-kernel-model/src/main/java/io/dddbyexamples/factory/demand/forecasting/DemandedLevelsChanged.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting; 2 | 3 | import lombok.Value; 4 | import io.dddbyexamples.factory.product.management.RefNoId; 5 | 6 | import java.util.Map; 7 | 8 | @Value 9 | public class DemandedLevelsChanged { 10 | RefNoId refNo; 11 | Map results; 12 | 13 | @Value 14 | public static class Change { 15 | Demand previous; 16 | Demand current; 17 | 18 | public long getDiff() { 19 | return previous.getLevel() - current.getLevel(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /shared-kernel-model/src/main/java/io/dddbyexamples/factory/demand/forecasting/ReviewRequired.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.demand.forecasting; 2 | 3 | import lombok.Value; 4 | import io.dddbyexamples.factory.product.management.RefNoId; 5 | 6 | import java.time.LocalDate; 7 | import java.util.List; 8 | 9 | @Value 10 | public class ReviewRequired { 11 | RefNoId refNo; 12 | List reviews; 13 | 14 | @Value 15 | public static class ToReview { 16 | DailyId id; 17 | Demand previousDocumented; 18 | Demand adjustment; 19 | Demand newDocumented; 20 | 21 | public String getRefNo() { 22 | return id.getRefNo(); 23 | } 24 | 25 | public LocalDate getDate() { 26 | return id.getDate(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /shared-kernel-model/src/main/java/io/dddbyexamples/factory/product/management/RefNoId.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.product.management; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import lombok.ToString; 7 | 8 | @Getter 9 | @AllArgsConstructor 10 | @EqualsAndHashCode 11 | @ToString 12 | public class RefNoId { 13 | private final String refNo; 14 | } 15 | -------------------------------------------------------------------------------- /shortages-prediction-adapters/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'groovy' 2 | 3 | dependencies { 4 | compile(project(":shortages-prediction-model")) 5 | compile(project(":adapter-commons")) 6 | 7 | testCompile("org.springframework.boot:spring-boot-starter-test") 8 | testCompile("org.spockframework:spock-spring:1.1-groovy-2.4") 9 | testCompile("org.springframework.cloud:spring-cloud-starter-contract-stub-runner") 10 | testCompile("org.springframework.cloud:spring-cloud-starter-contract-verifier") 11 | testCompile("org.springframework.restdocs:spring-restdocs-mockmvc") 12 | testCompile("com.h2database:h2:1.4.194") 13 | } 14 | 15 | if (gradle.startParameter.taskRequests.any { it.args.any { it.contains("apiCompatibility") } }) { 16 | 17 | apply plugin: 'spring-cloud-contract' 18 | 19 | contracts { 20 | baseClassForTests = 'io.dddbyexamples.factory.shortages.prediction.monitoring.persistence.BaseClass' 21 | basePackageForTests = 'io.dddbyexamples.contracttests' 22 | contractsPath = "META-INF/${project.rootProject.ext.projectGroupId}/${project.rootProject.ext.projectArtifactId}/${getProp("latestProductionVersion")}/${project.name}/contracts" 23 | contractRepository { 24 | repositoryUrl(getProp('REPO_WITH_BINARIES') ?: 'http://localhost:8081/artifactory/libs-release-local') 25 | } 26 | testMode("EXPLICIT") 27 | contractsMode("REMOTE") 28 | // contractsMode("LOCAL") 29 | targetFramework("SPOCK") 30 | // deleteStubsAfterTest(false) 31 | contractDependency { 32 | groupId = "${project.rootProject.ext.projectGroupId}" 33 | artifactId = "${project.rootProject.ext.projectArtifactId}" 34 | delegate.classifier = "stubs" 35 | delegate.version = getProp("latestProductionVersion") 36 | } 37 | } 38 | 39 | tasks.withType(Test) { 40 | afterSuite { desc, result -> 41 | if (result.testCount == 0) { 42 | throw new IllegalStateException("No tests were found. Failing the build") 43 | } 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /shortages-prediction-adapters/src/main/java/io/dddbyexamples/factory/shortages/prediction/monitoring/MonitoringConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.monitoring; 2 | 3 | import io.dddbyexamples.factory.shortages.prediction.ConfigurationParams; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | class MonitoringConfiguration { 10 | 11 | @Autowired 12 | private ShortagePredictionProcessRepository repository; 13 | 14 | @Bean 15 | ShortagePredictionService shortagePredictionService() { 16 | return new ShortagePredictionService(repository); 17 | } 18 | 19 | @Bean 20 | ShortageDiffPolicy policy() { 21 | return ShortageDiffPolicy.ValuesAreNotSame; 22 | } 23 | 24 | @Bean 25 | ConfigurationParams configuration() { 26 | return () -> 14; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /shortages-prediction-adapters/src/main/java/io/dddbyexamples/factory/shortages/prediction/monitoring/ShortagePredictionProcessORMRepository.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.monitoring; 2 | 3 | import io.dddbyexamples.factory.shortages.prediction.ConfigurationParams; 4 | import lombok.AllArgsConstructor; 5 | import org.springframework.stereotype.Component; 6 | import io.dddbyexamples.factory.product.management.RefNoId; 7 | import io.dddbyexamples.factory.shortages.prediction.calculation.ShortageForecasts; 8 | import io.dddbyexamples.factory.shortages.prediction.monitoring.persistence.ShortagesDao; 9 | import io.dddbyexamples.factory.shortages.prediction.monitoring.persistence.ShortagesEntity; 10 | import io.dddbyexamples.tools.TechnicalId; 11 | 12 | import java.util.Optional; 13 | 14 | @Component 15 | @AllArgsConstructor 16 | class ShortagePredictionProcessORMRepository implements ShortagePredictionProcessRepository { 17 | 18 | private final ShortagesDao dao; 19 | private final ShortageDiffPolicy policy = ShortageDiffPolicy.ValuesAreNotSame; 20 | private final ShortageForecasts forecasts; 21 | private final ConfigurationParams configuration = () -> 14; 22 | private final ShortageEvents events; 23 | 24 | @Override 25 | public ShortagePredictionProcess get(RefNoId refNo) { 26 | Optional entity = dao.findByRefNo(refNo.getRefNo()); 27 | return new ShortagePredictionProcess( 28 | entity.map(ShortagesEntity::createId) 29 | .orElseGet(() -> ShortagesEntity.createId(refNo)), 30 | entity.map(ShortagesEntity::getShortage).orElse(null), 31 | policy, forecasts, configuration, new EventsHandler() 32 | ); 33 | } 34 | 35 | @Override 36 | public void save(ShortagePredictionProcess model) { 37 | // persisted after event 38 | } 39 | 40 | private void save(NewShortage event) { 41 | RefNoId refNo = event.getRefNo(); 42 | 43 | ShortagesEntity entity = TechnicalId.get(refNo) 44 | .flatMap(dao::findById) 45 | .orElseGet(() -> dao.save(new ShortagesEntity(refNo.getRefNo()))); 46 | entity.setShortage(event.getShortage()); 47 | } 48 | 49 | private void delete(ShortageSolved event) { 50 | TechnicalId.get(event.getRefNo()) 51 | .ifPresent(dao::deleteById); 52 | } 53 | 54 | private class EventsHandler implements ShortageEvents { 55 | @Override 56 | public void emit(NewShortage event) { 57 | save(event); 58 | events.emit(event); 59 | } 60 | 61 | @Override 62 | public void emit(ShortageSolved event) { 63 | delete(event); 64 | events.emit(event); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /shortages-prediction-adapters/src/main/java/io/dddbyexamples/factory/shortages/prediction/monitoring/persistence/ShortagesDao.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.monitoring.persistence; 2 | 3 | import io.dddbyexamples.tools.ProjectionRepository; 4 | import org.springframework.data.repository.query.Param; 5 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 6 | import org.springframework.data.rest.core.annotation.RestResource; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import java.util.Optional; 10 | 11 | @Repository 12 | @RepositoryRestResource(path = "shortages", 13 | collectionResourceRel = "shortages", 14 | itemResourceRel = "shortage") 15 | public interface ShortagesDao extends ProjectionRepository { 16 | @RestResource(path = "refNos", rel = "refNos") 17 | Optional findByRefNo(@Param("refNo") String refNo); 18 | 19 | void deleteAllInBatch(); 20 | } 21 | -------------------------------------------------------------------------------- /shortages-prediction-adapters/src/main/java/io/dddbyexamples/factory/shortages/prediction/monitoring/persistence/ShortagesEntity.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.monitoring.persistence; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | import io.dddbyexamples.factory.product.management.RefNoId; 7 | import io.dddbyexamples.factory.shortages.prediction.Shortage; 8 | import io.dddbyexamples.tools.JsonConverter; 9 | import io.dddbyexamples.tools.TechnicalId; 10 | 11 | import javax.persistence.*; 12 | import java.io.Serializable; 13 | 14 | @Entity(name = "Shortage") 15 | @Table(schema = "shortages_prediction") 16 | @Getter 17 | @NoArgsConstructor 18 | public class ShortagesEntity implements Serializable { 19 | 20 | @Id 21 | @GeneratedValue 22 | private Long id; 23 | @Version 24 | private Long version; 25 | private String refNo; 26 | @Setter 27 | @Convert(converter = ShortageAsJson.class) 28 | private Shortage shortage; 29 | 30 | public ShortagesEntity(String refNo) { 31 | this.refNo = refNo; 32 | } 33 | 34 | public RefNoId createId() { 35 | return new ShortagesEntityId(refNo, id); 36 | } 37 | 38 | public static RefNoId createId(RefNoId id) { 39 | return id instanceof ShortagesEntityId ? id : new ShortagesEntityId(id.getRefNo()); 40 | } 41 | 42 | public static class ShortageAsJson extends JsonConverter { 43 | public ShortageAsJson() { 44 | super(Shortage.class); 45 | } 46 | } 47 | 48 | @Getter 49 | static class ShortagesEntityId extends RefNoId implements TechnicalId { 50 | 51 | private Long id; 52 | 53 | ShortagesEntityId(String refNo) { 54 | super(refNo); 55 | } 56 | 57 | ShortagesEntityId(String refNo, long id) { 58 | super(refNo); 59 | this.id = id; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /shortages-prediction-adapters/src/main/java/io/dddbyexamples/factory/shortages/prediction/notification/NotificationConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.notification; 2 | 3 | import io.dddbyexamples.factory.shortages.prediction.Shortage; 4 | import lombok.AllArgsConstructor; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | import java.time.Clock; 9 | 10 | @Configuration 11 | @AllArgsConstructor 12 | public class NotificationConfiguration { 13 | 14 | private final Clock clock; 15 | private final QualityTasks qualityTasks = new MockedJiraIntegration(); 16 | private final Notifications notifications = new MockedPlannerPushNotifications(); 17 | private final RecoveryTaskPriorityChangePolicy policy = 18 | RecoveryTaskPriorityChangePolicy.shortageInDays(2); 19 | 20 | @Bean 21 | public NotificationOfShortage notificationOfShortage() { 22 | return new NotificationOfShortage( 23 | qualityTasks, clock, policy, 24 | NotificationOfShortage.rulesOfPlannerNotification(notifications) 25 | ); 26 | } 27 | 28 | private static class MockedPlannerPushNotifications implements Notifications { 29 | @Override 30 | public void alertPlanner(Shortage shortage) { 31 | 32 | } 33 | 34 | @Override 35 | public void softNotifyPlanner(Shortage shortage) { 36 | 37 | } 38 | 39 | @Override 40 | public void markOnPlan(Shortage shortage) { 41 | 42 | } 43 | } 44 | 45 | private class MockedJiraIntegration implements QualityTasks { 46 | @Override 47 | public void increasePriorityFor(String productRefNo) { 48 | 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /shortages-prediction-adapters/src/main/resources/schema/db.changelog.yml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - include: 3 | file: /schema/commons.yml 4 | - include: 5 | file: /schema/shortages-prediction.yml 6 | -------------------------------------------------------------------------------- /shortages-prediction-adapters/src/main/resources/schema/shortages-prediction.yml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - property: 3 | name: json 4 | value: clob 5 | dbms: h2 6 | - property: 7 | name: json 8 | value: jsonb 9 | dbms: postgresql 10 | 11 | - changeSet: 12 | id: 0.shortages-prediction.schema 13 | author: Michal Michaluk 14 | 15 | changes: 16 | - sql: CREATE SCHEMA shortages_prediction 17 | rolback: 18 | - sql: DROP SCHEMA shortages_prediction 19 | 20 | - changeSet: 21 | id: 1.shortages-prediction.init 22 | author: Michal Michaluk 23 | 24 | changes: 25 | - createTable: 26 | schemaName: shortages_prediction 27 | tableName: shortage 28 | columns: 29 | - column: 30 | name: id 31 | type: serial 32 | autoIncrement: true 33 | constraints: 34 | primaryKey: true 35 | primaryKeyName: shortage_pkey 36 | - column: 37 | name: version 38 | type: bigint 39 | constraints: 40 | nullable: false 41 | - column: 42 | name: ref_no 43 | type: varchar(64) 44 | constraints: 45 | nullable: false 46 | - column: 47 | name: shortages 48 | type: ${json} 49 | constraints: 50 | nullable: false 51 | 52 | - addUniqueConstraint: 53 | schemaName: shortages_prediction 54 | tableName: shortage 55 | columnNames: ref_no 56 | constraintName: shortage_ref_no_key 57 | 58 | - createTable: 59 | schemaName: shortages_prediction 60 | tableName: stock_forecast 61 | columns: 62 | - column: 63 | name: ref_no 64 | type: varchar(64) 65 | constraints: 66 | primaryKey: true 67 | primaryKeyName: stock_forecast_pkey 68 | 69 | - changeSet: 70 | id: 2.rename.shortages.column 71 | author: Michal Michaluk 72 | 73 | changes: 74 | - renameColumn: 75 | schemaName: shortages_prediction 76 | tableName: shortage 77 | oldColumnName: shortages 78 | newColumnName: shortage 79 | -------------------------------------------------------------------------------- /shortages-prediction-adapters/src/test/groovy/e2e/E2eSpec.groovy: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import spock.lang.Specification 4 | import spock.util.concurrent.PollingConditions 5 | 6 | import org.springframework.beans.factory.annotation.Value 7 | import org.springframework.boot.autoconfigure.ImportAutoConfiguration 8 | import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration 9 | import org.springframework.boot.test.context.SpringBootTest 10 | import org.springframework.boot.test.web.client.TestRestTemplate 11 | import org.springframework.http.ResponseEntity 12 | 13 | /** 14 | * @author Marcin Grzejszczak 15 | */ 16 | @SpringBootTest(classes = E2eSpec.class, 17 | webEnvironment = SpringBootTest.WebEnvironment.NONE) 18 | @ImportAutoConfiguration(PropertyPlaceholderAutoConfiguration) 19 | class E2eSpec extends Specification { 20 | 21 | @Value('${application.url}') String applicationUrl 22 | @Value('${test.timeout:60}') Long timeout 23 | 24 | TestRestTemplate testRestTemplate = new TestRestTemplate(); 25 | 26 | def 'should work'() { 27 | given: 28 | def conditions = new PollingConditions(timeout: this.timeout) 29 | expect: 30 | conditions.eventually { 31 | ResponseEntity entity = this.testRestTemplate 32 | .getForEntity("http://" + this.applicationUrl + "/shortages", String.class); 33 | 34 | assert entity.getStatusCode().is2xxSuccessful() 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /shortages-prediction-adapters/src/test/groovy/io/dddbyexamples/factory/PredictionAdaptersConfiguration.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | import org.springframework.boot.autoconfigure.domain.EntityScan; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters; 7 | import org.springframework.scheduling.annotation.EnableScheduling; 8 | import io.dddbyexamples.factory.product.management.RefNoId; 9 | import io.dddbyexamples.factory.shortages.prediction.calculation.ShortageForecast; 10 | import io.dddbyexamples.factory.shortages.prediction.calculation.ShortageForecasts; 11 | import io.dddbyexamples.factory.shortages.prediction.monitoring.NewShortage; 12 | import io.dddbyexamples.factory.shortages.prediction.monitoring.ShortageEvents; 13 | import io.dddbyexamples.factory.shortages.prediction.monitoring.ShortageSolved; 14 | 15 | import java.time.Clock; 16 | 17 | @SpringBootApplication 18 | @EnableScheduling 19 | @EntityScan( 20 | basePackageClasses = [PredictionAdaptersConfiguration.class, Jsr310JpaConverters.class] 21 | ) 22 | public class PredictionAdaptersConfiguration { 23 | @Bean 24 | public Clock clock() { 25 | return Clock.systemDefaultZone(); 26 | } 27 | 28 | @Bean 29 | public ShortageForecasts forecasts() { 30 | return new ShortageForecastsFake(); 31 | } 32 | 33 | @Bean 34 | public ShortageEventsFake shortageEvents() { 35 | return new ShortageEventsFake(); 36 | } 37 | 38 | private class ShortageEventsFake implements ShortageEvents { 39 | @Override 40 | public void emit(NewShortage event) { 41 | 42 | } 43 | 44 | @Override 45 | public void emit(ShortageSolved event) { 46 | 47 | } 48 | } 49 | 50 | private class ShortageForecastsFake implements ShortageForecasts { 51 | @Override 52 | public ShortageForecast get(RefNoId refNo, int daysAhead) { 53 | return null; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /shortages-prediction-adapters/src/test/groovy/io/dddbyexamples/factory/shortages/prediction/monitoring/persistence/BaseClass.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.monitoring.persistence 2 | 3 | import groovy.transform.CompileStatic 4 | import io.restassured.RestAssured 5 | import io.restassured.module.mockmvc.RestAssuredMockMvc 6 | import spock.lang.Specification 7 | 8 | import org.springframework.beans.factory.annotation.Autowired 9 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase 10 | import org.springframework.boot.test.context.SpringBootTest 11 | import org.springframework.web.context.WebApplicationContext 12 | 13 | import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.MOCK 14 | 15 | /** 16 | * @author Marcin Grzejszczak 17 | */ 18 | @CompileStatic 19 | // TODO: Migrate to MockMvc - need to know how to set a port 20 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, 21 | properties = ["server.port=8080"], 22 | classes = Config) 23 | @AutoConfigureTestDatabase 24 | abstract class BaseClass extends Specification { 25 | 26 | @Autowired WebApplicationContext context 27 | @Autowired ShortagesDao shortagesDao 28 | 29 | def setup() { 30 | RestAssured.baseURI = "http://localhost:8080" 31 | Config.defaultStubbing(shortagesDao) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /shortages-prediction-adapters/src/test/groovy/io/dddbyexamples/factory/shortages/prediction/monitoring/persistence/Config.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.monitoring.persistence 2 | 3 | import groovy.transform.CompileDynamic 4 | import groovy.transform.CompileStatic 5 | import org.mockito.Mockito 6 | 7 | import org.springframework.beans.BeansException 8 | import org.springframework.beans.factory.config.BeanPostProcessor 9 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration 10 | import org.springframework.context.annotation.Bean 11 | import org.springframework.context.annotation.ComponentScan 12 | import org.springframework.context.annotation.Configuration 13 | 14 | /** 15 | * @author Marcin Grzejszczak 16 | */ 17 | @Configuration 18 | @EnableAutoConfiguration 19 | @ComponentScan("io.dddbyexamples.factory.shortages.prediction.persistence") 20 | @CompileStatic 21 | class Config { 22 | 23 | // https://github.com/spring-projects/spring-boot/issues/7033 24 | @Bean 25 | BeanPostProcessor shortagesBeanPostProcessor() { 26 | return new BeanPostProcessor() { 27 | @Override 28 | Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { 29 | if (bean instanceof ShortagesDao) { 30 | return Mockito.mock(ShortagesDao) 31 | } 32 | return bean 33 | } 34 | } 35 | } 36 | 37 | @CompileDynamic 38 | static ShortagesDao defaultStubbing(ShortagesDao dao) { 39 | ShortagesEntity entity = new ShortagesEntity("1") 40 | entity.id = 1L 41 | entity.version = 1L 42 | Mockito.doReturn([entity]).when(dao).findAll() 43 | return dao 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /shortages-prediction-adapters/src/test/groovy/io/dddbyexamples/factory/shortages/prediction/monitoring/persistence/ShortagesDaoSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.monitoring.persistence 2 | 3 | import org.springframework.beans.factory.annotation.Autowired 4 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase 5 | import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs 6 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc 7 | import org.springframework.boot.test.context.SpringBootTest 8 | import org.springframework.cloud.contract.wiremock.restdocs.SpringCloudContractRestDocs 9 | import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation 10 | import org.springframework.test.context.ActiveProfiles 11 | import org.springframework.test.web.servlet.MockMvc 12 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers 13 | import spock.lang.Specification 14 | 15 | import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.MOCK 16 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get 17 | 18 | /** 19 | * @author Marcin Grzejszczak 20 | */ 21 | @SpringBootTest(webEnvironment = MOCK, classes = Config) 22 | @AutoConfigureMockMvc 23 | // would love to get rid of this 24 | @AutoConfigureTestDatabase 25 | @AutoConfigureRestDocs 26 | @ActiveProfiles("test") 27 | class ShortagesDaoSpec extends Specification { 28 | 29 | @Autowired 30 | ShortagesDao shortagesDao 31 | @Autowired 32 | MockMvc mockMvc 33 | 34 | def "should find ref by no"() { 35 | given: 36 | Config.defaultStubbing(shortagesDao) 37 | expect: 38 | mockMvc.perform(get("/shortages?refNo=1")) 39 | .andExpect(MockMvcResultMatchers.status().isOk()) 40 | .andDo(MockMvcRestDocumentation.document("find_ref_by_no", 41 | SpringCloudContractRestDocs.dslContract())) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /shortages-prediction-adapters/src/test/groovy/smoke/SmokeSpec.groovy: -------------------------------------------------------------------------------- 1 | package smoke 2 | 3 | import spock.lang.Specification 4 | import spock.util.concurrent.PollingConditions 5 | 6 | import org.springframework.beans.factory.annotation.Value 7 | import org.springframework.boot.autoconfigure.ImportAutoConfiguration 8 | import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration 9 | import org.springframework.boot.test.context.SpringBootTest 10 | import org.springframework.boot.test.web.client.TestRestTemplate 11 | import org.springframework.http.ResponseEntity 12 | 13 | /** 14 | * @author Marcin Grzejszczak 15 | */ 16 | @SpringBootTest(classes = SmokeSpec.class, 17 | webEnvironment = SpringBootTest.WebEnvironment.NONE) 18 | @ImportAutoConfiguration(PropertyPlaceholderAutoConfiguration) 19 | class SmokeSpec extends Specification { 20 | 21 | //@Value('${stubrunner.url}') String stubRunnerUrl 22 | @Value('${application.url}') String applicationUrl 23 | @Value('${test.timeout:60}') Long timeout 24 | 25 | TestRestTemplate testRestTemplate = new TestRestTemplate(); 26 | 27 | def 'should work'() { 28 | given: 29 | def conditions = new PollingConditions(timeout: this.timeout) 30 | expect: 31 | conditions.eventually { 32 | ResponseEntity entity = this.testRestTemplate 33 | .getForEntity("http://" + this.applicationUrl + "/shortages", String.class); 34 | 35 | assert entity.getStatusCode().is2xxSuccessful() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /shortages-prediction-model/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'groovy' 2 | apply plugin: "jacoco" 3 | 4 | dependencies { 5 | compile(project(":shared-kernel-model")) 6 | } 7 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/main/java/io/dddbyexamples/factory/shortages/prediction/ConfigurationParams.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction; 2 | 3 | /** 4 | * Created by michal on 02.02.2017. 5 | */ 6 | public interface ConfigurationParams { 7 | int shortagePredictionDaysAhead(); 8 | } 9 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/main/java/io/dddbyexamples/factory/shortages/prediction/Shortage.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Value; 5 | 6 | import java.time.LocalDateTime; 7 | import java.util.Collections; 8 | import java.util.Optional; 9 | import java.util.SortedMap; 10 | import java.util.TreeMap; 11 | 12 | /** 13 | * Levels missing to satisfy customer demand of particular product. 14 | *

15 | * Created by michal on 22.10.2015. 16 | */ 17 | @Value 18 | public class Shortage { 19 | 20 | private final String refNo; 21 | private final long lockedParts; 22 | private final LocalDateTime found; 23 | private final SortedMap shortages; 24 | 25 | public static Shortage.Builder builder(String refNo, long locked, LocalDateTime found) { 26 | return new Builder(refNo, locked, found); 27 | } 28 | 29 | public static boolean areNotSame(Shortage first, Shortage second) { 30 | return !areSame(first, second); 31 | } 32 | 33 | public static boolean areSame(Shortage first, Shortage second) { 34 | boolean noShortages = first == null && second == null; 35 | boolean onlyOne = first == null && second != null || first != null && second == null; 36 | if (noShortages || onlyOne) return false; 37 | boolean sameProduct = first.refNo.equals(second.refNo); 38 | boolean sameNumbers = first.shortages.equals(second.shortages); 39 | return sameProduct && sameNumbers; 40 | } 41 | 42 | public boolean anyBefore(LocalDateTime time) { 43 | return shortages.firstKey().isBefore(time); 44 | } 45 | 46 | @AllArgsConstructor 47 | public static class Builder { 48 | private final String refNo; 49 | private final long locked; 50 | private final LocalDateTime found; 51 | private final SortedMap gaps = new TreeMap<>(); 52 | 53 | public Builder missing(LocalDateTime time, long level) { 54 | gaps.put(time, Math.abs(level)); 55 | return this; 56 | } 57 | 58 | public Optional build() { 59 | if (gaps.isEmpty()) { 60 | return Optional.empty(); 61 | } else { 62 | return Optional.of(new Shortage(refNo, locked, found, 63 | Collections.unmodifiableSortedMap(gaps))); 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/main/java/io/dddbyexamples/factory/shortages/prediction/calculation/DeliveriesForecast.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.calculation; 2 | 3 | import lombok.AllArgsConstructor; 4 | 5 | import java.time.LocalDateTime; 6 | import java.util.Map; 7 | 8 | @AllArgsConstructor 9 | class DeliveriesForecast { 10 | 11 | private final Map forecast; 12 | 13 | long get(LocalDateTime time) { 14 | return forecast.getOrDefault(time, 0L); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/main/java/io/dddbyexamples/factory/shortages/prediction/calculation/ProductionForecast.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.calculation; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Value; 5 | 6 | import java.time.Duration; 7 | import java.time.LocalDateTime; 8 | import java.util.List; 9 | import java.util.Set; 10 | import java.util.function.Function; 11 | import java.util.stream.Collectors; 12 | import java.util.stream.Stream; 13 | 14 | @Value 15 | class ProductionForecast { 16 | List items; 17 | 18 | ProductionOutputs outputsInTimes(LocalDateTime now, Set times) { 19 | return new ProductionOutputs( 20 | (times.contains(now) ? times.stream() : Stream.concat(Stream.of(now), times.stream())) 21 | .parallel() 22 | .collect(Collectors.toMap( 23 | Function.identity(), 24 | time -> items.parallelStream() 25 | .mapToLong(item -> item.partsAt(time)) 26 | .sum() 27 | )) 28 | ); 29 | } 30 | 31 | @AllArgsConstructor 32 | static class Item { 33 | final LocalDateTime start; 34 | final Duration duration; 35 | final int partsPerMinute; 36 | 37 | long partsAt(LocalDateTime time) { 38 | if (startsAfter(time)) { 39 | return 0; 40 | } 41 | if (endsBefore(time)) { 42 | return duration.toMinutes() * partsPerMinute; 43 | } 44 | return Duration.between(start, time).getSeconds() * partsPerMinute / 60; 45 | } 46 | 47 | boolean startsAfter(LocalDateTime time) { 48 | return start.isAfter(time); 49 | } 50 | 51 | boolean endsBefore(LocalDateTime time) { 52 | return start.plus(duration).isBefore(time); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/main/java/io/dddbyexamples/factory/shortages/prediction/calculation/ProductionOutputs.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.calculation; 2 | 3 | import lombok.AllArgsConstructor; 4 | 5 | import java.time.LocalDateTime; 6 | import java.util.Map; 7 | 8 | @AllArgsConstructor 9 | class ProductionOutputs { 10 | 11 | private final Map outputs; 12 | 13 | long getOutput(LocalDateTime from, LocalDateTime to) { 14 | if (!outputs.containsKey(from) || !outputs.containsKey(to)) { 15 | throw new IllegalArgumentException("No pre-calculated output for time " + to); 16 | } 17 | return outputs.get(to) - outputs.get(from); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/main/java/io/dddbyexamples/factory/shortages/prediction/calculation/ShortageForecast.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.calculation; 2 | 3 | import io.dddbyexamples.factory.shortages.prediction.Shortage; 4 | import lombok.AllArgsConstructor; 5 | 6 | import java.time.LocalDateTime; 7 | import java.util.Optional; 8 | import java.util.SortedSet; 9 | 10 | @AllArgsConstructor 11 | public class ShortageForecast { 12 | 13 | private final String refNo; 14 | private final LocalDateTime created; 15 | private final SortedSet deliveryTimes; 16 | private final Stock stock; 17 | private final ProductionOutputs outputs; 18 | private final DeliveriesForecast deliveries; 19 | 20 | public Optional findShortages() { 21 | long level = stock.getLevel(); 22 | 23 | Shortage.Builder found = Shortage.builder(refNo, stock.getLocked(), created); 24 | LocalDateTime lastTime = created; 25 | for (LocalDateTime time : deliveryTimes) { 26 | long demand = deliveries.get(time); 27 | long produced = outputs.getOutput(lastTime, time); 28 | 29 | long levelOnDelivery = level + produced - demand; 30 | 31 | if (levelOnDelivery < 0) { 32 | found.missing(time, levelOnDelivery); 33 | } 34 | level = Math.max(levelOnDelivery, 0); 35 | lastTime = time; 36 | } 37 | return found.build(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/main/java/io/dddbyexamples/factory/shortages/prediction/calculation/ShortageForecasts.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.calculation; 2 | 3 | import io.dddbyexamples.factory.product.management.RefNoId; 4 | 5 | public interface ShortageForecasts { 6 | ShortageForecast get(RefNoId refNo, int daysAhead); 7 | } 8 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/main/java/io/dddbyexamples/factory/shortages/prediction/calculation/Stock.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.calculation; 2 | 3 | import lombok.Value; 4 | 5 | @Value 6 | public class Stock { 7 | long level; 8 | long locked; 9 | } 10 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/main/java/io/dddbyexamples/factory/shortages/prediction/monitoring/NewShortage.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.monitoring; 2 | 3 | import lombok.Value; 4 | import io.dddbyexamples.factory.product.management.RefNoId; 5 | import io.dddbyexamples.factory.shortages.prediction.Shortage; 6 | 7 | /** 8 | * Created by michal on 03.02.2017. 9 | */ 10 | @Value 11 | public class NewShortage { 12 | 13 | public enum After {DemandChanged, PlanChanged, StockChanged, LockedParts} 14 | 15 | RefNoId refNo; 16 | After trigger; 17 | Shortage shortage; 18 | } 19 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/main/java/io/dddbyexamples/factory/shortages/prediction/monitoring/ShortageDiffPolicy.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.monitoring; 2 | 3 | import io.dddbyexamples.factory.shortages.prediction.Shortage; 4 | 5 | interface ShortageDiffPolicy { 6 | 7 | ShortageDiffPolicy ValuesAreNotSame = Shortage::areNotSame; 8 | 9 | boolean areDifferent(Shortage previous, Shortage found); 10 | } 11 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/main/java/io/dddbyexamples/factory/shortages/prediction/monitoring/ShortageEvents.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.monitoring; 2 | 3 | public interface ShortageEvents { 4 | void emit(NewShortage event); 5 | 6 | void emit(ShortageSolved event); 7 | } 8 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/main/java/io/dddbyexamples/factory/shortages/prediction/monitoring/ShortagePredictionProcess.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.monitoring; 2 | 3 | import io.dddbyexamples.factory.product.management.RefNoId; 4 | import io.dddbyexamples.factory.shortages.prediction.ConfigurationParams; 5 | import io.dddbyexamples.factory.shortages.prediction.Shortage; 6 | import io.dddbyexamples.factory.shortages.prediction.calculation.ShortageForecast; 7 | import io.dddbyexamples.factory.shortages.prediction.calculation.ShortageForecasts; 8 | import lombok.AllArgsConstructor; 9 | 10 | import java.util.Optional; 11 | 12 | /** 13 | * Created by michal on 02.02.2017. 14 | */ 15 | @AllArgsConstructor 16 | class ShortagePredictionProcess { 17 | 18 | private final RefNoId refNo; 19 | private Shortage known; 20 | 21 | private final ShortageDiffPolicy diffPolicy; 22 | private final ShortageForecasts forecasts; 23 | private final ConfigurationParams configuration; 24 | private final ShortageEvents events; 25 | 26 | void onDemandChanged() { 27 | predict(NewShortage.After.DemandChanged); 28 | } 29 | 30 | void onPlanChanged() { 31 | predict(NewShortage.After.PlanChanged); 32 | } 33 | 34 | void onStockChanged() { 35 | predict(NewShortage.After.StockChanged); 36 | } 37 | 38 | void onLockedParts() { 39 | predict(NewShortage.After.LockedParts); 40 | } 41 | 42 | private void predict(NewShortage.After event) { 43 | ShortageForecast forecast = forecasts.get(refNo, 44 | configuration.shortagePredictionDaysAhead()); 45 | 46 | Optional newlyFound = forecast.findShortages(); 47 | 48 | boolean areDifferent = diffPolicy.areDifferent(this.known, newlyFound.orElse(null)); 49 | if (areDifferent && newlyFound.isPresent()) { 50 | this.known = newlyFound.get(); 51 | events.emit(new NewShortage(refNo, event, known)); 52 | } else if (known != null && !newlyFound.isPresent()) { 53 | this.known = null; 54 | events.emit(new ShortageSolved(refNo)); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/main/java/io/dddbyexamples/factory/shortages/prediction/monitoring/ShortagePredictionProcessRepository.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.monitoring; 2 | 3 | import io.dddbyexamples.factory.product.management.RefNoId; 4 | 5 | interface ShortagePredictionProcessRepository { 6 | ShortagePredictionProcess get(RefNoId refNo); 7 | 8 | void save(ShortagePredictionProcess model); 9 | } 10 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/main/java/io/dddbyexamples/factory/shortages/prediction/monitoring/ShortagePredictionService.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.monitoring; 2 | 3 | import io.dddbyexamples.factory.demand.forecasting.DemandedLevelsChanged; 4 | import lombok.AllArgsConstructor; 5 | 6 | @AllArgsConstructor 7 | public class ShortagePredictionService { 8 | 9 | private final ShortagePredictionProcessRepository repository; 10 | 11 | public void predictShortages(DemandedLevelsChanged event) { 12 | ShortagePredictionProcess model = repository.get(event.getRefNo()); 13 | model.onDemandChanged(); 14 | repository.save(model); 15 | } 16 | 17 | //public void predictShortages(ProductionChanged event) { service.onPlanChanged(event.getId().getRefNo()); } 18 | 19 | //public void predictShortages(StockChanged event) { service.onStockChanged(event.getId().getRefNo()); } 20 | } 21 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/main/java/io/dddbyexamples/factory/shortages/prediction/monitoring/ShortageSolved.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.monitoring; 2 | 3 | import lombok.Value; 4 | import io.dddbyexamples.factory.product.management.RefNoId; 5 | 6 | @Value 7 | public class ShortageSolved { 8 | RefNoId refNo; 9 | } 10 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/main/java/io/dddbyexamples/factory/shortages/prediction/notification/NotificationOfShortage.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.notification; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Singular; 6 | import lombok.Value; 7 | import io.dddbyexamples.factory.shortages.prediction.Shortage; 8 | import io.dddbyexamples.factory.shortages.prediction.monitoring.NewShortage; 9 | 10 | import java.time.Clock; 11 | import java.time.LocalDateTime; 12 | import java.util.Map; 13 | 14 | @AllArgsConstructor 15 | public class NotificationOfShortage { 16 | 17 | private final QualityTasks qualityTasks; 18 | private final Clock clock; 19 | 20 | private final RecoveryTaskPriorityChangePolicy policy; 21 | private final NotificationRules rules; 22 | 23 | static NotificationRules rulesOfPlannerNotification(Notifications notifications) { 24 | return NotificationRules.builder() 25 | .rule(NewShortage.After.DemandChanged, notifications::alertPlanner) 26 | .rule(NewShortage.After.PlanChanged, notifications::markOnPlan) 27 | .rule(NewShortage.After.StockChanged, notifications::alertPlanner) 28 | .rule(NewShortage.After.LockedParts, notifications::softNotifyPlanner) 29 | .otherwise(notifications::alertPlanner) 30 | .build(); 31 | } 32 | 33 | public void notifyAbout(NewShortage event) { 34 | Shortage shortage = event.getShortage(); 35 | rules.wayOfNotificationAfter(event.getTrigger()) 36 | .notifyAbout(event.getShortage()); 37 | 38 | if (policy.shouldIncreasePriority(LocalDateTime.now(clock), shortage)) { 39 | qualityTasks.increasePriorityFor(shortage.getRefNo()); 40 | } 41 | } 42 | 43 | @Value 44 | @Builder 45 | static class NotificationRules { 46 | @Singular 47 | Map rules; 48 | Notificator otherwise; 49 | 50 | Notificator wayOfNotificationAfter(NewShortage.After trigger) { 51 | return rules.getOrDefault(trigger, otherwise); 52 | } 53 | } 54 | 55 | interface Notificator { 56 | void notifyAbout(Shortage shortage); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/main/java/io/dddbyexamples/factory/shortages/prediction/notification/Notifications.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.notification; 2 | 3 | import io.dddbyexamples.factory.shortages.prediction.Shortage; 4 | 5 | /** 6 | * Created by michal on 02.02.2017. 7 | */ 8 | interface Notifications { 9 | void alertPlanner(Shortage shortage); 10 | 11 | void softNotifyPlanner(Shortage shortage); 12 | 13 | void markOnPlan(Shortage shortage); 14 | } 15 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/main/java/io/dddbyexamples/factory/shortages/prediction/notification/QualityTasks.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.notification; 2 | 3 | /** 4 | * Created by michal on 02.02.2017. 5 | */ 6 | public interface QualityTasks { 7 | void increasePriorityFor(String productRefNo); 8 | } 9 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/main/java/io/dddbyexamples/factory/shortages/prediction/notification/RecoveryTaskPriorityChangePolicy.java: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.notification; 2 | 3 | import io.dddbyexamples.factory.shortages.prediction.Shortage; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | /** 8 | * Created by michal on 18.05.2017. 9 | */ 10 | public interface RecoveryTaskPriorityChangePolicy { 11 | 12 | static RecoveryTaskPriorityChangePolicy never() { 13 | return (LocalDateTime now, Shortage shortage) -> false; 14 | } 15 | 16 | static RecoveryTaskPriorityChangePolicy onlyIn1DaysAhead() { 17 | return shortageInDays(1); 18 | } 19 | 20 | static RecoveryTaskPriorityChangePolicy shortageInDays(long shortageInXDays) { 21 | return (LocalDateTime now, Shortage shortage) -> 22 | shortage.getLockedParts() > 0 && shortage.anyBefore( 23 | now.plusDays(shortageInXDays)); 24 | } 25 | 26 | boolean shouldIncreasePriority(LocalDateTime now, Shortage shortage); 27 | } 28 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/test/groovy/io/dddbyexamples/factory/shortages/prediction/calculation/ShortagesCalculationAssembler.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.calculation 2 | 3 | class ShortagesCalculationAssembler implements ShortagesCalculationTrait { 4 | } 5 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/test/groovy/io/dddbyexamples/factory/shortages/prediction/calculation/ShortagesCalculationTrait.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.calculation 2 | 3 | import io.dddbyexamples.factory.product.management.RefNoId 4 | import io.dddbyexamples.factory.shortages.prediction.Shortage 5 | 6 | import java.time.Duration 7 | import java.time.LocalDateTime 8 | 9 | trait ShortagesCalculationTrait { 10 | 11 | LocalDateTime now = LocalDateTime.now() 12 | String refNo = "3009000" 13 | SortedSet times 14 | 15 | ShortageForecasts forecastProvider(Stock stock, DeliveriesForecast demands, ProductionOutputs outputs) { 16 | def forecast = forecast(stock, demands, outputs) 17 | return { RefNoId refNo, int daysAhead -> forecast } as ShortageForecasts 18 | } 19 | 20 | ShortageForecast forecast(Stock stock, DeliveriesForecast demands, ProductionOutputs outputs) { 21 | new ShortageForecast(refNo, now, times, stock, outputs, demands) 22 | } 23 | 24 | ProductionOutputs noProductions() { 25 | new ProductionForecast([]) 26 | .outputsInTimes(now, times) 27 | } 28 | 29 | ProductionOutputs plan(List productions) { 30 | new ProductionForecast(productions) 31 | .outputsInTimes(now, times) 32 | } 33 | 34 | ProductionForecast.Item production(LocalDateTime start, Duration duration, int partsPerMinute) { 35 | new ProductionForecast.Item(start, duration, partsPerMinute) 36 | } 37 | 38 | DeliveriesForecast noDeliveries() { 39 | times = Collections.emptySortedSet() 40 | new DeliveriesForecast([:]) 41 | } 42 | 43 | DeliveriesForecast deliveries(Map demands) { 44 | times = new TreeSet<>(demands.keySet()) 45 | new DeliveriesForecast(demands) 46 | } 47 | 48 | Stock stock(long levels) { 49 | new Stock(levels, 0) 50 | } 51 | 52 | Stock stock(long level, long locked) { 53 | new Stock(level, locked) 54 | } 55 | 56 | Optional noShortages() { 57 | Optional.empty() 58 | } 59 | 60 | Optional shortage(Map missing, long locked = 0) { 61 | def shortages = Shortage.builder(refNo, locked, now) 62 | 63 | missing.each { time, level -> shortages.missing(time, level) } 64 | 65 | shortages.build() 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/test/groovy/io/dddbyexamples/factory/shortages/prediction/calculation/TimeGrammar.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.calculation 2 | 3 | import java.time.Duration 4 | import java.time.LocalDateTime 5 | import java.time.format.DateTimeFormatter 6 | 7 | class TimeGrammar { 8 | static getDays(Integer self) { Duration.ofDays self } 9 | 10 | static getDay(Integer self) { Duration.ofDays self } 11 | 12 | static getH(Integer self) { Duration.ofHours self } 13 | 14 | static getMin(Integer self) { Duration.ofMinutes self } 15 | 16 | static String toString(LocalDateTime self, String pattern) { self.format(DateTimeFormatter.ofPattern(pattern)) } 17 | 18 | static apply() { 19 | Integer.mixin(TimeGrammar) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/test/groovy/io/dddbyexamples/factory/shortages/prediction/monitoring/InMemoryConfigurationParams.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.monitoring 2 | 3 | import io.dddbyexamples.factory.shortages.prediction.ConfigurationParams 4 | 5 | class InMemoryConfigurationParams implements ConfigurationParams { 6 | int daysAhead; 7 | 8 | @Override 9 | int shortagePredictionDaysAhead() { 10 | return daysAhead; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/test/groovy/io/dddbyexamples/factory/shortages/prediction/monitoring/ShortagePredictionProcessSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.monitoring 2 | 3 | import spock.lang.Specification 4 | 5 | import static NewShortage.After 6 | 7 | class ShortagePredictionProcessSpec extends Specification implements ShortagePredictionProcessTrait { 8 | 9 | void setup() { 10 | events = Mock(ShortageEvents) 11 | } 12 | 13 | def "Emits no events when there is still no shortages"() { 14 | given: 15 | def process = predictionProcess( 16 | noShortagesWasPreviouslyFound(), 17 | noShortagesWillBeFound() 18 | ) 19 | 20 | when: 21 | process.onDemandChanged() 22 | 23 | then: 24 | 0 * events.emit(_ as NewShortage) 25 | 0 * events.emit(_ as ShortageSolved) 26 | } 27 | 28 | def "Emits NewShortage found when shortage was found first time"() { 29 | given: 30 | def process = predictionProcess( 31 | noShortagesWasPreviouslyFound(), 32 | willFindShortages(someShortages()) 33 | ) 34 | 35 | when: 36 | process.onDemandChanged() 37 | 38 | then: 39 | 1 * events.emit(newShortage(After.DemandChanged, someShortages())) 40 | 0 * events.emit(_ as ShortageSolved) 41 | } 42 | 43 | def "Emits ShortageSolved when shortage disappear"() { 44 | given: 45 | def process = predictionProcess( 46 | wasPreviouslyFound(someShortages()), 47 | noShortagesWillBeFound() 48 | ) 49 | 50 | when: 51 | process.onDemandChanged() 52 | 53 | then: 54 | 0 * events.emit(_ as NewShortage) 55 | 1 * events.emit(shortageSolved()) 56 | } 57 | 58 | def "Emits no events when there is still 'same' shortages"() { 59 | given: 60 | def process = predictionProcess( 61 | wasPreviouslyFound(someShortages()), 62 | willFindShortages(someShortages()) 63 | ) 64 | 65 | when: 66 | process.onDemandChanged() 67 | 68 | then: 69 | 0 * events.emit(_ as NewShortage) 70 | 0 * events.emit(_ as ShortageSolved) 71 | } 72 | 73 | def "Emits NewShortage found when 'different' shortages will be found"() { 74 | given: 75 | def process = predictionProcess( 76 | wasPreviouslyFound(someShortages()), 77 | willFindShortages(someDifferentShortages()) 78 | ) 79 | 80 | when: 81 | process.onDemandChanged() 82 | 83 | then: 84 | 1 * events.emit(newShortage(After.DemandChanged, someDifferentShortages())) 85 | 0 * events.emit(_ as ShortageSolved) 86 | } 87 | 88 | def "Remembers last found shortage"() { 89 | given: 90 | def process = predictionProcess( 91 | noShortagesWasPreviouslyFound(), 92 | willFindShortages(someShortages()) 93 | ) 94 | 95 | when: 96 | process.onDemandChanged() 97 | 98 | then: 99 | 1 * events.emit(newShortage(After.DemandChanged, someShortages())) 100 | 0 * events.emit(_ as ShortageSolved) 101 | 102 | when: 103 | process.onDemandChanged() 104 | process.onLockedParts() 105 | process.onPlanChanged() 106 | process.onStockChanged() 107 | 108 | then: 109 | 0 * events.emit(_ as NewShortage) 110 | 0 * events.emit(_ as ShortageSolved) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/test/groovy/io/dddbyexamples/factory/shortages/prediction/monitoring/ShortagePredictionProcessTrait.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.monitoring 2 | 3 | import io.dddbyexamples.factory.product.management.RefNoId 4 | import io.dddbyexamples.factory.shortages.prediction.ConfigurationParams 5 | import io.dddbyexamples.factory.shortages.prediction.Shortage 6 | import io.dddbyexamples.factory.shortages.prediction.calculation.ShortageForecasts 7 | import io.dddbyexamples.factory.shortages.prediction.calculation.ShortagesCalculationAssembler 8 | 9 | import java.time.LocalDateTime 10 | 11 | trait ShortagePredictionProcessTrait { 12 | 13 | def refNo = new RefNoId("3009000") 14 | def now = LocalDateTime.now() 15 | def forecastAssembler = new ShortagesCalculationAssembler(refNo: refNo, now: now) 16 | ShortageEvents events 17 | 18 | ShortagePredictionProcess predictionProcess( 19 | Shortage previouslyFound, 20 | ShortageForecasts forecastThatWillFindShortages) { 21 | 22 | new ShortagePredictionProcess( 23 | refNo, 24 | previouslyFound, 25 | ShortageDiffPolicy.ValuesAreNotSame, 26 | forecastThatWillFindShortages, 27 | defaultConfig(), 28 | events 29 | ) 30 | } 31 | 32 | Map someShortages() { 33 | [(now.plusHours(5)): 500L, 34 | (now.plusDays(1)) : 500L] 35 | } 36 | 37 | Map someDifferentShortages() { 38 | [(now.plusHours(5)): 100L, 39 | (now.plusDays(1)) : 900L] 40 | } 41 | 42 | Shortage noShortagesWasPreviouslyFound() { 43 | null 44 | } 45 | 46 | Shortage wasPreviouslyFound(Map shortages) { 47 | forecastAssembler.shortage(shortages).orElse(null) 48 | } 49 | 50 | ShortageForecasts noShortagesWillBeFound() { 51 | forecastAssembler.forecastProvider( 52 | forecastAssembler.stock(1000), 53 | forecastAssembler.noDeliveries(), 54 | forecastAssembler.noProductions() 55 | ) 56 | } 57 | 58 | ShortageForecasts willFindShortages(Map shortages) { 59 | forecastAssembler.forecastProvider( 60 | forecastAssembler.stock(0), 61 | forecastAssembler.deliveries(shortages), 62 | forecastAssembler.noProductions() 63 | ) 64 | } 65 | 66 | ConfigurationParams defaultConfig() { 67 | new InMemoryConfigurationParams(daysAhead: 14) 68 | } 69 | 70 | NewShortage newShortage(NewShortage.After after, Map missing) { 71 | new NewShortage(refNo, after, forecastAssembler.shortage(missing).get()) 72 | } 73 | 74 | ShortageSolved shortageSolved() { 75 | new ShortageSolved(refNo) 76 | } 77 | } -------------------------------------------------------------------------------- /shortages-prediction-model/src/test/groovy/io/dddbyexamples/factory/shortages/prediction/monitoring/ShortagePredictionServiceSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.monitoring 2 | 3 | import io.dddbyexamples.factory.demand.forecasting.DemandedLevelsChanged 4 | import spock.lang.Specification 5 | 6 | class ShortagePredictionServiceSpec extends Specification implements ShortagePredictionProcessTrait { 7 | 8 | def repo = Mock(ShortagePredictionProcessRepository) 9 | def service = new ShortagePredictionService(repo) 10 | 11 | def "Repository interactions while processing demanded changed event"() { 12 | given: 13 | def process = predictionProcess( 14 | noShortagesWasPreviouslyFound(), 15 | noShortagesWillBeFound() 16 | ) 17 | repo.get(refNo) >> process 18 | 19 | when: 20 | service.predictShortages(new DemandedLevelsChanged(refNo, [:])) 21 | 22 | then: 23 | 1 * repo.save(process) 24 | 0 * events.emit(_ as ShortageSolved) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /shortages-prediction-model/src/test/groovy/io/dddbyexamples/factory/shortages/prediction/notification/RecoveryTaskPriorityChangePolicySpec.groovy: -------------------------------------------------------------------------------- 1 | package io.dddbyexamples.factory.shortages.prediction.notification 2 | 3 | import io.dddbyexamples.factory.shortages.prediction.Shortage 4 | import spock.lang.Specification 5 | 6 | import java.time.Duration 7 | import java.time.LocalDateTime 8 | 9 | class RecoveryTaskPriorityChangePolicySpec extends Specification { 10 | 11 | def now = LocalDateTime.now() 12 | 13 | Shortage foundShortage(Duration firstShortageIn, long lockedStock) { 14 | Shortage.builder("3009000", lockedStock, now) 15 | .missing(now.plus(firstShortageIn), 500L) 16 | .build().get() 17 | } 18 | 19 | def "'never policy' don't increase priority... ever"() { 20 | given: 21 | def policy = RecoveryTaskPriorityChangePolicy.never() 22 | 23 | expect: 24 | policy.shouldIncreasePriority( 25 | now, 26 | foundShortage(firstShortageIn, lockedStock) 27 | ) == policyDecision 28 | 29 | where: 30 | firstShortageIn | lockedStock || policyDecision 31 | Duration.ofMinutes(5) | 500L || false 32 | Duration.ofMinutes(5) | 0L || false 33 | Duration.ofDays(15) | 500L || false 34 | Duration.ofDays(15) | 0L || false 35 | } 36 | 37 | def "'onlyIn1DaysAhead policy' increase priority for shortages in 24h"() { 38 | given: 39 | def policy = RecoveryTaskPriorityChangePolicy.onlyIn1DaysAhead() 40 | 41 | expect: 42 | policy.shouldIncreasePriority( 43 | now, 44 | foundShortage(firstShortageIn, lockedStock) 45 | ) == policyDecision 46 | 47 | where: 48 | firstShortageIn | lockedStock || policyDecision 49 | Duration.ofMinutes(5) | 500L || true 50 | Duration.ofMinutes(5) | 0L || false 51 | 52 | Duration.ofHours(24).minusMillis(1) | 500L || true 53 | Duration.ofHours(24) | 500L || false 54 | Duration.ofHours(24).plusMillis(1) | 500L || false 55 | } 56 | 57 | def "'shortageInDays(2) policy' increase priority for shortages in 48h"() { 58 | given: 59 | def policy = RecoveryTaskPriorityChangePolicy.shortageInDays(2) 60 | 61 | expect: 62 | policy.shouldIncreasePriority( 63 | now, 64 | foundShortage(firstShortageIn, lockedStock) 65 | ) == policyDecision 66 | 67 | where: 68 | firstShortageIn | lockedStock || policyDecision 69 | Duration.ofMinutes(5) | 500L || true 70 | Duration.ofMinutes(5) | 0L || false 71 | 72 | Duration.ofHours(48).minusMillis(1) | 500L || true 73 | Duration.ofHours(48) | 500L || false 74 | Duration.ofHours(48).plusMillis(1) | 500L || false 75 | } 76 | } 77 | --------------------------------------------------------------------------------