public class PlanViewDto {
} public interface PlannerService {

    void scheduleNewProduction(long lineId, long formId, LocalDateTime start, Duration duration);

    void adjustProductionTime(long productionId, LocalDateTime start, Duration duration);

    void setColor(long productionId, String color);

    void setNote(long productionId, String note);

    PlanViewDto getPlan(long line);
} public class FormEntity {

    private String refNo;
    private double outputPerMinute;
    private double utilization;
    private double weight;
    private Duration startAndWormUp;
    private Duration endAndCleaning;

    public FormEntity() {
    }

    public String getRefNo() {
        return this.refNo;
    }

    public double getOutputPerMinute() {
        return this.outputPerMinute;
    }

    public double getUtilization() {
        return this.utilization;
    }

    public double getWeight() {
        return this.weight;
    }

    public Duration getStartAndWormUp() {
        return this.startAndWormUp;
    }

    public Duration getEndAndCleaning() {
        return this.endAndCleaning;
    }

    public void setRefNo(String refNo) {
        this.refNo = refNo;
    }

    public void setOutputPerMinute(double outputPerMinute) {
        this.outputPerMinute = outputPerMinute;
    }

    public void setUtilization(double utilization) {
        this.utilization = utilization;
    }

    public void setWeight(double weight) {
        this.weight = weight;
    }

    public void setStartAndWormUp(Duration startAndWormUp) {
        this.startAndWormUp = startAndWormUp;
    }

    public void setEndAndCleaning(Duration endAndCleaning) {
        this.endAndCleaning = endAndCleaning;
    }

    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof FormEntity)) return false;
        final FormEntity other = (FormEntity) o;
        if (!other.canEqual((Object) this)) return false;
        final Object this$refNo = this.getRefNo();
        final Object other$refNo = other.getRefNo();
        if (this$refNo == null ? other$refNo != null : !this$refNo.equals(other$refNo)) return false;
        if (Double.compare(this.getOutputPerMinute(), other.getOutputPerMinute()) != 0) return false;
        if (Double.compare(this.getUtilization(), other.getUtilization()) != 0) return false;
        if (Double.compare(this.getWeight(), other.getWeight()) != 0) return false;
        final Object this$startAndWormUp = this.getStartAndWormUp();
        final Object other$startAndWormUp = other.getStartAndWormUp();
        if (this$startAndWormUp == null ? other$startAndWormUp != null : !this$startAndWormUp.equals(other$startAndWormUp))
            return false;
        final Object this$endAndCleaning = this.getEndAndCleaning();
        final Object other$endAndCleaning = other.getEndAndCleaning();
        if (this$endAndCleaning == null ? other$endAndCleaning != null : !this$endAndCleaning.equals(other$endAndCleaning))
            return false;
        return true;
    }

    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        final Object $refNo = this.getRefNo();
        result = result * PRIME + ($refNo == null ? 43 : $refNo.hashCode());
        final long $outputPerMinute = Double.doubleToLongBits(this.getOutputPerMinute());
        result = result * PRIME + (int) ($outputPerMinute >>> 32 ^ $outputPerMinute);
        final long $utilization = Double.doubleToLongBits(this.getUtilization());
        result = result * PRIME + (int) ($utilization >>> 32 ^ $utilization);
        final long $weight = Double.doubleToLongBits(this.getWeight());
        result = result * PRIME + (int) ($weight >>> 32 ^ $weight);
        final Object $startAndWormUp = this.getStartAndWormUp();
        result = result * PRIME + ($startAndWormUp == null ? 43 : $startAndWormUp.hashCode());
        final Object $endAndCleaning = this.getEndAndCleaning();
        result = result * PRIME + ($endAndCleaning == null ? 43 : $endAndCleaning.hashCode());
        return result;
    }

    protected boolean canEqual(Object other) {
        return other instanceof FormEntity;
    }

    public String toString() {
        return "entities.FormEntity(refNo=" + this.getRefNo() + ", outputPerMinute=" + this.getOutputPerMinute() + ", utilization=" + this.getUtilization() + ", weight=" + this.getWeight() + ", startAndWormUp=" + this.getStartAndWormUp() + ", endAndCleaning=" + this.getEndAndCleaning() + ")";
    }
} public class LineEntity {

    private long id;
    private double maxWeight;

    public LineEntity() {
    }

    public long getId() {
        return this.id;
    }

    public double getMaxWeight() {
        return this.maxWeight;
    }

    public void setId(long id) {
        this.id = id;
    }

    public void setMaxWeight(double maxWeight) {
        this.maxWeight = maxWeight;
    }

    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof LineEntity)) return false;
        final LineEntity other = (LineEntity) o;
        if (!other.canEqual((Object) this)) return false;
        if (this.getId() != other.getId()) return false;
        if (Double.compare(this.getMaxWeight(), other.getMaxWeight()) != 0) return false;
        return true;
    }

    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        final long $id = this.getId();
        result = result * PRIME + (int) ($id >>> 32 ^ $id);
        final long $maxWeight = Double.doubleToLongBits(this.getMaxWeight());
        result = result * PRIME + (int) ($maxWeight >>> 32 ^ $maxWeight);
        return result;
    }

    protected boolean canEqual(Object other) {
        return other instanceof LineEntity;
    }

    public String toString() {
        return "entities.LineEntity(id=" + this.getId() + ", maxWeight=" + this.getMaxWeight() + ")";
    }
} public class ProductionEntity {

    private long productionId;
    private LineEntity line;
    private FormEntity form;
    private LocalDateTime start;
    private Duration duration;
    private Duration startAndWormUp;
    private Duration endAndCleaning;
    private double speed;
    private long output;
    private double utilization;
    private String color;
    private String note;

    public ProductionEntity() {
    }

    public long getProductionId() {
        return this.productionId;
    }

    public LineEntity getLine() {
        return this.line;
    }

    public FormEntity getForm() {
        return this.form;
    }

    public LocalDateTime getStart() {
        return this.start;
    }

    public Duration getDuration() {
        return this.duration;
    }

    public Duration getStartAndWormUp() {
        return this.startAndWormUp;
    }

    public Duration getEndAndCleaning() {
        return this.endAndCleaning;
    }

    public double getSpeed() {
        return this.speed;
    }

    public long getOutput() {
        return this.output;
    }

    public double getUtilization() {
        return this.utilization;
    }

    public String getColor() {
        return this.color;
    }

    public String getNote() {
        return this.note;
    }

    public void setProductionId(long productionId) {
        this.productionId = productionId;
    }

    public void setLine(LineEntity line) {
        this.line = line;
    }

    public void setForm(FormEntity form) {
        this.form = form;
    }

    public void setStart(LocalDateTime start) {
        this.start = start;
    }

    public void setDuration(Duration duration) {
        this.duration = duration;
    }

    public void setStartAndWormUp(Duration startAndWormUp) {
        this.startAndWormUp = startAndWormUp;
    }

    public void setEndAndCleaning(Duration endAndCleaning) {
        this.endAndCleaning = endAndCleaning;
    }

    public void setSpeed(double speed) {
        this.speed = speed;
    }

    public void setOutput(long output) {
        this.output = output;
    }

    public void setUtilization(double utilization) {
        this.utilization = utilization;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public void setNote(String note) {
        this.note = note;
    }

    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof ProductionEntity)) return false;
        final ProductionEntity other = (ProductionEntity) o;
        if (!other.canEqual((Object) this)) return false;
        if (this.getProductionId() != other.getProductionId()) return false;
        final Object this$line = this.getLine();
        final Object other$line = other.getLine();
        if (this$line == null ? other$line != null : !this$line.equals(other$line)) return false;
        final Object this$form = this.getForm();
        final Object other$form = other.getForm();
        if (this$form == null ? other$form != null : !this$form.equals(other$form)) return false;
        final Object this$start = this.getStart();
        final Object other$start = other.getStart();
        if (this$start == null ? other$start != null : !this$start.equals(other$start)) return false;
        final Object this$duration = this.getDuration();
        final Object other$duration = other.getDuration();
        if (this$duration == null ? other$duration != null : !this$duration.equals(other$duration)) return false;
        final Object this$startAndWormUp = this.getStartAndWormUp();
        final Object other$startAndWormUp = other.getStartAndWormUp();
        if (this$startAndWormUp == null ? other$startAndWormUp != null : !this$startAndWormUp.equals(other$startAndWormUp))
            return false;
        final Object this$endAndCleaning = this.getEndAndCleaning();
        final Object other$endAndCleaning = other.getEndAndCleaning();
        if (this$endAndCleaning == null ? other$endAndCleaning != null : !this$endAndCleaning.equals(other$endAndCleaning))
            return false;
        if (Double.compare(this.getSpeed(), other.getSpeed()) != 0) return false;
        if (this.getOutput() != other.getOutput()) return false;
        if (Double.compare(this.getUtilization(), other.getUtilization()) != 0) return false;
        final Object this$color = this.getColor();
        final Object other$color = other.getColor();
        if (this$color == null ? other$color != null : !this$color.equals(other$color)) return false;
        final Object this$note = this.getNote();
        final Object other$note = other.getNote();
        if (this$note == null ? other$note != null : !this$note.equals(other$note)) return false;
        return true;
    }

    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        final long $productionId = this.getProductionId();
        result = result * PRIME + (int) ($productionId >>> 32 ^ $productionId);
        final Object $line = this.getLine();
        result = result * PRIME + ($line == null ? 43 : $line.hashCode());
        final Object $form = this.getForm();
        result = result * PRIME + ($form == null ? 43 : $form.hashCode());
        final Object $start = this.getStart();
        result = result * PRIME + ($start == null ? 43 : $start.hashCode());
        final Object $duration = this.getDuration();
        result = result * PRIME + ($duration == null ? 43 : $duration.hashCode());
        final Object $startAndWormUp = this.getStartAndWormUp();
        result = result * PRIME + ($startAndWormUp == null ? 43 : $startAndWormUp.hashCode());
        final Object $endAndCleaning = this.getEndAndCleaning();
        result = result * PRIME + ($endAndCleaning == null ? 43 : $endAndCleaning.hashCode());
        final long $speed = Double.doubleToLongBits(this.getSpeed());
        result = result * PRIME + (int) ($speed >>> 32 ^ $speed);
        final long $output = this.getOutput();
        result = result * PRIME + (int) ($output >>> 32 ^ $output);
        final long $utilization = Double.doubleToLongBits(this.getUtilization());
        result = result * PRIME + (int) ($utilization >>> 32 ^ $utilization);
        final Object $color = this.getColor();
        result = result * PRIME + ($color == null ? 43 : $color.hashCode());
        final Object $note = this.getNote();
        result = result * PRIME + ($note == null ? 43 : $note.hashCode());
        return result;
    }

    protected boolean canEqual(Object other) {
        return other instanceof ProductionEntity;
    }

    public String toString() {
        return "entities.ProductionEntity(productionId=" + this.getProductionId() + ", line=" + this.getLine() + ", form=" + this.getForm() + ", start=" + this.getStart() + ", duration=" + this.getDuration() + ", startAndWormUp=" + this.getStartAndWormUp() + ", endAndCleaning=" + this.getEndAndCleaning() + ", speed=" + this.getSpeed() + ", output=" + this.getOutput() + ", utilization=" + this.getUtilization() + ", color=" + this.getColor() + ", note=" + this.getNote() + ")";
    }
} public class ShortageEntity {
    private String refNo;
    private LocalDate found;
    private long missing;
    private LocalDate atDay;

    public ShortageEntity() {
    }

    public String getRefNo() {
        return this.refNo;
    }

    public LocalDate getFound() {
        return this.found;
    }

    public long getMissing() {
        return this.missing;
    }

    public LocalDate getAtDay() {
        return this.atDay;
    }

    public void setRefNo(String refNo) {
        this.refNo = refNo;
    }

    public void setFound(LocalDate found) {
        this.found = found;
    }

    public void setMissing(long missing) {
        this.missing = missing;
    }

    public void setAtDay(LocalDate atDay) {
        this.atDay = atDay;
    }

    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof ShortageEntity)) return false;
        final ShortageEntity other = (ShortageEntity) o;
        if (!other.canEqual((Object) this)) return false;
        final Object this$refNo = this.getRefNo();
        final Object other$refNo = other.getRefNo();
        if (this$refNo == null ? other$refNo != null : !this$refNo.equals(other$refNo)) return false;
        final Object this$found = this.getFound();
        final Object other$found = other.getFound();
        if (this$found == null ? other$found != null : !this$found.equals(other$found)) return false;
        if (this.getMissing() != other.getMissing()) return false;
        final Object this$atDay = this.getAtDay();
        final Object other$atDay = other.getAtDay();
        if (this$atDay == null ? other$atDay != null : !this$atDay.equals(other$atDay)) return false;
        return true;
    }

    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        final Object $refNo = this.getRefNo();
        result = result * PRIME + ($refNo == null ? 43 : $refNo.hashCode());
        final Object $found = this.getFound();
        result = result * PRIME + ($found == null ? 43 : $found.hashCode());
        final long $missing = this.getMissing();
        result = result * PRIME + (int) ($missing >>> 32 ^ $missing);
        final Object $atDay = this.getAtDay();
        result = result * PRIME + ($atDay == null ? 43 : $atDay.hashCode());
        return result;
    }

    protected boolean canEqual(Object other) {
        return other instanceof ShortageEntity;
    }

    public String toString() {
        return "entities.ShortageEntity(refNo=" + this.getRefNo() + ", found=" + this.getFound() + ", missing=" + this.getMissing() + ", atDay=" + this.getAtDay() + ")";
    }
} public class CurrentStock {
    private long level;
    private long locked;

    public CurrentStock(long level, long locked) {
        this.level = level;
        this.locked = locked;
    }

    public long getLevel() {
        return this.level;
    }

    public long getLocked() {
        return this.locked;
    }

    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof CurrentStock)) return false;
        final CurrentStock other = (CurrentStock) o;
        if (this.getLevel() != other.getLevel()) return false;
        if (this.getLocked() != other.getLocked()) return false;
        return true;
    }

    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        final long $level = this.getLevel();
        result = result * PRIME + (int) ($level >>> 32 ^ $level);
        final long $locked = this.getLocked();
        result = result * PRIME + (int) ($locked >>> 32 ^ $locked);
        return result;
    }

    public String toString() {
        return "external.CurrentStock(level=" + this.getLevel() + ", locked=" + this.getLocked() + ")";
    }
} public interface JiraService {
    void increasePriorityFor(String productRefNo);
} public interface NotificationsService {
    void alertPlanner(List<ShortageEntity> shortages);

    void softNotifyPlanner(List<ShortageEntity> shortages);

    void markOnPlan(List<ShortageEntity> shortages);
} public interface StockService {
    CurrentStock getCurrentStock(String productRefNo);
} public class ShortageFinder {

    /**
     * Production at day of expected delivery is quite complex:
     * We are able to produce and deliver just in time at same day
     * but depending on delivery time or scheme of multiple deliveries,
     * we need to plan properly to have right amount of parts ready before delivery time.
     * <p/>
     * Typical schemas are:
     * <li>Delivery at prod day start</li>
     * <li>Delivery till prod day end</li>
     * <li>Delivery during specified shift</li>
     * <li>Multiple deliveries at specified times</li>
     * Schema changes the way how we calculate shortages.
     * Pick of schema depends on customer demand on daily basis and for each product differently.
     * Some customers includes that information in callof document,
     * other stick to single schema per product. By manual adjustments of demand,
     * customer always specifies desired delivery schema
     * (increase amount in scheduled transport or organize extra transport at given time)
     *
     * TODO algorithm is finding wrong shortages, when more productions is planned in a single day
     */
    public static List<ShortageEntity> findShortages(LocalDate today, int daysAhead, CurrentStock stock,
                                                     List<ProductionEntity> productions, List<DemandEntity> demands) {

        ProductionOutputsFactory productionOutputsFactory = new ProductionOutputsFactory(productions);
        DemandsFactory demandsFactory = new DemandsFactory(demands);
        ShortagePredictionRepository repository = new ShortagePredictionRepository(stock, productionOutputsFactory, demandsFactory);

        ShortagePredictionAlgo algorithm = repository.get(today, daysAhead);

        return algorithm.findShortages();
    }

    private ShortageFinder() {
    }

} public class Util {

    private Util() {
    }

    public static long getLevel(DemandEntity demand) {
        if (demand.getAdjustment().isEmpty()) {
            return demand.getOriginal().getLevel();
        } else {
            return demand.getAdjustment().get(demand.getAdjustment().size() - 1).getLevel();
        }
    }

    public static DeliverySchema getDeliverySchema(DemandEntity demand) {
        DeliverySchema deliverySchema;
        if (demand.getAdjustment().isEmpty()) {
            deliverySchema = demand.getOriginal().getDeliverySchema();
        } else {
            deliverySchema = demand.getAdjustment().get(demand.getAdjustment().size() - 1).getDeliverySchema();
        }
        if (deliverySchema == null) {
            return defaultFor(demand.getProductRefNo());
        }
        return deliverySchema;
    }

    /**
     * <pre>
     * Rules for   Default Delivery Schema:
     * If delivery scheme is not defined in callof document, we apply following rules:
     *  Product refNo: 461952387712 and 461952816051 are delivered every 3 hours during day shifts every day.
     *   All products for customer „Pralki Wir” (refNo starts with 51) need to be delivered till end of production day.
     *  There is no need to produce other products just in time so we can treat them as delivery at start of production day.
     * </pre>
     */
    private static DeliverySchema defaultFor(String productRefNo) {
        if (productRefNo.equals("461952387712") || productRefNo.equals("461952816051")) {
            return DeliverySchema.every3hours;
        }
        if (productRefNo.startsWith("51")) {
            return DeliverySchema.tillEndOfDay;
        }
        return DeliverySchema.atDayStart;
    }

    public static boolean isFormAvailableAt(FormEntity form, LocalDateTime start, Duration duration) {
        // TODO How to store / calculate that ?
        return false;
    }

    public static boolean canProduceOn(LineEntity line, FormEntity form) {
        return line.getMaxWeight() > form.getWeight();
    }

}
import api.PlanViewDto;
import api.PlannerService;
import dao.*;
import entities.FormEntity;
import entities.LineEntity;
import entities.ProductionEntity;
import entities.ShortageEntity;
import external.CurrentStock;
import external.JiraService;
import external.NotificationsService;
import external.StockService;
import tools.ShortageFinder;
import tools.Util;
import java.time.Clock;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.LinkedList;
import java.util.List;
     *  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.
     *  Shortages may arise if insufficient production was planned.
     * </pre>
     */
    //Transactional
    @Override
    public void adjustProductionTime(long productionId, LocalDateTime start, Duration duration) {
        ProductionEntity production = productionDao.get(productionId);
        FormEntity form = production.getForm();
        if (!Util.isFormAvailableAt(form, start, duration)) {
            // TODO Ask: what in that case ?
        }
        List<ProductionEntity> currentlyPlanned = productionDao.findFromTime(production.getLine().getId(), start);
        ProductionEntity preceding = null;
        if (currentlyPlanned.size() > 0) {
            ProductionEntity first = currentlyPlanned.get(0);
            if (first.getStart().isBefore(start)) {
                preceding = first;
            }
        }
        List<ProductionEntity> changed = makeSpace(preceding, currentlyPlanned, start, duration);
        production.setStart(start);
        production.setDuration(duration);

        // Shortages may arise if insufficient production was planned.
        processShortages(changed);
    }

    //Transactional
    @Override
    public void setColor(long productionId, String color) {
        // just sets color of production block on screen
        ProductionEntity production = productionDao.get(productionId);
        production.setColor(color);
    }

    //Transactional
    @Override
    public void setNote(long productionId, String note) {
        // adds planner note for shift manager or production engineer
        ProductionEntity production = productionDao.get(productionId);
        production.setColor(note);
    }

    //ReadOnly
    @Override
    public PlanViewDto getPlan(long line) {
        return new PlanViewDto();
    }

    public List<ProductionEntity> makeSpace(ProductionEntity preceding,
                                            List<ProductionEntity> currentlyPlanned,
                                            LocalDateTime start, Duration duration) {
        List<ProductionEntity> changed = new LinkedList<>();

        if (preceding != null && overlaps(preceding.getStart(), preceding.getDuration(), start, duration)) {
            // If production on given line overlaps with other: preceding must shrink
            preceding.setDuration(Duration.between(preceding.getStart(), start));
            changed.add(preceding);
        }
        LocalDateTime lastStart = start;
        Duration lastDuration = duration;
        for (ProductionEntity production : currentlyPlanned) {
            if (production == preceding) {
                continue;
            }
            if (overlaps(production.getStart(), production.getDuration(), lastStart, lastDuration)) {
                // If production on given line overlaps with other: *ALL* succeeding one must start later.
                production.setStart(lastStart.plus(lastDuration));
                lastStart = production.getStart();
                lastDuration = production.getDuration();
                changed.add(production);
            } else {
                break;
            }
        }
        return changed;
    }

    /**
     * overlaps if:
     * <pre>
     *  first.start is after second.start && first.start is before second.end
     *        | ---  first --- |
     *  | --- second --- |
     *
     *  second.start is after first.start && second.start is before first.end
     *  | ---  first --- |
     *        | --- second --- |
     * </pre>
     */
    private boolean overlaps(LocalDateTime start, Duration duration, LocalDateTime lastStart, Duration lastDuration) {
        LocalDateTime firstStart;
        Duration firstDuration;
        LocalDateTime secondStart;
        Duration secondDuration;
        if (start.isBefore(lastStart)) {
            firstStart = start;
            firstDuration = duration;
            secondStart = lastStart;
            secondDuration = lastDuration;
        } else {
            firstStart = lastStart;
            firstDuration = lastDuration;
            secondStart = start;
            secondDuration = duration;
        }

        return firstStart.isAfter(secondStart) && firstStart.isBefore(secondStart.plus(secondDuration)) ||
                secondStart.isAfter(firstStart) && secondStart.isBefore(firstStart.plus(firstDuration))
                ;
    }

    private void processShortages(List<ProductionEntity> products) {
        LocalDate today = LocalDate.now(clock);

        for (ProductionEntity production : products) {
            CurrentStock currentStock = stockService.getCurrentStock(production.getForm().getRefNo());
            List<ShortageEntity> shortages = ShortageFinder.findShortages(