diff --git a/app-monolith/pom.xml b/app-monolith/pom.xml
new file mode 100644
index 0000000..bbd9082
--- /dev/null
+++ b/app-monolith/pom.xml
@@ -0,0 +1,95 @@
+
+
+ 4.0.0
+
+ pl.com.bottega
+ app-monolith
+ 1.0-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 1.5.8.RELEASE
+
+
+
+
+ pl.com.bottega
+ shared-kernel-model
+ 1.0-SNAPSHOT
+
+
+ pl.com.bottega
+ demand-forecasting-model
+ 1.0-SNAPSHOT
+
+
+ pl.com.bottega
+ shortages-prediction-model
+ 1.0-SNAPSHOT
+
+
+
+ org.projectlombok
+ lombok
+ 1.16.18
+ provided
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-data-rest
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.postgresql
+ postgresql
+ 42.1.4
+ runtime
+
+
+ org.spockframework
+ spock-core
+ 1.1-groovy-2.4
+ test
+
+
+ org.spockframework
+ spock-spring
+ 1.1-groovy-2.4
+ test
+
+
+ net.bytebuddy
+ byte-buddy
+ 1.7.9
+
+
+
+
+ 1.8
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/AppConfiguration.java b/app-monolith/src/main/java/pl/com/bottega/factory/AppConfiguration.java
new file mode 100644
index 0000000..3e3eeb2
--- /dev/null
+++ b/app-monolith/src/main/java/pl/com/bottega/factory/AppConfiguration.java
@@ -0,0 +1,25 @@
+package pl.com.bottega.factory;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters;
+
+import java.time.Clock;
+
+@SpringBootApplication
+@EntityScan(
+ basePackageClasses = {AppConfiguration.class, Jsr310JpaConverters.class}
+)
+public class AppConfiguration {
+
+ public static void main(String[] args) {
+ SpringApplication.run(AppConfiguration.class, args);
+ }
+
+ @Bean
+ public Clock clock() {
+ return Clock.systemDefaultZone();
+ }
+}
diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEntity.java b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEntity.java
new file mode 100644
index 0000000..4fdac11
--- /dev/null
+++ b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEntity.java
@@ -0,0 +1,25 @@
+package pl.com.bottega.factory.demand.forecasting;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.*;
+import java.time.LocalDate;
+
+@Data
+@Entity(name = "Demand")
+@NoArgsConstructor
+@EqualsAndHashCode(of = "id")
+public class DemandEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ Long id;
+
+ @ManyToOne
+ ProductDemandEntity product;
+ @Column
+ LocalDate date;
+
+}
diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEventsMapping.java b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEventsMapping.java
new file mode 100644
index 0000000..e5a7e46
--- /dev/null
+++ b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEventsMapping.java
@@ -0,0 +1,13 @@
+package pl.com.bottega.factory.demand.forecasting;
+
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Component;
+
+@Lazy
+@Component
+class DemandEventsMapping implements DemandEvents {
+
+ @Override
+ public void emit(DemandedLevelsChanged event) {
+ }
+}
diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandRepository.java b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandRepository.java
new file mode 100644
index 0000000..304fcf7
--- /dev/null
+++ b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandRepository.java
@@ -0,0 +1,25 @@
+package pl.com.bottega.factory.demand.forecasting;
+
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Component;
+import pl.com.bottega.factory.demand.forecasting.persistence.DemandDao;
+import pl.com.bottega.factory.demand.forecasting.persistence.ProductDemandDao;
+
+import java.time.Clock;
+
+@Component
+@AllArgsConstructor
+class DemandRepository {
+
+ private Clock clock;
+ private DemandEventsMapping events;
+ private ProductDemandDao rootDao;
+ private DemandDao demandDao;
+
+ ProductDemand get(String refNo) {
+ return null;
+ }
+
+ void save(ProductDemand model) {
+ }
+}
diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandService.java b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandService.java
new file mode 100644
index 0000000..db35df4
--- /dev/null
+++ b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandService.java
@@ -0,0 +1,24 @@
+package pl.com.bottega.factory.demand.forecasting;
+
+import org.springframework.stereotype.Service;
+
+import javax.transaction.Transactional;
+
+@Service
+@Transactional
+public class DemandService {
+
+ private DemandRepository repository;
+
+ public void process(Document document) {
+ ProductDemand model = repository.get(document.getRefNo());
+ model.process(document);
+ repository.save(model);
+ }
+
+ public void adjust(AdjustDemand adjustDemand) {
+ ProductDemand model = repository.get(adjustDemand.getRefNo());
+ model.adjust(adjustDemand);
+ repository.save(model);
+ }
+}
diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/ProductDemandEntity.java b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/ProductDemandEntity.java
new file mode 100644
index 0000000..60fccbc
--- /dev/null
+++ b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/ProductDemandEntity.java
@@ -0,0 +1,23 @@
+package pl.com.bottega.factory.demand.forecasting;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.*;
+
+@Data
+@Entity(name = "ProductDemand")
+@NoArgsConstructor
+@EqualsAndHashCode(of = "id")
+public class ProductDemandEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ Long id;
+ @Version
+ Long version;
+ @Column
+ String refNo;
+
+}
diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/persistence/DemandDao.java b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/persistence/DemandDao.java
new file mode 100644
index 0000000..9c34bff
--- /dev/null
+++ b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/persistence/DemandDao.java
@@ -0,0 +1,15 @@
+package pl.com.bottega.factory.demand.forecasting.persistence;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+import pl.com.bottega.factory.demand.forecasting.DemandEntity;
+
+import java.time.LocalDate;
+import java.util.List;
+
+@Repository
+public interface DemandDao extends JpaRepository {
+
+ List findByProductRefNoAndDateGreaterThanEqual(String refNo, LocalDate now);
+
+}
diff --git a/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/persistence/ProductDemandDao.java b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/persistence/ProductDemandDao.java
new file mode 100644
index 0000000..95c2d65
--- /dev/null
+++ b/app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/persistence/ProductDemandDao.java
@@ -0,0 +1,13 @@
+package pl.com.bottega.factory.demand.forecasting.persistence;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+import pl.com.bottega.factory.demand.forecasting.ProductDemandEntity;
+
+@Repository
+public interface ProductDemandDao extends JpaRepository {
+
+ ProductDemandEntity findById(Long id);
+
+ ProductDemandEntity findByRefNo(String refNo);
+}
diff --git a/app-monolith/src/main/java/pl/com/bottega/tools/JsonConverter.java b/app-monolith/src/main/java/pl/com/bottega/tools/JsonConverter.java
new file mode 100644
index 0000000..b77cef9
--- /dev/null
+++ b/app-monolith/src/main/java/pl/com/bottega/tools/JsonConverter.java
@@ -0,0 +1,45 @@
+package pl.com.bottega.tools;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
+import javax.persistence.AttributeConverter;
+import java.io.IOException;
+
+public abstract class JsonConverter implements AttributeConverter {
+
+ private static final ObjectMapper objectMapper = new ObjectMapper()
+ .setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE)
+ .setVisibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.NONE)
+ .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
+ .setVisibility(PropertyAccessor.CREATOR, JsonAutoDetect.Visibility.ANY)
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .enable(SerializationFeature.WRITE_DATES_WITH_ZONE_ID);
+
+ private final Class type;
+
+ public JsonConverter(Class type) {
+ this.type = type;
+ }
+
+ @Override
+ public String convertToDatabaseColumn(T object) {
+ try {
+ return objectMapper.writeValueAsString(object);
+ } catch (JsonProcessingException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ @Override
+ public T convertToEntityAttribute(String data) {
+ try {
+ return objectMapper.readValue(data, type);
+ } catch (IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+}
diff --git a/app-monolith/src/main/java/pl/com/bottega/tools/TechnicalId.java b/app-monolith/src/main/java/pl/com/bottega/tools/TechnicalId.java
new file mode 100644
index 0000000..ee568df
--- /dev/null
+++ b/app-monolith/src/main/java/pl/com/bottega/tools/TechnicalId.java
@@ -0,0 +1,18 @@
+package pl.com.bottega.tools;
+
+public interface TechnicalId {
+
+ Long getId();
+
+ default boolean isPersisted() {
+ return getId() != null;
+ }
+
+ static Long get(Object id) {
+ return (id instanceof TechnicalId) ? ((TechnicalId) id).getId() : null;
+ }
+
+ static boolean isPersisted(Object id) {
+ return (id instanceof TechnicalId) && ((TechnicalId) id).isPersisted();
+ }
+}
diff --git a/app-monolith/src/main/resources/application.properties b/app-monolith/src/main/resources/application.properties
new file mode 100644
index 0000000..9bd460b
--- /dev/null
+++ b/app-monolith/src/main/resources/application.properties
@@ -0,0 +1,12 @@
+spring.main.banner-mode=off
+
+spring.jpa.database=default
+spring.jpa.generate-ddl=false
+
+spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
+spring.datasource.username=postgres
+spring.datasource.password=
+spring.datasource.driver-class-name=org.postgresql.Driver
+
+logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n
+logging.level.=error
diff --git a/app-monolith/src/main/resources/tasks.txt b/app-monolith/src/main/resources/tasks.txt
new file mode 100644
index 0000000..78cadc1
--- /dev/null
+++ b/app-monolith/src/main/resources/tasks.txt
@@ -0,0 +1,37 @@
+
+Rest:
+1) crud for json document
+2) query calculated
+2') query for pre-calculated projection
+3) naive REST aggregate commands
+4) command based rest
+5) application domain
+
+6) Eventual consistency
+ Read your own writes (E-tag, Expect, Retry-After)
+
+
+Aggregate Persistence:
+1) optimistic force increment
+2) normalisation of aggregate entities (Daily Demand)
+3) mapper (Daily Demand)
+4) wrapper (Product Demand)
+5) event sourcing (Product Demand)
+
+http://pillopl.github.io/eventual-consistency-and-rest/
+https://github.com/pilloPl/orders-manager
+http://groovy-lang.org/json.html
+
+
+
+
+
+vattenfall:
+
+why status is active when card is EXPIRED?
+rfid: 04938C9AF74D80, point: EVB-P1552169 2 (EVB-P1552169_2) EXPIRED Revoked(authId=04938C9AF74D80, status=ACTIVE)
+
+exceptions:
+null pointer
+more results for query single card
+
diff --git a/app-monolith/src/test/groovy/pl/com/bottega/factory/demand/forecasting/DemandRepositoryTest.groovy b/app-monolith/src/test/groovy/pl/com/bottega/factory/demand/forecasting/DemandRepositoryTest.groovy
new file mode 100644
index 0000000..79cb027
--- /dev/null
+++ b/app-monolith/src/test/groovy/pl/com/bottega/factory/demand/forecasting/DemandRepositoryTest.groovy
@@ -0,0 +1,96 @@
+package pl.com.bottega.factory.demand.forecasting
+
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.test.annotation.Commit
+import pl.com.bottega.factory.demand.forecasting.persistence.DemandDao
+import pl.com.bottega.factory.demand.forecasting.persistence.ProductDemandDao
+import spock.lang.PendingFeature
+import spock.lang.Specification
+
+import javax.transaction.Transactional
+import java.time.Clock
+import java.time.Instant
+import java.time.LocalDate
+import java.time.ZoneId
+
+@SpringBootTest
+@Transactional
+@Commit
+class DemandRepositoryTest extends Specification {
+
+ def clock = Clock.fixed(Instant.now(), ZoneId.systemDefault())
+ def events = Mock(DemandEventsMapping)
+ @Autowired
+ ProductDemandDao rootDao
+ @Autowired
+ DemandDao demandDao
+
+ DemandRepository repository
+
+ final def today = LocalDate.now(clock)
+
+ def setup() {
+ demandDao.deleteAllInBatch()
+ rootDao.deleteAllInBatch()
+ repository = new DemandRepository(clock, events, rootDao, demandDao)
+ }
+
+ @PendingFeature
+ def "persists new demand"() {
+ given:
+ rootDao.save(new ProductDemandEntity("3009000"))
+
+ when:
+ def object = repository.get("3009000")
+ object.adjust(new AdjustDemand("3009000", [
+ (today): Adjustment.strong(Demand.of(2000))
+ ]))
+ repository.save(object)
+
+ then:
+ demandDao.findAll().size() == 1
+ }
+
+ @PendingFeature
+ def "updates existing demand"() {
+ given:
+ def root = rootDao.save(new ProductDemandEntity("3009000"))
+ def demand = new DemandEntity(root, today)
+ demand.setDemand(Demand.of(1000))
+ demandDao.save(demand)
+
+ when:
+ def object = repository.get("3009000")
+ object.adjust(new AdjustDemand("3009000", [
+ (today): Adjustment.strong(Demand.of(2000))
+ ]))
+ repository.save(object)
+
+ then:
+ def demands = demandDao.findAll()
+ demands.size() == 1
+ demand.every { it.getAdjustmentLevel() == 2000 }
+ }
+
+ @PendingFeature
+ def "doesn't fetch historical data"() {
+ given:
+ def root = rootDao.save(new ProductDemandEntity("3009000"))
+ def old = new DemandEntity(root, today.minusDays(1))
+ old.setDemand(Demand.of(10000))
+ demandDao.save(old)
+
+ def todays = new DemandEntity(root, today)
+ todays.setDemand(Demand.of(1000))
+ demandDao.save(todays)
+
+ when:
+ def demands = demandDao.findByProductRefNoAndDateGreaterThanEqual("3009000", today)
+
+ then:
+ demands.size() == 1
+ demands.contains(todays)
+ !demands.contains(old)
+ }
+}
diff --git a/app-monolith/src/test/resources/application.properties b/app-monolith/src/test/resources/application.properties
new file mode 100644
index 0000000..9c8fff7
--- /dev/null
+++ b/app-monolith/src/test/resources/application.properties
@@ -0,0 +1,13 @@
+spring.main.banner-mode=off
+
+spring.jpa.database=default
+spring.jpa.generate-ddl=true
+
+spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
+spring.datasource.username=postgres
+spring.datasource.password=
+spring.datasource.driver-class-name=org.postgresql.Driver
+
+logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n
+logging.level.org.hibernate.SQL=debug
+logging.level.=error