domain model and empty database
This commit is contained in:
7
.gitattributes
vendored
Normal file
7
.gitattributes
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
# Set default behaviour, in case users don't have core.autocrlf set.
|
||||
* text=auto
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
88
.gitignore
vendored
Normal file
88
.gitignore
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
|
||||
## maven
|
||||
target/
|
||||
target-test/
|
||||
|
||||
|
||||
## linux
|
||||
.*
|
||||
!.git*
|
||||
*~
|
||||
|
||||
|
||||
## windows
|
||||
# Windows image file caches
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
# Folder config file
|
||||
Desktop.ini
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
|
||||
## osx
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
Icon
|
||||
# Thumbnails
|
||||
._*
|
||||
# Files that might appear on external disk
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
|
||||
|
||||
## eclipse
|
||||
*.pydevproject
|
||||
.project
|
||||
.metadata
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~.nib
|
||||
local.properties
|
||||
.classpath
|
||||
.settings/
|
||||
.loadpath
|
||||
# External tool builders
|
||||
.externalToolBuilders/
|
||||
# Locally stored "Eclipse launch configurations"
|
||||
*.launch
|
||||
# CDT-specific
|
||||
.cproject
|
||||
# PDT-specific
|
||||
.buildpath
|
||||
|
||||
|
||||
## intelij
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
|
||||
## netbeans
|
||||
nbactions.xml
|
||||
nb-configuration.xml
|
||||
|
||||
|
||||
## emacs
|
||||
*~
|
||||
\#*\#
|
||||
/.emacs.desktop
|
||||
/.emacs.desktop.lock
|
||||
.elc
|
||||
auto-save-list
|
||||
tramp
|
||||
.\#*
|
||||
# Org-mode
|
||||
.org-id-locations
|
||||
*_archive
|
||||
|
||||
|
||||
## vim
|
||||
.*.s[a-w][a-z]
|
||||
*.un~
|
||||
Session.vim
|
||||
.netrwhist
|
||||
*~
|
||||
17
database/Dockerfile
Normal file
17
database/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
FROM postgres:10
|
||||
MAINTAINER Michał Michaluk <michal.michaluk@bottega.com.pl>
|
||||
|
||||
RUN apt-get update -y
|
||||
RUN apt-get install openjdk-8-jre-headless -y
|
||||
|
||||
ADD http://central.maven.org/maven2/org/liquibase/liquibase-core/3.4.2/liquibase-core-3.4.2.jar /lib/liquibase.jar
|
||||
ADD http://central.maven.org/maven2/org/postgresql/postgresql/9.4.1212/postgresql-9.4.1212.jar /lib/postgresql.jar
|
||||
RUN chmod a+r /lib/liquibase.jar
|
||||
RUN chmod a+r /lib/postgresql.jar
|
||||
|
||||
ADD schema /schema
|
||||
ADD initdb.sql /docker-entrypoint-initdb.d/
|
||||
ADD start-and-migrate.sh /
|
||||
RUN chmod +x /start-and-migrate.sh
|
||||
|
||||
CMD /start-and-migrate.sh postgres
|
||||
4
database/initdb.sql
Normal file
4
database/initdb.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
--create schema demands authorization postgres;
|
||||
--create schema shortages authorization postgres;
|
||||
--create schema products authorization postgres;
|
||||
8
database/schema/db.changelog.xml
Normal file
8
database/schema/db.changelog.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd">
|
||||
<!--<include file="/schema/product-management.sql"/>-->
|
||||
<!--<include file="/schema/demand-forecasting.sql"/>-->
|
||||
<!--<include file="/schema/shortages-prediction.sql"/>-->
|
||||
</databaseChangeLog>
|
||||
3
database/schema/product-management.sql
Normal file
3
database/schema/product-management.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
--liquibase formatted sql
|
||||
|
||||
--changeset michaluk.michal:1.init
|
||||
173
database/start-and-migrate.sh
Normal file
173
database/start-and-migrate.sh
Normal file
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# usage: file_env VAR [DEFAULT]
|
||||
# ie: file_env 'XYZ_DB_PASSWORD' 'example'
|
||||
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
|
||||
# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
|
||||
file_env() {
|
||||
local var="$1"
|
||||
local fileVar="${var}_FILE"
|
||||
local def="${2:-}"
|
||||
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
|
||||
echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
|
||||
exit 1
|
||||
fi
|
||||
local val="$def"
|
||||
if [ "${!var:-}" ]; then
|
||||
val="${!var}"
|
||||
elif [ "${!fileVar:-}" ]; then
|
||||
val="$(< "${!fileVar}")"
|
||||
fi
|
||||
export "$var"="$val"
|
||||
unset "$fileVar"
|
||||
}
|
||||
|
||||
if [ "${1:0:1}" = '-' ]; then
|
||||
set -- postgres "$@"
|
||||
fi
|
||||
|
||||
# allow the container to be started with `--user`
|
||||
if [ "$1" = 'postgres' ] && [ "$(id -u)" = '0' ]; then
|
||||
mkdir -p "$PGDATA"
|
||||
chown -R postgres "$PGDATA"
|
||||
chmod 700 "$PGDATA"
|
||||
|
||||
mkdir -p /var/run/postgresql
|
||||
chown -R postgres /var/run/postgresql
|
||||
chmod 775 /var/run/postgresql
|
||||
|
||||
# Create the transaction log directory before initdb is run (below) so the directory is owned by the correct user
|
||||
if [ "$POSTGRES_INITDB_XLOGDIR" ]; then
|
||||
mkdir -p "$POSTGRES_INITDB_XLOGDIR"
|
||||
chown -R postgres "$POSTGRES_INITDB_XLOGDIR"
|
||||
chmod 700 "$POSTGRES_INITDB_XLOGDIR"
|
||||
fi
|
||||
|
||||
exec gosu postgres "$BASH_SOURCE" "$@"
|
||||
fi
|
||||
|
||||
if [ "$1" = 'postgres' ]; then
|
||||
mkdir -p "$PGDATA"
|
||||
chown -R "$(id -u)" "$PGDATA" 2>/dev/null || :
|
||||
chmod 700 "$PGDATA" 2>/dev/null || :
|
||||
|
||||
# look specifically for PG_VERSION, as it is expected in the DB dir
|
||||
if [ ! -s "$PGDATA/PG_VERSION" ]; then
|
||||
file_env 'POSTGRES_INITDB_ARGS'
|
||||
if [ "$POSTGRES_INITDB_XLOGDIR" ]; then
|
||||
export POSTGRES_INITDB_ARGS="$POSTGRES_INITDB_ARGS --xlogdir $POSTGRES_INITDB_XLOGDIR"
|
||||
fi
|
||||
eval "initdb --username=postgres $POSTGRES_INITDB_ARGS"
|
||||
|
||||
# check password first so we can output the warning before postgres
|
||||
# messes it up
|
||||
file_env 'POSTGRES_PASSWORD'
|
||||
if [ "$POSTGRES_PASSWORD" ]; then
|
||||
pass="PASSWORD '$POSTGRES_PASSWORD'"
|
||||
authMethod=md5
|
||||
else
|
||||
# The - option suppresses leading tabs but *not* spaces. :)
|
||||
cat >&2 <<-'EOWARN'
|
||||
****************************************************
|
||||
WARNING: No password has been set for the database.
|
||||
This will allow anyone with access to the
|
||||
Postgres port to access your database. In
|
||||
Docker's default configuration, this is
|
||||
effectively any other container on the same
|
||||
system.
|
||||
|
||||
Use "-e POSTGRES_PASSWORD=password" to set
|
||||
it in "docker run".
|
||||
****************************************************
|
||||
EOWARN
|
||||
|
||||
pass=
|
||||
authMethod=trust
|
||||
fi
|
||||
|
||||
{
|
||||
echo
|
||||
echo "host all all all $authMethod"
|
||||
} >> "$PGDATA/pg_hba.conf"
|
||||
|
||||
# internal start of server in order to allow set-up using psql-client
|
||||
# does not listen on external TCP/IP and waits until start finishes
|
||||
PGUSER="${PGUSER:-postgres}" \
|
||||
pg_ctl -D "$PGDATA" \
|
||||
-o "-c listen_addresses='localhost'" \
|
||||
-w start
|
||||
|
||||
file_env 'POSTGRES_USER' 'postgres'
|
||||
file_env 'POSTGRES_DB' "$POSTGRES_USER"
|
||||
|
||||
psql=( psql -v ON_ERROR_STOP=1 )
|
||||
|
||||
if [ "$POSTGRES_DB" != 'postgres' ]; then
|
||||
"${psql[@]}" --username postgres <<-EOSQL
|
||||
CREATE DATABASE "$POSTGRES_DB" ;
|
||||
EOSQL
|
||||
echo
|
||||
fi
|
||||
|
||||
if [ "$POSTGRES_USER" = 'postgres' ]; then
|
||||
op='ALTER'
|
||||
else
|
||||
op='CREATE'
|
||||
fi
|
||||
"${psql[@]}" --username postgres <<-EOSQL
|
||||
$op USER "$POSTGRES_USER" WITH SUPERUSER $pass ;
|
||||
EOSQL
|
||||
echo
|
||||
|
||||
psql+=( --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" )
|
||||
|
||||
echo
|
||||
for f in /docker-entrypoint-initdb.d/*; do
|
||||
case "$f" in
|
||||
*.sh) echo "$0: running $f"; . "$f" ;;
|
||||
*.sql) echo "$0: running $f"; "${psql[@]}" -f "$f"; echo ;;
|
||||
*.sql.gz) echo "$0: running $f"; gunzip -c "$f" | "${psql[@]}"; echo ;;
|
||||
*) echo "$0: ignoring $f" ;;
|
||||
esac
|
||||
echo
|
||||
done
|
||||
|
||||
PGUSER="${PGUSER:-postgres}" \
|
||||
pg_ctl -D "$PGDATA" -m fast -w stop
|
||||
|
||||
echo
|
||||
echo 'PostgreSQL init process complete; ready for start up.'
|
||||
echo
|
||||
fi
|
||||
fi
|
||||
|
||||
# migrate schema with liquibase if anything in /schema has changed
|
||||
SCHEMASUM=`find /schema/ -type f | xargs cat | md5sum | cut -d ' ' -f1`
|
||||
if [ ! -f $PGDATA/schema.md5 ] || [[ $(< $PGDATA/schema.md5) != "$SCHEMASUM" ]]; then
|
||||
|
||||
PGUSER="${PGUSER:-postgres}" \
|
||||
pg_ctl -D "$PGDATA" \
|
||||
-o "-c listen_addresses='localhost'" \
|
||||
-w start
|
||||
|
||||
echo
|
||||
echo 'Starting Schema Migration'
|
||||
|
||||
java -jar /lib/liquibase.jar \
|
||||
--classpath=/lib/postgresql.jar \
|
||||
--changeLogFile=/schema/db.changelog.xml \
|
||||
--driver=org.postgresql.Driver \
|
||||
--url="jdbc:postgresql://localhost:5432/postgres" \
|
||||
--username="postgres" \
|
||||
--password="" \
|
||||
update
|
||||
|
||||
echo
|
||||
|
||||
PGUSER="${PGUSER:-postgres}" \
|
||||
pg_ctl -D "$PGDATA" -m fast -w stop
|
||||
echo "$SCHEMASUM" > $PGDATA/schema.md5
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
93
demand-forecasting-model/pom.xml
Normal file
93
demand-forecasting-model/pom.xml
Normal file
@@ -0,0 +1,93 @@
|
||||
<?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-model</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.7.0</version>
|
||||
<configuration>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
<compilerArgument>-parameters</compilerArgument>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.gmavenplus</groupId>
|
||||
<artifactId>gmavenplus-plugin</artifactId>
|
||||
<version>1.6</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>compileTests</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.20.1</version>
|
||||
<configuration>
|
||||
<useFile>false</useFile>
|
||||
<includes>
|
||||
<include>**/*Spec.java</include>
|
||||
<include>**/*Test.java</include>
|
||||
</includes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>pl.com.bottega</groupId>
|
||||
<artifactId>shared-kernel-model</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>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.12</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>3.8.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>info.cukes</groupId>
|
||||
<artifactId>cucumber-java</artifactId>
|
||||
<version>1.2.5</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>info.cukes</groupId>
|
||||
<artifactId>cucumber-core</artifactId>
|
||||
<version>1.2.5</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.spockframework</groupId>
|
||||
<artifactId>spock-core</artifactId>
|
||||
<version>1.1-groovy-2.4</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,28 @@
|
||||
package pl.com.bottega.factory.delivery.planning;
|
||||
|
||||
import pl.com.bottega.factory.demand.forecasting.Demand;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
interface DeliveriesSuggestion {
|
||||
|
||||
DeliveriesSuggestion DUMMY = (refNo, date, demand) ->
|
||||
Stream.of(new Delivery(refNo, date.atStartOfDay(), demand.getLevel()));
|
||||
|
||||
static DeliveriesSuggestion allAtTime(LocalTime time) {
|
||||
return (refNo, date, demand) ->
|
||||
Stream.of(new Delivery(refNo, date.atTime(time), demand.getLevel()));
|
||||
}
|
||||
|
||||
static DeliveriesSuggestion twoAtTimes(LocalTime first, LocalTime second) {
|
||||
return (refNo, date, demand) ->
|
||||
Stream.of(
|
||||
new Delivery(refNo, date.atTime(first), demand.getLevel() / 2),
|
||||
new Delivery(refNo, date.atTime(second), demand.getLevel() - (demand.getLevel() / 2))
|
||||
);
|
||||
}
|
||||
|
||||
Stream<Delivery> deliveriesFor(String refNo, LocalDate date, Demand demand);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package pl.com.bottega.factory.delivery.planning;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Value
|
||||
public class Delivery {
|
||||
String refNo;
|
||||
LocalDateTime time;
|
||||
long level;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package pl.com.bottega.factory.delivery.planning;
|
||||
|
||||
import pl.com.bottega.factory.demand.forecasting.Demand;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@AllArgsConstructor
|
||||
public class DeliveryAutoPlanner {
|
||||
private String refNo;
|
||||
private Map<Demand.Schema, DeliveriesSuggestion> policies;
|
||||
|
||||
public Stream<Delivery> propose(LocalDate date, Demand demand) {
|
||||
return policies.getOrDefault(demand.getSchema(), DeliveriesSuggestion.DUMMY)
|
||||
.deliveriesFor(refNo, date, demand);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package pl.com.bottega.factory.demand.forecasting;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
@Value
|
||||
public class AdjustDemand {
|
||||
String refNo;
|
||||
Map<LocalDate, Adjustment> adjustments;
|
||||
|
||||
public AdjustDemand(String refNo,
|
||||
Map<LocalDate, Adjustment> adjustments) {
|
||||
this.refNo = refNo;
|
||||
this.adjustments = Collections.unmodifiableMap(adjustments);
|
||||
}
|
||||
|
||||
public void forEachStartingFrom(LocalDate date, BiConsumer<LocalDate, Adjustment> f) {
|
||||
adjustments.entrySet().stream()
|
||||
.filter(e -> !e.getKey().isBefore(date))
|
||||
.forEach(e -> f.accept(e.getKey(), e.getValue()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package pl.com.bottega.factory.demand.forecasting;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
@Value
|
||||
class Adjustment {
|
||||
|
||||
Demand demand;
|
||||
boolean strong;
|
||||
|
||||
static Adjustment strong(Demand demand) {
|
||||
return new Adjustment(demand, true);
|
||||
}
|
||||
|
||||
static Adjustment week(Demand demand) {
|
||||
return new Adjustment(demand, false);
|
||||
}
|
||||
|
||||
static boolean isStrong(Adjustment adjustment) {
|
||||
return adjustment != null && adjustment.strong;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package pl.com.bottega.factory.demand.forecasting;
|
||||
|
||||
import pl.com.bottega.factory.demand.forecasting.DemandEvents.DemandedLevelsChanged.Change;
|
||||
import lombok.Value;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
class DailyDemand {
|
||||
|
||||
private final DailyId id;
|
||||
private final Events events;
|
||||
|
||||
private Demand documented;
|
||||
private Adjustment adjustment;
|
||||
|
||||
interface Events {
|
||||
void emit(LevelChanged event);
|
||||
|
||||
void emit(ReviewRequest event);
|
||||
|
||||
void emit(DemandUpdated event);
|
||||
}
|
||||
|
||||
DailyDemand(DailyId id, Events events,
|
||||
Demand documented, Adjustment adjustment) {
|
||||
this.id = id;
|
||||
this.events = events;
|
||||
this.documented = Optional.ofNullable(documented)
|
||||
.orElse(Demand.nothingDemanded());
|
||||
this.adjustment = adjustment;
|
||||
}
|
||||
|
||||
void adjust(Adjustment adjustment) {
|
||||
State state = state();
|
||||
this.adjustment = adjustment;
|
||||
|
||||
if (state.updated()) {
|
||||
events.emit(new DemandUpdated(id, documented, adjustment));
|
||||
}
|
||||
if (state.levelChanged()) {
|
||||
events.emit(new LevelChanged(id, state.getLevelChange()));
|
||||
}
|
||||
}
|
||||
|
||||
void update(Demand documented) {
|
||||
State state = state();
|
||||
if (!Adjustment.isStrong(this.adjustment)) {
|
||||
this.adjustment = null;
|
||||
}
|
||||
this.documented = documented;
|
||||
|
||||
if (state.updated()) {
|
||||
events.emit(new DemandUpdated(id, documented, adjustment));
|
||||
}
|
||||
if (state.levelChanged()) {
|
||||
events.emit(new LevelChanged(id, state.getLevelChange()));
|
||||
}
|
||||
}
|
||||
|
||||
Demand getLevel() {
|
||||
return Optional.ofNullable(adjustment)
|
||||
.map(Adjustment::getDemand)
|
||||
.orElse(documented);
|
||||
}
|
||||
|
||||
@Value
|
||||
static class LevelChanged {
|
||||
DailyId id;
|
||||
Change change;
|
||||
}
|
||||
|
||||
@Value
|
||||
static class ReviewRequest {
|
||||
DailyId id;
|
||||
Demand previousDocumented;
|
||||
Demand strongAdjustment;
|
||||
Demand newDocumented;
|
||||
}
|
||||
|
||||
@Value
|
||||
static class DemandUpdated {
|
||||
DailyId id;
|
||||
Demand documented;
|
||||
Adjustment adjustment;
|
||||
}
|
||||
|
||||
private State state() {
|
||||
return new State();
|
||||
}
|
||||
|
||||
private class State {
|
||||
final Demand documented;
|
||||
final Adjustment adjustment;
|
||||
final Demand level;
|
||||
|
||||
State() {
|
||||
this.documented = DailyDemand.this.documented;
|
||||
this.adjustment = DailyDemand.this.adjustment;
|
||||
this.level = getLevel();
|
||||
}
|
||||
|
||||
boolean updated() {
|
||||
return !Objects.equals(this.documented, DailyDemand.this.documented)
|
||||
|| !Objects.equals(this.adjustment, DailyDemand.this.adjustment);
|
||||
}
|
||||
|
||||
Change getLevelChange() {
|
||||
return new Change(level, getLevel());
|
||||
}
|
||||
|
||||
boolean levelChanged() {
|
||||
return !level.equals(getLevel());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package pl.com.bottega.factory.demand.forecasting;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Collections.unmodifiableList;
|
||||
|
||||
class Demands implements ProductDemand.Demands, DailyDemand.Events {
|
||||
final Map<LocalDate, DailyDemand> fetched = new HashMap<>();
|
||||
final List<DailyDemand.LevelChanged> changes = new ArrayList<>();
|
||||
final List<DailyDemand.DemandUpdated> updates = new ArrayList<>();
|
||||
final List<DailyDemand.ReviewRequest> warnings = new ArrayList<>();
|
||||
Function<LocalDate, DailyDemand> fetch;
|
||||
|
||||
@Override
|
||||
public DailyDemand get(LocalDate date) {
|
||||
return fetched.computeIfAbsent(date, fetch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DailyDemand.LevelChanged> getChanges() {
|
||||
return unmodifiableList(changes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DailyDemand.DemandUpdated> getUpdates() {
|
||||
return unmodifiableList(updates);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DailyDemand.ReviewRequest> getReviewRequests() {
|
||||
return unmodifiableList(warnings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void emit(DailyDemand.LevelChanged event) {
|
||||
changes.add(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void emit(DailyDemand.ReviewRequest event) {
|
||||
warnings.add(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void emit(DailyDemand.DemandUpdated event) {
|
||||
updates.add(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean anyChanges() {
|
||||
return !getChanges().isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<DailyId, DemandEvents.DemandedLevelsChanged.Change> changes() {
|
||||
return getChanges().stream().collect(Collectors.toMap(
|
||||
DailyDemand.LevelChanged::getId, DailyDemand.LevelChanged::getChange));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package pl.com.bottega.factory.demand.forecasting;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collections;
|
||||
import java.util.SortedMap;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
@Value
|
||||
public class Document {
|
||||
|
||||
Instant created;
|
||||
String refNo;
|
||||
SortedMap<LocalDate, Demand> demands;
|
||||
|
||||
public Document(Instant created, String refNo, SortedMap<LocalDate, Demand> demands) {
|
||||
this.created = created;
|
||||
this.refNo = refNo;
|
||||
this.demands = Collections.unmodifiableSortedMap(demands);
|
||||
}
|
||||
|
||||
public void forEachStartingFrom(LocalDate date, BiConsumer<LocalDate, Demand> f) {
|
||||
demands.entrySet().stream()
|
||||
.filter(e -> !e.getKey().isBefore(date))
|
||||
.forEach(e -> f.accept(e.getKey(), e.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package pl.com.bottega.factory.demand.forecasting;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import pl.com.bottega.factory.demand.forecasting.DailyDemand.DemandUpdated;
|
||||
import pl.com.bottega.factory.demand.forecasting.DailyDemand.LevelChanged;
|
||||
import pl.com.bottega.factory.demand.forecasting.DailyDemand.ReviewRequest;
|
||||
import pl.com.bottega.factory.demand.forecasting.DemandEvents.DemandedLevelsChanged;
|
||||
import pl.com.bottega.factory.demand.forecasting.DemandEvents.DemandedLevelsChanged.Change;
|
||||
import pl.com.bottega.factory.product.management.RefNoId;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@AllArgsConstructor
|
||||
class ProductDemand {
|
||||
|
||||
final RefNoId id;
|
||||
final Demands demands;
|
||||
|
||||
final Clock clock;
|
||||
final DemandEvents events;
|
||||
|
||||
interface Demands {
|
||||
DailyDemand get(LocalDate date);
|
||||
|
||||
List<LevelChanged> getChanges();
|
||||
|
||||
List<DemandUpdated> getUpdates();
|
||||
|
||||
List<ReviewRequest> getReviewRequests();
|
||||
|
||||
boolean anyChanges();
|
||||
|
||||
Map<DailyId, Change> changes();
|
||||
}
|
||||
|
||||
void adjust(AdjustDemand adjustDemand) {
|
||||
LocalDate today = LocalDate.now(clock);
|
||||
|
||||
adjustDemand.forEachStartingFrom(today, this::adjustDaily);
|
||||
|
||||
if (demands.anyChanges()) {
|
||||
events.emit(new DemandedLevelsChanged(id, demands.changes()));
|
||||
}
|
||||
}
|
||||
|
||||
void process(Document document) {
|
||||
LocalDate today = LocalDate.now(clock);
|
||||
|
||||
document.forEachStartingFrom(today, this::updateDaily);
|
||||
|
||||
if (demands.anyChanges()) {
|
||||
events.emit(new DemandedLevelsChanged(id, demands.changes()));
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustDaily(LocalDate date, Adjustment adjustment) {
|
||||
DailyDemand demand = demands.get(date);
|
||||
demand.adjust(adjustment);
|
||||
}
|
||||
|
||||
private void updateDaily(LocalDate date, Demand demand) {
|
||||
DailyDemand daily = demands.get(date);
|
||||
daily.update(demand);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package pl.com.bottega.factory.demand.forecasting
|
||||
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
|
||||
import static pl.com.bottega.factory.demand.forecasting.DemandEvents.DemandedLevelsChanged.Change;
|
||||
|
||||
class DailyDemandBuilder {
|
||||
|
||||
Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault())
|
||||
DailyDemand.Events events
|
||||
|
||||
String refNo = "3009000"
|
||||
private LocalDate date = LocalDate.now(clock)
|
||||
private Demand base
|
||||
private Adjustment adjustment
|
||||
|
||||
DailyDemand build() {
|
||||
new DailyDemand(new DailyId(refNo, date), events, base, adjustment)
|
||||
}
|
||||
|
||||
DailyDemandBuilder reset() {
|
||||
nothingDemanded()
|
||||
noAdjustments()
|
||||
}
|
||||
|
||||
Object asType(Class clazz) {
|
||||
clazz == DailyDemand ? build() : super.asType(clazz)
|
||||
}
|
||||
|
||||
DailyDemandBuilder events(DailyDemand.Events events) {
|
||||
this.events = events
|
||||
this
|
||||
}
|
||||
|
||||
DailyDemandBuilder nextDate() {
|
||||
date.plusDays(1)
|
||||
this
|
||||
}
|
||||
|
||||
DailyDemandBuilder date(LocalDate date) {
|
||||
this.date = date
|
||||
this
|
||||
}
|
||||
|
||||
DailyDemandBuilder nothingDemanded() {
|
||||
base = null
|
||||
this
|
||||
}
|
||||
|
||||
DailyDemandBuilder noAdjustments() {
|
||||
adjustment = null
|
||||
this
|
||||
}
|
||||
|
||||
DailyDemandBuilder demandedLevels(long level) {
|
||||
base = Demand.of(level)
|
||||
this
|
||||
}
|
||||
|
||||
DailyDemandBuilder adjustedTo(long level) {
|
||||
adjustment = new Adjustment(Demand.of(level), false)
|
||||
this
|
||||
}
|
||||
|
||||
DailyDemandBuilder stronglyAdjustedTo(long level) {
|
||||
adjustment = new Adjustment(Demand.of(level), true)
|
||||
this
|
||||
}
|
||||
|
||||
Demand newCallOffDemand(long level) {
|
||||
Demand.of(level)
|
||||
}
|
||||
|
||||
Adjustment adjustDemandTo(long level) {
|
||||
new Adjustment(Demand.of(level), false)
|
||||
}
|
||||
|
||||
DailyDemand.LevelChanged levelChanged(long previous, long current) {
|
||||
new DailyDemand.LevelChanged(
|
||||
new DailyId(refNo, date),
|
||||
new Change(Demand.of(previous), Demand.of(current))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
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
|
||||
|
||||
class DemandAdjustmentSpec extends Specification {
|
||||
|
||||
def events = Mock(DemandEvents)
|
||||
def builder = new ProductDemandBuilder(events: events)
|
||||
|
||||
def "Adjusted demands should be stored"() {
|
||||
given:
|
||||
def today = LocalDate.now(builder.clock)
|
||||
def demand = demand(2800, 0)
|
||||
def adjustments = adjustments([(today): 1000])
|
||||
|
||||
when:
|
||||
demand.adjust(adjustments)
|
||||
|
||||
then:
|
||||
1 * events.emit(levelChanged([2800, 1000]))
|
||||
}
|
||||
|
||||
def "Adjustment of future demands is possible"() {
|
||||
given:
|
||||
def today = LocalDate.now(builder.clock)
|
||||
def demand = demand(2800)
|
||||
def adjustments = adjustments([(today.plusDays(1)): 1000])
|
||||
|
||||
when:
|
||||
demand.adjust(adjustments)
|
||||
|
||||
then:
|
||||
1 * events.emit(levelChanged(notChanged(), [0, 1000]))
|
||||
}
|
||||
|
||||
def "Adjustment without changes should not generate event"() {
|
||||
given:
|
||||
def today = LocalDate.now(builder.clock)
|
||||
def demand = demand(2800, 1000)
|
||||
def adjustments = adjustments([(today): 2800, (today.plusDays(1)): 1000])
|
||||
|
||||
when:
|
||||
demand.adjust(adjustments)
|
||||
|
||||
then:
|
||||
0 * events.emit(_ as DemandedLevelsChanged)
|
||||
}
|
||||
|
||||
def "Should skip past demands adjustments"() {
|
||||
given:
|
||||
def pastDate = LocalDate.now(builder.clock).minusDays(2)
|
||||
def demand = demand(2800, 0)
|
||||
def adjustments = adjustments([(pastDate): 1000])
|
||||
|
||||
when:
|
||||
demand.adjust(adjustments)
|
||||
|
||||
then:
|
||||
0 * events.emit(_ as DemandedLevelsChanged)
|
||||
}
|
||||
|
||||
def "Adjustment should be idempotent"() {
|
||||
given:
|
||||
def today = LocalDate.now(builder.clock)
|
||||
def demand = demand(2800, 0)
|
||||
def adjustments = adjustments((today): 2000, (today.plusDays(1)): 3500)
|
||||
|
||||
when:
|
||||
demand.adjust(adjustments)
|
||||
|
||||
then:
|
||||
1 * events.emit(levelChanged([2800, 2000], [0, 3500]))
|
||||
|
||||
when:
|
||||
builder.demands.clearUnitOfWork()
|
||||
demand.adjust(adjustments)
|
||||
|
||||
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() {
|
||||
[]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package pl.com.bottega.factory.demand.forecasting
|
||||
|
||||
import java.time.Clock
|
||||
import java.time.LocalDate
|
||||
|
||||
class DemandsRepositoryFake extends Demands {
|
||||
|
||||
DailyDemandBuilder builder
|
||||
|
||||
DemandsRepositoryFake(String refNo, Clock clock) {
|
||||
this.builder = new DailyDemandBuilder(refNo: refNo, clock: clock, events: this)
|
||||
fetch = { date -> nothingDemanded(date) }
|
||||
}
|
||||
|
||||
DailyDemand nothingDemanded(LocalDate date) {
|
||||
def demand = builder.reset()
|
||||
.date(date)
|
||||
.build()
|
||||
fetched.put(date, demand)
|
||||
demand
|
||||
}
|
||||
|
||||
DailyDemand demanded(LocalDate date, long level) {
|
||||
def demand = builder.date(date)
|
||||
.demandedLevels(level)
|
||||
.noAdjustments()
|
||||
.build()
|
||||
fetched.put(date, demand)
|
||||
demand
|
||||
}
|
||||
|
||||
void clearUnitOfWork() {
|
||||
super.@changes.clear()
|
||||
super.@warnings.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
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
|
||||
|
||||
class DocumentProcessingSpec extends Specification {
|
||||
|
||||
def events = Mock(DemandEvents)
|
||||
def builder = new ProductDemandBuilder(events: events)
|
||||
|
||||
def "Updated demands should be stored"() {
|
||||
given:
|
||||
def today = LocalDate.now(builder.clock)
|
||||
def demand = demand(2800, 0)
|
||||
def document = document(today, 2000, 3500)
|
||||
|
||||
when:
|
||||
demand.process(document)
|
||||
|
||||
then:
|
||||
1 * events.emit(levelChanged([2800, 2000], [0, 3500]))
|
||||
}
|
||||
|
||||
def "Demands for dates not present in system should be stored "() {
|
||||
given:
|
||||
def today = LocalDate.now(builder.clock)
|
||||
def demand = demand(1000)
|
||||
def document = document(today, 1000, 3500, 1000)
|
||||
|
||||
when:
|
||||
demand.process(document)
|
||||
|
||||
then:
|
||||
1 * events.emit(levelChanged(notChanged(), [0, 3500], [0, 1000]))
|
||||
}
|
||||
|
||||
def "Document without changes should not generate event"() {
|
||||
given:
|
||||
def today = LocalDate.now(builder.clock)
|
||||
def demand = demand(2800, 0)
|
||||
def document = document(today, 2800, 0)
|
||||
|
||||
when:
|
||||
demand.process(document)
|
||||
|
||||
then:
|
||||
0 * events.emit(_ as DemandedLevelsChanged)
|
||||
}
|
||||
|
||||
def "Should skip past demands from document"() {
|
||||
given:
|
||||
def pastDate = LocalDate.now(builder.clock).minusDays(2)
|
||||
def demand = demand(0, 0)
|
||||
def document = document(pastDate, 2800, 2800, 3500, 1000)
|
||||
|
||||
when:
|
||||
demand.process(document)
|
||||
|
||||
then:
|
||||
1 * events.emit(levelChanged([0, 3500], [0, 1000]))
|
||||
}
|
||||
|
||||
def "Document processing should be idempotent"() {
|
||||
given:
|
||||
def today = LocalDate.now(builder.clock)
|
||||
def demand = demand(2800, 0)
|
||||
def document = document(today, 2000, 3500)
|
||||
|
||||
when:
|
||||
demand.process(document)
|
||||
|
||||
then:
|
||||
1 * events.emit(levelChanged([2800, 2000], [0, 3500]))
|
||||
|
||||
when:
|
||||
builder.demands.clearUnitOfWork()
|
||||
demand.process(document)
|
||||
|
||||
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() {
|
||||
[]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package pl.com.bottega.factory.demand.forecasting
|
||||
|
||||
import spock.lang.PendingFeature
|
||||
import spock.lang.Specification
|
||||
|
||||
class KeepingDailyDemandsSpec extends Specification {
|
||||
|
||||
def builder = new DailyDemandBuilder()
|
||||
def events = Mock(DailyDemand.Events)
|
||||
|
||||
def "Adjusted demands should be stored"() {
|
||||
given:
|
||||
def demand = demand()
|
||||
.demandedLevels(2800)
|
||||
.noAdjustments().build()
|
||||
|
||||
when:
|
||||
demand.adjust(adjustDemandTo(3500))
|
||||
|
||||
then:
|
||||
demand.getLevel() == Demand.of(3500)
|
||||
1 * events.emit(levelChanged(2800, 3500))
|
||||
}
|
||||
|
||||
def "Adjusted demands should be stored when there is no demand for product"() {
|
||||
given:
|
||||
def demand = demand()
|
||||
.nothingDemanded()
|
||||
.noAdjustments().build()
|
||||
|
||||
when:
|
||||
demand.adjust(adjustDemandTo(3500))
|
||||
|
||||
then:
|
||||
demand.getLevel() == Demand.of(3500)
|
||||
1 * events.emit(levelChanged(0, 3500))
|
||||
}
|
||||
|
||||
def "In standard case documented demands overrides adjustments"() {
|
||||
given:
|
||||
def demand = demand()
|
||||
.demandedLevels(2800)
|
||||
.adjustedTo(3500).build()
|
||||
|
||||
when:
|
||||
demand.update(newCallOffDemand(4000))
|
||||
|
||||
then:
|
||||
demand.getLevel() == Demand.of(4000)
|
||||
1 * events.emit(levelChanged(3500, 4000))
|
||||
}
|
||||
|
||||
def "Strong adjustment is kept even after processing of document"() {
|
||||
given:
|
||||
def demand = demand()
|
||||
.demandedLevels(2800)
|
||||
.stronglyAdjustedTo(3500).build()
|
||||
|
||||
when:
|
||||
demand.update(newCallOffDemand(2800))
|
||||
|
||||
then:
|
||||
demand.getLevel() == Demand.of(3500)
|
||||
0 * events.emit(_ as DailyDemand.LevelChanged)
|
||||
}
|
||||
|
||||
@PendingFeature
|
||||
def "Document update hidden by strong adjustment should rise warning"() {
|
||||
given:
|
||||
def demand = demand()
|
||||
.demandedLevels(2800)
|
||||
.stronglyAdjustedTo(3500).build()
|
||||
|
||||
when:
|
||||
demand.update(newCallOffDemand(5000))
|
||||
|
||||
then:
|
||||
demand.getLevel() == Demand.of(3500)
|
||||
1 * events.emit(_ as DailyDemand.ReviewRequest)
|
||||
}
|
||||
|
||||
DailyDemandBuilder demand() {
|
||||
builder.events = events
|
||||
builder
|
||||
}
|
||||
|
||||
Demand newCallOffDemand(long level) {
|
||||
builder.newCallOffDemand(level)
|
||||
}
|
||||
|
||||
Adjustment adjustDemandTo(long level) {
|
||||
builder.adjustDemandTo(level)
|
||||
}
|
||||
|
||||
def levelChanged(long previous, long current) {
|
||||
builder.levelChanged(previous, current)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package pl.com.bottega.factory.demand.forecasting
|
||||
|
||||
import pl.com.bottega.factory.product.management.RefNoId
|
||||
|
||||
import java.time.*
|
||||
|
||||
import static DemandedLevelsChanged.Change
|
||||
import static pl.com.bottega.factory.demand.forecasting.DemandEvents.DemandedLevelsChanged
|
||||
|
||||
class ProductDemandBuilder {
|
||||
|
||||
def refNo = "3009000"
|
||||
def demands = new DemandsRepositoryFake(refNo, clock)
|
||||
def clock = Clock.fixed(Instant.now(), ZoneId.systemDefault())
|
||||
DemandEvents events
|
||||
|
||||
ProductDemand demand(long ... levels) {
|
||||
def date = LocalDate.now(clock)
|
||||
for (long level : levels) {
|
||||
demands.demanded(date, level)
|
||||
date = date.plusDays(1)
|
||||
}
|
||||
new ProductDemand(new RefNoId(refNo), demands, clock, events)
|
||||
}
|
||||
|
||||
Document 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) {
|
||||
results.put(date, Demand.of(level))
|
||||
date = date.plusDays(1)
|
||||
}
|
||||
new Document(created, refNo, results)
|
||||
}
|
||||
|
||||
AdjustDemand adjustDemand(Map<LocalDate, Long> adjustments) {
|
||||
Map<LocalDate, Adjustment> results = new HashMap<>()
|
||||
adjustments.forEach { date, level ->
|
||||
results.put(date, Adjustment.week(Demand.of(level)))
|
||||
}
|
||||
new AdjustDemand(refNo, results)
|
||||
}
|
||||
|
||||
DemandedLevelsChanged levelChanged(List<Long>... changes) {
|
||||
def date = LocalDate.now(clock)
|
||||
Map<DailyId, Change> results = new HashMap<>()
|
||||
for (def change : changes) {
|
||||
if (change.size() == 2) {
|
||||
results.put(new DailyId(refNo, date), new Change(
|
||||
Demand.of(change[0]),
|
||||
Demand.of(change[1])))
|
||||
}
|
||||
date = date.plusDays(1)
|
||||
}
|
||||
new DemandedLevelsChanged(new RefNoId(refNo), results)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package pl.com.bottega.factory.demand.forecasting;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class DemandsFake extends Demands {
|
||||
|
||||
public DemandsFake(Map<LocalDate, DailyDemand> fetched,
|
||||
Function<LocalDate, DailyDemand> factory) {
|
||||
super();
|
||||
this.fetched.putAll(fetched);
|
||||
this.fetch = factory;
|
||||
}
|
||||
|
||||
public void clearUnitOfWork() {
|
||||
this.changes.clear();
|
||||
this.warnings.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
Feature: manual adjustments of demand
|
||||
|
||||
sub domain: demand forecasting
|
||||
keeps track of current and future customer needs for our products
|
||||
|
||||
Domain story:
|
||||
Adjust demand at day to amount, delivered.
|
||||
>> demand.adjust(productRefNo, atDay, amount)
|
||||
We can change only Demands for today and future.
|
||||
|
||||
New demand is stored for further reference
|
||||
Data from call-off document should be preserved (DON’T OVERRIDE THEM).
|
||||
Adjust demand should be possible even
|
||||
if there was no call-off document for that product.
|
||||
In standard case future call-off documents should be stronger (overrides) adjustment,
|
||||
but if customer warn us about opposite case import of call-off document should not remove previous adjustments.
|
||||
|
||||
emit domain event demand changed
|
||||
|
||||
Logistician note should be kept along with adjustment.
|
||||
|
||||
outside of context boundary:
|
||||
If new demand is not fulfilled by
|
||||
current product stock and production forecast
|
||||
there is a shortage in particular days and we need to rise an alert.
|
||||
planner should be notified,
|
||||
|
||||
if there are locked parts on stock,
|
||||
QA task for recovering them should have high priority.
|
||||
|
||||
Scenario: demand increased but fulfilled by current stock level
|
||||
Given demand for product refNo "3009000":
|
||||
| 0 | 100 | 300 | 0 | 0 | 0 | 0 |
|
||||
When demand of refNo "3009000" is adjusted for "tomorrow" to 400
|
||||
Then demand is changed to 400
|
||||
Then current demands are
|
||||
| 0 | 400 | 300 | 0 | 0 | 0 | 0 |
|
||||
Then no shortage was found
|
||||
Given current stock of proper parts of refNo "3009000" is 750
|
||||
|
||||
Scenario: demand increased, shortage is found
|
||||
Given demand for product refNo "3009000":
|
||||
| 0 | 100 | 300 | 0 | 0 | 0 | 0 |
|
||||
Given no production of refNo "3009000" is planned
|
||||
Given current stock of proper parts of refNo "3009000" is 350
|
||||
When demand of refNo "3009000" is adjusted for "tomorrow" to 400
|
||||
Then demand is changed
|
||||
Then shortage of 250 parts for "day after tomorrow" is found
|
||||
Then current demands are
|
||||
| 0 | 400 | 300 | 0 | 0 | 0 | 0 |
|
||||
|
||||
Scenario: demand increased, but stock forecast including production will be enough
|
||||
Given demand for product refNo "3009000":
|
||||
| 0 | 100 | 300 | 0 | 0 | 0 | 0 |
|
||||
Given planned production of refNo "3009000":
|
||||
| 300 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||
Given current stock of proper parts of refNo "3009000" is 450
|
||||
When demand of refNo "3009000" is adjusted for "tomorrow" to 400
|
||||
Then demand is changed
|
||||
Then no shortage was found
|
||||
Then current demands are
|
||||
| 0 | 400 | 300 | 0 | 0 | 0 | 0 |
|
||||
|
||||
Scenario: next call-off will be stronger than adjustment
|
||||
Given demand for product refNo "3009000":
|
||||
| 0 | 100 | 0 | 0 | 0 | 0 | 0 |
|
||||
When demand of refNo "3009000" is adjusted for "tomorrow" to 400
|
||||
Then demand is changed
|
||||
When new call-off document for product refNo "3009000" contains:
|
||||
| 0 | 100 | 0 | 0 | 0 | 0 | 0 |
|
||||
Then demand is not changed
|
||||
Then current demands are
|
||||
| 0 | 100 | 0 | 0 | 0 | 0 | 0 |
|
||||
|
||||
Scenario: in some cases adjustment will be stronger that future call-off
|
||||
Given demand for product refNo "3009000":
|
||||
| 0 | 100 | 0 | 0 | 0 | 0 | 0 |
|
||||
When demand of refNo "3009000" is strongly adjusted for "tomorrow" to 400
|
||||
Then demand is changed
|
||||
When new call-off document for product refNo "3009000" contains:
|
||||
| 0 | 100 | 0 | 0 | 0 | 0 | 0 |
|
||||
Then demand is changed
|
||||
Then current demands are
|
||||
| 0 | 400 | 0 | 0 | 0 | 0 | 0 |
|
||||
|
||||
Scenario: strong adjustment will be reported if new call-off contains other value then previous
|
||||
Given demand for product refNo "3009000":
|
||||
| 0 | 100 | 0 | 0 | 0 | 0 | 0 |
|
||||
When demand of refNo "3009000" is strongly adjusted for "tomorrow" to 400
|
||||
Then demand is changed
|
||||
When new call-off document for product refNo "3009000" contains:
|
||||
| 0 | 600 | 0 | 0 | 0 | 0 | 0 |
|
||||
Then review request is shown on report [demand for refNo "3009000" was 100, adjusted to 400, but call-off has 600]
|
||||
Then demand is not changed
|
||||
Then current demands are
|
||||
| 0 | 400 | 0 | 0 | 0 | 0 | 0 |
|
||||
@@ -0,0 +1,90 @@
|
||||
Feature: Production planning
|
||||
|
||||
// https://github.com/michal-michaluk/production/tree/master
|
||||
|
||||
sub domain: production planning
|
||||
Keep track of correct production plan
|
||||
to ensure sufficient capacity
|
||||
and high utilization of resources in tune with forecast demand
|
||||
|
||||
Domain story:
|
||||
Production plan schedule new production on line, using form, at time for duration.
|
||||
>> productionPlan.scheduleNewProduction(line, form, time, duration)
|
||||
Only if form will be available at the time for whole duration, production may by planned.
|
||||
Production includes retooling time, parts output of used form and utilization of human resources
|
||||
on that production stage.
|
||||
If production on given line overlaps with other: preceding must shrink,
|
||||
succeeding one must start later.
|
||||
If consecutive productions use same from, retooling time between is zero. [not mentioned prev.]
|
||||
Production forecasts (parts output * production duration) changes new and changed productions.
|
||||
Overall utilization of human resources increases.
|
||||
Shortage may arise if insufficient production was planned.
|
||||
|
||||
|
||||
Production plan adjust production to time and duration.
|
||||
>> productionPlan.adjust(production, time, duration)
|
||||
If production on given line overlaps with other: preceding must shrink,
|
||||
succeeding one must start later.
|
||||
If consecutive productions use same from, retooling time between is zero.
|
||||
Production forecasts (parts output * production duration) changes for changed productions.
|
||||
Overall utilization of human resources may change.
|
||||
Shortage may arise if insufficient production was planned.
|
||||
|
||||
Scenario: schedule new production consecutive production must shrink
|
||||
Given plan for next monday:
|
||||
| line | beginAt | duration | productRefNo |
|
||||
| 1 | 6:00 | 3.5h | 3009000 |
|
||||
When new production is scheduled for next monday:
|
||||
| line | beginAt | duration | productRefNo |
|
||||
| 1 | 8:00 | 2.0h | 4400800 |
|
||||
Then demand is changed
|
||||
| line | beginAt | duration | productRefNo |
|
||||
| 1 | 6:00 | 2.0h | 3009000 |
|
||||
| 1 | 8:00 | 2.0h | 4400800 |
|
||||
Then current production outputs for "3009000" are
|
||||
| 2000 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||
Then current production outputs for "4400800" are
|
||||
| 3500 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||
Then no shortage was found
|
||||
|
||||
Scenario: schedule new production succeeding productions must start later
|
||||
Given plan for next monday:
|
||||
| line | beginAt | duration | productRefNo |
|
||||
| 1 | 6:00 | 2.0h | 3009000 |
|
||||
| 1 | 9:30 | 3.5h | 3009000A |
|
||||
When new production is scheduled for next monday:
|
||||
| line | beginAt | duration | productRefNo |
|
||||
| 1 | 8:00 | 2.0h | 4400800 |
|
||||
Then demand is changed
|
||||
| line | beginAt | duration | productRefNo |
|
||||
| 1 | 6:00 | 2.0h | 3009000 |
|
||||
| 1 | 8:00 | 2.0h | 4400800 |
|
||||
| 1 | 10:00 | 3.5h | 3009000A |
|
||||
Then current production outputs for "3009000" are
|
||||
| 2000 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||
Then current production outputs for "3009000A" are
|
||||
| 2000 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||
Then current production outputs for "4400800" are
|
||||
| 3500 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||
Then no shortage was found
|
||||
|
||||
Scenario: schedule new production consecutive production must shrink and succeeding must start later
|
||||
Given plan for next monday:
|
||||
| line | beginAt | duration | productRefNo |
|
||||
| 1 | 6:00 | 3.5h | 3009000 |
|
||||
| 1 | 9:30 | 3.5h | 3009000A |
|
||||
When new production is scheduled for next monday:
|
||||
| line | beginAt | duration | productRefNo |
|
||||
| 1 | 8:00 | 2.0h | 4400800 |
|
||||
Then demand is changed
|
||||
| line | beginAt | duration | productRefNo |
|
||||
| 1 | 6:00 | 2.0h | 3009000 |
|
||||
| 1 | 8:00 | 2.0h | 4400800 |
|
||||
| 1 | 10:00 | 3.5h | 3009000A |
|
||||
Then current production outputs for "3009000" are
|
||||
| 2000 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||
Then current production outputs for "3009000A" are
|
||||
| 2000 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||
Then current production outputs for "4400800" are
|
||||
| 3500 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||
Then no shortage was found
|
||||
@@ -0,0 +1,81 @@
|
||||
|
||||
Production plan schedule new production on line, using form, at time for duration.
|
||||
>> productionPlan.scheduleNewProduction(line, form, time, duration)
|
||||
Only if form will be available at the time for whole duration, production may by planned.
|
||||
Production includes retooling time, parts output of used form and utilization of human resources
|
||||
on that production stage.
|
||||
If production on given line overlaps with other: preceding must shrink,
|
||||
succeeding one must start later.
|
||||
If consecutive productions use same from, retooling time between is zero. [not mentioned prev.]
|
||||
Production forecasts (parts output * production duration) changes new and changed productions.
|
||||
Overall utilization of human resources increases.
|
||||
Shortage may arise if insufficient production was planned.
|
||||
|
||||
|
||||
Production plan adjust production to time and duration.
|
||||
>> productionPlan.adjust(production, time, duration)
|
||||
If production on given line overlaps with other: preceding must shrink,
|
||||
succeeding one must start later.
|
||||
If consecutive productions use same from, retooling time between is zero.
|
||||
Production forecasts (parts output * production duration) changes for changed productions.
|
||||
Overall utilization of human resources may change.
|
||||
Shortage may arise if insufficient production was planned.
|
||||
|
||||
|
||||
Daily processing of callof document:
|
||||
>> callofProcess.process(document)
|
||||
for all products included in callof document
|
||||
New demand are stored for further reference
|
||||
If new demand is not fulfilled by product stock and production forecast
|
||||
there is a shortage in particular days and we need to rise an alert.
|
||||
planner should be notified in that case,
|
||||
if there are locked parts on stock,
|
||||
QA task for recovering them should have high priority.
|
||||
|
||||
|
||||
Adjust demand at day to amount, delivered.
|
||||
>> demand.require(productRefNo, atDay, amount)
|
||||
New demand is stored for further reference
|
||||
If new demand is not fulfilled by
|
||||
current product stock and production forecast
|
||||
there is a shortage in particular days and we need to rise an alert.
|
||||
planner should be notified,
if there are locked parts on stock,
|
||||
QA task for recovering them should have high priority.
|
||||
|
||||
ADDED:
We can change only Demands for today and future.
|
||||
Data from callof document should be preserved in database (DON’T OVERRIDE THEM).
|
||||
Should be possible to adjust demand even
|
||||
if there was no callof document for that product.
|
||||
Logistician note should be kept along with adjustment.
|
||||
|
||||
|
||||
Lock all parts from storage unit on stock.
|
||||
>> stock.lock(storageUnit)
|
||||
parts from storage unit are locked on stock
|
||||
If locking parts can lead to insufficient stock for next deliveries,
|
||||
parts recovery should have high priority.
|
||||
If there is a potential shortage in particular days,
|
||||
we need to rise an soft notification to planner.
|
||||
|
||||
|
||||
Unlock storage unit, recover X parts, Y parts was scrapped.
|
||||
>> stock.unlock(storageUnit, recovered, scrapped)
|
||||
Recovered parts are back on stock.
|
||||
Scrapped parts are removed from stock.
|
||||
If demand is not fulfilled by current product stock and production forecast
|
||||
there is a shortage in particular days and we need to rise an alert.
|
||||
|
||||
|
||||
Register newly produced parts on stock.
|
||||
>> stock.registerNew(storageUnit)
|
||||
new parts are available on stock.
|
||||
If output from production is smaller than planned
|
||||
it may lead to shortage in next days.
|
||||
|
||||
|
||||
Remove delivered parts from stock.
|
||||
>> stock.delivered(deliveryNote)
|
||||
If parts delivered during day exceed registered customer demand,
|
||||
demand for next day will be probably corrected with upcoming callof document,
|
||||
but in rare cases it may be caused by not registered additional delivery
|
||||
(lack of manual adjustments of demand in system).
|
||||
13
docker-compose.yml
Normal file
13
docker-compose.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
version: "2"
|
||||
|
||||
volumes:
|
||||
data:
|
||||
driver: local
|
||||
|
||||
services:
|
||||
db:
|
||||
build: ./database/
|
||||
volumes:
|
||||
- data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
20
pom.xml
Normal file
20
pom.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?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>factory</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<modules>
|
||||
<!--<module>app-monolith</module>-->
|
||||
<module>shared-kernel-model</module>
|
||||
<module>demand-forecasting-model</module>
|
||||
<module>shortages-prediction-model</module>
|
||||
<!--<module>playground-model</module>-->
|
||||
</modules>
|
||||
|
||||
</project>
|
||||
82
shared-kernel-model/pom.xml
Normal file
82
shared-kernel-model/pom.xml
Normal file
@@ -0,0 +1,82 @@
|
||||
<?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>shared-kernel-model</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.7.0</version>
|
||||
<configuration>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
<compilerArgument>-parameters</compilerArgument>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.gmavenplus</groupId>
|
||||
<artifactId>gmavenplus-plugin</artifactId>
|
||||
<version>1.6</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>compileTests</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.20.1</version>
|
||||
<configuration>
|
||||
<useFile>false</useFile>
|
||||
<includes>
|
||||
<include>**/*Spec.java</include>
|
||||
<include>**/*Test.java</include>
|
||||
</includes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.16.18</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>3.8.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>info.cukes</groupId>
|
||||
<artifactId>cucumber-java</artifactId>
|
||||
<version>1.2.5</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>info.cukes</groupId>
|
||||
<artifactId>cucumber-core</artifactId>
|
||||
<version>1.2.5</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.spockframework</groupId>
|
||||
<artifactId>spock-core</artifactId>
|
||||
<version>1.1-groovy-2.4</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,15 @@
|
||||
package pl.com.bottega.factory.demand.forecasting;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode
|
||||
public class DailyId {
|
||||
private final String refNo;
|
||||
private final LocalDate date;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package pl.com.bottega.factory.demand.forecasting;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
@Value
|
||||
public class Demand {
|
||||
long level;
|
||||
Schema schema;
|
||||
|
||||
public enum Schema {
|
||||
AtDayStart, Every3hours, TillDayEnd
|
||||
}
|
||||
|
||||
public static Demand nothingDemanded() {
|
||||
return of(0);
|
||||
}
|
||||
|
||||
public static Demand of(long level) {
|
||||
return new Demand(level, Schema.TillDayEnd);
|
||||
}
|
||||
|
||||
public static Demand of(long level, Schema schema) {
|
||||
return new Demand(level, schema);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package pl.com.bottega.factory.demand.forecasting;
|
||||
|
||||
import lombok.Value;
|
||||
import pl.com.bottega.factory.product.management.RefNoId;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
public interface DemandEvents {
|
||||
void emit(DemandedLevelsChanged event);
|
||||
|
||||
@Value
|
||||
class DemandedLevelsChanged {
|
||||
RefNoId id;
|
||||
Map<DailyId, Change> results;
|
||||
|
||||
public DemandedLevelsChanged(RefNoId id, Map<DailyId, Change> results) {
|
||||
this.id = id;
|
||||
this.results = Collections.unmodifiableMap(results);
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class Change {
|
||||
Demand previous;
|
||||
Demand current;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package pl.com.bottega.factory.product.management;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode
|
||||
public class RefNoId {
|
||||
private final String refNo;
|
||||
}
|
||||
87
shortages-prediction-model/pom.xml
Normal file
87
shortages-prediction-model/pom.xml
Normal file
@@ -0,0 +1,87 @@
|
||||
<?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>shortages-prediction-model</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.7.0</version>
|
||||
<configuration>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
<compilerArgument>-parameters</compilerArgument>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.gmavenplus</groupId>
|
||||
<artifactId>gmavenplus-plugin</artifactId>
|
||||
<version>1.6</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>compileTests</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.20.1</version>
|
||||
<configuration>
|
||||
<useFile>false</useFile>
|
||||
<includes>
|
||||
<include>**/*Spec.java</include>
|
||||
<include>**/*Test.java</include>
|
||||
</includes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>pl.com.bottega</groupId>
|
||||
<artifactId>shared-kernel-model</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.16.18</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>3.8.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>info.cukes</groupId>
|
||||
<artifactId>cucumber-java</artifactId>
|
||||
<version>1.2.5</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>info.cukes</groupId>
|
||||
<artifactId>cucumber-core</artifactId>
|
||||
<version>1.2.5</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.spockframework</groupId>
|
||||
<artifactId>spock-core</artifactId>
|
||||
<version>1.1-groovy-2.4</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,8 @@
|
||||
package pl.com.bottega.factory.shortages.prediction;
|
||||
|
||||
/**
|
||||
* Created by michal on 02.02.2017.
|
||||
*/
|
||||
public interface Configuration {
|
||||
int shortagePredictionDaysAhead();
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package pl.com.bottega.factory.shortages.prediction;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Value;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* Levels missing to satisfy customer demand of particular product.
|
||||
* <p>
|
||||
* Created by michal on 22.10.2015.
|
||||
*/
|
||||
@Value
|
||||
public class Shortages {
|
||||
|
||||
private final String refNo;
|
||||
private final long lockedParts;
|
||||
private final LocalDateTime found;
|
||||
private final SortedMap<LocalDateTime, Long> shortages;
|
||||
|
||||
public static Shortages.Builder builder(String refNo, long locked, LocalDateTime found) {
|
||||
return new Builder(refNo, locked, found);
|
||||
}
|
||||
|
||||
public static boolean areNotSame(Shortages first, Shortages second) {
|
||||
return !areSame(first, second);
|
||||
}
|
||||
|
||||
public static boolean areSame(Shortages first, Shortages second) {
|
||||
boolean noShortages = first == null && second == null;
|
||||
boolean onlyOne = first == null && second != null || first != null && second == null;
|
||||
if (noShortages || onlyOne) return false;
|
||||
boolean sameProduct = first.refNo.equals(second.refNo);
|
||||
boolean sameNumbers = first.shortages.equals(second.shortages);
|
||||
return sameProduct && sameNumbers;
|
||||
}
|
||||
|
||||
public boolean anyBefore(LocalDateTime time) {
|
||||
return shortages.firstKey().isBefore(time);
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
public static class Builder {
|
||||
private final String refNo;
|
||||
private final long locked;
|
||||
private final LocalDateTime found;
|
||||
private final SortedMap<LocalDateTime, Long> gaps = new TreeMap<>();
|
||||
|
||||
public Builder add(LocalDateTime time, long level) {
|
||||
gaps.put(time, Math.abs(level));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Optional<Shortages> build() {
|
||||
if (gaps.isEmpty()) {
|
||||
return Optional.empty();
|
||||
} else {
|
||||
return Optional.of(new Shortages(refNo, locked, found,
|
||||
Collections.unmodifiableSortedMap(gaps)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.calculation;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
@Value
|
||||
public class CurrentStock {
|
||||
long level;
|
||||
long locked;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.calculation;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
@AllArgsConstructor
|
||||
class Demands {
|
||||
|
||||
private final Map<LocalDateTime, Long> demands;
|
||||
|
||||
long get(LocalDateTime time) {
|
||||
return demands.getOrDefault(time, 0L);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.calculation;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import pl.com.bottega.factory.shortages.prediction.Shortages;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@AllArgsConstructor
|
||||
public class Forecast {
|
||||
|
||||
String refNo;
|
||||
LocalDateTime created;
|
||||
List<LocalDateTime> times;
|
||||
CurrentStock stock;
|
||||
ProductionOutputs outputs;
|
||||
Demands demands;
|
||||
|
||||
public Optional<Shortages> findShortages() {
|
||||
// TODO ASK including locked or only proper parts
|
||||
// TODO ASK current stock or on day start? what if we are in the middle of production a day?
|
||||
long level = stock.getLevel();
|
||||
|
||||
Shortages.Builder found = Shortages.builder(refNo, stock.getLocked(), created);
|
||||
LocalDateTime lastTime = created;
|
||||
for (LocalDateTime time : times) {
|
||||
long demand = demands.get(time);
|
||||
long produced = outputs.getOutput(lastTime, time);
|
||||
|
||||
long levelOnDelivery = level + produced - demand;
|
||||
|
||||
if (levelOnDelivery < 0) {
|
||||
found.add(time, levelOnDelivery);
|
||||
}
|
||||
// TODO: ASK accumulated shortages or reset when under zero?
|
||||
level = levelOnDelivery >= 0 ? levelOnDelivery : 0;
|
||||
lastTime = time;
|
||||
}
|
||||
return found.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.calculation;
|
||||
|
||||
public interface Forecasts {
|
||||
Forecast get(String refNo, int daysAhead);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.calculation;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Value;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Value
|
||||
class ProductionForecast {
|
||||
List<Item> items;
|
||||
|
||||
ProductionOutputs outputsInTimes(LocalDateTime now, Set<LocalDateTime> times) {
|
||||
return new ProductionOutputs(
|
||||
(times.contains(now) ? times.stream() : Stream.concat(Stream.of(now), times.stream()))
|
||||
.parallel()
|
||||
.collect(Collectors.toMap(
|
||||
Function.identity(),
|
||||
time -> items.parallelStream()
|
||||
.mapToLong(item -> item.partsAt(time))
|
||||
.sum()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
static class Item {
|
||||
final LocalDateTime start;
|
||||
final Duration duration;
|
||||
final int partsPerMinute;
|
||||
|
||||
long partsAt(LocalDateTime time) {
|
||||
if (startsAfter(time)) {
|
||||
return 0;
|
||||
}
|
||||
if (endsBefore(time)) {
|
||||
return duration.toMinutes() * partsPerMinute;
|
||||
}
|
||||
return Duration.between(start, time).getSeconds() * partsPerMinute / 60;
|
||||
}
|
||||
|
||||
boolean startsAfter(LocalDateTime time) {
|
||||
return start.isAfter(time);
|
||||
}
|
||||
|
||||
boolean endsBefore(LocalDateTime time) {
|
||||
return start.plus(duration).isBefore(time);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.calculation;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
@AllArgsConstructor
|
||||
class ProductionOutputs {
|
||||
|
||||
private final Map<LocalDateTime, Long> outputs;
|
||||
|
||||
long getOutput(LocalDateTime from, LocalDateTime to) {
|
||||
if (!outputs.containsKey(from) || !outputs.containsKey(to)) {
|
||||
throw new IllegalArgumentException("No pre-calculated output for time " + to);
|
||||
}
|
||||
return outputs.get(to) - outputs.get(from);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.monitoring;
|
||||
|
||||
import lombok.Value;
|
||||
import pl.com.bottega.factory.shortages.prediction.Shortages;
|
||||
|
||||
/**
|
||||
* Created by michal on 03.02.2017.
|
||||
*/
|
||||
@Value
|
||||
public class NewShortage {
|
||||
|
||||
public enum After {DemandChanged, PlanChanged, StockChanged, LockedParts}
|
||||
|
||||
After trigger;
|
||||
Shortages shortages;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.monitoring;
|
||||
|
||||
import pl.com.bottega.factory.shortages.prediction.Shortages;
|
||||
|
||||
interface ShortageDiffPolicy {
|
||||
|
||||
ShortageDiffPolicy ValuesAreEquals = Shortages::areNotSame;
|
||||
|
||||
boolean areDifferent(Shortages previous, Shortages found);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.monitoring;
|
||||
|
||||
public interface ShortageEvents {
|
||||
void emit(NewShortage event);
|
||||
|
||||
void emit(ShortageSolved event);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.monitoring;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import pl.com.bottega.factory.shortages.prediction.Configuration;
|
||||
import pl.com.bottega.factory.shortages.prediction.Shortages;
|
||||
import pl.com.bottega.factory.shortages.prediction.calculation.Forecast;
|
||||
import pl.com.bottega.factory.shortages.prediction.calculation.Forecasts;
|
||||
import pl.com.bottega.factory.shortages.prediction.monitoring.NewShortage.After;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Created by michal on 02.02.2017.
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class ShortagePredictionProcess {
|
||||
|
||||
private final String refNo;
|
||||
private Shortages known;
|
||||
|
||||
private final ShortageDiffPolicy diffPolicy;
|
||||
private final Forecasts forecasts;
|
||||
private final Configuration configuration;
|
||||
private final ShortageEvents events;
|
||||
|
||||
public void onDemandChanged() {
|
||||
predict(After.DemandChanged);
|
||||
}
|
||||
|
||||
public void onPlanChanged() {
|
||||
predict(After.PlanChanged);
|
||||
}
|
||||
|
||||
public void onStockChanged() {
|
||||
predict(After.StockChanged);
|
||||
}
|
||||
|
||||
public void onLockedParts() {
|
||||
predict(After.LockedParts);
|
||||
}
|
||||
|
||||
private void predict(After event) {
|
||||
Forecast forecast = forecasts.get(refNo,
|
||||
configuration.shortagePredictionDaysAhead());
|
||||
|
||||
Optional<Shortages> newlyFound = forecast.findShortages();
|
||||
|
||||
boolean areDifferent = diffPolicy.areDifferent(this.known, newlyFound.orElse(null));
|
||||
if (areDifferent && newlyFound.isPresent()) {
|
||||
this.known = newlyFound.get();
|
||||
events.emit(new NewShortage(event, newlyFound.get()));
|
||||
} else if (known != null && !newlyFound.isPresent()) {
|
||||
this.known = null;
|
||||
events.emit(new ShortageSolved(refNo));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.monitoring;
|
||||
|
||||
import pl.com.bottega.factory.product.management.RefNoId;
|
||||
|
||||
/**
|
||||
* Created by michal on 03.02.2017.
|
||||
*/
|
||||
public interface ShortagePredictionProcessRepository {
|
||||
ShortagePredictionProcess get(RefNoId refNo);
|
||||
|
||||
void save(ShortagePredictionProcess model);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.monitoring;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
@Value
|
||||
public class ShortageSolved {
|
||||
String refNo;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.notification;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Singular;
|
||||
import lombok.Value;
|
||||
import pl.com.bottega.factory.shortages.prediction.Shortages;
|
||||
import pl.com.bottega.factory.shortages.prediction.monitoring.NewShortage;
|
||||
import pl.com.bottega.factory.shortages.prediction.monitoring.NewShortage.After;
|
||||
import pl.com.bottega.factory.shortages.prediction.monitoring.ShortageEvents;
|
||||
import pl.com.bottega.factory.shortages.prediction.monitoring.ShortageSolved;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
@AllArgsConstructor
|
||||
class NotificationOfShortage implements ShortageEvents {
|
||||
|
||||
private final QualityTasks qualityTasks;
|
||||
private final Clock clock;
|
||||
|
||||
private final RecoveryTaskPriorityChangePolicy policy;
|
||||
private final NotificationRules rules;
|
||||
|
||||
static NotificationRules rulesOfPlannerNotification(Notifications notifications) {
|
||||
return NotificationRules.builder()
|
||||
.rule(After.DemandChanged, notifications::alertPlanner)
|
||||
.rule(After.PlanChanged, notifications::markOnPlan)
|
||||
.rule(After.StockChanged, notifications::alertPlanner)
|
||||
.rule(After.LockedParts, notifications::softNotifyPlanner)
|
||||
.otherwise(notifications::alertPlanner)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void emit(NewShortage event) {
|
||||
Shortages shortage = event.getShortages();
|
||||
rules.wayOfNotificationAfter(event.getTrigger())
|
||||
.notifyAbout(event.getShortages());
|
||||
|
||||
if (policy.shouldIncreasePriority(LocalDateTime.now(clock), shortage)) {
|
||||
qualityTasks.increasePriorityFor(shortage.getRefNo());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void emit(ShortageSolved event) {
|
||||
|
||||
}
|
||||
|
||||
@Value
|
||||
@Builder
|
||||
static class NotificationRules {
|
||||
@Singular
|
||||
Map<After, Notificator> rules;
|
||||
Notificator otherwise;
|
||||
|
||||
Notificator wayOfNotificationAfter(After trigger) {
|
||||
return rules.getOrDefault(trigger, otherwise);
|
||||
}
|
||||
}
|
||||
|
||||
interface Notificator {
|
||||
void notifyAbout(Shortages shortages);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.notification;
|
||||
|
||||
import pl.com.bottega.factory.shortages.prediction.Shortages;
|
||||
|
||||
/**
|
||||
* Created by michal on 02.02.2017.
|
||||
*/
|
||||
public interface Notifications {
|
||||
void alertPlanner(Shortages shortage);
|
||||
|
||||
void softNotifyPlanner(Shortages shortage);
|
||||
|
||||
void markOnPlan(Shortages shortage);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.notification;
|
||||
|
||||
/**
|
||||
* Created by michal on 02.02.2017.
|
||||
*/
|
||||
public interface QualityTasks {
|
||||
void increasePriorityFor(String productRefNo);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.notification;
|
||||
|
||||
import pl.com.bottega.factory.shortages.prediction.Shortages;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Created by michal on 18.05.2017.
|
||||
*/
|
||||
public interface RecoveryTaskPriorityChangePolicy {
|
||||
|
||||
static RecoveryTaskPriorityChangePolicy never() {
|
||||
return (LocalDateTime now, Shortages shortage) -> false;
|
||||
}
|
||||
|
||||
static RecoveryTaskPriorityChangePolicy onlyIn1DaysAhead() {
|
||||
return shortageInDays(1);
|
||||
}
|
||||
|
||||
static RecoveryTaskPriorityChangePolicy shortageInDays(long shortageInXDays) {
|
||||
return (LocalDateTime now, Shortages shortage) ->
|
||||
shortage.getLockedParts() > 0 && shortage.anyBefore(
|
||||
now.plusDays(shortageInXDays));
|
||||
}
|
||||
|
||||
boolean shouldIncreasePriority(LocalDateTime now, Shortages shortage);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.calculation
|
||||
|
||||
import spock.lang.Specification
|
||||
|
||||
class ShortagesCalculationAlgorithmSpec extends Specification
|
||||
implements ShortagesCalculationAssemblerTrait {
|
||||
|
||||
void setup() {
|
||||
TimeGrammar.apply()
|
||||
}
|
||||
|
||||
def "Takes stock into account"() {
|
||||
given:
|
||||
def forecast = forecast(
|
||||
stock(1000L),
|
||||
demands((now + 20.min): 500L),
|
||||
noProductions()
|
||||
)
|
||||
|
||||
when:
|
||||
def shortages = forecast.findShortages()
|
||||
|
||||
then:
|
||||
shortages == noShortages()
|
||||
}
|
||||
|
||||
def "Locked stock is not included"() {
|
||||
given:
|
||||
def forecast = forecast(
|
||||
stock(0L, 1000L),
|
||||
demands((now + 20.min): 500L),
|
||||
noProductions()
|
||||
)
|
||||
|
||||
when:
|
||||
def shortages = forecast.findShortages()
|
||||
|
||||
then:
|
||||
shortages == shortage((now + 20.min): 500L, 1000L)
|
||||
}
|
||||
|
||||
def "Shortages are not cumulative"() {
|
||||
given:
|
||||
def forecast = forecast(
|
||||
stock(0),
|
||||
demands((now + 20.min): 500L, (now + 1.day): 500L),
|
||||
noProductions()
|
||||
)
|
||||
|
||||
when:
|
||||
def shortages = forecast.findShortages()
|
||||
|
||||
then:
|
||||
shortages == shortage(
|
||||
(now + 20.min): 500L,
|
||||
(now + 1.day): 500L
|
||||
)
|
||||
}
|
||||
|
||||
def "Includes momentary delivery"() {
|
||||
given:
|
||||
def forecast = forecast(
|
||||
stock(0),
|
||||
demands((now): 500L),
|
||||
noProductions()
|
||||
)
|
||||
|
||||
when:
|
||||
def shortages = forecast.findShortages()
|
||||
|
||||
then:
|
||||
shortages == shortage((now): 500L)
|
||||
}
|
||||
|
||||
def "No demands means no shortages"() {
|
||||
given:
|
||||
def forecast = forecast(
|
||||
stock(0),
|
||||
noDemands(),
|
||||
noProductions()
|
||||
)
|
||||
|
||||
when:
|
||||
def shortages = forecast.findShortages()
|
||||
|
||||
then:
|
||||
shortages == noShortages()
|
||||
}
|
||||
|
||||
def "Takes production plan into account"() {
|
||||
given:
|
||||
def forecast = forecast(
|
||||
stock(0),
|
||||
demands((now + 5.h): 500L),
|
||||
plan([production(now, 50.min, 10)])
|
||||
)
|
||||
|
||||
when:
|
||||
def shortages = forecast.findShortages()
|
||||
|
||||
then:
|
||||
shortages == noShortages()
|
||||
}
|
||||
|
||||
def "Takes current production partially into account"() {
|
||||
given:
|
||||
def forecast = forecast(
|
||||
stock(0),
|
||||
demands((now + 20.min): 500L),
|
||||
plan([production(now, 50.min, 10)])
|
||||
)
|
||||
|
||||
when:
|
||||
def shortages = forecast.findShortages()
|
||||
|
||||
then:
|
||||
shortages == shortage((now + 20.min): 300L)
|
||||
}
|
||||
|
||||
def "Future productions cannot be counted into delivery"() {
|
||||
given:
|
||||
def forecast = forecast(
|
||||
stock(0),
|
||||
demands((now): 500L),
|
||||
plan([production(now + 1.h, 50.min, 10)])
|
||||
)
|
||||
|
||||
when:
|
||||
def shortages = forecast.findShortages()
|
||||
|
||||
then:
|
||||
shortages == shortage((now): 500L)
|
||||
}
|
||||
|
||||
def "Production outputs are cumulative"() {
|
||||
given:
|
||||
def forecast = forecast(
|
||||
stock(0),
|
||||
demands((now + 30.min): 600L),
|
||||
plan([production(now, 50.min, 10), production(now, 50.min, 10)])
|
||||
)
|
||||
|
||||
when:
|
||||
def shortages = forecast.findShortages()
|
||||
|
||||
then:
|
||||
shortages == noShortages()
|
||||
}
|
||||
|
||||
def "Production outputs are not cumulative between deliveries"() {
|
||||
given:
|
||||
def forecast = forecast(
|
||||
stock(0),
|
||||
demands(
|
||||
(now + 60.min): 500L, // first delivery
|
||||
(now + 1.day + 60.min): 500L + 100L), // second delivery
|
||||
plan([
|
||||
production(now, 50.min, 10), // consumed by first delivery
|
||||
production(now + 1.day, 50.min, 10) // provides 500 for second delivery
|
||||
])
|
||||
)
|
||||
|
||||
when:
|
||||
def shortages = forecast.findShortages()
|
||||
|
||||
then:
|
||||
shortages == shortage(
|
||||
(now + 1.day + 60.min): 100L
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.calculation
|
||||
|
||||
class ShortagesCalculationAssembler implements ShortagesCalculationAssemblerTrait {
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.calculation
|
||||
|
||||
import pl.com.bottega.factory.shortages.prediction.Shortages
|
||||
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
|
||||
trait ShortagesCalculationAssemblerTrait {
|
||||
|
||||
LocalDateTime now = LocalDateTime.now()
|
||||
String refNo = "3009000"
|
||||
Set<LocalDateTime> times
|
||||
|
||||
Forecasts forecastProvider(CurrentStock stock, Demands demands, ProductionOutputs outputs) {
|
||||
def forecast = forecast(stock, demands, outputs)
|
||||
return { String refNo, int daysAhead -> forecast } as Forecasts
|
||||
}
|
||||
|
||||
Forecast forecast(CurrentStock stock, Demands demands, ProductionOutputs outputs) {
|
||||
new Forecast(refNo, now, times as List, stock, outputs, demands)
|
||||
}
|
||||
|
||||
ProductionOutputs noProductions() {
|
||||
new ProductionForecast([])
|
||||
.outputsInTimes(now, times)
|
||||
}
|
||||
|
||||
ProductionOutputs plan(List<ProductionForecast.Item> productions) {
|
||||
new ProductionForecast(productions)
|
||||
.outputsInTimes(now, times)
|
||||
}
|
||||
|
||||
ProductionForecast.Item production(LocalDateTime start, Duration duration, int partsPerMinute) {
|
||||
new ProductionForecast.Item(start, duration, partsPerMinute)
|
||||
}
|
||||
|
||||
Demands noDemands() {
|
||||
times = Collections.emptySet()
|
||||
new Demands([:])
|
||||
}
|
||||
|
||||
Demands demands(Map<LocalDateTime, Long> demands) {
|
||||
times = demands.keySet()
|
||||
new Demands(demands)
|
||||
}
|
||||
|
||||
CurrentStock stock(long levels) {
|
||||
new CurrentStock(levels, 0)
|
||||
}
|
||||
|
||||
CurrentStock stock(long level, long locked) {
|
||||
new CurrentStock(level, locked)
|
||||
}
|
||||
|
||||
Optional<Shortages> noShortages() {
|
||||
Optional.empty()
|
||||
}
|
||||
|
||||
Optional<Shortages> shortage(Map<LocalDateTime, Long> missing, long locked = 0) {
|
||||
def shortages = Shortages.builder(refNo, locked, now)
|
||||
|
||||
missing.each { time, level -> shortages.add(time, level) }
|
||||
|
||||
shortages.build()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.calculation
|
||||
|
||||
import spock.lang.Specification
|
||||
|
||||
class ShortagesCalculationExamplesSpec extends Specification
|
||||
implements ShortagesCalculationAssemblerTrait {
|
||||
|
||||
void setup() {
|
||||
TimeGrammar.apply()
|
||||
}
|
||||
|
||||
def "Constant week delivery covered by current stock"() {
|
||||
given:
|
||||
def forecast = forecast(
|
||||
stock(7 * 500L),
|
||||
demands(
|
||||
(now): 500L,
|
||||
(now + 1.day): 500L,
|
||||
(now + 2.day): 500L,
|
||||
(now + 3.day): 500L,
|
||||
(now + 4.day): 500L,
|
||||
(now + 5.day): 500L,
|
||||
(now + 6.day): 500L
|
||||
),
|
||||
noProductions()
|
||||
)
|
||||
|
||||
when:
|
||||
def shortages = forecast.findShortages()
|
||||
|
||||
then:
|
||||
shortages == noShortages()
|
||||
}
|
||||
|
||||
def "Constant week delivery partially covered by current stock"() {
|
||||
given:
|
||||
def forecast = forecast(
|
||||
stock(4 * 500L),
|
||||
demands(
|
||||
(now): 500L,
|
||||
(now + 1.day): 500L,
|
||||
(now + 2.day): 500L,
|
||||
(now + 3.day): 500L,
|
||||
(now + 4.day): 500L,
|
||||
(now + 5.day): 500L,
|
||||
(now + 6.day): 500L
|
||||
),
|
||||
noProductions()
|
||||
)
|
||||
|
||||
when:
|
||||
def shortages = forecast.findShortages()
|
||||
|
||||
then:
|
||||
shortages == shortage(
|
||||
(now + 4.day): 500L,
|
||||
(now + 5.day): 500L,
|
||||
(now + 6.day): 500L
|
||||
)
|
||||
}
|
||||
|
||||
def "Constant week delivery covered by stock and plan"() {
|
||||
given:
|
||||
def forecast = forecast(
|
||||
stock(6 * 500L),
|
||||
demands(
|
||||
(now): 500L,
|
||||
(now + 1.day): 500L,
|
||||
(now + 2.day): 500L,
|
||||
(now + 3.day): 500L,
|
||||
(now + 4.day): 500L,
|
||||
(now + 5.day): 500L,
|
||||
(now + 6.day): 500L
|
||||
),
|
||||
plan([production((now + 1.day), 50.min, 10)])
|
||||
)
|
||||
|
||||
when:
|
||||
def shortages = forecast.findShortages()
|
||||
|
||||
then:
|
||||
shortages == noShortages()
|
||||
}
|
||||
|
||||
def "Constant delivery with regular productions"() {
|
||||
given:
|
||||
def forecast = forecast(
|
||||
stock(500L),
|
||||
demands(
|
||||
(now): 500L,
|
||||
(now + 1.day): 500L,
|
||||
(now + 2.day): 500L,
|
||||
(now + 3.day): 500L,
|
||||
(now + 4.day): 500L,
|
||||
(now + 5.day): 500L,
|
||||
(now + 6.day): 500L
|
||||
),
|
||||
plan([
|
||||
production((now), 50.min, 10),
|
||||
production((now + 1.day), 50.min, 10),
|
||||
production((now + 2.day), 50.min, 10),
|
||||
production((now + 3.day), 50.min, 10)
|
||||
])
|
||||
)
|
||||
|
||||
when:
|
||||
def shortages = forecast.findShortages()
|
||||
|
||||
then:
|
||||
shortages == shortage(
|
||||
(now + 5.day): 500L,
|
||||
(now + 6.day): 500L,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.calculation
|
||||
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class TimeGrammar {
|
||||
static getDays(Integer self) { Duration.ofDays self }
|
||||
|
||||
static getDay(Integer self) { Duration.ofDays self }
|
||||
|
||||
static getH(Integer self) { Duration.ofHours self }
|
||||
|
||||
static getMin(Integer self) { Duration.ofMinutes self }
|
||||
|
||||
static String toString(LocalDateTime self, String pattern) { self.format(DateTimeFormatter.ofPattern(pattern)) }
|
||||
|
||||
static apply() {
|
||||
Integer.mixin(TimeGrammar)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.monitoring
|
||||
|
||||
import pl.com.bottega.factory.shortages.prediction.Configuration
|
||||
|
||||
class InMemoryConfiguration implements Configuration {
|
||||
int daysAhead;
|
||||
|
||||
@Override
|
||||
int shortagePredictionDaysAhead() {
|
||||
return daysAhead;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.monitoring
|
||||
|
||||
import pl.com.bottega.factory.shortages.prediction.Shortages
|
||||
import pl.com.bottega.factory.shortages.prediction.calculation.TimeGrammar
|
||||
import spock.lang.Specification
|
||||
|
||||
import java.time.LocalDateTime
|
||||
|
||||
class ShortageDiffPolicySpec extends Specification {
|
||||
|
||||
def now = LocalDateTime.now()
|
||||
|
||||
void setup() {
|
||||
TimeGrammar.apply()
|
||||
}
|
||||
|
||||
def "'ValuesAreEquals policy' ignores time when shortage was found"() {
|
||||
given:
|
||||
def policy = ShortageDiffPolicy.ValuesAreEquals
|
||||
|
||||
Shortages one = Shortages.builder("3009000", 0, now)
|
||||
.add(now + 1.day, 500L)
|
||||
.build().orElse(null)
|
||||
|
||||
Shortages another = Shortages.builder("3009000", 0, now + 5.min)
|
||||
.add(now + 1.day, 500L)
|
||||
.build().orElse(null)
|
||||
|
||||
expect:
|
||||
!policy.areDifferent(one, another)
|
||||
}
|
||||
|
||||
def "'ValuesAreEquals policy' ignores current locked stock"() {
|
||||
given:
|
||||
def policy = ShortageDiffPolicy.ValuesAreEquals
|
||||
|
||||
Shortages one = Shortages.builder("3009000", 0, now)
|
||||
.add(now + 1.day, 500L)
|
||||
.build().orElse(null)
|
||||
|
||||
Shortages another = Shortages.builder("3009000", 1000, now)
|
||||
.add(now + 1.day, 500L)
|
||||
.build().orElse(null)
|
||||
|
||||
expect:
|
||||
!policy.areDifferent(one, another)
|
||||
}
|
||||
|
||||
def "'ValuesAreEquals policy' can NOT ignore product refNo"() {
|
||||
given:
|
||||
def policy = ShortageDiffPolicy.ValuesAreEquals
|
||||
|
||||
Shortages one = Shortages.builder("3009000XXX", 0, now)
|
||||
.add(now + 1.day, 500L)
|
||||
.build().orElse(null)
|
||||
|
||||
Shortages another = Shortages.builder("3009000", 0, now)
|
||||
.add(now + 1.day, 500L)
|
||||
.build().orElse(null)
|
||||
|
||||
expect:
|
||||
policy.areDifferent(one, another)
|
||||
}
|
||||
|
||||
def "'ValuesAreEquals policy' can NOT ignore shortage level diff"() {
|
||||
given:
|
||||
def policy = ShortageDiffPolicy.ValuesAreEquals
|
||||
|
||||
Shortages one = Shortages.builder("3009000", 0, now)
|
||||
.add(now + 1.day, 500L)
|
||||
.build().orElse(null)
|
||||
|
||||
Shortages another = Shortages.builder("3009000", 0, now)
|
||||
.add(now + 1.day, 1500L)
|
||||
.build().orElse(null)
|
||||
|
||||
expect:
|
||||
policy.areDifferent(one, another)
|
||||
}
|
||||
|
||||
def "'ValuesAreEquals policy' even minimal level diff is distinguished"() {
|
||||
given:
|
||||
def policy = ShortageDiffPolicy.ValuesAreEquals
|
||||
|
||||
Shortages one = Shortages.builder("3009000", 0, now)
|
||||
.add(now + 1.day, 500L)
|
||||
.build().orElse(null)
|
||||
|
||||
Shortages another = Shortages.builder("3009000", 0, now)
|
||||
.add(now + 1.day, 499L)
|
||||
.build().orElse(null)
|
||||
|
||||
expect:
|
||||
policy.areDifferent(one, another)
|
||||
}
|
||||
|
||||
def "'ValuesAreEquals policy' can NOT ignore shortage in different days"() {
|
||||
given:
|
||||
def policy = ShortageDiffPolicy.ValuesAreEquals
|
||||
|
||||
Shortages one = Shortages.builder("3009000", 0, now)
|
||||
.add(now + 1.day, 500L)
|
||||
.build().orElse(null)
|
||||
|
||||
Shortages another = Shortages.builder("3009000", 0, now)
|
||||
.add(now + 2.day, 500L)
|
||||
.build().orElse(null)
|
||||
|
||||
expect:
|
||||
policy.areDifferent(one, another)
|
||||
}
|
||||
|
||||
def "'ValuesAreEquals policy' can NOT ignore shortage in different time"() {
|
||||
given:
|
||||
def policy = ShortageDiffPolicy.ValuesAreEquals
|
||||
|
||||
Shortages one = Shortages.builder("3009000", 0, now)
|
||||
.add(now + 1.day, 500L)
|
||||
.build().orElse(null)
|
||||
|
||||
Shortages another = Shortages.builder("3009000", 0, now)
|
||||
.add(now + 1.day + 1.min, 500L)
|
||||
.build().orElse(null)
|
||||
|
||||
expect:
|
||||
policy.areDifferent(one, another)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.monitoring
|
||||
|
||||
import pl.com.bottega.factory.shortages.prediction.Configuration
|
||||
import pl.com.bottega.factory.shortages.prediction.Shortages
|
||||
import pl.com.bottega.factory.shortages.prediction.calculation.Forecasts
|
||||
import pl.com.bottega.factory.shortages.prediction.calculation.ShortagesCalculationAssembler
|
||||
import spock.lang.Specification
|
||||
|
||||
import java.time.LocalDateTime
|
||||
|
||||
import static pl.com.bottega.factory.shortages.prediction.monitoring.NewShortage.After;
|
||||
|
||||
class ShortagePredictionProcessSpec extends Specification {
|
||||
|
||||
def refNo = "3009000"
|
||||
def now = LocalDateTime.now()
|
||||
def forecastAssembler = new ShortagesCalculationAssembler(refNo: refNo, now: now)
|
||||
def events = Mock(ShortageEvents)
|
||||
|
||||
def "Emits no events when there is still no shortages"() {
|
||||
given:
|
||||
def process = predictionProcess(
|
||||
noShortagesWasPreviouslyFound(),
|
||||
noShortagesWillBeFound()
|
||||
)
|
||||
|
||||
when:
|
||||
process.onDemandChanged()
|
||||
|
||||
then:
|
||||
0 * events.emit(_ as NewShortage)
|
||||
0 * events.emit(_ as ShortageSolved)
|
||||
}
|
||||
|
||||
def "Emits NewShortage found when shortage was found first time"() {
|
||||
given:
|
||||
def process = predictionProcess(
|
||||
noShortagesWasPreviouslyFound(),
|
||||
willFindShortages(someShortages())
|
||||
)
|
||||
|
||||
when:
|
||||
process.onDemandChanged()
|
||||
|
||||
then:
|
||||
1 * events.emit(newShortage(After.DemandChanged, someShortages()))
|
||||
0 * events.emit(_ as ShortageSolved)
|
||||
}
|
||||
|
||||
def "Emits ShortageSolved when shortage disappear"() {
|
||||
given:
|
||||
def process = predictionProcess(
|
||||
wasPreviouslyFound(someShortages()),
|
||||
noShortagesWillBeFound()
|
||||
)
|
||||
|
||||
when:
|
||||
process.onDemandChanged()
|
||||
|
||||
then:
|
||||
0 * events.emit(_ as NewShortage)
|
||||
1 * events.emit(shortageSolved())
|
||||
}
|
||||
|
||||
def "Emits no events when there is still 'same' shortages"() {
|
||||
given:
|
||||
def process = predictionProcess(
|
||||
wasPreviouslyFound(someShortages()),
|
||||
willFindShortages(someShortages())
|
||||
)
|
||||
|
||||
when:
|
||||
process.onDemandChanged()
|
||||
|
||||
then:
|
||||
0 * events.emit(_ as NewShortage)
|
||||
0 * events.emit(_ as ShortageSolved)
|
||||
}
|
||||
|
||||
def "Emits NewShortage found when 'different' shortages will be found"() {
|
||||
given:
|
||||
def process = predictionProcess(
|
||||
wasPreviouslyFound(someShortages()),
|
||||
willFindShortages(someDifferentShortages())
|
||||
)
|
||||
|
||||
when:
|
||||
process.onDemandChanged()
|
||||
|
||||
then:
|
||||
1 * events.emit(newShortage(After.DemandChanged, someDifferentShortages()))
|
||||
0 * events.emit(_ as ShortageSolved)
|
||||
}
|
||||
|
||||
def "Remembers last found shortage"() {
|
||||
given:
|
||||
def process = predictionProcess(
|
||||
noShortagesWasPreviouslyFound(),
|
||||
willFindShortages(someShortages())
|
||||
)
|
||||
|
||||
when:
|
||||
process.onDemandChanged()
|
||||
|
||||
then:
|
||||
1 * events.emit(newShortage(After.DemandChanged, someShortages()))
|
||||
0 * events.emit(_ as ShortageSolved)
|
||||
|
||||
when:
|
||||
process.onDemandChanged()
|
||||
process.onLockedParts()
|
||||
process.onPlanChanged()
|
||||
process.onStockChanged()
|
||||
|
||||
then:
|
||||
0 * events.emit(_ as NewShortage)
|
||||
0 * events.emit(_ as ShortageSolved)
|
||||
}
|
||||
|
||||
ShortagePredictionProcess predictionProcess(
|
||||
Shortages previouslyFound,
|
||||
Forecasts forecastThatWillFindShortages) {
|
||||
|
||||
new ShortagePredictionProcess(
|
||||
refNo,
|
||||
previouslyFound,
|
||||
ShortageDiffPolicy.ValuesAreEquals,
|
||||
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]
|
||||
}
|
||||
|
||||
Shortages noShortagesWasPreviouslyFound() {
|
||||
null
|
||||
}
|
||||
|
||||
Shortages wasPreviouslyFound(Map<LocalDateTime, Long> shortages) {
|
||||
forecastAssembler.shortage(shortages).orElse(null)
|
||||
}
|
||||
|
||||
Forecasts noShortagesWillBeFound() {
|
||||
forecastAssembler.forecastProvider(
|
||||
forecastAssembler.stock(1000),
|
||||
forecastAssembler.noDemands(),
|
||||
forecastAssembler.noProductions()
|
||||
)
|
||||
}
|
||||
|
||||
Forecasts willFindShortages(Map<LocalDateTime, Long> shortages) {
|
||||
forecastAssembler.forecastProvider(
|
||||
forecastAssembler.stock(0),
|
||||
forecastAssembler.demands(shortages),
|
||||
forecastAssembler.noProductions()
|
||||
)
|
||||
}
|
||||
|
||||
Configuration defaultConfig() {
|
||||
new InMemoryConfiguration(daysAhead: 14)
|
||||
}
|
||||
|
||||
NewShortage newShortage(After after, Map<LocalDateTime, Long> missing) {
|
||||
new NewShortage(after, forecastAssembler.shortage(missing).get())
|
||||
}
|
||||
|
||||
ShortageSolved shortageSolved() {
|
||||
new ShortageSolved(refNo)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.notification
|
||||
|
||||
import pl.com.bottega.factory.shortages.prediction.Shortages
|
||||
import pl.com.bottega.factory.shortages.prediction.monitoring.NewShortage
|
||||
import pl.com.bottega.factory.shortages.prediction.monitoring.ShortageSolved
|
||||
import spock.lang.Specification
|
||||
|
||||
import java.time.*
|
||||
|
||||
import static pl.com.bottega.factory.shortages.prediction.monitoring.NewShortage.After
|
||||
|
||||
class NotificationOfShortageSpec extends Specification {
|
||||
|
||||
def refNo = "3009000"
|
||||
def now = LocalDateTime.now()
|
||||
|
||||
def notifications = Mock(Notifications)
|
||||
def tasks = Mock(QualityTasks)
|
||||
def clock = Clock.fixed(Instant.now(), ZoneId.systemDefault())
|
||||
def policy = RecoveryTaskPriorityChangePolicy.shortageInDays(2)
|
||||
|
||||
def "Alerts planner after DemandChanged"() {
|
||||
given:
|
||||
def notificator = notificator()
|
||||
|
||||
when:
|
||||
notificator.emit(newShortage(After.DemandChanged, withShortage()))
|
||||
|
||||
then:
|
||||
1 * notifications.alertPlanner(withShortage())
|
||||
}
|
||||
|
||||
def "Warns planner after DemandChanged"() {
|
||||
given:
|
||||
def notificator = notificator()
|
||||
|
||||
when:
|
||||
notificator.emit(newShortage(After.LockedParts, withShortage()))
|
||||
|
||||
then:
|
||||
1 * notifications.softNotifyPlanner(withShortage())
|
||||
}
|
||||
|
||||
def "Marks shortage on plan during adjustment of plan"() {
|
||||
given:
|
||||
def notificator = notificator()
|
||||
|
||||
when:
|
||||
notificator.emit(newShortage(After.PlanChanged, withShortage()))
|
||||
|
||||
then:
|
||||
1 * notifications.markOnPlan(withShortage())
|
||||
}
|
||||
|
||||
def "Alerts planner after StockChanged"() {
|
||||
given:
|
||||
def notificator = notificator()
|
||||
|
||||
when:
|
||||
notificator.emit(newShortage(After.StockChanged, withShortage()))
|
||||
|
||||
then:
|
||||
1 * notifications.alertPlanner(withShortage())
|
||||
}
|
||||
|
||||
def "Increase task priority if there are locked parts and shortage is 'close'"() {
|
||||
given:
|
||||
def notificator = notificator()
|
||||
|
||||
when:
|
||||
notificator.emit(newShortage(After.StockChanged,
|
||||
withShortage(Duration.ofDays(1), 500))
|
||||
)
|
||||
|
||||
then:
|
||||
1 * tasks.increasePriorityFor(refNo)
|
||||
}
|
||||
|
||||
def "Don't try to increase task priority if there are NO locked parts"() {
|
||||
given:
|
||||
def notificator = notificator()
|
||||
|
||||
when:
|
||||
notificator.emit(newShortage(After.StockChanged,
|
||||
withShortage(Duration.ofDays(1), 0))
|
||||
)
|
||||
|
||||
then:
|
||||
0 * tasks.increasePriorityFor(_)
|
||||
}
|
||||
|
||||
def "Don't try to increase task priority if there is shortage in far future"() {
|
||||
given:
|
||||
def notificator = notificator()
|
||||
|
||||
when:
|
||||
notificator.emit(newShortage(After.StockChanged,
|
||||
withShortage(Duration.ofDays(10), 500))
|
||||
)
|
||||
|
||||
then:
|
||||
0 * tasks.increasePriorityFor(_)
|
||||
}
|
||||
|
||||
def "No notification after shortage solved specified for now"() {
|
||||
given:
|
||||
def notificator = notificator()
|
||||
|
||||
when:
|
||||
notificator.emit(new ShortageSolved(refNo))
|
||||
|
||||
then:
|
||||
0 * tasks.increasePriorityFor(_)
|
||||
0 * notifications._(_)
|
||||
}
|
||||
|
||||
def notificator() {
|
||||
new NotificationOfShortage(
|
||||
tasks, clock, policy,
|
||||
NotificationOfShortage.rulesOfPlannerNotification(notifications)
|
||||
)
|
||||
}
|
||||
|
||||
NewShortage newShortage(After after, Shortages shortages) {
|
||||
new NewShortage(after, shortages)
|
||||
}
|
||||
|
||||
Shortages withShortage(
|
||||
Duration firstShortageIn = Duration.ofDays(4),
|
||||
long lockedStock = 0) {
|
||||
Shortages.builder(refNo, lockedStock, now)
|
||||
.add(now.plus(firstShortageIn), 500L)
|
||||
.build().get()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package pl.com.bottega.factory.shortages.prediction.notification
|
||||
|
||||
import pl.com.bottega.factory.shortages.prediction.Shortages
|
||||
import spock.lang.Specification
|
||||
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
|
||||
class RecoveryTaskPriorityChangePolicySpec extends Specification {
|
||||
|
||||
def now = LocalDateTime.now()
|
||||
|
||||
Shortages foundShortage(Duration firstShortageIn, long lockedStock) {
|
||||
Shortages.builder("3009000", lockedStock, now)
|
||||
.add(now.plus(firstShortageIn), 500L)
|
||||
.build().get()
|
||||
}
|
||||
|
||||
def "'never policy' don't increase priority... ever"() {
|
||||
given:
|
||||
def policy = RecoveryTaskPriorityChangePolicy.never()
|
||||
|
||||
expect:
|
||||
policy.shouldIncreasePriority(
|
||||
now,
|
||||
foundShortage(firstShortageIn, lockedStock)
|
||||
) == policyDecision
|
||||
|
||||
where:
|
||||
firstShortageIn | lockedStock || policyDecision
|
||||
Duration.ofMinutes(5) | 500L || false
|
||||
Duration.ofMinutes(5) | 0L || false
|
||||
Duration.ofDays(15) | 500L || false
|
||||
Duration.ofDays(15) | 0L || false
|
||||
}
|
||||
|
||||
def "'onlyIn1DaysAhead policy' increase priority for shortages in 24h"() {
|
||||
given:
|
||||
def policy = RecoveryTaskPriorityChangePolicy.onlyIn1DaysAhead()
|
||||
|
||||
expect:
|
||||
policy.shouldIncreasePriority(
|
||||
now,
|
||||
foundShortage(firstShortageIn, lockedStock)
|
||||
) == policyDecision
|
||||
|
||||
where:
|
||||
firstShortageIn | lockedStock || policyDecision
|
||||
Duration.ofMinutes(5) | 500L || true
|
||||
Duration.ofMinutes(5) | 0L || false
|
||||
|
||||
Duration.ofHours(24).minusMillis(1) | 500L || true
|
||||
Duration.ofHours(24) | 500L || false
|
||||
Duration.ofHours(24).plusMillis(1) | 500L || false
|
||||
}
|
||||
|
||||
def "'shortageInDays(2) policy' increase priority for shortages in 48h"() {
|
||||
given:
|
||||
def policy = RecoveryTaskPriorityChangePolicy.shortageInDays(2)
|
||||
|
||||
expect:
|
||||
policy.shouldIncreasePriority(
|
||||
now,
|
||||
foundShortage(firstShortageIn, lockedStock)
|
||||
) == policyDecision
|
||||
|
||||
where:
|
||||
firstShortageIn | lockedStock || policyDecision
|
||||
Duration.ofMinutes(5) | 500L || true
|
||||
Duration.ofMinutes(5) | 0L || false
|
||||
|
||||
Duration.ofHours(48).minusMillis(1) | 500L || true
|
||||
Duration.ofHours(48) | 500L || false
|
||||
Duration.ofHours(48).plusMillis(1) | 500L || false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user