improved code coverage and bug fix
This commit is contained in:
@@ -17,7 +17,7 @@ interface DeliveriesSuggestion {
|
|||||||
timesAndFractions.entrySet().stream()
|
timesAndFractions.entrySet().stream()
|
||||||
.map(e -> new Delivery(
|
.map(e -> new Delivery(
|
||||||
refNo,
|
refNo,
|
||||||
date.atTime(e.getKey()), ((long) ((double) demand.getLevel() / e.getValue())))
|
date.atTime(e.getKey()), ((long) ((double) demand.getLevel() * e.getValue())))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package pl.com.bottega.factory.delivery.planning
|
||||||
|
|
||||||
|
import pl.com.bottega.factory.demand.forecasting.Demand
|
||||||
|
import spock.lang.PendingFeature
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.LocalTime
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
|
import static pl.com.bottega.factory.demand.forecasting.Demand.Schema.AtDayStart
|
||||||
|
|
||||||
|
class DeliveriesSuggestionSpec extends Specification {
|
||||||
|
|
||||||
|
def final refNo = "3009000"
|
||||||
|
def final date = LocalDate.now()
|
||||||
|
def final midnight = LocalTime.of(0, 0)
|
||||||
|
def final sevenAM = LocalTime.of(7, 0)
|
||||||
|
def final nineAM = LocalTime.of(9, 0)
|
||||||
|
|
||||||
|
def "Times and fractions - all at once"() {
|
||||||
|
given:
|
||||||
|
def suggestion = DeliveriesSuggestion.timesAndFractions([(sevenAM): 1.0d])
|
||||||
|
|
||||||
|
when:
|
||||||
|
def deliveries = suggestion.deliveriesFor(refNo, date, atDayStart(2000))
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
|
||||||
|
then:
|
||||||
|
deliveries == [deliveryAt(date.atTime(sevenAM), 2000)]
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Times and fractions - half twice"() {
|
||||||
|
given:
|
||||||
|
def suggestion = DeliveriesSuggestion.timesAndFractions([
|
||||||
|
(sevenAM): 0.5d,
|
||||||
|
(nineAM): 0.5d,
|
||||||
|
])
|
||||||
|
|
||||||
|
when:
|
||||||
|
def deliveries = suggestion.deliveriesFor(refNo, date, atDayStart(2000))
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
|
||||||
|
then:
|
||||||
|
deliveries == [
|
||||||
|
deliveryAt(date.atTime(sevenAM), 1000),
|
||||||
|
deliveryAt(date.atTime(nineAM), 1000),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Times and fractions - not represented number"() {
|
||||||
|
given:
|
||||||
|
def suggestion = DeliveriesSuggestion.timesAndFractions([
|
||||||
|
(midnight): 1/3d,
|
||||||
|
(sevenAM): 1/3d,
|
||||||
|
(nineAM): 1/3d,
|
||||||
|
])
|
||||||
|
|
||||||
|
when:
|
||||||
|
def deliveries = suggestion.deliveriesFor(refNo, date, atDayStart(2000))
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
|
||||||
|
then:
|
||||||
|
deliveries == [
|
||||||
|
deliveryAt(date.atTime(midnight), 666),
|
||||||
|
deliveryAt(date.atTime(sevenAM), 666),
|
||||||
|
deliveryAt(date.atTime(nineAM), 666),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@PendingFeature
|
||||||
|
def "Times and fractions - do not lost parts on not represented number"() {
|
||||||
|
given:
|
||||||
|
def suggestion = DeliveriesSuggestion.timesAndFractions([
|
||||||
|
(midnight): 1/3d,
|
||||||
|
(sevenAM): 1/3d,
|
||||||
|
(nineAM): 1/3d,
|
||||||
|
])
|
||||||
|
|
||||||
|
when:
|
||||||
|
def deliveries = suggestion.deliveriesFor(refNo, date, atDayStart(2000))
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
|
||||||
|
then:
|
||||||
|
deliveries == [
|
||||||
|
deliveryAt(date.atTime(midnight), 666),
|
||||||
|
deliveryAt(date.atTime(sevenAM), 666),
|
||||||
|
deliveryAt(date.atTime(nineAM), 668),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def deliveryAt(LocalDateTime time, Integer level) {
|
||||||
|
new Delivery(refNo, time, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
def atDayStart(long level) {
|
||||||
|
Demand.of(level, AtDayStart)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package pl.com.bottega.factory.delivery.planning
|
||||||
|
|
||||||
|
import pl.com.bottega.factory.demand.forecasting.Demand
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.LocalTime
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
|
import static pl.com.bottega.factory.delivery.planning.DeliveriesSuggestion.timesAndFractions
|
||||||
|
import static pl.com.bottega.factory.demand.forecasting.Demand.Schema.AtDayStart
|
||||||
|
|
||||||
|
class DeliveryAutoPlannerSpec extends Specification {
|
||||||
|
|
||||||
|
def final refNo = "3009000"
|
||||||
|
def final date = LocalDate.now()
|
||||||
|
def final midnight = LocalTime.of(0, 0)
|
||||||
|
def final sevenAM = LocalTime.of(7, 0)
|
||||||
|
|
||||||
|
def "Picks dummy suggestion"() {
|
||||||
|
given:
|
||||||
|
def planner = deliveryDefinitions([:])
|
||||||
|
|
||||||
|
when:
|
||||||
|
def deliveries = planner.propose(date, atDayStart(2000))
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
|
||||||
|
then:
|
||||||
|
deliveries == [deliveryAt(date.atTime(midnight), 2000)]
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Suggests according to defined policy"() {
|
||||||
|
given:
|
||||||
|
def planner = deliveryDefinitions([
|
||||||
|
(AtDayStart): timesAndFractions([(sevenAM): 1.0d])
|
||||||
|
])
|
||||||
|
|
||||||
|
when:
|
||||||
|
def deliveries = planner.propose(date, atDayStart(2000))
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
|
||||||
|
then:
|
||||||
|
deliveries == [deliveryAt(date.atTime(sevenAM), 2000)]
|
||||||
|
}
|
||||||
|
|
||||||
|
def deliveryDefinitions(Map<Demand.Schema, DeliveriesSuggestion> suggestions) {
|
||||||
|
new DeliveryAutoPlanner(refNo, suggestions)
|
||||||
|
}
|
||||||
|
|
||||||
|
def deliveryAt(LocalDateTime time, Integer level) {
|
||||||
|
new Delivery(refNo, time, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
def atDayStart(long level) {
|
||||||
|
Demand.of(level, AtDayStart)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package pl.com.bottega.factory.demand.forecasting
|
||||||
|
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
import static pl.com.bottega.factory.demand.forecasting.ReviewDecision.PICK_NEW
|
||||||
|
|
||||||
|
class DemandServiceSpec extends Specification implements ProductDemandTrait {
|
||||||
|
|
||||||
|
def events = Mock(DemandEvents)
|
||||||
|
def repo = Mock(ProductDemandRepository)
|
||||||
|
def service = new DemandService(repo)
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
builder = new ProductDemandBuilder(events: events)
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Repository interactions while product demand initialization"() {
|
||||||
|
given:
|
||||||
|
def refNo = "3009000"
|
||||||
|
|
||||||
|
when:
|
||||||
|
service.initNewProduct(refNo)
|
||||||
|
|
||||||
|
then:
|
||||||
|
1 * repo.initNewProduct(refNo)
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Repository interactions while document processing"() {
|
||||||
|
given:
|
||||||
|
def today = LocalDate.now(builder.clock)
|
||||||
|
def demand = demanded(2800, 0)
|
||||||
|
def document = document(today, 2000, 3500)
|
||||||
|
repo.get(document.refNo) >> demand
|
||||||
|
|
||||||
|
when:
|
||||||
|
service.process(document)
|
||||||
|
|
||||||
|
then:
|
||||||
|
1 * repo.save(demand)
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Repository interactions while demand adjustments"() {
|
||||||
|
given:
|
||||||
|
def today = LocalDate.now(builder.clock)
|
||||||
|
def demand = demanded(2800, 0)
|
||||||
|
def adjustments = adjustments([(today): 1000])
|
||||||
|
repo.get(adjustments.refNo) >> demand
|
||||||
|
|
||||||
|
when:
|
||||||
|
service.adjust(adjustments)
|
||||||
|
|
||||||
|
then:
|
||||||
|
1 * repo.save(demand)
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Repository interactions while review processing"() {
|
||||||
|
given:
|
||||||
|
def today = LocalDate.now(builder.clock)
|
||||||
|
def demand = demand(0, 2800)
|
||||||
|
.stronglyAdjusted((today): 3500)
|
||||||
|
.build()
|
||||||
|
def review = review(today, 0, 3500, 2800)
|
||||||
|
repo.get(review.refNo) >> demand
|
||||||
|
|
||||||
|
when:
|
||||||
|
service.review(review, PICK_NEW)
|
||||||
|
|
||||||
|
then:
|
||||||
|
1 * repo.save(demand)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
version: "2"
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
services:
|
|
||||||
db:
|
|
||||||
build: ./database/
|
|
||||||
volumes:
|
|
||||||
- data:/var/lib/postgresql/data
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
@@ -3,7 +3,7 @@ package pl.com.bottega.factory.shortages.prediction.calculation
|
|||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
|
|
||||||
class ShortageCalculationAlgorithmSpec extends Specification
|
class ShortageCalculationAlgorithmSpec extends Specification
|
||||||
implements ShortagesCalculationAssemblerTrait {
|
implements ShortagesCalculationTrait {
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
TimeGrammar.apply()
|
TimeGrammar.apply()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package pl.com.bottega.factory.shortages.prediction.calculation
|
|||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
|
|
||||||
class ShortageCalculationExamplesSpec extends Specification
|
class ShortageCalculationExamplesSpec extends Specification
|
||||||
implements ShortagesCalculationAssemblerTrait {
|
implements ShortagesCalculationTrait {
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
TimeGrammar.apply()
|
TimeGrammar.apply()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package pl.com.bottega.factory.shortages.prediction.calculation
|
package pl.com.bottega.factory.shortages.prediction.calculation
|
||||||
|
|
||||||
class ShortagesCalculationAssembler implements ShortagesCalculationAssemblerTrait {
|
class ShortagesCalculationAssembler implements ShortagesCalculationTrait {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import pl.com.bottega.factory.shortages.prediction.Shortage
|
|||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
trait ShortagesCalculationAssemblerTrait {
|
trait ShortagesCalculationTrait {
|
||||||
|
|
||||||
LocalDateTime now = LocalDateTime.now()
|
LocalDateTime now = LocalDateTime.now()
|
||||||
String refNo = "3009000"
|
String refNo = "3009000"
|
||||||
@@ -1,22 +1,14 @@
|
|||||||
package pl.com.bottega.factory.shortages.prediction.monitoring
|
package pl.com.bottega.factory.shortages.prediction.monitoring
|
||||||
|
|
||||||
import pl.com.bottega.factory.product.management.RefNoId
|
|
||||||
import pl.com.bottega.factory.shortages.prediction.ConfigurationParams
|
|
||||||
import pl.com.bottega.factory.shortages.prediction.Shortage
|
|
||||||
import pl.com.bottega.factory.shortages.prediction.calculation.ShortageForecasts
|
|
||||||
import pl.com.bottega.factory.shortages.prediction.calculation.ShortagesCalculationAssembler
|
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
|
|
||||||
import static pl.com.bottega.factory.shortages.prediction.monitoring.NewShortage.After
|
import static pl.com.bottega.factory.shortages.prediction.monitoring.NewShortage.After
|
||||||
|
|
||||||
class ShortagePredictionProcessSpec extends Specification {
|
class ShortagePredictionProcessSpec extends Specification implements ShortagePredictionProcessTrait {
|
||||||
|
|
||||||
def refNo = new RefNoId("3009000")
|
void setup() {
|
||||||
def now = LocalDateTime.now()
|
events = Mock(ShortageEvents)
|
||||||
def forecastAssembler = new ShortagesCalculationAssembler(refNo: refNo, now: now)
|
}
|
||||||
def events = Mock(ShortageEvents)
|
|
||||||
|
|
||||||
def "Emits no events when there is still no shortages"() {
|
def "Emits no events when there is still no shortages"() {
|
||||||
given:
|
given:
|
||||||
@@ -117,64 +109,4 @@ class ShortagePredictionProcessSpec extends Specification {
|
|||||||
0 * events.emit(_ as NewShortage)
|
0 * events.emit(_ as NewShortage)
|
||||||
0 * events.emit(_ as ShortageSolved)
|
0 * events.emit(_ as ShortageSolved)
|
||||||
}
|
}
|
||||||
|
|
||||||
ShortagePredictionProcess predictionProcess(
|
|
||||||
Shortage previouslyFound,
|
|
||||||
ShortageForecasts forecastThatWillFindShortages) {
|
|
||||||
|
|
||||||
new ShortagePredictionProcess(
|
|
||||||
refNo,
|
|
||||||
previouslyFound,
|
|
||||||
ShortageDiffPolicy.ValuesAreNotSame,
|
|
||||||
forecastThatWillFindShortages,
|
|
||||||
defaultConfig(),
|
|
||||||
events
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<LocalDateTime, Long> someShortages() {
|
|
||||||
[(now.plusHours(5)): 500L,
|
|
||||||
(now.plusDays(1)) : 500L]
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<LocalDateTime, Long> someDifferentShortages() {
|
|
||||||
[(now.plusHours(5)): 100L,
|
|
||||||
(now.plusDays(1)) : 900L]
|
|
||||||
}
|
|
||||||
|
|
||||||
Shortage noShortagesWasPreviouslyFound() {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
Shortage wasPreviouslyFound(Map<LocalDateTime, Long> shortages) {
|
|
||||||
forecastAssembler.shortage(shortages).orElse(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
ShortageForecasts noShortagesWillBeFound() {
|
|
||||||
forecastAssembler.forecastProvider(
|
|
||||||
forecastAssembler.stock(1000),
|
|
||||||
forecastAssembler.noDeliveries(),
|
|
||||||
forecastAssembler.noProductions()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ShortageForecasts willFindShortages(Map<LocalDateTime, Long> shortages) {
|
|
||||||
forecastAssembler.forecastProvider(
|
|
||||||
forecastAssembler.stock(0),
|
|
||||||
forecastAssembler.deliveries(shortages),
|
|
||||||
forecastAssembler.noProductions()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigurationParams defaultConfig() {
|
|
||||||
new InMemoryConfigurationParams(daysAhead: 14)
|
|
||||||
}
|
|
||||||
|
|
||||||
NewShortage newShortage(After after, Map<LocalDateTime, Long> missing) {
|
|
||||||
new NewShortage(refNo, after, forecastAssembler.shortage(missing).get())
|
|
||||||
}
|
|
||||||
|
|
||||||
ShortageSolved shortageSolved() {
|
|
||||||
new ShortageSolved(refNo)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package pl.com.bottega.factory.shortages.prediction.monitoring
|
||||||
|
|
||||||
|
import pl.com.bottega.factory.product.management.RefNoId
|
||||||
|
import pl.com.bottega.factory.shortages.prediction.ConfigurationParams
|
||||||
|
import pl.com.bottega.factory.shortages.prediction.Shortage
|
||||||
|
import pl.com.bottega.factory.shortages.prediction.calculation.ShortageForecasts
|
||||||
|
import pl.com.bottega.factory.shortages.prediction.calculation.ShortagesCalculationAssembler
|
||||||
|
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
trait ShortagePredictionProcessTrait {
|
||||||
|
|
||||||
|
def refNo = new RefNoId("3009000")
|
||||||
|
def now = LocalDateTime.now()
|
||||||
|
def forecastAssembler = new ShortagesCalculationAssembler(refNo: refNo, now: now)
|
||||||
|
ShortageEvents events
|
||||||
|
|
||||||
|
ShortagePredictionProcess predictionProcess(
|
||||||
|
Shortage previouslyFound,
|
||||||
|
ShortageForecasts forecastThatWillFindShortages) {
|
||||||
|
|
||||||
|
new ShortagePredictionProcess(
|
||||||
|
refNo,
|
||||||
|
previouslyFound,
|
||||||
|
ShortageDiffPolicy.ValuesAreNotSame,
|
||||||
|
forecastThatWillFindShortages,
|
||||||
|
defaultConfig(),
|
||||||
|
events
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<LocalDateTime, Long> someShortages() {
|
||||||
|
[(now.plusHours(5)): 500L,
|
||||||
|
(now.plusDays(1)) : 500L]
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<LocalDateTime, Long> someDifferentShortages() {
|
||||||
|
[(now.plusHours(5)): 100L,
|
||||||
|
(now.plusDays(1)) : 900L]
|
||||||
|
}
|
||||||
|
|
||||||
|
Shortage noShortagesWasPreviouslyFound() {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
Shortage wasPreviouslyFound(Map<LocalDateTime, Long> shortages) {
|
||||||
|
forecastAssembler.shortage(shortages).orElse(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
ShortageForecasts noShortagesWillBeFound() {
|
||||||
|
forecastAssembler.forecastProvider(
|
||||||
|
forecastAssembler.stock(1000),
|
||||||
|
forecastAssembler.noDeliveries(),
|
||||||
|
forecastAssembler.noProductions()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ShortageForecasts willFindShortages(Map<LocalDateTime, Long> shortages) {
|
||||||
|
forecastAssembler.forecastProvider(
|
||||||
|
forecastAssembler.stock(0),
|
||||||
|
forecastAssembler.deliveries(shortages),
|
||||||
|
forecastAssembler.noProductions()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigurationParams defaultConfig() {
|
||||||
|
new InMemoryConfigurationParams(daysAhead: 14)
|
||||||
|
}
|
||||||
|
|
||||||
|
NewShortage newShortage(NewShortage.After after, Map<LocalDateTime, Long> missing) {
|
||||||
|
new NewShortage(refNo, after, forecastAssembler.shortage(missing).get())
|
||||||
|
}
|
||||||
|
|
||||||
|
ShortageSolved shortageSolved() {
|
||||||
|
new ShortageSolved(refNo)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package pl.com.bottega.factory.shortages.prediction.monitoring
|
||||||
|
|
||||||
|
import pl.com.bottega.factory.demand.forecasting.DemandedLevelsChanged
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
|
class ShortagePredictionServiceSpec extends Specification implements ShortagePredictionProcessTrait {
|
||||||
|
|
||||||
|
def repo = Mock(ShortagePredictionProcessRepository)
|
||||||
|
def service = new ShortagePredictionService(repo)
|
||||||
|
|
||||||
|
def "Repository interactions while processing demanded changed event"() {
|
||||||
|
given:
|
||||||
|
def process = predictionProcess(
|
||||||
|
noShortagesWasPreviouslyFound(),
|
||||||
|
noShortagesWillBeFound()
|
||||||
|
)
|
||||||
|
repo.get(refNo) >> process
|
||||||
|
|
||||||
|
when:
|
||||||
|
service.predictShortages(new DemandedLevelsChanged(refNo, [:]))
|
||||||
|
|
||||||
|
then:
|
||||||
|
1 * repo.save(process)
|
||||||
|
0 * events.emit(_ as ShortageSolved)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user