modular monolith, context maps left in app-monolith
This commit is contained in:
84
demand-forecasting-adapters/pom.xml
Normal file
84
demand-forecasting-adapters/pom.xml
Normal file
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>pl.com.bottega</groupId>
|
||||
<artifactId>demand-forecasting-adapters</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>1.5.8.RELEASE</version>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>pl.com.bottega</groupId>
|
||||
<artifactId>demand-forecasting-model</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>pl.com.bottega</groupId>
|
||||
<artifactId>adapter-commons</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.16.18</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.1.4</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.spockframework</groupId>
|
||||
<artifactId>spock-core</artifactId>
|
||||
<version>1.1-groovy-2.4</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.spockframework</groupId>
|
||||
<artifactId>spock-spring</artifactId>
|
||||
<version>1.1-groovy-2.4</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.bytebuddy</groupId>
|
||||
<artifactId>byte-buddy</artifactId>
|
||||
<version>1.7.9</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<compilerArgs>
|
||||
<arg>-parameters</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,25 @@
|
||||
package pl.com.bottega.factory.delivery.planning;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import pl.com.bottega.factory.delivery.planning.definition.DeliveryPlannerDefinitionDao;
|
||||
import pl.com.bottega.factory.delivery.planning.definition.DeliveryPlannerDefinitionEntity;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class DeliveryAutoPlannerORMRepository {
|
||||
|
||||
DeliveryPlannerDefinitionDao dao;
|
||||
|
||||
public DeliveryAutoPlanner get(String refNo) {
|
||||
return new DeliveryAutoPlanner(refNo,
|
||||
ofNullable(dao.findByRefNo(refNo))
|
||||
.map(DeliveryPlannerDefinitionEntity::getDefinition)
|
||||
.map(x -> x.map(DeliveriesSuggestion::timesAndFractions))
|
||||
.orElse(Collections.emptyMap()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package pl.com.bottega.factory.delivery.planning.definition;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Singular;
|
||||
import pl.com.bottega.factory.demand.forecasting.Demand;
|
||||
|
||||
import java.time.LocalTime;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode
|
||||
public class DeliveryPlannerDefinition {
|
||||
@Singular
|
||||
private final Map<Demand.Schema, Map<LocalTime, Double>> definitions;
|
||||
|
||||
public static Map<LocalTime, Double> of(LocalTime time, Double fraction) {
|
||||
return Collections.singletonMap(time, fraction);
|
||||
}
|
||||
|
||||
public <T> Map<Demand.Schema, T> map(Function<Map<LocalTime, Double>, T> timesAndFractions) {
|
||||
return definitions.entrySet().stream()
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
e -> timesAndFractions.apply(e.getValue())
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package pl.com.bottega.factory.delivery.planning.definition;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
|
||||
import org.springframework.data.rest.core.annotation.RestResource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
@RepositoryRestResource(
|
||||
path = "delivery-definitions",
|
||||
collectionResourceRel = "delivery-definitions",
|
||||
itemResourceRel = "delivery-definition")
|
||||
public interface DeliveryPlannerDefinitionDao extends JpaRepository<DeliveryPlannerDefinitionEntity, Long> {
|
||||
@RestResource(path = "refNos", rel = "refNos")
|
||||
DeliveryPlannerDefinitionEntity findByRefNo(String refNo);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package pl.com.bottega.factory.delivery.planning.definition;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import pl.com.bottega.tools.JsonConverter;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.io.Serializable;
|
||||
|
||||
@Entity(name = "DeliveryPlannerDefinition")
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode(of = "refNo")
|
||||
public class DeliveryPlannerDefinitionEntity implements Serializable {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
@Column
|
||||
private String refNo;
|
||||
@Column
|
||||
@Convert(converter = DescriptionAsJson.class)
|
||||
private DeliveryPlannerDefinition definition;
|
||||
|
||||
public DeliveryPlannerDefinitionEntity(String refNo, DeliveryPlannerDefinition definition) {
|
||||
this.refNo = refNo;
|
||||
this.definition = definition;
|
||||
}
|
||||
|
||||
public static class DescriptionAsJson extends JsonConverter<DeliveryPlannerDefinition> {
|
||||
public DescriptionAsJson() {
|
||||
super(DeliveryPlannerDefinition.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package pl.com.bottega.factory.delivery.planning.projection;
|
||||
|
||||
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
|
||||
import org.springframework.data.rest.core.annotation.RestResource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import pl.com.bottega.tools.ProjectionRepository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
@RepositoryRestResource(path = "delivery-forecasts",
|
||||
collectionResourceRel = "delivery-forecasts",
|
||||
itemResourceRel = "delivery-forecast")
|
||||
public interface DeliveryForecastDao extends ProjectionRepository<DeliveryForecastEntity, Long> {
|
||||
|
||||
@RestResource(path = "refNos", rel = "refNos")
|
||||
List<DeliveryForecastEntity> findByRefNoAndTimeBetween(String refNo, LocalDateTime from, LocalDateTime to);
|
||||
|
||||
@RestResource(exported = false)
|
||||
void deleteByRefNoAndDate(String refNo, LocalDate date);
|
||||
|
||||
@RestResource(exported = false)
|
||||
void deleteByRefNo(String refNo);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package pl.com.bottega.factory.delivery.planning.projection;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity(name = "DeliveryForecast")
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode(of = {"refNo", "date"})
|
||||
public class DeliveryForecastEntity implements Serializable {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
@Column
|
||||
private String refNo;
|
||||
@Column
|
||||
private LocalDate date;
|
||||
@Column
|
||||
private LocalDateTime time;
|
||||
@Column
|
||||
private long level;
|
||||
|
||||
DeliveryForecastEntity(String refNo, LocalDateTime time, long level) {
|
||||
this.refNo = refNo;
|
||||
this.date = time.toLocalDate();
|
||||
this.time = time;
|
||||
this.level = level;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package pl.com.bottega.factory.delivery.planning.projection;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import pl.com.bottega.factory.delivery.planning.DeliveryAutoPlanner;
|
||||
import pl.com.bottega.factory.delivery.planning.DeliveryAutoPlannerORMRepository;
|
||||
import pl.com.bottega.factory.demand.forecasting.Demand;
|
||||
import pl.com.bottega.factory.demand.forecasting.DemandedLevelsChanged;
|
||||
import pl.com.bottega.factory.demand.forecasting.projection.CurrentDemandDao;
|
||||
import pl.com.bottega.factory.demand.forecasting.projection.CurrentDemandEntity;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class DeliveryForecastProjection {
|
||||
|
||||
private final Clock clock;
|
||||
private final DeliveryForecastDao forecastDao;
|
||||
private final CurrentDemandDao demandDao;
|
||||
private final DeliveryAutoPlannerORMRepository planners;
|
||||
|
||||
public void persistDeliveryForecasts(DemandedLevelsChanged event) {
|
||||
DeliveryAutoPlanner planner = planners.get(event.getRefNo().getRefNo());
|
||||
event.getResults().keySet()
|
||||
.forEach(daily -> forecastDao.deleteByRefNoAndDate(
|
||||
daily.getRefNo(),
|
||||
daily.getDate())
|
||||
);
|
||||
event.getResults().entrySet().stream()
|
||||
.flatMap(entry -> planner.propose(
|
||||
entry.getKey().getDate(),
|
||||
entry.getValue().getCurrent()))
|
||||
.forEach(delivery ->
|
||||
forecastDao.save(new DeliveryForecastEntity(
|
||||
delivery.getRefNo(),
|
||||
delivery.getTime(),
|
||||
delivery.getLevel())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public void handleDeliveryPlannerDefinitionChange(String refNo) {
|
||||
List<CurrentDemandEntity> demands = demandDao.findByRefNoAndDateGreaterThanEqual(refNo, LocalDate.now(clock));
|
||||
DeliveryAutoPlanner planner = planners.get(refNo);
|
||||
forecastDao.deleteByRefNo(refNo);
|
||||
demands.stream()
|
||||
.flatMap(entry -> planner.propose(
|
||||
entry.getDate(),
|
||||
Demand.of(entry.getLevel(), entry.getSchema())))
|
||||
.forEach(delivery ->
|
||||
forecastDao.save(new DeliveryForecastEntity(
|
||||
delivery.getRefNo(),
|
||||
delivery.getTime(),
|
||||
delivery.getLevel())
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package pl.com.bottega.factory.demand.forecasting;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import pl.com.bottega.factory.demand.forecasting.DailyDemand.DemandUpdated;
|
||||
import pl.com.bottega.factory.demand.forecasting.persistence.DemandDao;
|
||||
import pl.com.bottega.factory.demand.forecasting.persistence.DemandEntity;
|
||||
import pl.com.bottega.factory.demand.forecasting.persistence.DemandEntity.DemandEntityId;
|
||||
import pl.com.bottega.factory.demand.forecasting.persistence.ProductDemandDao;
|
||||
import pl.com.bottega.factory.demand.forecasting.persistence.ProductDemandEntity;
|
||||
import pl.com.bottega.factory.product.management.RefNoId;
|
||||
import pl.com.bottega.tools.TechnicalId;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.LockModeType;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
class DemandORMRepository {
|
||||
|
||||
private final Clock clock;
|
||||
private final DemandEvents events;
|
||||
private final ReviewPolicy reviewPolicy = ReviewPolicy.BASIC;
|
||||
private final EntityManager em;
|
||||
private final ProductDemandDao rootDao;
|
||||
private final DemandDao demandDao;
|
||||
|
||||
ProductDemand get(String refNo) {
|
||||
ProductDemandEntity root = rootDao.findByRefNo(refNo);
|
||||
RefNoId id = root.createId();
|
||||
|
||||
Map<LocalDate, DemandEntity> data =
|
||||
demandDao.findByProductRefNoAndDateGreaterThanEqual(refNo, LocalDate.now(clock)).stream()
|
||||
.collect(toMap(
|
||||
DemandEntity::getDate,
|
||||
Function.identity()
|
||||
));
|
||||
|
||||
UnitOfWork unitOfWork = new UnitOfWork();
|
||||
Demands demands = new Demands();
|
||||
demands.fetch = date -> map(refNo, date, data, unitOfWork);
|
||||
return new ProductDemand(id, demands, unitOfWork, clock, events);
|
||||
}
|
||||
|
||||
private DailyDemand map(String refNo, LocalDate date,
|
||||
Map<LocalDate, DemandEntity> data,
|
||||
UnitOfWork unitOfWork) {
|
||||
return ofNullable(data.get(date))
|
||||
.map(entity -> new DailyDemand(
|
||||
entity.createId(),
|
||||
unitOfWork,
|
||||
reviewPolicy,
|
||||
entity.getValue().getDocumented(),
|
||||
entity.getValue().getAdjustment()))
|
||||
.orElseGet(() -> new DailyDemand(
|
||||
new DemandEntityId(refNo, date),
|
||||
unitOfWork,
|
||||
reviewPolicy,
|
||||
null,
|
||||
null
|
||||
));
|
||||
}
|
||||
|
||||
void save(ProductDemand model) {
|
||||
ProductDemandEntity root = rootDao.findOne(TechnicalId.get(model.id));
|
||||
if (model.unit.updates().size() > 0) {
|
||||
em.lock(root, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
|
||||
}
|
||||
for (DemandUpdated updated : model.unit.updates()) {
|
||||
DemandEntity entity;
|
||||
if (TechnicalId.isPersisted(updated.getId())) {
|
||||
entity = demandDao.getOne(TechnicalId.get(updated.getId()));
|
||||
} else {
|
||||
entity = new DemandEntity(
|
||||
updated.getId().getRefNo(),
|
||||
updated.getId().getDate()
|
||||
);
|
||||
demandDao.save(entity);
|
||||
}
|
||||
entity.setValue(new DemandValue(
|
||||
updated.getDocumented().nullIfNone(),
|
||||
updated.getAdjustment()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void initDemandsFor(String refNo) {
|
||||
if (rootDao.findByRefNo(refNo) == null) {
|
||||
rootDao.save(new ProductDemandEntity(refNo));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package pl.com.bottega.factory.demand.forecasting;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import pl.com.bottega.factory.demand.forecasting.ReviewRequested.ReviewNeeded;
|
||||
|
||||
import javax.transaction.Transactional;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
@AllArgsConstructor
|
||||
public class DemandService {
|
||||
|
||||
private final DemandORMRepository repository;
|
||||
|
||||
public void init(String refNo) {
|
||||
repository.initDemandsFor(refNo);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public void review(ReviewNeeded review, ReviewDecision decision) {
|
||||
ProductDemand model = repository.get(review.getRefNo());
|
||||
model.review(review, decision);
|
||||
repository.save(model);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package pl.com.bottega.factory.demand.forecasting;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
@Value
|
||||
public class DemandValue {
|
||||
Demand documented;
|
||||
Adjustment adjustment;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package pl.com.bottega.factory.demand.forecasting.command;
|
||||
|
||||
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
|
||||
import org.springframework.data.rest.core.annotation.RestResource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import pl.com.bottega.tools.CommandRepository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Repository
|
||||
@RepositoryRestResource(path = "demand-adjustments",
|
||||
collectionResourceRel = "demand-adjustments",
|
||||
itemResourceRel = "demand-adjustment")
|
||||
public interface DemandAdjustmentDao extends CommandRepository<DemandAdjustmentEntity, Long> {
|
||||
@RestResource(exported = false)
|
||||
void deleteByCleanAfterGreaterThanEqual(LocalDate date);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package pl.com.bottega.factory.demand.forecasting.command;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import pl.com.bottega.factory.demand.forecasting.AdjustDemand;
|
||||
import pl.com.bottega.tools.JsonConverter;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity(name = "DemandAdjustment")
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode(of = "id")
|
||||
public class DemandAdjustmentEntity implements Serializable {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@Column
|
||||
private String note;
|
||||
@Column
|
||||
private String customerRepresentative;
|
||||
@Column
|
||||
@Setter
|
||||
private LocalDate cleanAfter;
|
||||
|
||||
@Column(length = 4096)
|
||||
@Convert(converter = AdjustDemandAsJson.class)
|
||||
private AdjustDemand adjustment;
|
||||
|
||||
public static class AdjustDemandAsJson extends JsonConverter<AdjustDemand> {
|
||||
public AdjustDemandAsJson() {
|
||||
super(AdjustDemand.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package pl.com.bottega.factory.demand.forecasting.command;
|
||||
|
||||
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
|
||||
import org.springframework.data.rest.core.annotation.RestResource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import pl.com.bottega.tools.CommandRepository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
@RepositoryRestResource(
|
||||
path = "demand-reviews",
|
||||
collectionResourceRel = "demand-reviews",
|
||||
itemResourceRel = "demand-review")
|
||||
public interface DemandReviewDao extends CommandRepository<DemandReviewEntity, Long> {
|
||||
@RestResource(path = "refNos", rel = "refNos")
|
||||
List<DemandReviewEntity> findByRefNoAndDecisionIsNull(String refNo);
|
||||
|
||||
@RestResource(exported = false)
|
||||
void deleteByCleanAfterGreaterThanEqual(LocalDate date);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package pl.com.bottega.factory.demand.forecasting.command;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import pl.com.bottega.factory.demand.forecasting.ReviewDecision;
|
||||
import pl.com.bottega.factory.demand.forecasting.ReviewRequested.ReviewNeeded;
|
||||
import pl.com.bottega.tools.JsonConverter;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity(name = "DemandReview")
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode(of = "id")
|
||||
public class DemandReviewEntity implements Serializable {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@Column
|
||||
private String refNo;
|
||||
@Column
|
||||
private LocalDate date;
|
||||
@Column
|
||||
private Instant timestamp;
|
||||
@Column
|
||||
@Convert(converter = ReviewAsJson.class)
|
||||
private ReviewNeeded review;
|
||||
|
||||
@Column
|
||||
@Enumerated(EnumType.STRING)
|
||||
private ReviewDecision decision;
|
||||
@Column
|
||||
@Setter
|
||||
private LocalDate cleanAfter;
|
||||
|
||||
public DemandReviewEntity(Instant timestamp, ReviewNeeded review) {
|
||||
this.timestamp = timestamp;
|
||||
this.refNo = review.getId().getRefNo();
|
||||
this.date = review.getId().getDate();
|
||||
this.review = review;
|
||||
}
|
||||
|
||||
public boolean decisionTaken() {
|
||||
return decision != null;
|
||||
}
|
||||
|
||||
public static class ReviewAsJson extends JsonConverter<ReviewNeeded> {
|
||||
public ReviewAsJson() {
|
||||
super(ReviewNeeded.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
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.HandleBeforeCreate;
|
||||
import org.springframework.data.rest.core.annotation.HandleBeforeSave;
|
||||
import org.springframework.data.rest.core.annotation.RepositoryEventHandler;
|
||||
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 java.time.Clock;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
@RepositoryEventHandler
|
||||
public class Handler {
|
||||
|
||||
private final DemandService service;
|
||||
private final DemandAdjustmentDao adjustments;
|
||||
private final DemandReviewDao reviews;
|
||||
private final Clock clock;
|
||||
|
||||
@HandleBeforeCreate
|
||||
@HandleBeforeSave
|
||||
public void adjust(DemandAdjustmentEntity adjustment) {
|
||||
LocalDate latest = adjustment.getAdjustment()
|
||||
.latestAdjustment()
|
||||
.orElse(LocalDate.now(clock));
|
||||
adjustment.setCleanAfter(latest.plusDays(7));
|
||||
service.adjust(adjustment.getAdjustment());
|
||||
}
|
||||
|
||||
@HandleBeforeSave
|
||||
public void review(DemandReviewEntity review) {
|
||||
if (review.decisionTaken()) {
|
||||
review.setCleanAfter(LocalDate.now(clock).plusDays(7));
|
||||
service.review(review.getReview(), review.getDecision());
|
||||
}
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 0 12 * * ?")
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
@Transactional
|
||||
public void clean() {
|
||||
adjustments.deleteByCleanAfterGreaterThanEqual(LocalDate.now(clock));
|
||||
reviews.deleteByCleanAfterGreaterThanEqual(LocalDate.now(clock));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package pl.com.bottega.factory.demand.forecasting.persistence;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.rest.core.annotation.RestResource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
@RestResource(exported = false)
|
||||
public interface DemandDao extends JpaRepository<DemandEntity, Long> {
|
||||
List<DemandEntity> findByProductRefNoAndDateGreaterThanEqual(String refNo, LocalDate now);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package pl.com.bottega.factory.demand.forecasting.persistence;
|
||||
|
||||
import lombok.*;
|
||||
import pl.com.bottega.factory.demand.forecasting.DailyId;
|
||||
import pl.com.bottega.factory.demand.forecasting.DemandValue;
|
||||
import pl.com.bottega.tools.JsonConverter;
|
||||
import pl.com.bottega.tools.TechnicalId;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity(name = "Demand")
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode(of = "id")
|
||||
@ToString
|
||||
public class DemandEntity implements Serializable {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
@Column
|
||||
String refNo;
|
||||
@Column
|
||||
private LocalDate date;
|
||||
@Column
|
||||
@Convert(converter = DemandAsJson.class)
|
||||
@Setter
|
||||
private DemandValue value;
|
||||
|
||||
public DemandEntity(String refNo, LocalDate date) {
|
||||
this.refNo = refNo;
|
||||
this.date = date;
|
||||
this.value = new DemandValue(null, null);
|
||||
}
|
||||
|
||||
public DemandEntityId createId() {
|
||||
return new DemandEntityId(refNo, date, id);
|
||||
}
|
||||
|
||||
public static class DemandAsJson extends JsonConverter<DemandValue> {
|
||||
public DemandAsJson() {
|
||||
super(DemandValue.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static class DemandEntityId extends DailyId implements TechnicalId {
|
||||
|
||||
private Long id;
|
||||
|
||||
public DemandEntityId(String refNo, LocalDate date) {
|
||||
super(refNo, date);
|
||||
}
|
||||
|
||||
DemandEntityId(String refNo, LocalDate date, Long id) {
|
||||
super(refNo, date);
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package pl.com.bottega.factory.demand.forecasting.persistence;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.rest.core.annotation.RestResource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
@RestResource(exported = false)
|
||||
public interface ProductDemandDao extends JpaRepository<ProductDemandEntity, Long> {
|
||||
ProductDemandEntity findByRefNo(String refNo);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package pl.com.bottega.factory.demand.forecasting.persistence;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import pl.com.bottega.factory.product.management.RefNoId;
|
||||
import pl.com.bottega.tools.TechnicalId;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.io.Serializable;
|
||||
|
||||
@Entity(name = "ProductDemand")
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode(of = "id")
|
||||
public class ProductDemandEntity implements Serializable {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
@Version
|
||||
private Long version;
|
||||
@Column
|
||||
String refNo;
|
||||
|
||||
public ProductDemandEntity(String refNo) {
|
||||
this.refNo = refNo;
|
||||
}
|
||||
|
||||
public ProductDemandEntityId createId() {
|
||||
return new ProductDemandEntityId(refNo, id);
|
||||
}
|
||||
|
||||
@Getter
|
||||
static class ProductDemandEntityId extends RefNoId implements TechnicalId {
|
||||
|
||||
Long id;
|
||||
|
||||
ProductDemandEntityId(String refNo) {
|
||||
super(refNo);
|
||||
}
|
||||
|
||||
ProductDemandEntityId(String refNo, long id) {
|
||||
super(refNo);
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package pl.com.bottega.factory.demand.forecasting.projection;
|
||||
|
||||
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
|
||||
import org.springframework.data.rest.core.annotation.RestResource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import pl.com.bottega.tools.ProjectionRepository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
@RepositoryRestResource(path = "demand-forecasts",
|
||||
collectionResourceRel = "demand-forecasts",
|
||||
itemResourceRel = "demand-forecast")
|
||||
public interface CurrentDemandDao extends ProjectionRepository<CurrentDemandEntity, Long> {
|
||||
@RestResource(path = "refNos", rel = "refNos")
|
||||
List<CurrentDemandEntity> findByRefNoAndDateGreaterThanEqual(String refNo, LocalDate date);
|
||||
|
||||
@RestResource(exported = false)
|
||||
void deleteByRefNoAndDate(String refNo, LocalDate date);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package pl.com.bottega.factory.demand.forecasting.projection;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import pl.com.bottega.factory.demand.forecasting.Demand;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity(name = "CurrentDemand")
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode(of = "id")
|
||||
public class CurrentDemandEntity implements Serializable {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
@Column
|
||||
private String refNo;
|
||||
@Column
|
||||
private LocalDate date;
|
||||
@Column
|
||||
private long level;
|
||||
@Column
|
||||
@Enumerated(EnumType.STRING)
|
||||
private Demand.Schema schema;
|
||||
|
||||
CurrentDemandEntity(String refNo, LocalDate date, long level, Demand.Schema schema) {
|
||||
this.refNo = refNo;
|
||||
this.date = date;
|
||||
this.level = level;
|
||||
this.schema = schema;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package pl.com.bottega.factory.demand.forecasting.projection;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import pl.com.bottega.factory.demand.forecasting.DemandedLevelsChanged;
|
||||
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class CurrentDemandProjection {
|
||||
|
||||
private final CurrentDemandDao demandDao;
|
||||
|
||||
public void persistCurrentDemands(DemandedLevelsChanged event) {
|
||||
event.getResults().forEach((daily, change) -> {
|
||||
demandDao.deleteByRefNoAndDate(
|
||||
daily.getRefNo(),
|
||||
daily.getDate());
|
||||
demandDao.save(new CurrentDemandEntity(
|
||||
daily.getRefNo(),
|
||||
daily.getDate(),
|
||||
change.getCurrent().getLevel(),
|
||||
change.getCurrent().getSchema())
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package pl.com.bottega.factory.delivery.planning
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import pl.com.bottega.factory.delivery.planning.definition.DeliveryPlannerDefinition
|
||||
import pl.com.bottega.factory.delivery.planning.definition.DeliveryPlannerDefinitionDao
|
||||
import pl.com.bottega.factory.delivery.planning.definition.DeliveryPlannerDefinitionEntity
|
||||
import spock.lang.Specification
|
||||
|
||||
import static java.time.LocalTime.of as time
|
||||
import static pl.com.bottega.factory.delivery.planning.definition.DeliveryPlannerDefinition.of
|
||||
import static pl.com.bottega.factory.demand.forecasting.Demand.Schema.*
|
||||
|
||||
@SpringBootTest
|
||||
class DeliveryPlannerDefinitionTest extends Specification {
|
||||
|
||||
@Autowired
|
||||
DeliveryPlannerDefinitionDao dao
|
||||
|
||||
void setup() {
|
||||
dao.deleteAllInBatch()
|
||||
}
|
||||
|
||||
def "verify access to DeliveryPlannerDefinition data"() {
|
||||
given:
|
||||
def definition = DeliveryPlannerDefinition.builder()
|
||||
.definition(AtDayStart, of(time(06, 00), 1.0d))
|
||||
.definition(TillDayEnd, of(time(22, 00), 1.0d))
|
||||
.definition(Twice, [(time(16, 00)): 0.5d, (time(20, 00)): 0.5d])
|
||||
.build()
|
||||
|
||||
dao.save(new DeliveryPlannerDefinitionEntity("3009000", definition))
|
||||
|
||||
when:
|
||||
def entities = dao.findAll()
|
||||
|
||||
then:
|
||||
entities.size() == 1
|
||||
entities.get(0).definition == definition
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
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.DemandEntity
|
||||
import pl.com.bottega.factory.demand.forecasting.persistence.ProductDemandDao
|
||||
import pl.com.bottega.factory.demand.forecasting.persistence.ProductDemandEntity
|
||||
import spock.lang.Specification
|
||||
|
||||
import javax.persistence.EntityManager
|
||||
import javax.transaction.Transactional
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
|
||||
@SpringBootTest
|
||||
@Transactional
|
||||
@Commit
|
||||
class DemandORMRepositoryTest extends Specification {
|
||||
|
||||
def clock = Clock.fixed(Instant.now(), ZoneId.systemDefault())
|
||||
def events = Mock(DemandEvents)
|
||||
@Autowired
|
||||
EntityManager em
|
||||
@Autowired
|
||||
ProductDemandDao rootDao
|
||||
@Autowired
|
||||
DemandDao demandDao
|
||||
|
||||
DemandORMRepository repository
|
||||
|
||||
final def today = LocalDate.now(clock)
|
||||
final def refNo = "3009000"
|
||||
|
||||
def setup() {
|
||||
demandDao.deleteAllInBatch()
|
||||
rootDao.deleteAllInBatch()
|
||||
repository = new DemandORMRepository(clock, events, em, rootDao, demandDao)
|
||||
}
|
||||
|
||||
def "persists new demand"() {
|
||||
given:
|
||||
noDemandsInDB()
|
||||
|
||||
when:
|
||||
def object = demandIsLoadedFromDB()
|
||||
object.adjust(demandAdjustment(today, 2000))
|
||||
repository.save(object)
|
||||
|
||||
then:
|
||||
def demandsInDB = demandDao.findAll()
|
||||
demandsInDB.size() == 1
|
||||
demandsInDB.every hasAdjustment(2000)
|
||||
}
|
||||
|
||||
def "updates existing demand"() {
|
||||
given:
|
||||
demandInDB((today): 1000)
|
||||
|
||||
when:
|
||||
def object = demandIsLoadedFromDB()
|
||||
object.adjust(demandAdjustment(today, 2000))
|
||||
repository.save(object)
|
||||
|
||||
then:
|
||||
def demandsInDB = demandDao.findAll()
|
||||
demandsInDB.size() == 1
|
||||
demandsInDB.every hasAdjustment(2000)
|
||||
}
|
||||
|
||||
def "doesn't fetch historical data"() {
|
||||
given:
|
||||
demandInDB((today.minusDays(1)): 10000, (today): 1000)
|
||||
|
||||
when:
|
||||
def demands = demandDao.findByProductRefNoAndDateGreaterThanEqual(refNo, today)
|
||||
|
||||
then:
|
||||
demands.size() == 1
|
||||
demands.every { it -> it.date == today }
|
||||
}
|
||||
|
||||
private ProductDemandEntity noDemandsInDB() {
|
||||
rootDao.save(new ProductDemandEntity(refNo))
|
||||
}
|
||||
|
||||
private void demandInDB(Map<LocalDate, Long> demands) {
|
||||
def root = rootDao.save(new ProductDemandEntity(refNo))
|
||||
demands.each { date, level ->
|
||||
def demand = new DemandEntity(root, date)
|
||||
demand.setValue(new DemandValue(Demand.of(level), null))
|
||||
demandDao.save(demand)
|
||||
}
|
||||
}
|
||||
|
||||
private AdjustDemand demandAdjustment(LocalDate date, long level) {
|
||||
new AdjustDemand(refNo, [
|
||||
(date): Adjustment.strong(Demand.of(level))
|
||||
])
|
||||
}
|
||||
|
||||
private ProductDemand demandIsLoadedFromDB() {
|
||||
repository.get(refNo)
|
||||
}
|
||||
|
||||
private def hasAdjustment(long level) {
|
||||
return { it.get().getAdjustment() == Adjustment.strong(Demand.of(level)) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user