modular monolith, context maps left in app-monolith

This commit is contained in:
Michał Michaluk
2017-12-16 19:49:41 +01:00
parent 84b3f63c00
commit a3682be9b8
82 changed files with 698 additions and 344 deletions

View 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>

View File

@@ -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()));
}
}

View File

@@ -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())
));
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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())
)
);
}
}

View File

@@ -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));
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,9 @@
package pl.com.bottega.factory.demand.forecasting;
import lombok.Value;
@Value
public class DemandValue {
Demand documented;
Adjustment adjustment;
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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())
);
}
);
}
}

View File

@@ -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
}
}

View File

@@ -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)) }
}
}