diff --git a/adapter-commons/pom.xml b/adapter-commons/pom.xml index 3128142..22703c0 100644 --- a/adapter-commons/pom.xml +++ b/adapter-commons/pom.xml @@ -28,6 +28,11 @@ jackson-datatype-jsr310 2.8.5 + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/adapter-commons/src/test/java/pl/com/bottega/tools/IntegrationTest.java b/adapter-commons/src/test/java/pl/com/bottega/tools/IntegrationTest.java new file mode 100644 index 0000000..c9ed86d --- /dev/null +++ b/adapter-commons/src/test/java/pl/com/bottega/tools/IntegrationTest.java @@ -0,0 +1,18 @@ +package pl.com.bottega.tools; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.annotation.AliasFor; +import org.springframework.test.context.ActiveProfiles; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@SpringBootTest +@ActiveProfiles +public @interface IntegrationTest { + @AliasFor(annotation = ActiveProfiles.class, attribute = "profiles") String[] activeProfiles() default {"test"}; +} diff --git a/app-monolith/pom.xml b/app-monolith/pom.xml index ebef45f..4d8a205 100644 --- a/app-monolith/pom.xml +++ b/app-monolith/pom.xml @@ -99,6 +99,12 @@ 1.1-groovy-2.4 test + + com.h2database + h2 + 1.4.194 + test + net.bytebuddy byte-buddy @@ -125,6 +131,29 @@ + + org.codehaus.gmavenplus + gmavenplus-plugin + 1.6 + + + + compileTests + + + + + + maven-surefire-plugin + 2.20.1 + + false + + **/*Spec.java + **/*Test.java + + + diff --git a/app-monolith/src/main/resources/schema/commons.sql b/app-monolith/src/main/resources/schema/commons.sql deleted file mode 100644 index 495f961..0000000 --- a/app-monolith/src/main/resources/schema/commons.sql +++ /dev/null @@ -1,12 +0,0 @@ ---liquibase formatted sql - ---changeset michaluk.michal:1.init - -CREATE SEQUENCE hibernate_sequence - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - -CREATE CAST (character varying AS json) WITH INOUT AS ASSIGNMENT; diff --git a/app-monolith/src/main/resources/schema/commons.xml b/app-monolith/src/main/resources/schema/commons.xml new file mode 100644 index 0000000..2a2b72c --- /dev/null +++ b/app-monolith/src/main/resources/schema/commons.xml @@ -0,0 +1,11 @@ + + + + + + + CREATE CAST (character varying AS jsonb) WITH INOUT AS ASSIGNMENT + + diff --git a/app-monolith/src/main/resources/schema/db.changelog.xml b/app-monolith/src/main/resources/schema/db.changelog.xml index 0e94a27..f5d9b8e 100644 --- a/app-monolith/src/main/resources/schema/db.changelog.xml +++ b/app-monolith/src/main/resources/schema/db.changelog.xml @@ -2,10 +2,10 @@ - - - - - - + + + + + + diff --git a/app-monolith/src/main/resources/schema/delivery-planning.sql b/app-monolith/src/main/resources/schema/delivery-planning.sql deleted file mode 100644 index 0b3e86b..0000000 --- a/app-monolith/src/main/resources/schema/delivery-planning.sql +++ /dev/null @@ -1,17 +0,0 @@ ---liquibase formatted sql - ---changeset michaluk.michal:1.init -CREATE SCHEMA delivery_planning; - -CREATE TABLE delivery_planning.delivery_planner_definition ( - ref_no character varying(64) NOT NULL PRIMARY KEY, - definition json NOT NULL -); - -CREATE TABLE delivery_planning.delivery_forecast ( - id serial NOT NULL PRIMARY KEY, - ref_no character varying(64) NOT NULL, - "time" timestamp without time zone NOT NULL, - "date" timestamp without time zone NOT NULL, - level bigint NOT NULL -); diff --git a/app-monolith/src/main/resources/schema/delivery-planning.xml b/app-monolith/src/main/resources/schema/delivery-planning.xml new file mode 100644 index 0000000..8012ae9 --- /dev/null +++ b/app-monolith/src/main/resources/schema/delivery-planning.xml @@ -0,0 +1,42 @@ + + + + + + + + + + CREATE SCHEMA delivery_planning + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app-monolith/src/main/resources/schema/demand-forecasting.sql b/app-monolith/src/main/resources/schema/demand-forecasting.sql deleted file mode 100644 index ce91d92..0000000 --- a/app-monolith/src/main/resources/schema/demand-forecasting.sql +++ /dev/null @@ -1,49 +0,0 @@ ---liquibase formatted sql - ---changeset michaluk.michal:1.init -CREATE SCHEMA demand_forecasting; - -CREATE TABLE demand_forecasting.product_demand ( - id serial NOT NULL PRIMARY KEY, - version bigint NOT NULL, - ref_no character varying(64) NOT NULL, - UNIQUE(ref_no) -); - -CREATE TABLE demand_forecasting."demand" ( - id serial NOT NULL PRIMARY KEY, - ref_no character varying(64) NOT NULL, - "date" timestamp without time zone NOT NULL, - value json NOT NULL, - UNIQUE(ref_no, "date") -); - -CREATE TABLE demand_forecasting.current_demand ( - id serial NOT NULL PRIMARY KEY, - ref_no character varying(64) NOT NULL, - "date" timestamp without time zone NOT NULL, - level bigint NOT NULL, - "schema" character varying(64) NOT NULL, - UNIQUE(ref_no, "date") -); - -CREATE TABLE demand_forecasting.demand_adjustment ( - id serial NOT NULL PRIMARY KEY, - customer_representative character varying(255) NOT NULL, - note character varying(255) NOT NULL, - adjustment json NOT NULL, - clean_after timestamp without time zone -); - -CREATE TABLE demand_forecasting.demand_review ( - id serial NOT NULL PRIMARY KEY, - ref_no character varying(64) NOT NULL, - "date" timestamp without time zone NOT NULL, - "timestamp" timestamp without time zone NOT NULL, - review json NOT NULL, - decision character varying(64), - clean_after timestamp without time zone -); - ---changeset michaluk.michal:2.rename.review.table -ALTER TABLE demand_forecasting.demand_review RENAME TO required_review; diff --git a/app-monolith/src/main/resources/schema/demand-forecasting.xml b/app-monolith/src/main/resources/schema/demand-forecasting.xml new file mode 100644 index 0000000..18b552a --- /dev/null +++ b/app-monolith/src/main/resources/schema/demand-forecasting.xml @@ -0,0 +1,134 @@ + + + + + + + + + + CREATE SCHEMA demand_forecasting + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app-monolith/src/main/resources/schema/product-management.sql b/app-monolith/src/main/resources/schema/product-management.sql deleted file mode 100644 index f7b66f5..0000000 --- a/app-monolith/src/main/resources/schema/product-management.sql +++ /dev/null @@ -1,9 +0,0 @@ ---liquibase formatted sql - ---changeset michaluk.michal:1.init -CREATE SCHEMA product_management; - -CREATE TABLE product_management.product_description ( - ref_no character varying(64) NOT NULL PRIMARY KEY, - description json NOT NULL -); diff --git a/app-monolith/src/main/resources/schema/product-management.xml b/app-monolith/src/main/resources/schema/product-management.xml new file mode 100644 index 0000000..8761062 --- /dev/null +++ b/app-monolith/src/main/resources/schema/product-management.xml @@ -0,0 +1,24 @@ + + + + + + + + + + CREATE SCHEMA product_management + + + + + + + + + + + + \ No newline at end of file diff --git a/app-monolith/src/main/resources/schema/production-planning.sql b/app-monolith/src/main/resources/schema/production-planning.sql deleted file mode 100644 index 025cf40..0000000 --- a/app-monolith/src/main/resources/schema/production-planning.sql +++ /dev/null @@ -1,22 +0,0 @@ ---liquibase formatted sql - ---changeset michaluk.michal:1.init -CREATE SCHEMA production_planning; - -CREATE TABLE production_planning.production_daily_output ( - id serial NOT NULL PRIMARY KEY, - ref_no character varying(64) NOT NULL, - "date" timestamp without time zone NOT NULL, - output bigint NOT NULL, - UNIQUE(ref_no, "date") -); - -CREATE TABLE production_planning.production_output ( - id serial NOT NULL PRIMARY KEY, - ref_no character varying(64) NOT NULL, - "start" timestamp without time zone NOT NULL, - "end" timestamp without time zone NOT NULL, - duration bigint NOT NULL, - parts_per_minute integer NOT NULL, - total bigint NOT NULL -); diff --git a/app-monolith/src/main/resources/schema/production-planning.xml b/app-monolith/src/main/resources/schema/production-planning.xml new file mode 100644 index 0000000..3e6a323 --- /dev/null +++ b/app-monolith/src/main/resources/schema/production-planning.xml @@ -0,0 +1,60 @@ + + + + + + + + + + CREATE SCHEMA production_planning + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app-monolith/src/main/resources/schema/shortages-prediction.sql b/app-monolith/src/main/resources/schema/shortages-prediction.sql deleted file mode 100644 index 95333f3..0000000 --- a/app-monolith/src/main/resources/schema/shortages-prediction.sql +++ /dev/null @@ -1,19 +0,0 @@ ---liquibase formatted sql - ---changeset michaluk.michal:1.init -CREATE SCHEMA shortages_prediction; - -CREATE TABLE shortages_prediction.shortage ( - id serial NOT NULL PRIMARY KEY, - version bigint NOT NULL, - ref_no character varying(64) NOT NULL, - shortages json NOT NULL, - UNIQUE(ref_no) -); - -CREATE TABLE shortages_prediction.stock_forecast ( - ref_no character varying(64) NOT NULL PRIMARY KEY -); - ---changeset michaluk.michal:2.rename.shortages.column -ALTER TABLE shortages_prediction.shortage RENAME shortages TO shortage; diff --git a/app-monolith/src/main/resources/schema/shortages-prediction.xml b/app-monolith/src/main/resources/schema/shortages-prediction.xml new file mode 100644 index 0000000..6bf377e --- /dev/null +++ b/app-monolith/src/main/resources/schema/shortages-prediction.xml @@ -0,0 +1,40 @@ + + + + + + + + + + CREATE SCHEMA shortages_prediction + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app-monolith/src/test/groovy/pl/com/bottega/factory/ProductTrait.groovy b/app-monolith/src/test/groovy/pl/com/bottega/factory/ProductTrait.groovy new file mode 100644 index 0000000..7ae2b03 --- /dev/null +++ b/app-monolith/src/test/groovy/pl/com/bottega/factory/ProductTrait.groovy @@ -0,0 +1,35 @@ +package pl.com.bottega.factory + +import pl.com.bottega.factory.demand.forecasting.Demand +import pl.com.bottega.factory.demand.forecasting.Document +import pl.com.bottega.factory.demand.forecasting.persistence.DocumentEntity +import pl.com.bottega.factory.product.management.ProductDescription +import pl.com.bottega.factory.product.management.ProductDescriptionEntity + +import java.time.Instant +import java.time.LocalDate +import java.time.OffsetTime +import java.time.ZoneOffset + +trait ProductTrait { + + DocumentEntity documentFor(String refNo, LocalDate date, long ... levels) { + Document document = document(refNo, date, levels) + return new DocumentEntity("uri", "storedUri", document) + } + + ProductDescriptionEntity productDescription(String refNo) { + ProductDescription desc = new ProductDescription("461952398951", ["PROWAD.POJ.NA JARZ.ESSENT"]) + return new ProductDescriptionEntity(refNo, desc) + } + + Document document(String refNo, LocalDate date, long ... levels) { + Instant created = date.atTime(OffsetTime.of(8, 0, 0, 0, ZoneOffset.UTC)).toInstant() + SortedMap results = new TreeMap<>() + for (long level : levels) { + results.put(date, Demand.of(level)) + date = date.plusDays(1) + } + new Document(created, refNo, results) + } +} diff --git a/app-monolith/src/test/groovy/pl/com/bottega/factory/integration/CallOffDocumentIntegrationSpec.groovy b/app-monolith/src/test/groovy/pl/com/bottega/factory/integration/CallOffDocumentIntegrationSpec.groovy new file mode 100644 index 0000000..f897e6c --- /dev/null +++ b/app-monolith/src/test/groovy/pl/com/bottega/factory/integration/CallOffDocumentIntegrationSpec.groovy @@ -0,0 +1,86 @@ +package pl.com.bottega.factory.integration + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.ParameterizedTypeReference +import org.springframework.hateoas.Resources +import org.springframework.http.HttpMethod +import org.springframework.http.ResponseEntity +import pl.com.bottega.factory.AppConfiguration +import pl.com.bottega.factory.ProductTrait +import pl.com.bottega.factory.demand.forecasting.persistence.DocumentEntity +import pl.com.bottega.factory.demand.forecasting.projection.CurrentDemandEntity +import pl.com.bottega.factory.product.management.ProductDescriptionEntity +import pl.com.bottega.tools.IntegrationTest +import spock.lang.Specification + +import java.time.Clock +import java.time.LocalDate +import java.time.ZoneId + +import static java.time.Instant.from +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT + +@IntegrationTest +@SpringBootTest(webEnvironment = RANDOM_PORT, classes = AppConfiguration) +class CallOffDocumentIntegrationSpec extends Specification implements ProductTrait { + + public static final String PRODUCT_REF_NO = "3009000" + public static final LocalDate ANY_DATE = LocalDate.now() + + @Autowired TestRestTemplate restTemplate + + def 'receiving call off document should create new product demands for subsequent days'() { + given: + productDescriptionIsSuccessfullyCreated(PRODUCT_REF_NO) + when: + callOffDocumentIsSuccessfullyRequested(PRODUCT_REF_NO, ANY_DATE, 100, 200, 300) + and: + Collection demands = + demandsForProductStartingFromDateAreRequested(PRODUCT_REF_NO, ANY_DATE.minusDays(1)) + then: + demands.size() == 3 + thereIsDemand(demands, ANY_DATE, 100) + thereIsDemand(demands, ANY_DATE.plusDays(1), 200) + thereIsDemand(demands, ANY_DATE.plusDays(2), 300) + } + + void productDescriptionIsSuccessfullyCreated(String refNo) { + ResponseEntity response = restTemplate + .postForEntity("/product-descriptions", productDescription(refNo), ProductDescriptionEntity) + assert response.statusCode.is2xxSuccessful() + } + + void callOffDocumentIsSuccessfullyRequested(String refNo, LocalDate date, long ... levels) { + ResponseEntity response = restTemplate + .postForEntity("/demand-documents", documentFor(refNo, date, levels), DocumentEntity) + assert response.statusCode.is2xxSuccessful() + } + + Collection demandsForProductStartingFromDateAreRequested(String refNo, LocalDate date) { + ResponseEntity> res = restTemplate + .exchange("/demand-forecasts/search/refNos?refNo={refNo}&date={date}", + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}, + ["refNo": refNo, "date": date]) + assert res.statusCode.is2xxSuccessful() + return res.getBody().getContent() + } + + void thereIsDemand(Collection demands, LocalDate date, long expectedLevel) { + assert demands.find { it.date == date && it.level == expectedLevel } + } + + @Configuration + static class TestConfiguration { + + @Bean + Clock clock() { + return Clock.fixed(from(ANY_DATE), ZoneId.systemDefault()) + } + } +} \ No newline at end of file diff --git a/app-monolith/src/test/resources/application-test.properties b/app-monolith/src/test/resources/application-test.properties new file mode 100644 index 0000000..79eedd9 --- /dev/null +++ b/app-monolith/src/test/resources/application-test.properties @@ -0,0 +1,4 @@ +spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.username=sa +spring.datasource.password=sa +spring.datasource.driver-class-name=org.h2.Driver diff --git a/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/command/CommandsHandler.java b/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/command/CommandsHandler.java index 2937b16..3cad57b 100644 --- a/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/command/CommandsHandler.java +++ b/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/command/CommandsHandler.java @@ -3,6 +3,7 @@ package pl.com.bottega.factory.demand.forecasting.command; import lombok.AllArgsConstructor; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; +import org.springframework.data.rest.core.annotation.HandleAfterCreate; import org.springframework.data.rest.core.annotation.HandleBeforeCreate; import org.springframework.data.rest.core.annotation.HandleBeforeSave; import org.springframework.data.rest.core.annotation.RepositoryEventHandler; @@ -10,6 +11,7 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import pl.com.bottega.factory.demand.forecasting.DemandService; +import pl.com.bottega.factory.demand.forecasting.persistence.DocumentEntity; import java.time.Clock; import java.time.LocalDate; @@ -35,6 +37,11 @@ public class CommandsHandler { service.adjust(adjustment.getAdjustment()); } + @HandleAfterCreate + public void process(DocumentEntity document) { + service.process(document.getDocument()); + } + @HandleBeforeSave public void review(RequiredReviewEntity review) { if (review.decisionTaken()) { diff --git a/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/persistence/DocumentDao.java b/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/persistence/DocumentDao.java new file mode 100644 index 0000000..69a4810 --- /dev/null +++ b/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/persistence/DocumentDao.java @@ -0,0 +1,13 @@ +package pl.com.bottega.factory.demand.forecasting.persistence; + +import org.springframework.data.rest.core.annotation.RepositoryRestResource; +import org.springframework.stereotype.Repository; +import pl.com.bottega.tools.CommandRepository; + +@Repository("documentDao") +@RepositoryRestResource(path = "demand-documents", + collectionResourceRel = "demand-documents", + itemResourceRel = "demand-document") +public interface DocumentDao extends CommandRepository { + +} diff --git a/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/persistence/DocumentEntity.java b/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/persistence/DocumentEntity.java new file mode 100644 index 0000000..67aab6d --- /dev/null +++ b/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/persistence/DocumentEntity.java @@ -0,0 +1,49 @@ +package pl.com.bottega.factory.demand.forecasting.persistence; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.LastModifiedDate; +import pl.com.bottega.factory.demand.forecasting.Document; +import pl.com.bottega.tools.JsonConverter; + +import javax.persistence.*; +import java.io.Serializable; +import java.time.Instant; +import java.time.LocalDate; + +@Entity(name = "Document") +@Table(schema = "demand_forecasting") +@Getter +@NoArgsConstructor +public class DocumentEntity implements Serializable { + + @Id + @GeneratedValue + private Long id; + private String refNo; + @LastModifiedDate + private Instant saved; + private String originalUri; + private String storedUri; + @Setter + @Convert(converter = DocumentAsJson.class) + private Document document; + + @Setter + private LocalDate cleanAfter; + + public DocumentEntity(String originalUri, String storedUri, Document document) { + saved = Instant.now(); + this.originalUri = originalUri; + this.storedUri = storedUri; + this.document = document; + this.refNo = document.getRefNo(); + } + + public static class DocumentAsJson extends JsonConverter { + public DocumentAsJson() { + super(Document.class); + } + } +} diff --git a/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/projection/CurrentDemandDao.java b/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/projection/CurrentDemandDao.java index cd1a426..85d4fed 100644 --- a/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/projection/CurrentDemandDao.java +++ b/demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/projection/CurrentDemandDao.java @@ -1,7 +1,9 @@ package pl.com.bottega.factory.demand.forecasting.projection; +import org.springframework.data.repository.query.Param; import org.springframework.data.rest.core.annotation.RepositoryRestResource; import org.springframework.data.rest.core.annotation.RestResource; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.stereotype.Repository; import pl.com.bottega.tools.ProjectionRepository; @@ -14,7 +16,7 @@ import java.util.List; itemResourceRel = "demand-forecast") public interface CurrentDemandDao extends ProjectionRepository { @RestResource(path = "refNos", rel = "refNos") - List findByRefNoAndDateGreaterThanEqual(String refNo, LocalDate date); + List findByRefNoAndDateGreaterThanEqual(@Param("refNo") String refNo, @Param("date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date); @RestResource(exported = false) void deleteByRefNoAndDate(String refNo, LocalDate date); diff --git a/demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/Document.java b/demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/Document.java index 0a4b670..239a7ee 100644 --- a/demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/Document.java +++ b/demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/Document.java @@ -1,6 +1,6 @@ package pl.com.bottega.factory.demand.forecasting; -import lombok.AllArgsConstructor; +import lombok.Value; import java.time.Instant; import java.time.LocalDate; @@ -9,7 +9,7 @@ import java.util.SortedMap; import java.util.function.BiFunction; import java.util.stream.Collectors; -@AllArgsConstructor +@Value public class Document { private final Instant created; @@ -27,4 +27,3 @@ public class Document { return refNo; } } -