demand changes review processing

This commit is contained in:
Michał Michaluk
2017-12-12 23:23:15 +01:00
parent 277f5f9822
commit c6e0b9baf8
27 changed files with 550 additions and 110 deletions

View File

@@ -5,7 +5,7 @@ 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.DemandEvents;
import pl.com.bottega.factory.demand.forecasting.DemandEvents.DemandedLevelsChanged;
import pl.com.bottega.factory.demand.forecasting.projection.CurrentDemandDao;
import pl.com.bottega.factory.demand.forecasting.projection.CurrentDemandEntity;
@@ -15,15 +15,14 @@ import java.util.List;
@Component
@AllArgsConstructor
public class DeliveryForecastProjection implements DemandEvents {
public class DeliveryForecastProjection {
private final Clock clock;
private final DeliveryForecastDao forecastDao;
private final CurrentDemandDao demandDao;
private final DeliveryAutoPlannerORMRepository planners;
@Override
public void emit(DemandedLevelsChanged event) {
public void persistDeliveryForecasts(DemandedLevelsChanged event) {
DeliveryAutoPlanner planner = planners.get(event.getRefNo().getRefNo());
event.getResults().keySet()
.forEach(daily -> forecastDao.deleteByRefNoAndDate(

View File

@@ -4,9 +4,15 @@ import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import pl.com.bottega.factory.delivery.planning.projection.DeliveryForecastProjection;
import pl.com.bottega.factory.demand.forecasting.command.DemandReviewDao;
import pl.com.bottega.factory.demand.forecasting.command.DemandReviewEntity;
import pl.com.bottega.factory.demand.forecasting.projection.CurrentDemandProjection;
import pl.com.bottega.factory.shortages.prediction.ShortagePredictionEventsMapping;
import java.time.Clock;
import java.time.Instant;
import java.util.stream.Collectors;
@Lazy
@Component
@AllArgsConstructor
@@ -15,11 +21,21 @@ class DemandEventsMapping implements DemandEvents {
private final CurrentDemandProjection demands;
private final DeliveryForecastProjection deliveries;
private final ShortagePredictionEventsMapping predictions;
private final DemandReviewDao reviews;
private final Clock clock;
@Override
public void emit(DemandedLevelsChanged event) {
demands.emit(event);
deliveries.emit(event);
predictions.emit(event);
demands.persistCurrentDemands(event);
deliveries.persistDeliveryForecasts(event);
predictions.predictShortages(event);
}
@Override
public void emit(ReviewRequested event) {
reviews.save(event.getReviews().stream()
.map(review -> new DemandReviewEntity(Instant.now(clock), review))
.collect(Collectors.toList())
);
}
}

View File

@@ -23,11 +23,12 @@ import static java.util.stream.Collectors.toMap;
@AllArgsConstructor
class DemandORMRepository {
private Clock clock;
private DemandEventsMapping events;
private EntityManager em;
private ProductDemandDao rootDao;
private DemandDao demandDao;
private final Clock clock;
private final DemandEventsMapping 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);
@@ -53,11 +54,13 @@ class DemandORMRepository {
.map(entity -> new DailyDemand(
entity.createId(),
unitOfWork,
reviewPolicy,
entity.get().getDocumented(),
entity.get().getAdjustment()))
.orElseGet(() -> new DailyDemand(
new DemandEntityId(refNo, date),
unitOfWork,
reviewPolicy,
null,
null
));

View File

@@ -2,6 +2,7 @@ package pl.com.bottega.factory.demand.forecasting;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import pl.com.bottega.factory.demand.forecasting.DemandEvents.ReviewRequested.ReviewNeeded;
import javax.transaction.Transactional;
@@ -23,4 +24,10 @@ public class DemandService {
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,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.DemandEvents.ReviewRequested.ReviewNeeded;
import pl.com.bottega.factory.demand.forecasting.ReviewDecision;
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

@@ -21,16 +21,25 @@ public class Handler {
private final DemandService service;
private final DemandAdjustmentDao adjustments;
private final DemandReviewDao reviews;
private final Clock clock;
@HandleBeforeCreate
@HandleBeforeSave
public void adjustDemand(DemandAdjustmentEntity resource) {
LocalDate latest = resource.getAdjustment()
public void adjust(DemandAdjustmentEntity adjustment) {
LocalDate latest = adjustment.getAdjustment()
.latestAdjustment()
.orElse(LocalDate.now(clock));
resource.setCleanAfter(latest.plusDays(7));
service.adjust(resource.getAdjustment());
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 * * ?")
@@ -38,5 +47,6 @@ public class Handler {
@Transactional
public void clean() {
adjustments.deleteByCleanAfterGreaterThanEqual(LocalDate.now(clock));
reviews.deleteByCleanAfterGreaterThanEqual(LocalDate.now(clock));
}
}

View File

@@ -2,16 +2,15 @@ package pl.com.bottega.factory.demand.forecasting.projection;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
import pl.com.bottega.factory.demand.forecasting.DemandEvents;
import pl.com.bottega.factory.demand.forecasting.DemandEvents.DemandedLevelsChanged;
@Component
@AllArgsConstructor
public class CurrentDemandProjection implements DemandEvents {
public class CurrentDemandProjection {
private final CurrentDemandDao demandDao;
@Override
public void emit(DemandedLevelsChanged event) {
public void persistCurrentDemands(DemandedLevelsChanged event) {
event.getResults().forEach((daily, change) -> {
demandDao.deleteByRefNoAndDate(
daily.getRefNo(),

View File

@@ -3,25 +3,24 @@ package pl.com.bottega.factory.shortages.prediction;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import pl.com.bottega.factory.demand.forecasting.DemandEvents;
import pl.com.bottega.factory.demand.forecasting.DemandEvents.DemandedLevelsChanged;
import pl.com.bottega.factory.shortages.prediction.monitoring.ShortagePredictionProcess;
import pl.com.bottega.factory.shortages.prediction.monitoring.ShortagePredictionProcessRepository;
@Lazy
@Component
@AllArgsConstructor
public class ShortagePredictionEventsMapping implements DemandEvents {
public class ShortagePredictionEventsMapping {
private final ShortagePredictionProcessRepository repository;
@Override
public void emit(DemandedLevelsChanged event) {
public void predictShortages(DemandedLevelsChanged event) {
ShortagePredictionProcess model = repository.get(event.getRefNo());
model.onDemandChanged();
repository.save(model);
}
//public void emit(ProductionChanged event) { service.onPlanChanged(event.getId().getRefNo()); }
//public void predictShortages(ProductionChanged event) { service.onPlanChanged(event.getId().getRefNo()); }
//public void emit(StockChanged event) { service.onStockChanged(event.getId().getRefNo()); }
//public void predictShortages(StockChanged event) { service.onStockChanged(event.getId().getRefNo()); }
}

View File

@@ -12,7 +12,7 @@ class Adjustment {
return new Adjustment(demand, true);
}
static Adjustment week(Demand demand) {
static Adjustment weak(Demand demand) {
return new Adjustment(demand, false);
}

View File

@@ -2,6 +2,7 @@ package pl.com.bottega.factory.demand.forecasting;
import lombok.Value;
import pl.com.bottega.factory.demand.forecasting.DemandEvents.DemandedLevelsChanged.Change;
import pl.com.bottega.factory.demand.forecasting.DemandEvents.ReviewRequested.ReviewNeeded;
import java.util.Objects;
import java.util.Optional;
@@ -10,6 +11,7 @@ class DailyDemand {
private final DailyId id;
private final Events events;
private final ReviewPolicy policy;
private Demand documented;
private Adjustment adjustment;
@@ -17,15 +19,16 @@ class DailyDemand {
interface Events {
void emit(LevelChanged event);
void emit(ReviewRequest event);
void emit(ReviewNeeded event);
void emit(DemandUpdated event);
}
DailyDemand(DailyId id, Events events,
DailyDemand(DailyId id, Events events, ReviewPolicy policy,
Demand documented, Adjustment adjustment) {
this.id = id;
this.events = events;
this.policy = policy;
this.documented = Optional.ofNullable(documented)
.orElse(Demand.nothingDemanded());
this.adjustment = adjustment;
@@ -45,6 +48,13 @@ class DailyDemand {
void update(Demand documented) {
State state = state();
if (policy.reviewNeeded(this.documented, this.adjustment, documented)) {
events.emit(new ReviewNeeded(id,
this.documented,
this.adjustment.getDemand(),
documented)
);
}
if (!Adjustment.isStrong(this.adjustment)) {
this.adjustment = null;
}
@@ -70,14 +80,6 @@ class DailyDemand {
Change change;
}
@Value
static class ReviewRequest {
DailyId id;
Demand previousDocumented;
Demand strongAdjustment;
Demand newDocumented;
}
@Value
static class DemandUpdated {
DailyId id;

View File

@@ -2,6 +2,8 @@ package pl.com.bottega.factory.demand.forecasting;
import lombok.AllArgsConstructor;
import pl.com.bottega.factory.demand.forecasting.DemandEvents.DemandedLevelsChanged;
import pl.com.bottega.factory.demand.forecasting.DemandEvents.ReviewRequested;
import pl.com.bottega.factory.demand.forecasting.DemandEvents.ReviewRequested.ReviewNeeded;
import pl.com.bottega.factory.product.management.RefNoId;
import java.time.Clock;
@@ -39,6 +41,15 @@ class ProductDemand {
if (unit.anyChanges()) {
events.emit(new DemandedLevelsChanged(id, unit.changes()));
}
if (unit.anyReviews()) {
events.emit(new ReviewRequested(id, unit.reviews()));
}
}
void review(ReviewNeeded review, ReviewDecision decision) {
if (decision.requireAdjustment()) {
adjust(decision.toAdjustment(review));
}
}
private void adjustDaily(LocalDate date, Adjustment adjustment) {

View File

@@ -0,0 +1,33 @@
package pl.com.bottega.factory.demand.forecasting;
import lombok.AllArgsConstructor;
import pl.com.bottega.factory.demand.forecasting.DemandEvents.ReviewRequested.ReviewNeeded;
import java.util.Collections;
import java.util.function.Function;
@AllArgsConstructor
public enum ReviewDecision {
IGNORE(r -> null),
PICK_PREVIOUS(ReviewNeeded::getPreviousDocumented),
MAKE_ADJUSTMENT_WEAK(ReviewNeeded::getAdjustment),
PICK_NEW(ReviewNeeded::getNewDocumented);
private final Function<ReviewNeeded, Demand> pick;
public AdjustDemand toAdjustment(ReviewNeeded review) {
if (this == IGNORE) {
throw new IllegalStateException("can't convert " + this + " to adjustment");
}
return new AdjustDemand(review.getRefNo(),
Collections.singletonMap(
review.getDate(),
Adjustment.weak(pick.apply(review))
)
);
}
public boolean requireAdjustment() {
return this != IGNORE;
}
}

View File

@@ -0,0 +1,15 @@
package pl.com.bottega.factory.demand.forecasting;
public interface ReviewPolicy {
ReviewPolicy BASIC = (previousDocumented, adjustment, newDocumented) ->
Adjustment.isStrong(adjustment)
&& !newDocumented.equals(previousDocumented)
&& !newDocumented.equals(adjustment.getDemand());
boolean reviewNeeded(
Demand previousDocumented,
Adjustment adjustment,
Demand newDocumented
);
}

View File

@@ -1,20 +1,23 @@
package pl.com.bottega.factory.demand.forecasting;
import pl.com.bottega.factory.demand.forecasting.DemandEvents.DemandedLevelsChanged;
import pl.com.bottega.factory.demand.forecasting.DemandEvents.ReviewRequested.ReviewNeeded;
import java.util.*;
import static java.util.Collections.unmodifiableList;
class UnitOfWork implements DailyDemand.Events {
Map<DailyId, DemandEvents.DemandedLevelsChanged.Change> changes = new HashMap<>();
List<DailyDemand.ReviewRequest> reviews = new LinkedList<>();
Map<DailyId, DemandedLevelsChanged.Change> changes = new HashMap<>();
List<ReviewNeeded> reviews = new LinkedList<>();
List<DailyDemand.DemandUpdated> updates = new LinkedList<>();
boolean anyChanges() {
return !changes.isEmpty();
}
Map<DailyId, DemandEvents.DemandedLevelsChanged.Change> changes() {
Map<DailyId, DemandedLevelsChanged.Change> changes() {
return Collections.unmodifiableMap(changes);
}
@@ -22,7 +25,7 @@ class UnitOfWork implements DailyDemand.Events {
return !reviews.isEmpty();
}
List<DailyDemand.ReviewRequest> reviews() {
List<ReviewNeeded> reviews() {
return Collections.unmodifiableList(reviews);
}
@@ -36,7 +39,7 @@ class UnitOfWork implements DailyDemand.Events {
}
@Override
public void emit(DailyDemand.ReviewRequest event) {
public void emit(ReviewNeeded event) {
reviews.add(event);
}

View File

@@ -5,12 +5,14 @@ import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import static pl.com.bottega.factory.demand.forecasting.DemandEvents.DemandedLevelsChanged.Change;
import static pl.com.bottega.factory.demand.forecasting.DemandEvents.DemandedLevelsChanged.Change
import static pl.com.bottega.factory.demand.forecasting.DemandEvents.ReviewRequested.ReviewNeeded
class DailyDemandBuilder {
Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault())
DailyDemand.Events events
ReviewPolicy policy = ReviewPolicy.BASIC
String refNo = "3009000"
private LocalDate date = LocalDate.now(clock)
@@ -18,7 +20,7 @@ class DailyDemandBuilder {
private Adjustment adjustment
DailyDemand build() {
new DailyDemand(new DailyId(refNo, date), events, base, adjustment)
new DailyDemand(new DailyId(refNo, date), events, policy, base, adjustment)
}
DailyDemandBuilder reset() {
@@ -60,6 +62,11 @@ class DailyDemandBuilder {
this
}
DailyDemandBuilder demandedLevels(Demand level) {
base = level
this
}
DailyDemandBuilder adjustedTo(long level) {
adjustment = new Adjustment(Demand.of(level), false)
this
@@ -84,4 +91,13 @@ class DailyDemandBuilder {
new Change(Demand.of(previous), Demand.of(current))
)
}
ReviewNeeded reviewRequest(long previousDocumented, long adjustment, long newDocumented) {
new ReviewNeeded(
new DailyId(refNo, date),
Demand.of(previousDocumented),
Demand.of(adjustment),
Demand.of(newDocumented)
)
}
}

View File

@@ -6,15 +6,18 @@ import java.time.LocalDate
import static pl.com.bottega.factory.demand.forecasting.DemandEvents.DemandedLevelsChanged
class DemandAdjustmentSpec extends Specification {
class DemandAdjustmentSpec extends Specification implements ProductDemandTrait {
def events = Mock(DemandEvents)
def builder = new ProductDemandBuilder(events: events)
void setup() {
builder = new ProductDemandBuilder(events: events)
}
def "Adjusted demands should be stored"() {
given:
def today = LocalDate.now(builder.clock)
def demand = demand(2800, 0)
def demand = demanded(2800, 0)
def adjustments = adjustments([(today): 1000])
when:
@@ -27,7 +30,7 @@ class DemandAdjustmentSpec extends Specification {
def "Adjustment of future demands is possible"() {
given:
def today = LocalDate.now(builder.clock)
def demand = demand(2800)
def demand = demanded(2800)
def adjustments = adjustments([(today.plusDays(1)): 1000])
when:
@@ -40,7 +43,7 @@ class DemandAdjustmentSpec extends Specification {
def "Adjustment without changes should not generate event"() {
given:
def today = LocalDate.now(builder.clock)
def demand = demand(2800, 1000)
def demand = demanded(2800, 1000)
def adjustments = adjustments([(today): 2800, (today.plusDays(1)): 1000])
when:
@@ -53,7 +56,7 @@ class DemandAdjustmentSpec extends Specification {
def "Should skip past demands adjustments"() {
given:
def pastDate = LocalDate.now(builder.clock).minusDays(2)
def demand = demand(2800, 0)
def demand = demanded(2800, 0)
def adjustments = adjustments([(pastDate): 1000])
when:
@@ -66,7 +69,7 @@ class DemandAdjustmentSpec extends Specification {
def "Adjustment should be idempotent"() {
given:
def today = LocalDate.now(builder.clock)
def demand = demand(2800, 0)
def demand = demanded(2800, 0)
def adjustments = adjustments((today): 2000, (today.plusDays(1)): 3500)
when:
@@ -82,20 +85,4 @@ class DemandAdjustmentSpec extends Specification {
then:
0 * events.emit(_ as DemandedLevelsChanged)
}
ProductDemand demand(long ... levels) {
builder.demand(levels)
}
AdjustDemand adjustments(Map<LocalDate, Long> map) {
builder.adjustDemand(map)
}
DemandedLevelsChanged levelChanged(List<Long>... changes) {
builder.levelChanged(changes)
}
List<Long> notChanged() {
[]
}
}

View File

@@ -3,11 +3,11 @@ package pl.com.bottega.factory.demand.forecasting
import java.time.Clock
import java.time.LocalDate
class DemandsRepositoryFake extends Demands {
class DemandsFake extends Demands {
DailyDemandBuilder builder
DemandsRepositoryFake(String refNo, UnitOfWork unitOfWork, Clock clock) {
DemandsFake(String refNo, UnitOfWork unitOfWork, Clock clock) {
this.builder = new DailyDemandBuilder(refNo: refNo, events: unitOfWork, clock: clock)
fetch = { date -> nothingDemanded(date) }
}
@@ -28,4 +28,24 @@ class DemandsRepositoryFake extends Demands {
fetched.put(date, demand)
demand
}
DailyDemand adjusted(LocalDate date, long level) {
def demand = builder.date(date)
.demandedLevels(fetched.get(date)?.level)
.adjustedTo(level)
.build()
fetched.put(date, demand)
demand
}
DailyDemand stronglyAdjusted(LocalDate date, long level) {
def demand = builder.date(date)
.demandedLevels(fetched.get(date)?.level)
.stronglyAdjustedTo(level)
.build()
fetched.put(date, demand)
demand
}
}

View File

@@ -6,15 +6,18 @@ import java.time.LocalDate
import static pl.com.bottega.factory.demand.forecasting.DemandEvents.DemandedLevelsChanged
class DocumentProcessingSpec extends Specification {
class DocumentProcessingSpec extends Specification implements ProductDemandTrait {
def events = Mock(DemandEvents)
def builder = new ProductDemandBuilder(events: events)
void setup() {
builder = new ProductDemandBuilder(events: events)
}
def "Updated demands should be stored"() {
given:
def today = LocalDate.now(builder.clock)
def demand = demand(2800, 0)
def demand = demanded(2800, 0)
def document = document(today, 2000, 3500)
when:
@@ -27,7 +30,7 @@ class DocumentProcessingSpec extends Specification {
def "Demands for dates not present in system should be stored "() {
given:
def today = LocalDate.now(builder.clock)
def demand = demand(1000)
def demand = demanded(1000)
def document = document(today, 1000, 3500, 1000)
when:
@@ -40,7 +43,7 @@ class DocumentProcessingSpec extends Specification {
def "Document without changes should not generate event"() {
given:
def today = LocalDate.now(builder.clock)
def demand = demand(2800, 0)
def demand = demanded(2800, 0)
def document = document(today, 2800, 0)
when:
@@ -53,7 +56,7 @@ class DocumentProcessingSpec extends Specification {
def "Should skip past demands from document"() {
given:
def pastDate = LocalDate.now(builder.clock).minusDays(2)
def demand = demand(0, 0)
def demand = demanded(0, 0)
def document = document(pastDate, 2800, 2800, 3500, 1000)
when:
@@ -66,7 +69,7 @@ class DocumentProcessingSpec extends Specification {
def "Document processing should be idempotent"() {
given:
def today = LocalDate.now(builder.clock)
def demand = demand(2800, 0)
def demand = demanded(2800, 0)
def document = document(today, 2000, 3500)
when:
@@ -82,20 +85,4 @@ class DocumentProcessingSpec extends Specification {
then:
0 * events.emit(_ as DemandedLevelsChanged)
}
ProductDemand demand(long ... levels) {
builder.demand(levels)
}
Document document(LocalDate date, long ... levels) {
builder.document(date, levels)
}
DemandedLevelsChanged levelChanged(List<Long>... changes) {
builder.levelChanged(changes)
}
List<Long> notChanged() {
[]
}
}

View File

@@ -1,6 +1,5 @@
package pl.com.bottega.factory.demand.forecasting
import spock.lang.PendingFeature
import spock.lang.Specification
class KeepingDailyDemandsSpec extends Specification {
@@ -64,8 +63,7 @@ class KeepingDailyDemandsSpec extends Specification {
0 * events.emit(_ as DailyDemand.LevelChanged)
}
@PendingFeature
def "Document update hidden by strong adjustment should rise warning"() {
def "Document update ignored by strong adjustment should rise warning"() {
given:
def demand = demand()
.demandedLevels(2800)
@@ -76,7 +74,7 @@ class KeepingDailyDemandsSpec extends Specification {
then:
demand.getLevel() == Demand.of(3500)
1 * events.emit(_ as DailyDemand.ReviewRequest)
1 * events.emit(reviewRequest(2800, 3500, 5000))
}
DailyDemandBuilder demand() {
@@ -95,4 +93,8 @@ class KeepingDailyDemandsSpec extends Specification {
def levelChanged(long previous, long current) {
builder.levelChanged(previous, current)
}
def reviewRequest(long previousDocumented, long adjustment, long newDocumented) {
builder.reviewRequest(previousDocumented, adjustment, newDocumented)
}
}

View File

@@ -5,26 +5,46 @@ import pl.com.bottega.factory.product.management.RefNoId
import java.time.*
import static DemandedLevelsChanged.Change
import static ReviewRequested.ReviewNeeded
import static pl.com.bottega.factory.demand.forecasting.DemandEvents.DemandedLevelsChanged
import static pl.com.bottega.factory.demand.forecasting.DemandEvents.ReviewRequested
class ProductDemandBuilder {
def refNo = "3009000"
def unitOfWork = new UnitOfWork()
def demands = new DemandsRepositoryFake(refNo, unitOfWork, clock)
def demands = new DemandsFake(refNo, unitOfWork, clock)
def clock = Clock.fixed(Instant.now(), ZoneId.systemDefault())
DemandEvents events
ProductDemand demand(long ... levels) {
def demand(long ... levels) {
def date = LocalDate.now(clock)
for (long level : levels) {
demands.demanded(date, level)
date = date.plusDays(1)
}
this
}
def adjusted(Map<LocalDate, Long> adjustments) {
adjustments.each { date, level ->
demands.adjusted(date, level)
}
this
}
def stronglyAdjusted(Map<LocalDate, Long> adjustments) {
adjustments.each { date, level ->
demands.stronglyAdjusted(date, level)
}
this
}
def build() {
new ProductDemand(new RefNoId(refNo), demands, unitOfWork, clock, events)
}
Document document(LocalDate date, long ... levels) {
def document(LocalDate date, long ... levels) {
def created = date.atTime(OffsetTime.of(8, 0, 0, 0, ZoneOffset.UTC)).toInstant()
SortedMap<LocalDate, Demand> results = new TreeMap<>()
for (def level : levels) {
@@ -34,15 +54,15 @@ class ProductDemandBuilder {
new Document(created, refNo, results)
}
AdjustDemand adjustDemand(Map<LocalDate, Long> adjustments) {
def adjustDemand(Map<LocalDate, Long> adjustments) {
Map<LocalDate, Adjustment> results = new HashMap<>()
adjustments.forEach { date, level ->
results.put(date, Adjustment.week(Demand.of(level)))
results.put(date, Adjustment.weak(Demand.of(level)))
}
new AdjustDemand(refNo, results)
}
DemandedLevelsChanged levelChanged(List<Long>... changes) {
def levelChanged(List<Long>... changes) {
def date = LocalDate.now(clock)
Map<DailyId, Change> results = new HashMap<>()
for (def change : changes) {
@@ -50,12 +70,27 @@ class ProductDemandBuilder {
results.put(new DailyId(refNo, date), new Change(
Demand.of(change[0]),
Demand.of(change[1])))
}
} else if (!change.empty) throw new IllegalAccessException()
date = date.plusDays(1)
}
new DemandedLevelsChanged(new RefNoId(refNo), results)
}
ReviewRequested reviewRequest(ReviewNeeded... reviews) {
new ReviewRequested(new RefNoId(refNo), reviews as List)
}
ReviewNeeded review(LocalDate date,
long previousDocumented,
long strongAdjustment,
long newDocumented) {
new ReviewNeeded(
new DailyId(refNo, date),
Demand.of(previousDocumented),
Demand.of(strongAdjustment),
Demand.of(newDocumented))
}
void clearUnitOfWork() {
unitOfWork.@changes.clear()
unitOfWork.@reviews.clear()

View File

@@ -0,0 +1,48 @@
package pl.com.bottega.factory.demand.forecasting
import java.time.LocalDate
import static ReviewRequested.ReviewNeeded
import static pl.com.bottega.factory.demand.forecasting.DemandEvents.DemandedLevelsChanged
import static pl.com.bottega.factory.demand.forecasting.DemandEvents.ReviewRequested
trait ProductDemandTrait {
ProductDemandBuilder builder
ProductDemand demanded(long ... levels) {
builder.demand(levels).build()
}
ProductDemandBuilder demand(long ... levels) {
builder.demand(levels)
}
Document document(LocalDate date, long ... levels) {
builder.document(date, levels)
}
AdjustDemand adjustments(Map<LocalDate, Long> map) {
builder.adjustDemand(map)
}
DemandedLevelsChanged levelChanged(List<Long>... changes) {
builder.levelChanged(changes)
}
List<Long> notChanged() {
[]
}
ReviewRequested reviewRequest(ReviewNeeded... reviews) {
builder.reviewRequest(reviews)
}
ReviewNeeded review(
LocalDate date,
long previousDocumented,
long strongAdjustment,
long newDocumented) {
return builder.review(date, previousDocumented, strongAdjustment, newDocumented)
}
}

View File

@@ -0,0 +1,44 @@
package pl.com.bottega.factory.demand.forecasting
import spock.lang.Specification
import static pl.com.bottega.factory.demand.forecasting.Adjustment.strong
import static pl.com.bottega.factory.demand.forecasting.Adjustment.weak
import static pl.com.bottega.factory.demand.forecasting.Demand.of
class ReviewPolicySpec extends Specification {
def "'basic review policy' requires review only after strong adjustment \
when new document doesn't match neither previous document nor adjustment"() {
given:
def policy = ReviewPolicy.BASIC
expect:
policy.reviewNeeded(
previousDocument,
adjustment,
newDocument
) == review
where:
previousDocument | adjustment | newDocument || review
of(1000) | strong(of(1000)) | of(1000) || notNeeded()
of(1000) | strong(of(2000)) | of(2000) || notNeeded()
of(1000) | strong(of(2000)) | of(1000) || notNeeded()
of(1000) | strong(of(2000)) | of(1500) || needed()
of(1000) | strong(of(2000)) | of(0) || needed()
of(1000) | weak(of(1000)) | of(1000) || notNeeded()
of(1000) | weak(of(2000)) | of(2000) || notNeeded()
of(1000) | weak(of(2000)) | of(1000) || notNeeded()
of(1000) | weak(of(2000)) | of(1500) || notNeeded()
of(1000) | weak(of(2000)) | of(0) || notNeeded()
}
private static boolean needed() {
true
}
private static boolean notNeeded() {
false
}
}

View File

@@ -0,0 +1,92 @@
package pl.com.bottega.factory.demand.forecasting
import spock.lang.Specification
import java.time.LocalDate
import static pl.com.bottega.factory.demand.forecasting.DemandEvents.DemandedLevelsChanged
import static pl.com.bottega.factory.demand.forecasting.ReviewDecision.*
class ReviewProcessingSpec extends Specification implements ProductDemandTrait {
def events = Mock(DemandEvents)
void setup() {
builder = new ProductDemandBuilder(events: events)
}
def "Review requested"() {
given:
def today = LocalDate.now(builder.clock)
def tomorrow = today.plusDays(1)
def demand = demand(0, 0)
.stronglyAdjusted((tomorrow): 3500)
.build()
when:
demand.process(document(today, 0, 2800))
then:
1 * events.emit(reviewRequest(review(tomorrow, 0, 3500, 2800)))
}
def "decision to 'ignore'"() {
given:
def today = LocalDate.now(builder.clock)
def tomorrow = today.plusDays(1)
def demand = demand(0, 2800)
.stronglyAdjusted((tomorrow): 3500)
.build()
when:
demand.review(review(tomorrow, 0, 3500, 2800), IGNORE)
then:
0 * events.emit(_ as DemandedLevelsChanged)
}
def "decision to 'pick new'"() {
given:
def today = LocalDate.now(builder.clock)
def tomorrow = today.plusDays(1)
def demand = demand(0, 2800)
.stronglyAdjusted((tomorrow): 3500)
.build()
when:
demand.review(review(tomorrow, 0, 3500, 2800), PICK_NEW)
then:
1 * events.emit(levelChanged([], [3500, 2800]))
}
def "decision to 'pick previous'"() {
given:
def today = LocalDate.now(builder.clock)
def tomorrow = today.plusDays(1)
def demand = demand(0, 2800)
.stronglyAdjusted((tomorrow): 3500)
.build()
when:
demand.review(review(tomorrow, 0, 3500, 2800), PICK_PREVIOUS)
then:
1 * events.emit(levelChanged([], [3500, 0]))
}
def "decision to 'make adjustment weak'"() {
given:
def today = LocalDate.now(builder.clock)
def tomorrow = today.plusDays(1)
def demand = demand(0, 2800)
.stronglyAdjusted((tomorrow): 3500)
.build()
when:
demand.review(review(tomorrow, 0, 3500, 2800), MAKE_ADJUSTMENT_WEAK)
then:
0 * events.emit(_ as DemandedLevelsChanged)
}
}

View File

@@ -3,12 +3,14 @@ package pl.com.bottega.factory.demand.forecasting;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import java.time.LocalDate;
@Getter
@AllArgsConstructor
@EqualsAndHashCode
@ToString
public class DailyId {
private final String refNo;
private final LocalDate date;

View File

@@ -3,30 +3,57 @@ package pl.com.bottega.factory.demand.forecasting;
import lombok.Value;
import pl.com.bottega.factory.product.management.RefNoId;
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public interface DemandEvents {
void emit(DemandedLevelsChanged event);
void emit(ReviewRequested event);
@Value
class DemandedLevelsChanged {
RefNoId id;
RefNoId refNo;
Map<DailyId, Change> results;
public DemandedLevelsChanged(RefNoId id, Map<DailyId, Change> results) {
this.id = id;
public DemandedLevelsChanged(RefNoId refNo, Map<DailyId, Change> results) {
this.refNo = refNo;
this.results = Collections.unmodifiableMap(results);
}
public RefNoId getRefNo() {
return id;
}
@Value
public static class Change {
Demand previous;
Demand current;
}
}
@Value
class ReviewRequested {
RefNoId refNo;
List<ReviewNeeded> reviews;
public ReviewRequested(RefNoId refNo, List<ReviewNeeded> reviews) {
this.refNo = refNo;
this.reviews = Collections.unmodifiableList(reviews);
}
@Value
public static class ReviewNeeded {
DailyId id;
Demand previousDocumented;
Demand adjustment;
Demand newDocumented;
public String getRefNo() {
return id.getRefNo();
}
public LocalDate getDate() {
return id.getDate();
}
}
}
}

View File

@@ -3,10 +3,12 @@ package pl.com.bottega.factory.product.management;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
@Getter
@AllArgsConstructor
@EqualsAndHashCode
@ToString
public class RefNoId {
private final String refNo;
}