domain and design in readme
This commit is contained in:
30
README.md
30
README.md
@@ -1,4 +1,4 @@
|
|||||||
# Missing complete example of Domain-Driven Design enterprise application
|
# The missing, complete example of Domain-Driven Design enterprise application
|
||||||
|
|
||||||
## Command Query CRUD Responsibility Segregation
|
## Command Query CRUD Responsibility Segregation
|
||||||
Not every piece of software is equally important...
|
Not every piece of software is equally important...
|
||||||
@@ -63,22 +63,28 @@ Making useful application from the Domain Model and the technology.
|
|||||||
In most projects the biggest risk is lack of domain knowledge among developers. We all known Java,
|
In most projects the biggest risk is lack of domain knowledge among developers. We all known Java,
|
||||||
databases and bunch of handy frameworks, but what about: Investment Banking, Automotive Manufacturing or even e-Commerce.
|
databases and bunch of handy frameworks, but what about: Investment Banking, Automotive Manufacturing or even e-Commerce.
|
||||||
|
|
||||||
Lets face those risk at first, maintain and explore domain knowledge
|
Let's face the risk at first, maintain and explore domain knowledge
|
||||||
with **Model Exploration Whirlpool** and build **Ubiquitous Language** with your executable **Domain Model**,
|
with **Model Exploration Whirlpool** and build **Ubiquitous Language** with your executable **Domain Model**,
|
||||||
**Domain Stories** and **Specification by Examples** from day one.
|
**Domain Stories** and **Specification by Examples** from day one.
|
||||||
Adding infrastructure and technology later is easy thanks to Hexagonal Architecture.
|
Adding infrastructure and technology later is easy thanks to Hexagonal Architecture.
|
||||||
|
|
||||||
Starting from ZERO business knowledge through initial domain and opportunity exploration with **Big Picture Event Storming**:
|
Simply starting from ZERO business knowledge through initial domain and opportunity exploration with **Big Picture Event Storming**:
|
||||||
<big-picture-es>
|

|
||||||
|
|
||||||
Looking for system boundaries, impacted and required actors and there interactions with system under design:
|
after cleaning and trimming initial model to most valuable and needed areas:
|
||||||
<actors-and-boundaries>
|

|
||||||
|
|
||||||
Estimating depth of domain model and Command Query CRUD segregation:
|
Deep dive in **Demand Forecasting** sub-domain with **Design Level Event Storming**:
|
||||||
<command-query-crud>
|

|
||||||
|
|
||||||
Design level Event Storming with Domain Stories and Specification by Examples:
|
is excellent canvas to cooperative exploration of:
|
||||||
<demand-forecasting-design-es>
|
- impacted and required actors,
|
||||||
<adjust-demand.feature>
|
- initial / desired system boundaries,
|
||||||
|
- actors interactions with system under design.
|
||||||
|
|
||||||
<shortage-prediction-design-es>
|
With use of **Domain Stories** and **Specification by Examples** it is easy to find:
|
||||||
|
- business rules and invariants,
|
||||||
|
- acceptance criteria,
|
||||||
|
- estimation of Domain Model depth,
|
||||||
|
- CRUD-suspected activities,
|
||||||
|
- missing parts.
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class DemandEventsMapping implements DemandEvents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void emit(ReviewRequested event) {
|
public void emit(ReviewRequired event) {
|
||||||
Instant timestamp = Instant.now(clock);
|
Instant timestamp = Instant.now(clock);
|
||||||
demandReviews.save(event.getReviews().stream()
|
demandReviews.save(event.getReviews().stream()
|
||||||
.map(r -> new DemandReviewEntity(timestamp, r))
|
.map(r -> new DemandReviewEntity(timestamp, r))
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import static java.util.stream.Collectors.toMap;
|
|||||||
|
|
||||||
@Component
|
@Component
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
class ForecastORMRepository implements Forecasts {
|
class ForecastORMRepository implements ShortageForecasts {
|
||||||
|
|
||||||
private final WarehouseService stocks;
|
private final WarehouseService stocks;
|
||||||
private final DeliveryForecastDao deliveries;
|
private final DeliveryForecastDao deliveries;
|
||||||
@@ -29,7 +29,7 @@ class ForecastORMRepository implements Forecasts {
|
|||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Forecast get(RefNoId refNo, int daysAhead) {
|
public ShortageForecast get(RefNoId refNo, int daysAhead) {
|
||||||
Stock stock = stocks.forRefNo(refNo);
|
Stock stock = stocks.forRefNo(refNo);
|
||||||
LocalDateTime time = LocalDateTime.now(clock);
|
LocalDateTime time = LocalDateTime.now(clock);
|
||||||
LocalDateTime max = time.plusDays(daysAhead).truncatedTo(ChronoUnit.DAYS);
|
LocalDateTime max = time.plusDays(daysAhead).truncatedTo(ChronoUnit.DAYS);
|
||||||
@@ -53,6 +53,6 @@ class ForecastORMRepository implements Forecasts {
|
|||||||
.collect(Collectors.toList())
|
.collect(Collectors.toList())
|
||||||
).outputsInTimes(time, deliveryTimes);
|
).outputsInTimes(time, deliveryTimes);
|
||||||
|
|
||||||
return new Forecast(refNo.getRefNo(), time, deliveryTimes, stock, outputs, demand);
|
return new ShortageForecast(refNo.getRefNo(), time, deliveryTimes, stock, outputs, demand);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package pl.com.bottega.factory.demand.forecasting;
|
|||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import pl.com.bottega.factory.demand.forecasting.ReviewRequested.ReviewNeeded;
|
import pl.com.bottega.factory.demand.forecasting.ReviewRequired.ToReview;
|
||||||
|
|
||||||
import javax.transaction.Transactional;
|
import javax.transaction.Transactional;
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ public class DemandService {
|
|||||||
repository.save(model);
|
repository.save(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void review(ReviewNeeded review, ReviewDecision decision) {
|
public void review(ToReview review, ReviewDecision decision) {
|
||||||
ProductDemand model = repository.get(review.getRefNo());
|
ProductDemand model = repository.get(review.getRefNo());
|
||||||
model.review(review, decision);
|
model.review(review, decision);
|
||||||
repository.save(model);
|
repository.save(model);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import lombok.Getter;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import pl.com.bottega.factory.demand.forecasting.ReviewDecision;
|
import pl.com.bottega.factory.demand.forecasting.ReviewDecision;
|
||||||
import pl.com.bottega.factory.demand.forecasting.ReviewRequested.ReviewNeeded;
|
import pl.com.bottega.factory.demand.forecasting.ReviewRequired.ToReview;
|
||||||
import pl.com.bottega.tools.JsonConverter;
|
import pl.com.bottega.tools.JsonConverter;
|
||||||
|
|
||||||
import javax.persistence.*;
|
import javax.persistence.*;
|
||||||
@@ -25,14 +25,14 @@ public class DemandReviewEntity implements Serializable {
|
|||||||
private LocalDate date;
|
private LocalDate date;
|
||||||
private Instant timestamp;
|
private Instant timestamp;
|
||||||
@Convert(converter = ReviewAsJson.class)
|
@Convert(converter = ReviewAsJson.class)
|
||||||
private ReviewNeeded review;
|
private ToReview review;
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
private ReviewDecision decision;
|
private ReviewDecision decision;
|
||||||
@Setter
|
@Setter
|
||||||
private LocalDate cleanAfter;
|
private LocalDate cleanAfter;
|
||||||
|
|
||||||
public DemandReviewEntity(Instant timestamp, ReviewNeeded review) {
|
public DemandReviewEntity(Instant timestamp, ToReview review) {
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
this.refNo = review.getId().getRefNo();
|
this.refNo = review.getId().getRefNo();
|
||||||
this.date = review.getId().getDate();
|
this.date = review.getId().getDate();
|
||||||
@@ -43,9 +43,9 @@ public class DemandReviewEntity implements Serializable {
|
|||||||
return decision != null;
|
return decision != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ReviewAsJson extends JsonConverter<ReviewNeeded> {
|
public static class ReviewAsJson extends JsonConverter<ToReview> {
|
||||||
public ReviewAsJson() {
|
public ReviewAsJson() {
|
||||||
super(ReviewNeeded.class);
|
super(ToReview.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters;
|
|||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
import pl.com.bottega.factory.demand.forecasting.DemandEvents;
|
import pl.com.bottega.factory.demand.forecasting.DemandEvents;
|
||||||
import pl.com.bottega.factory.demand.forecasting.DemandedLevelsChanged;
|
import pl.com.bottega.factory.demand.forecasting.DemandedLevelsChanged;
|
||||||
import pl.com.bottega.factory.demand.forecasting.ReviewRequested;
|
import pl.com.bottega.factory.demand.forecasting.ReviewRequired;
|
||||||
|
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ public class Configuration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void emit(ReviewRequested event) {
|
public void emit(ReviewRequired event) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package pl.com.bottega.factory.demand.forecasting;
|
|||||||
|
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
import pl.com.bottega.factory.demand.forecasting.DemandedLevelsChanged.Change;
|
import pl.com.bottega.factory.demand.forecasting.DemandedLevelsChanged.Change;
|
||||||
import pl.com.bottega.factory.demand.forecasting.ReviewRequested.ReviewNeeded;
|
import pl.com.bottega.factory.demand.forecasting.ReviewRequired.ToReview;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -19,7 +19,7 @@ class DailyDemand {
|
|||||||
interface Events {
|
interface Events {
|
||||||
void emit(LevelChanged event);
|
void emit(LevelChanged event);
|
||||||
|
|
||||||
void emit(ReviewNeeded event);
|
void emit(ToReview event);
|
||||||
|
|
||||||
void emit(DemandUpdated event);
|
void emit(DemandUpdated event);
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@ class DailyDemand {
|
|||||||
void update(Demand documented) {
|
void update(Demand documented) {
|
||||||
State state = state();
|
State state = state();
|
||||||
if (policy.reviewNeeded(this.documented, this.adjustment, documented)) {
|
if (policy.reviewNeeded(this.documented, this.adjustment, documented)) {
|
||||||
events.emit(new ReviewNeeded(id,
|
events.emit(new ToReview(id,
|
||||||
this.documented,
|
this.documented,
|
||||||
this.adjustment.getDemand(),
|
this.adjustment.getDemand(),
|
||||||
documented)
|
documented)
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ package pl.com.bottega.factory.demand.forecasting;
|
|||||||
public interface DemandEvents {
|
public interface DemandEvents {
|
||||||
void emit(DemandedLevelsChanged event);
|
void emit(DemandedLevelsChanged event);
|
||||||
|
|
||||||
void emit(ReviewRequested event);
|
void emit(ReviewRequired event);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package pl.com.bottega.factory.demand.forecasting;
|
package pl.com.bottega.factory.demand.forecasting;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import pl.com.bottega.factory.demand.forecasting.ReviewRequested.ReviewNeeded;
|
import pl.com.bottega.factory.demand.forecasting.ReviewRequired.ToReview;
|
||||||
import pl.com.bottega.factory.product.management.RefNoId;
|
import pl.com.bottega.factory.product.management.RefNoId;
|
||||||
|
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
@@ -40,11 +40,11 @@ class ProductDemand {
|
|||||||
events.emit(new DemandedLevelsChanged(id, unit.changes()));
|
events.emit(new DemandedLevelsChanged(id, unit.changes()));
|
||||||
}
|
}
|
||||||
if (unit.anyReviews()) {
|
if (unit.anyReviews()) {
|
||||||
events.emit(new ReviewRequested(id, unit.reviews()));
|
events.emit(new ReviewRequired(id, unit.reviews()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void review(ReviewNeeded review, ReviewDecision decision) {
|
void review(ToReview review, ReviewDecision decision) {
|
||||||
if (decision.requireAdjustment()) {
|
if (decision.requireAdjustment()) {
|
||||||
adjust(decision.toAdjustment(review));
|
adjust(decision.toAdjustment(review));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package pl.com.bottega.factory.demand.forecasting;
|
package pl.com.bottega.factory.demand.forecasting;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import pl.com.bottega.factory.demand.forecasting.ReviewRequested.ReviewNeeded;
|
import pl.com.bottega.factory.demand.forecasting.ReviewRequired.ToReview;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
@@ -9,13 +9,13 @@ import java.util.function.Function;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public enum ReviewDecision {
|
public enum ReviewDecision {
|
||||||
IGNORE(r -> null),
|
IGNORE(r -> null),
|
||||||
PICK_PREVIOUS(ReviewNeeded::getPreviousDocumented),
|
PICK_PREVIOUS(ToReview::getPreviousDocumented),
|
||||||
MAKE_ADJUSTMENT_WEAK(ReviewNeeded::getAdjustment),
|
MAKE_ADJUSTMENT_WEAK(ToReview::getAdjustment),
|
||||||
PICK_NEW(ReviewNeeded::getNewDocumented);
|
PICK_NEW(ToReview::getNewDocumented);
|
||||||
|
|
||||||
private final Function<ReviewNeeded, Demand> pick;
|
private final Function<ToReview, Demand> pick;
|
||||||
|
|
||||||
public AdjustDemand toAdjustment(ReviewNeeded review) {
|
public AdjustDemand toAdjustment(ToReview review) {
|
||||||
if (this == IGNORE) {
|
if (this == IGNORE) {
|
||||||
throw new IllegalStateException("can't convert " + this + " to adjustment");
|
throw new IllegalStateException("can't convert " + this + " to adjustment");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package pl.com.bottega.factory.demand.forecasting;
|
package pl.com.bottega.factory.demand.forecasting;
|
||||||
|
|
||||||
import pl.com.bottega.factory.demand.forecasting.ReviewRequested.ReviewNeeded;
|
import pl.com.bottega.factory.demand.forecasting.ReviewRequired.ToReview;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ import static java.util.Collections.unmodifiableList;
|
|||||||
class UnitOfWork implements DailyDemand.Events {
|
class UnitOfWork implements DailyDemand.Events {
|
||||||
|
|
||||||
Map<DailyId, DemandedLevelsChanged.Change> changes = new HashMap<>();
|
Map<DailyId, DemandedLevelsChanged.Change> changes = new HashMap<>();
|
||||||
List<ReviewNeeded> reviews = new LinkedList<>();
|
List<ToReview> reviews = new LinkedList<>();
|
||||||
List<DailyDemand.DemandUpdated> updates = new LinkedList<>();
|
List<DailyDemand.DemandUpdated> updates = new LinkedList<>();
|
||||||
|
|
||||||
boolean anyChanges() {
|
boolean anyChanges() {
|
||||||
@@ -24,7 +24,7 @@ class UnitOfWork implements DailyDemand.Events {
|
|||||||
return !reviews.isEmpty();
|
return !reviews.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ReviewNeeded> reviews() {
|
List<ToReview> reviews() {
|
||||||
return Collections.unmodifiableList(reviews);
|
return Collections.unmodifiableList(reviews);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ class UnitOfWork implements DailyDemand.Events {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void emit(ReviewNeeded event) {
|
public void emit(ToReview event) {
|
||||||
reviews.add(event);
|
reviews.add(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import java.time.LocalDate
|
|||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
|
|
||||||
import static DemandedLevelsChanged.Change
|
import static DemandedLevelsChanged.Change
|
||||||
import static ReviewRequested.ReviewNeeded
|
import static pl.com.bottega.factory.demand.forecasting.ReviewRequired.ToReview
|
||||||
|
|
||||||
class DailyDemandBuilder {
|
class DailyDemandBuilder {
|
||||||
|
|
||||||
@@ -92,8 +92,8 @@ class DailyDemandBuilder {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ReviewNeeded reviewRequest(long previousDocumented, long adjustment, long newDocumented) {
|
ToReview reviewRequest(long previousDocumented, long adjustment, long newDocumented) {
|
||||||
new ReviewNeeded(
|
new ToReview(
|
||||||
new DailyId(refNo, date),
|
new DailyId(refNo, date),
|
||||||
Demand.of(previousDocumented),
|
Demand.of(previousDocumented),
|
||||||
Demand.of(adjustment),
|
Demand.of(adjustment),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import pl.com.bottega.factory.product.management.RefNoId
|
|||||||
import java.time.*
|
import java.time.*
|
||||||
|
|
||||||
import static DemandedLevelsChanged.Change
|
import static DemandedLevelsChanged.Change
|
||||||
import static ReviewRequested.ReviewNeeded
|
import static pl.com.bottega.factory.demand.forecasting.ReviewRequired.ToReview
|
||||||
|
|
||||||
class ProductDemandBuilder {
|
class ProductDemandBuilder {
|
||||||
|
|
||||||
@@ -74,15 +74,15 @@ class ProductDemandBuilder {
|
|||||||
new DemandedLevelsChanged(new RefNoId(refNo), results)
|
new DemandedLevelsChanged(new RefNoId(refNo), results)
|
||||||
}
|
}
|
||||||
|
|
||||||
ReviewRequested reviewRequest(ReviewNeeded... reviews) {
|
ReviewRequired reviewRequest(ToReview... reviews) {
|
||||||
new ReviewRequested(new RefNoId(refNo), reviews as List)
|
new ReviewRequired(new RefNoId(refNo), reviews as List)
|
||||||
}
|
}
|
||||||
|
|
||||||
ReviewNeeded review(LocalDate date,
|
ToReview review(LocalDate date,
|
||||||
long previousDocumented,
|
long previousDocumented,
|
||||||
long strongAdjustment,
|
long strongAdjustment,
|
||||||
long newDocumented) {
|
long newDocumented) {
|
||||||
new ReviewNeeded(
|
new ToReview(
|
||||||
new DailyId(refNo, date),
|
new DailyId(refNo, date),
|
||||||
Demand.of(previousDocumented),
|
Demand.of(previousDocumented),
|
||||||
Demand.of(strongAdjustment),
|
Demand.of(strongAdjustment),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package pl.com.bottega.factory.demand.forecasting
|
|||||||
|
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|
||||||
import static ReviewRequested.ReviewNeeded
|
import static pl.com.bottega.factory.demand.forecasting.ReviewRequired.ToReview
|
||||||
|
|
||||||
trait ProductDemandTrait {
|
trait ProductDemandTrait {
|
||||||
|
|
||||||
@@ -32,11 +32,11 @@ trait ProductDemandTrait {
|
|||||||
[]
|
[]
|
||||||
}
|
}
|
||||||
|
|
||||||
ReviewRequested reviewRequest(ReviewNeeded... reviews) {
|
ReviewRequired reviewRequest(ToReview... reviews) {
|
||||||
builder.reviewRequest(reviews)
|
builder.reviewRequest(reviews)
|
||||||
}
|
}
|
||||||
|
|
||||||
ReviewNeeded review(
|
ToReview review(
|
||||||
LocalDate date,
|
LocalDate date,
|
||||||
long previousDocumented,
|
long previousDocumented,
|
||||||
long strongAdjustment,
|
long strongAdjustment,
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
Feature: manual adjustments of demand
|
Feature: manual adjustments of demand
|
||||||
|
|
||||||
sub domain: demand forecasting
|
Sub Domain: demand forecasting
|
||||||
keeps track of current and future customer needs for our products
|
keeps track of current and future customer needs for our products
|
||||||
|
|
||||||
Domain story:
|
Domain story:
|
||||||
Adjust demand at day to amount, delivered.
|
Adjust demand at day to amount, delivered.
|
||||||
>> demand.adjust(productRefNo, atDay, amount)
|
|
||||||
We can change only Demands for today and future.
|
We can change only Demands for today and future.
|
||||||
|
|
||||||
New demand is stored for further reference
|
New demand is stored for further reference
|
||||||
Data from call-off document should be preserved (DON’T OVERRIDE THEM).
|
Data from call-off document should be preserved.
|
||||||
Adjust demand should be possible even
|
Adjust demand should be possible even
|
||||||
if there was no call-off document for that product.
|
if there was no document for that product.
|
||||||
In standard case future call-off documents should be stronger (overrides) adjustment,
|
In standard case future call-off documents should override adjustment,
|
||||||
but if customer warn us about opposite case import of call-off document should not remove previous adjustments.
|
but if customer warn us about opposite case
|
||||||
|
import of document should not remove previous adjustments.
|
||||||
|
Logistician note should be kept with adjustment.
|
||||||
|
|
||||||
emit domain event demand changed
|
Domain event: demanded levels changed
|
||||||
|
[context boundary]
|
||||||
|
|
||||||
Logistician note should be kept along with adjustment.
|
Sub Domain: shortage prediction
|
||||||
|
continuously monitors demands, production plan and stock levels
|
||||||
|
predicts potential shortage based on forecasts
|
||||||
|
notifies personal about potential shortages
|
||||||
|
|
||||||
outside of context boundary:
|
|
||||||
If new demand is not fulfilled by
|
If new demand is not fulfilled by
|
||||||
current product stock and production forecast
|
current product stock and production forecast
|
||||||
there is a shortage in particular days and we need to rise an alert.
|
there is a shortage in particular days and we need to rise an alert.
|
||||||
|
|||||||
BIN
es-big-picture-cleaned.jpg
Normal file
BIN
es-big-picture-cleaned.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 258 KiB |
BIN
es-big-picture-original.jpg
Normal file
BIN
es-big-picture-original.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
BIN
es-design-demand-forecasting.jpg
Normal file
BIN
es-design-demand-forecasting.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 340 KiB |
@@ -8,17 +8,17 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
public class ReviewRequested {
|
public class ReviewRequired {
|
||||||
RefNoId refNo;
|
RefNoId refNo;
|
||||||
List<ReviewNeeded> reviews;
|
List<ToReview> reviews;
|
||||||
|
|
||||||
public ReviewRequested(RefNoId refNo, List<ReviewNeeded> reviews) {
|
public ReviewRequired(RefNoId refNo, List<ToReview> reviews) {
|
||||||
this.refNo = refNo;
|
this.refNo = refNo;
|
||||||
this.reviews = Collections.unmodifiableList(reviews);
|
this.reviews = Collections.unmodifiableList(reviews);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
public static class ReviewNeeded {
|
public static class ToReview {
|
||||||
DailyId id;
|
DailyId id;
|
||||||
Demand previousDocumented;
|
Demand previousDocumented;
|
||||||
Demand adjustment;
|
Demand adjustment;
|
||||||
@@ -4,7 +4,7 @@ import lombok.AllArgsConstructor;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import pl.com.bottega.factory.product.management.RefNoId;
|
import pl.com.bottega.factory.product.management.RefNoId;
|
||||||
import pl.com.bottega.factory.shortages.prediction.Configuration;
|
import pl.com.bottega.factory.shortages.prediction.Configuration;
|
||||||
import pl.com.bottega.factory.shortages.prediction.calculation.Forecasts;
|
import pl.com.bottega.factory.shortages.prediction.calculation.ShortageForecasts;
|
||||||
import pl.com.bottega.factory.shortages.prediction.monitoring.persistence.ShortagesDao;
|
import pl.com.bottega.factory.shortages.prediction.monitoring.persistence.ShortagesDao;
|
||||||
import pl.com.bottega.factory.shortages.prediction.monitoring.persistence.ShortagesEntity;
|
import pl.com.bottega.factory.shortages.prediction.monitoring.persistence.ShortagesEntity;
|
||||||
import pl.com.bottega.tools.TechnicalId;
|
import pl.com.bottega.tools.TechnicalId;
|
||||||
@@ -17,7 +17,7 @@ class ShortagePredictionProcessORMRepository {
|
|||||||
|
|
||||||
private final ShortagesDao dao;
|
private final ShortagesDao dao;
|
||||||
private final ShortageDiffPolicy policy = ShortageDiffPolicy.ValuesAreNotSame;
|
private final ShortageDiffPolicy policy = ShortageDiffPolicy.ValuesAreNotSame;
|
||||||
private final Forecasts forecasts;
|
private final ShortageForecasts forecasts;
|
||||||
private final Configuration configuration = () -> 14;
|
private final Configuration configuration = () -> 14;
|
||||||
private final ShortageEvents events;
|
private final ShortageEvents events;
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ class ShortagePredictionProcessORMRepository {
|
|||||||
ShortagesEntity entity = TechnicalId.findOrDefault(
|
ShortagesEntity entity = TechnicalId.findOrDefault(
|
||||||
refNo, dao::findOne,
|
refNo, dao::findOne,
|
||||||
() -> dao.save(new ShortagesEntity(refNo.getRefNo())));
|
() -> dao.save(new ShortagesEntity(refNo.getRefNo())));
|
||||||
entity.setShortages(event.getShortages());
|
entity.setShortages(event.getShortage());
|
||||||
events.emit(event);
|
events.emit(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import lombok.Getter;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import pl.com.bottega.factory.product.management.RefNoId;
|
import pl.com.bottega.factory.product.management.RefNoId;
|
||||||
import pl.com.bottega.factory.shortages.prediction.Shortages;
|
import pl.com.bottega.factory.shortages.prediction.Shortage;
|
||||||
import pl.com.bottega.tools.JsonConverter;
|
import pl.com.bottega.tools.JsonConverter;
|
||||||
import pl.com.bottega.tools.TechnicalId;
|
import pl.com.bottega.tools.TechnicalId;
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ public class ShortagesEntity implements Serializable {
|
|||||||
private String refNo;
|
private String refNo;
|
||||||
@Setter
|
@Setter
|
||||||
@Convert(converter = ShortagesAsJson.class)
|
@Convert(converter = ShortagesAsJson.class)
|
||||||
private Shortages shortages;
|
private Shortage shortages;
|
||||||
|
|
||||||
public ShortagesEntity(String refNo) {
|
public ShortagesEntity(String refNo) {
|
||||||
this.refNo = refNo;
|
this.refNo = refNo;
|
||||||
@@ -39,9 +39,9 @@ public class ShortagesEntity implements Serializable {
|
|||||||
return id instanceof ShortagesEntityId ? id : new ShortagesEntityId(id.getRefNo());
|
return id instanceof ShortagesEntityId ? id : new ShortagesEntityId(id.getRefNo());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ShortagesAsJson extends JsonConverter<Shortages> {
|
public static class ShortagesAsJson extends JsonConverter<Shortage> {
|
||||||
public ShortagesAsJson() {
|
public ShortagesAsJson() {
|
||||||
super(Shortages.class);
|
super(Shortage.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package pl.com.bottega.factory.shortages.prediction.notification;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import pl.com.bottega.factory.shortages.prediction.Shortages;
|
import pl.com.bottega.factory.shortages.prediction.Shortage;
|
||||||
|
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
|
|
||||||
@@ -27,17 +27,17 @@ public class NotificationConfiguration {
|
|||||||
|
|
||||||
private static class MockedPlannerPushNotifications implements Notifications {
|
private static class MockedPlannerPushNotifications implements Notifications {
|
||||||
@Override
|
@Override
|
||||||
public void alertPlanner(Shortages shortage) {
|
public void alertPlanner(Shortage shortage) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void softNotifyPlanner(Shortages shortage) {
|
public void softNotifyPlanner(Shortage shortage) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void markOnPlan(Shortages shortage) {
|
public void markOnPlan(Shortage shortage) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import org.springframework.context.annotation.Bean;
|
|||||||
import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters;
|
import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
import pl.com.bottega.factory.product.management.RefNoId;
|
import pl.com.bottega.factory.product.management.RefNoId;
|
||||||
import pl.com.bottega.factory.shortages.prediction.calculation.Forecast;
|
import pl.com.bottega.factory.shortages.prediction.calculation.ShortageForecast;
|
||||||
import pl.com.bottega.factory.shortages.prediction.calculation.Forecasts;
|
import pl.com.bottega.factory.shortages.prediction.calculation.ShortageForecasts;
|
||||||
import pl.com.bottega.factory.shortages.prediction.monitoring.NewShortage;
|
import pl.com.bottega.factory.shortages.prediction.monitoring.NewShortage;
|
||||||
import pl.com.bottega.factory.shortages.prediction.monitoring.ShortageEvents;
|
import pl.com.bottega.factory.shortages.prediction.monitoring.ShortageEvents;
|
||||||
import pl.com.bottega.factory.shortages.prediction.monitoring.ShortageSolved;
|
import pl.com.bottega.factory.shortages.prediction.monitoring.ShortageSolved;
|
||||||
@@ -26,8 +26,8 @@ public class Configuration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public Forecasts forecasts() {
|
public ShortageForecasts forecasts() {
|
||||||
return new ForecastsFake();
|
return new ShortageForecastsFake();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -47,9 +47,9 @@ public class Configuration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ForecastsFake implements Forecasts {
|
private class ShortageForecastsFake implements ShortageForecasts {
|
||||||
@Override
|
@Override
|
||||||
public Forecast get(RefNoId refNo, int daysAhead) {
|
public ShortageForecast get(RefNoId refNo, int daysAhead) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import org.springframework.beans.factory.annotation.Autowired
|
|||||||
import org.springframework.boot.test.context.SpringBootTest
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
import org.springframework.test.annotation.Commit
|
import org.springframework.test.annotation.Commit
|
||||||
import pl.com.bottega.factory.product.management.RefNoId
|
import pl.com.bottega.factory.product.management.RefNoId
|
||||||
import pl.com.bottega.factory.shortages.prediction.Shortages
|
import pl.com.bottega.factory.shortages.prediction.Shortage
|
||||||
import pl.com.bottega.factory.shortages.prediction.calculation.Forecasts
|
import pl.com.bottega.factory.shortages.prediction.calculation.ShortageForecasts
|
||||||
import pl.com.bottega.factory.shortages.prediction.monitoring.persistence.ShortagesDao
|
import pl.com.bottega.factory.shortages.prediction.monitoring.persistence.ShortagesDao
|
||||||
import pl.com.bottega.factory.shortages.prediction.monitoring.persistence.ShortagesEntity
|
import pl.com.bottega.factory.shortages.prediction.monitoring.persistence.ShortagesEntity
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
@@ -25,7 +25,7 @@ class ShortagePredictionProcessORMRepositoryTest extends Specification {
|
|||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
ShortagesDao dao
|
ShortagesDao dao
|
||||||
def forecasts = Mock(Forecasts)
|
def forecasts = Mock(ShortageForecasts)
|
||||||
def notifications = Mock(ShortageEvents)
|
def notifications = Mock(ShortageEvents)
|
||||||
ShortagePredictionProcessORMRepository repository
|
ShortagePredictionProcessORMRepository repository
|
||||||
|
|
||||||
@@ -88,13 +88,13 @@ class ShortagePredictionProcessORMRepositoryTest extends Specification {
|
|||||||
noShortagesPersisted()
|
noShortagesPersisted()
|
||||||
}
|
}
|
||||||
|
|
||||||
def persistedShortage(Shortages shortages) {
|
def persistedShortage(Shortage shortages) {
|
||||||
def entity = new ShortagesEntity(refNo)
|
def entity = new ShortagesEntity(refNo)
|
||||||
entity.setShortages(shortages)
|
entity.setShortages(shortages)
|
||||||
dao.save(entity)
|
dao.save(entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
Shortages shortagesCurrentlyPersisted() {
|
Shortage shortagesCurrentlyPersisted() {
|
||||||
dao.findByRefNo(refNo).get().shortages
|
dao.findByRefNo(refNo).get().shortages
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,29 +106,29 @@ class ShortagePredictionProcessORMRepositoryTest extends Specification {
|
|||||||
repository.get(new RefNoId(refNo))
|
repository.get(new RefNoId(refNo))
|
||||||
}
|
}
|
||||||
|
|
||||||
Shortages noShortages() {
|
Shortage noShortages() {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
Shortages someShortages() {
|
Shortage someShortages() {
|
||||||
Shortages.builder(refNo, 0, now)
|
Shortage.builder(refNo, 0, now)
|
||||||
.missing(now.plusDays(1), 500)
|
.missing(now.plusDays(1), 500)
|
||||||
.build()
|
.build()
|
||||||
.orElse(null)
|
.orElse(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
Shortages someOldShortages() {
|
Shortage someOldShortages() {
|
||||||
Shortages.builder(refNo, 0, now.minusDays(1))
|
Shortage.builder(refNo, 0, now.minusDays(1))
|
||||||
.missing(now.plusDays(2), 2500)
|
.missing(now.plusDays(2), 2500)
|
||||||
.build()
|
.build()
|
||||||
.orElse(null)
|
.orElse(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
Shortages shortagesCurrentlyKnownBy(ShortagePredictionProcess process) {
|
Shortage shortagesCurrentlyKnownBy(ShortagePredictionProcess process) {
|
||||||
process.known
|
process.known
|
||||||
}
|
}
|
||||||
|
|
||||||
void processEmitsNewShortage(ShortagePredictionProcess process, Shortages shortages) {
|
void processEmitsNewShortage(ShortagePredictionProcess process, Shortage shortages) {
|
||||||
process.events.emit(new NewShortage(process.refNo, DemandChanged, shortages))
|
process.events.emit(new NewShortage(process.refNo, DemandChanged, shortages))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,22 +15,22 @@ import java.util.TreeMap;
|
|||||||
* Created by michal on 22.10.2015.
|
* Created by michal on 22.10.2015.
|
||||||
*/
|
*/
|
||||||
@Value
|
@Value
|
||||||
public class Shortages {
|
public class Shortage {
|
||||||
|
|
||||||
private final String refNo;
|
private final String refNo;
|
||||||
private final long lockedParts;
|
private final long lockedParts;
|
||||||
private final LocalDateTime found;
|
private final LocalDateTime found;
|
||||||
private final SortedMap<LocalDateTime, Long> shortages;
|
private final SortedMap<LocalDateTime, Long> shortages;
|
||||||
|
|
||||||
public static Shortages.Builder builder(String refNo, long locked, LocalDateTime found) {
|
public static Shortage.Builder builder(String refNo, long locked, LocalDateTime found) {
|
||||||
return new Builder(refNo, locked, found);
|
return new Builder(refNo, locked, found);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean areNotSame(Shortages first, Shortages second) {
|
public static boolean areNotSame(Shortage first, Shortage second) {
|
||||||
return !areSame(first, second);
|
return !areSame(first, second);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean areSame(Shortages first, Shortages second) {
|
public static boolean areSame(Shortage first, Shortage second) {
|
||||||
boolean noShortages = first == null && second == null;
|
boolean noShortages = first == null && second == null;
|
||||||
boolean onlyOne = first == null && second != null || first != null && second == null;
|
boolean onlyOne = first == null && second != null || first != null && second == null;
|
||||||
if (noShortages || onlyOne) return false;
|
if (noShortages || onlyOne) return false;
|
||||||
@@ -55,11 +55,11 @@ public class Shortages {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<Shortages> build() {
|
public Optional<Shortage> build() {
|
||||||
if (gaps.isEmpty()) {
|
if (gaps.isEmpty()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
} else {
|
} else {
|
||||||
return Optional.of(new Shortages(refNo, locked, found,
|
return Optional.of(new Shortage(refNo, locked, found,
|
||||||
Collections.unmodifiableSortedMap(gaps)));
|
Collections.unmodifiableSortedMap(gaps)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
package pl.com.bottega.factory.shortages.prediction.calculation;
|
package pl.com.bottega.factory.shortages.prediction.calculation;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import pl.com.bottega.factory.shortages.prediction.Shortages;
|
import pl.com.bottega.factory.shortages.prediction.Shortage;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.SortedSet;
|
import java.util.SortedSet;
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class Forecast {
|
public class ShortageForecast {
|
||||||
|
|
||||||
private final String refNo;
|
private final String refNo;
|
||||||
private final LocalDateTime created;
|
private final LocalDateTime created;
|
||||||
@@ -17,10 +17,10 @@ public class Forecast {
|
|||||||
private final ProductionOutputs outputs;
|
private final ProductionOutputs outputs;
|
||||||
private final DeliveriesForecast deliveries;
|
private final DeliveriesForecast deliveries;
|
||||||
|
|
||||||
public Optional<Shortages> findShortages() {
|
public Optional<Shortage> findShortages() {
|
||||||
long level = stock.getLevel();
|
long level = stock.getLevel();
|
||||||
|
|
||||||
Shortages.Builder found = Shortages.builder(refNo, stock.getLocked(), created);
|
Shortage.Builder found = Shortage.builder(refNo, stock.getLocked(), created);
|
||||||
LocalDateTime lastTime = created;
|
LocalDateTime lastTime = created;
|
||||||
for (LocalDateTime time : deliveryTimes) {
|
for (LocalDateTime time : deliveryTimes) {
|
||||||
long demand = deliveries.get(time);
|
long demand = deliveries.get(time);
|
||||||
@@ -2,6 +2,6 @@ package pl.com.bottega.factory.shortages.prediction.calculation;
|
|||||||
|
|
||||||
import pl.com.bottega.factory.product.management.RefNoId;
|
import pl.com.bottega.factory.product.management.RefNoId;
|
||||||
|
|
||||||
public interface Forecasts {
|
public interface ShortageForecasts {
|
||||||
Forecast get(RefNoId refNo, int daysAhead);
|
ShortageForecast get(RefNoId refNo, int daysAhead);
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ package pl.com.bottega.factory.shortages.prediction.monitoring;
|
|||||||
|
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
import pl.com.bottega.factory.product.management.RefNoId;
|
import pl.com.bottega.factory.product.management.RefNoId;
|
||||||
import pl.com.bottega.factory.shortages.prediction.Shortages;
|
import pl.com.bottega.factory.shortages.prediction.Shortage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by michal on 03.02.2017.
|
* Created by michal on 03.02.2017.
|
||||||
@@ -14,5 +14,5 @@ public class NewShortage {
|
|||||||
|
|
||||||
RefNoId refNo;
|
RefNoId refNo;
|
||||||
After trigger;
|
After trigger;
|
||||||
Shortages shortages;
|
Shortage shortage;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package pl.com.bottega.factory.shortages.prediction.monitoring;
|
package pl.com.bottega.factory.shortages.prediction.monitoring;
|
||||||
|
|
||||||
import pl.com.bottega.factory.shortages.prediction.Shortages;
|
import pl.com.bottega.factory.shortages.prediction.Shortage;
|
||||||
|
|
||||||
interface ShortageDiffPolicy {
|
interface ShortageDiffPolicy {
|
||||||
|
|
||||||
ShortageDiffPolicy ValuesAreNotSame = Shortages::areNotSame;
|
ShortageDiffPolicy ValuesAreNotSame = Shortage::areNotSame;
|
||||||
|
|
||||||
boolean areDifferent(Shortages previous, Shortages found);
|
boolean areDifferent(Shortage previous, Shortage found);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package pl.com.bottega.factory.shortages.prediction.monitoring;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import pl.com.bottega.factory.product.management.RefNoId;
|
import pl.com.bottega.factory.product.management.RefNoId;
|
||||||
import pl.com.bottega.factory.shortages.prediction.Configuration;
|
import pl.com.bottega.factory.shortages.prediction.Configuration;
|
||||||
import pl.com.bottega.factory.shortages.prediction.Shortages;
|
import pl.com.bottega.factory.shortages.prediction.Shortage;
|
||||||
import pl.com.bottega.factory.shortages.prediction.calculation.Forecast;
|
import pl.com.bottega.factory.shortages.prediction.calculation.ShortageForecast;
|
||||||
import pl.com.bottega.factory.shortages.prediction.calculation.Forecasts;
|
import pl.com.bottega.factory.shortages.prediction.calculation.ShortageForecasts;
|
||||||
import pl.com.bottega.factory.shortages.prediction.monitoring.NewShortage.After;
|
import pl.com.bottega.factory.shortages.prediction.monitoring.NewShortage.After;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -17,10 +17,10 @@ import java.util.Optional;
|
|||||||
class ShortagePredictionProcess {
|
class ShortagePredictionProcess {
|
||||||
|
|
||||||
private final RefNoId refNo;
|
private final RefNoId refNo;
|
||||||
private Shortages known;
|
private Shortage known;
|
||||||
|
|
||||||
private final ShortageDiffPolicy diffPolicy;
|
private final ShortageDiffPolicy diffPolicy;
|
||||||
private final Forecasts forecasts;
|
private final ShortageForecasts forecasts;
|
||||||
private final Configuration configuration;
|
private final Configuration configuration;
|
||||||
private final ShortageEvents events;
|
private final ShortageEvents events;
|
||||||
|
|
||||||
@@ -41,10 +41,10 @@ class ShortagePredictionProcess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void predict(After event) {
|
private void predict(After event) {
|
||||||
Forecast forecast = forecasts.get(refNo,
|
ShortageForecast forecast = forecasts.get(refNo,
|
||||||
configuration.shortagePredictionDaysAhead());
|
configuration.shortagePredictionDaysAhead());
|
||||||
|
|
||||||
Optional<Shortages> newlyFound = forecast.findShortages();
|
Optional<Shortage> newlyFound = forecast.findShortages();
|
||||||
|
|
||||||
boolean areDifferent = diffPolicy.areDifferent(this.known, newlyFound.orElse(null));
|
boolean areDifferent = diffPolicy.areDifferent(this.known, newlyFound.orElse(null));
|
||||||
if (areDifferent && newlyFound.isPresent()) {
|
if (areDifferent && newlyFound.isPresent()) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Singular;
|
import lombok.Singular;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
import pl.com.bottega.factory.shortages.prediction.Shortages;
|
import pl.com.bottega.factory.shortages.prediction.Shortage;
|
||||||
import pl.com.bottega.factory.shortages.prediction.monitoring.NewShortage;
|
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.NewShortage.After;
|
||||||
|
|
||||||
@@ -32,9 +32,9 @@ public class NotificationOfShortage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void notifyAbout(NewShortage event) {
|
public void notifyAbout(NewShortage event) {
|
||||||
Shortages shortage = event.getShortages();
|
Shortage shortage = event.getShortage();
|
||||||
rules.wayOfNotificationAfter(event.getTrigger())
|
rules.wayOfNotificationAfter(event.getTrigger())
|
||||||
.notifyAbout(event.getShortages());
|
.notifyAbout(event.getShortage());
|
||||||
|
|
||||||
if (policy.shouldIncreasePriority(LocalDateTime.now(clock), shortage)) {
|
if (policy.shouldIncreasePriority(LocalDateTime.now(clock), shortage)) {
|
||||||
qualityTasks.increasePriorityFor(shortage.getRefNo());
|
qualityTasks.increasePriorityFor(shortage.getRefNo());
|
||||||
@@ -54,6 +54,6 @@ public class NotificationOfShortage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Notificator {
|
interface Notificator {
|
||||||
void notifyAbout(Shortages shortages);
|
void notifyAbout(Shortage shortage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
package pl.com.bottega.factory.shortages.prediction.notification;
|
package pl.com.bottega.factory.shortages.prediction.notification;
|
||||||
|
|
||||||
import pl.com.bottega.factory.shortages.prediction.Shortages;
|
import pl.com.bottega.factory.shortages.prediction.Shortage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by michal on 02.02.2017.
|
* Created by michal on 02.02.2017.
|
||||||
*/
|
*/
|
||||||
public interface Notifications {
|
public interface Notifications {
|
||||||
void alertPlanner(Shortages shortage);
|
void alertPlanner(Shortage shortage);
|
||||||
|
|
||||||
void softNotifyPlanner(Shortages shortage);
|
void softNotifyPlanner(Shortage shortage);
|
||||||
|
|
||||||
void markOnPlan(Shortages shortage);
|
void markOnPlan(Shortage shortage);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package pl.com.bottega.factory.shortages.prediction.notification;
|
package pl.com.bottega.factory.shortages.prediction.notification;
|
||||||
|
|
||||||
import pl.com.bottega.factory.shortages.prediction.Shortages;
|
import pl.com.bottega.factory.shortages.prediction.Shortage;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ import java.time.LocalDateTime;
|
|||||||
public interface RecoveryTaskPriorityChangePolicy {
|
public interface RecoveryTaskPriorityChangePolicy {
|
||||||
|
|
||||||
static RecoveryTaskPriorityChangePolicy never() {
|
static RecoveryTaskPriorityChangePolicy never() {
|
||||||
return (LocalDateTime now, Shortages shortage) -> false;
|
return (LocalDateTime now, Shortage shortage) -> false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static RecoveryTaskPriorityChangePolicy onlyIn1DaysAhead() {
|
static RecoveryTaskPriorityChangePolicy onlyIn1DaysAhead() {
|
||||||
@@ -18,10 +18,10 @@ public interface RecoveryTaskPriorityChangePolicy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static RecoveryTaskPriorityChangePolicy shortageInDays(long shortageInXDays) {
|
static RecoveryTaskPriorityChangePolicy shortageInDays(long shortageInXDays) {
|
||||||
return (LocalDateTime now, Shortages shortage) ->
|
return (LocalDateTime now, Shortage shortage) ->
|
||||||
shortage.getLockedParts() > 0 && shortage.anyBefore(
|
shortage.getLockedParts() > 0 && shortage.anyBefore(
|
||||||
now.plusDays(shortageInXDays));
|
now.plusDays(shortageInXDays));
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean shouldIncreasePriority(LocalDateTime now, Shortages shortage);
|
boolean shouldIncreasePriority(LocalDateTime now, Shortage shortage);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package pl.com.bottega.factory.shortages.prediction.calculation
|
|||||||
|
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
|
|
||||||
class ShortagesCalculationAlgorithmSpec extends Specification
|
class ShortageCalculationAlgorithmSpec extends Specification
|
||||||
implements ShortagesCalculationAssemblerTrait {
|
implements ShortagesCalculationAssemblerTrait {
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
@@ -2,7 +2,7 @@ package pl.com.bottega.factory.shortages.prediction.calculation
|
|||||||
|
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
|
|
||||||
class ShortagesCalculationExamplesSpec extends Specification
|
class ShortageCalculationExamplesSpec extends Specification
|
||||||
implements ShortagesCalculationAssemblerTrait {
|
implements ShortagesCalculationAssemblerTrait {
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package pl.com.bottega.factory.shortages.prediction.calculation
|
package pl.com.bottega.factory.shortages.prediction.calculation
|
||||||
|
|
||||||
import pl.com.bottega.factory.product.management.RefNoId
|
import pl.com.bottega.factory.product.management.RefNoId
|
||||||
import pl.com.bottega.factory.shortages.prediction.Shortages
|
import pl.com.bottega.factory.shortages.prediction.Shortage
|
||||||
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@@ -12,13 +12,13 @@ trait ShortagesCalculationAssemblerTrait {
|
|||||||
String refNo = "3009000"
|
String refNo = "3009000"
|
||||||
SortedSet<LocalDateTime> times
|
SortedSet<LocalDateTime> times
|
||||||
|
|
||||||
Forecasts forecastProvider(Stock stock, DeliveriesForecast demands, ProductionOutputs outputs) {
|
ShortageForecasts forecastProvider(Stock stock, DeliveriesForecast demands, ProductionOutputs outputs) {
|
||||||
def forecast = forecast(stock, demands, outputs)
|
def forecast = forecast(stock, demands, outputs)
|
||||||
return { RefNoId refNo, int daysAhead -> forecast } as Forecasts
|
return { RefNoId refNo, int daysAhead -> forecast } as ShortageForecasts
|
||||||
}
|
}
|
||||||
|
|
||||||
Forecast forecast(Stock stock, DeliveriesForecast demands, ProductionOutputs outputs) {
|
ShortageForecast forecast(Stock stock, DeliveriesForecast demands, ProductionOutputs outputs) {
|
||||||
new Forecast(refNo, now, times, stock, outputs, demands)
|
new ShortageForecast(refNo, now, times, stock, outputs, demands)
|
||||||
}
|
}
|
||||||
|
|
||||||
ProductionOutputs noProductions() {
|
ProductionOutputs noProductions() {
|
||||||
@@ -53,12 +53,12 @@ trait ShortagesCalculationAssemblerTrait {
|
|||||||
new Stock(level, locked)
|
new Stock(level, locked)
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Shortages> noShortages() {
|
Optional<Shortage> noShortages() {
|
||||||
Optional.empty()
|
Optional.empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Shortages> shortage(Map<LocalDateTime, Long> missing, long locked = 0) {
|
Optional<Shortage> shortage(Map<LocalDateTime, Long> missing, long locked = 0) {
|
||||||
def shortages = Shortages.builder(refNo, locked, now)
|
def shortages = Shortage.builder(refNo, locked, now)
|
||||||
|
|
||||||
missing.each { time, level -> shortages.missing(time, level) }
|
missing.each { time, level -> shortages.missing(time, level) }
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package pl.com.bottega.factory.shortages.prediction.monitoring
|
package pl.com.bottega.factory.shortages.prediction.monitoring
|
||||||
|
|
||||||
import pl.com.bottega.factory.shortages.prediction.Shortages
|
import pl.com.bottega.factory.shortages.prediction.Shortage
|
||||||
import pl.com.bottega.factory.shortages.prediction.calculation.TimeGrammar
|
import pl.com.bottega.factory.shortages.prediction.calculation.TimeGrammar
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
|
|
||||||
@@ -18,11 +18,11 @@ class ShortageDiffPolicySpec extends Specification {
|
|||||||
given:
|
given:
|
||||||
def policy = ShortageDiffPolicy.ValuesAreNotSame
|
def policy = ShortageDiffPolicy.ValuesAreNotSame
|
||||||
|
|
||||||
Shortages one = Shortages.builder("3009000", 0, now)
|
Shortage one = Shortage.builder("3009000", 0, now)
|
||||||
.missing(now + 1.day, 500L)
|
.missing(now + 1.day, 500L)
|
||||||
.build().orElse(null)
|
.build().orElse(null)
|
||||||
|
|
||||||
Shortages another = Shortages.builder("3009000", 0, now + 5.min)
|
Shortage another = Shortage.builder("3009000", 0, now + 5.min)
|
||||||
.missing(now + 1.day, 500L)
|
.missing(now + 1.day, 500L)
|
||||||
.build().orElse(null)
|
.build().orElse(null)
|
||||||
|
|
||||||
@@ -34,11 +34,11 @@ class ShortageDiffPolicySpec extends Specification {
|
|||||||
given:
|
given:
|
||||||
def policy = ShortageDiffPolicy.ValuesAreNotSame
|
def policy = ShortageDiffPolicy.ValuesAreNotSame
|
||||||
|
|
||||||
Shortages one = Shortages.builder("3009000", 0, now)
|
Shortage one = Shortage.builder("3009000", 0, now)
|
||||||
.missing(now + 1.day, 500L)
|
.missing(now + 1.day, 500L)
|
||||||
.build().orElse(null)
|
.build().orElse(null)
|
||||||
|
|
||||||
Shortages another = Shortages.builder("3009000", 1000, now)
|
Shortage another = Shortage.builder("3009000", 1000, now)
|
||||||
.missing(now + 1.day, 500L)
|
.missing(now + 1.day, 500L)
|
||||||
.build().orElse(null)
|
.build().orElse(null)
|
||||||
|
|
||||||
@@ -50,11 +50,11 @@ class ShortageDiffPolicySpec extends Specification {
|
|||||||
given:
|
given:
|
||||||
def policy = ShortageDiffPolicy.ValuesAreNotSame
|
def policy = ShortageDiffPolicy.ValuesAreNotSame
|
||||||
|
|
||||||
Shortages one = Shortages.builder("3009000XXX", 0, now)
|
Shortage one = Shortage.builder("3009000XXX", 0, now)
|
||||||
.missing(now + 1.day, 500L)
|
.missing(now + 1.day, 500L)
|
||||||
.build().orElse(null)
|
.build().orElse(null)
|
||||||
|
|
||||||
Shortages another = Shortages.builder("3009000", 0, now)
|
Shortage another = Shortage.builder("3009000", 0, now)
|
||||||
.missing(now + 1.day, 500L)
|
.missing(now + 1.day, 500L)
|
||||||
.build().orElse(null)
|
.build().orElse(null)
|
||||||
|
|
||||||
@@ -66,11 +66,11 @@ class ShortageDiffPolicySpec extends Specification {
|
|||||||
given:
|
given:
|
||||||
def policy = ShortageDiffPolicy.ValuesAreNotSame
|
def policy = ShortageDiffPolicy.ValuesAreNotSame
|
||||||
|
|
||||||
Shortages one = Shortages.builder("3009000", 0, now)
|
Shortage one = Shortage.builder("3009000", 0, now)
|
||||||
.missing(now + 1.day, 500L)
|
.missing(now + 1.day, 500L)
|
||||||
.build().orElse(null)
|
.build().orElse(null)
|
||||||
|
|
||||||
Shortages another = Shortages.builder("3009000", 0, now)
|
Shortage another = Shortage.builder("3009000", 0, now)
|
||||||
.missing(now + 1.day, 1500L)
|
.missing(now + 1.day, 1500L)
|
||||||
.build().orElse(null)
|
.build().orElse(null)
|
||||||
|
|
||||||
@@ -82,11 +82,11 @@ class ShortageDiffPolicySpec extends Specification {
|
|||||||
given:
|
given:
|
||||||
def policy = ShortageDiffPolicy.ValuesAreNotSame
|
def policy = ShortageDiffPolicy.ValuesAreNotSame
|
||||||
|
|
||||||
Shortages one = Shortages.builder("3009000", 0, now)
|
Shortage one = Shortage.builder("3009000", 0, now)
|
||||||
.missing(now + 1.day, 500L)
|
.missing(now + 1.day, 500L)
|
||||||
.build().orElse(null)
|
.build().orElse(null)
|
||||||
|
|
||||||
Shortages another = Shortages.builder("3009000", 0, now)
|
Shortage another = Shortage.builder("3009000", 0, now)
|
||||||
.missing(now + 1.day, 499L)
|
.missing(now + 1.day, 499L)
|
||||||
.build().orElse(null)
|
.build().orElse(null)
|
||||||
|
|
||||||
@@ -98,11 +98,11 @@ class ShortageDiffPolicySpec extends Specification {
|
|||||||
given:
|
given:
|
||||||
def policy = ShortageDiffPolicy.ValuesAreNotSame
|
def policy = ShortageDiffPolicy.ValuesAreNotSame
|
||||||
|
|
||||||
Shortages one = Shortages.builder("3009000", 0, now)
|
Shortage one = Shortage.builder("3009000", 0, now)
|
||||||
.missing(now + 1.day, 500L)
|
.missing(now + 1.day, 500L)
|
||||||
.build().orElse(null)
|
.build().orElse(null)
|
||||||
|
|
||||||
Shortages another = Shortages.builder("3009000", 0, now)
|
Shortage another = Shortage.builder("3009000", 0, now)
|
||||||
.missing(now + 2.day, 500L)
|
.missing(now + 2.day, 500L)
|
||||||
.build().orElse(null)
|
.build().orElse(null)
|
||||||
|
|
||||||
@@ -114,11 +114,11 @@ class ShortageDiffPolicySpec extends Specification {
|
|||||||
given:
|
given:
|
||||||
def policy = ShortageDiffPolicy.ValuesAreNotSame
|
def policy = ShortageDiffPolicy.ValuesAreNotSame
|
||||||
|
|
||||||
Shortages one = Shortages.builder("3009000", 0, now)
|
Shortage one = Shortage.builder("3009000", 0, now)
|
||||||
.missing(now + 1.day, 500L)
|
.missing(now + 1.day, 500L)
|
||||||
.build().orElse(null)
|
.build().orElse(null)
|
||||||
|
|
||||||
Shortages another = Shortages.builder("3009000", 0, now)
|
Shortage another = Shortage.builder("3009000", 0, now)
|
||||||
.missing(now + 1.day + 1.min, 500L)
|
.missing(now + 1.day + 1.min, 500L)
|
||||||
.build().orElse(null)
|
.build().orElse(null)
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package pl.com.bottega.factory.shortages.prediction.monitoring
|
|||||||
|
|
||||||
import pl.com.bottega.factory.product.management.RefNoId
|
import pl.com.bottega.factory.product.management.RefNoId
|
||||||
import pl.com.bottega.factory.shortages.prediction.Configuration
|
import pl.com.bottega.factory.shortages.prediction.Configuration
|
||||||
import pl.com.bottega.factory.shortages.prediction.Shortages
|
import pl.com.bottega.factory.shortages.prediction.Shortage
|
||||||
import pl.com.bottega.factory.shortages.prediction.calculation.Forecasts
|
import pl.com.bottega.factory.shortages.prediction.calculation.ShortageForecasts
|
||||||
import pl.com.bottega.factory.shortages.prediction.calculation.ShortagesCalculationAssembler
|
import pl.com.bottega.factory.shortages.prediction.calculation.ShortagesCalculationAssembler
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
|
|
||||||
@@ -119,8 +119,8 @@ class ShortagePredictionProcessSpec extends Specification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ShortagePredictionProcess predictionProcess(
|
ShortagePredictionProcess predictionProcess(
|
||||||
Shortages previouslyFound,
|
Shortage previouslyFound,
|
||||||
Forecasts forecastThatWillFindShortages) {
|
ShortageForecasts forecastThatWillFindShortages) {
|
||||||
|
|
||||||
new ShortagePredictionProcess(
|
new ShortagePredictionProcess(
|
||||||
refNo,
|
refNo,
|
||||||
@@ -142,15 +142,15 @@ class ShortagePredictionProcessSpec extends Specification {
|
|||||||
(now.plusDays(1)) : 900L]
|
(now.plusDays(1)) : 900L]
|
||||||
}
|
}
|
||||||
|
|
||||||
Shortages noShortagesWasPreviouslyFound() {
|
Shortage noShortagesWasPreviouslyFound() {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
Shortages wasPreviouslyFound(Map<LocalDateTime, Long> shortages) {
|
Shortage wasPreviouslyFound(Map<LocalDateTime, Long> shortages) {
|
||||||
forecastAssembler.shortage(shortages).orElse(null)
|
forecastAssembler.shortage(shortages).orElse(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
Forecasts noShortagesWillBeFound() {
|
ShortageForecasts noShortagesWillBeFound() {
|
||||||
forecastAssembler.forecastProvider(
|
forecastAssembler.forecastProvider(
|
||||||
forecastAssembler.stock(1000),
|
forecastAssembler.stock(1000),
|
||||||
forecastAssembler.noDeliveries(),
|
forecastAssembler.noDeliveries(),
|
||||||
@@ -158,7 +158,7 @@ class ShortagePredictionProcessSpec extends Specification {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Forecasts willFindShortages(Map<LocalDateTime, Long> shortages) {
|
ShortageForecasts willFindShortages(Map<LocalDateTime, Long> shortages) {
|
||||||
forecastAssembler.forecastProvider(
|
forecastAssembler.forecastProvider(
|
||||||
forecastAssembler.stock(0),
|
forecastAssembler.stock(0),
|
||||||
forecastAssembler.deliveries(shortages),
|
forecastAssembler.deliveries(shortages),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package pl.com.bottega.factory.shortages.prediction.notification
|
package pl.com.bottega.factory.shortages.prediction.notification
|
||||||
|
|
||||||
import pl.com.bottega.factory.product.management.RefNoId
|
import pl.com.bottega.factory.product.management.RefNoId
|
||||||
import pl.com.bottega.factory.shortages.prediction.Shortages
|
import pl.com.bottega.factory.shortages.prediction.Shortage
|
||||||
import pl.com.bottega.factory.shortages.prediction.monitoring.NewShortage
|
import pl.com.bottega.factory.shortages.prediction.monitoring.NewShortage
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
|
|
||||||
@@ -109,14 +109,14 @@ class NotificationOfShortageSpec extends Specification {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
NewShortage newShortage(After after, Shortages shortages) {
|
NewShortage newShortage(After after, Shortage shortages) {
|
||||||
new NewShortage(new RefNoId(refNo), after, shortages)
|
new NewShortage(new RefNoId(refNo), after, shortages)
|
||||||
}
|
}
|
||||||
|
|
||||||
Shortages withShortage(
|
Shortage withShortage(
|
||||||
Duration firstShortageIn = Duration.ofDays(4),
|
Duration firstShortageIn = Duration.ofDays(4),
|
||||||
long lockedStock = 0) {
|
long lockedStock = 0) {
|
||||||
Shortages.builder(refNo, lockedStock, now)
|
Shortage.builder(refNo, lockedStock, now)
|
||||||
.missing(now.plus(firstShortageIn), 500L)
|
.missing(now.plus(firstShortageIn), 500L)
|
||||||
.build().get()
|
.build().get()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package pl.com.bottega.factory.shortages.prediction.notification
|
package pl.com.bottega.factory.shortages.prediction.notification
|
||||||
|
|
||||||
import pl.com.bottega.factory.shortages.prediction.Shortages
|
import pl.com.bottega.factory.shortages.prediction.Shortage
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@@ -10,8 +10,8 @@ class RecoveryTaskPriorityChangePolicySpec extends Specification {
|
|||||||
|
|
||||||
def now = LocalDateTime.now()
|
def now = LocalDateTime.now()
|
||||||
|
|
||||||
Shortages foundShortage(Duration firstShortageIn, long lockedStock) {
|
Shortage foundShortage(Duration firstShortageIn, long lockedStock) {
|
||||||
Shortages.builder("3009000", lockedStock, now)
|
Shortage.builder("3009000", lockedStock, now)
|
||||||
.missing(now.plus(firstShortageIn), 500L)
|
.missing(now.plus(firstShortageIn), 500L)
|
||||||
.build().get()
|
.build().get()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user