demand changes review processing
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
[]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
[]
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user