├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── LICENSE ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── domaindrivers │ │ └── smartschedule │ │ ├── SmartScheduleApplication.java │ │ ├── allocation │ │ ├── AllocatedCapability.java │ │ ├── AllocationConfiguration.java │ │ ├── AllocationFacade.java │ │ ├── Allocations.java │ │ ├── CapabilitiesAllocated.java │ │ ├── CapabilityReleased.java │ │ ├── Demand.java │ │ ├── Demands.java │ │ ├── JpaProjectAllocationsRepository.java │ │ ├── NotSatisfiedDemands.java │ │ ├── PotentialTransfers.java │ │ ├── PotentialTransfersService.java │ │ ├── ProjectAllocationScheduled.java │ │ ├── ProjectAllocations.java │ │ ├── ProjectAllocationsDemandsScheduled.java │ │ ├── ProjectAllocationsId.java │ │ ├── ProjectAllocationsRepository.java │ │ ├── ProjectsAllocationsSummary.java │ │ ├── PublishMissingDemandsService.java │ │ ├── capabilityscheduling │ │ │ ├── AllocatableCapabilitiesSummary.java │ │ │ ├── AllocatableCapability.java │ │ │ ├── AllocatableCapabilityId.java │ │ │ ├── AllocatableCapabilityRepository.java │ │ │ ├── AllocatableCapabilitySummary.java │ │ │ ├── AllocatableResourceId.java │ │ │ ├── CapabilityFinder.java │ │ │ ├── CapabilityPlanningConfiguration.java │ │ │ ├── CapabilityScheduler.java │ │ │ └── legacyacl │ │ │ │ ├── EmployeeCreatedInLegacySystemMessageHandler.java │ │ │ │ └── TranslateToCapabilitySelector.java │ │ └── cashflow │ │ │ ├── CashFlowConfiguration.java │ │ │ ├── CashFlowFacade.java │ │ │ ├── Cashflow.java │ │ │ ├── CashflowRepository.java │ │ │ ├── Cost.java │ │ │ ├── Earnings.java │ │ │ ├── EarningsRecalculated.java │ │ │ ├── Income.java │ │ │ └── JpaCashflowRepository.java │ │ ├── availability │ │ ├── AvailabilityConfiguration.java │ │ ├── AvailabilityFacade.java │ │ ├── Blockade.java │ │ ├── Calendar.java │ │ ├── Calendars.java │ │ ├── Owner.java │ │ ├── ResourceAvailability.java │ │ ├── ResourceAvailabilityId.java │ │ ├── ResourceAvailabilityReadModel.java │ │ ├── ResourceAvailabilityRepository.java │ │ ├── ResourceGroupedAvailability.java │ │ ├── ResourceId.java │ │ ├── ResourceTakenOver.java │ │ └── segment │ │ │ ├── SegmentInMinutes.java │ │ │ ├── Segments.java │ │ │ ├── SlotToNormalizedSlot.java │ │ │ └── SlotToSegments.java │ │ ├── optimization │ │ ├── CapacityDimension.java │ │ ├── Item.java │ │ ├── OptimizationFacade.java │ │ ├── Result.java │ │ ├── TotalCapacity.java │ │ ├── TotalWeight.java │ │ └── WeightDimension.java │ │ ├── planning │ │ ├── CapabilitiesDemanded.java │ │ ├── ChosenResources.java │ │ ├── CreateProjectAllocations.java │ │ ├── CriticalStagePlanned.java │ │ ├── Demand.java │ │ ├── Demands.java │ │ ├── DemandsPerStage.java │ │ ├── EditStageDateService.java │ │ ├── NeededResourcesChosen.java │ │ ├── PlanChosenResources.java │ │ ├── PlanningConfiguration.java │ │ ├── PlanningFacade.java │ │ ├── Project.java │ │ ├── ProjectCard.java │ │ ├── ProjectId.java │ │ ├── ProjectRepository.java │ │ ├── RedisProjectRepository.java │ │ ├── parallelization │ │ │ ├── DurationCalculator.java │ │ │ ├── ParallelStages.java │ │ │ ├── ParallelStagesList.java │ │ │ ├── SortedNodesToParallelizedStages.java │ │ │ ├── Stage.java │ │ │ ├── StageParallelization.java │ │ │ └── StagesToNodes.java │ │ └── schedule │ │ │ ├── Schedule.java │ │ │ ├── ScheduleBasedOnChosenResourcesAvailabilityCalculator.java │ │ │ ├── ScheduleBasedOnReferenceStageCalculator.java │ │ │ └── ScheduleBasedOnStartDayCalculator.java │ │ ├── resource │ │ ├── ResourceConfiguration.java │ │ ├── ResourceFacade.java │ │ ├── device │ │ │ ├── Device.java │ │ │ ├── DeviceConfiguration.java │ │ │ ├── DeviceFacade.java │ │ │ ├── DeviceId.java │ │ │ ├── DeviceRepository.java │ │ │ ├── DeviceSummary.java │ │ │ └── ScheduleDeviceCapabilities.java │ │ └── employee │ │ │ ├── Employee.java │ │ │ ├── EmployeeAllocationPolicy.java │ │ │ ├── EmployeeConfiguration.java │ │ │ ├── EmployeeFacade.java │ │ │ ├── EmployeeId.java │ │ │ ├── EmployeeRepository.java │ │ │ ├── EmployeeSummary.java │ │ │ ├── ScheduleEmployeeCapabilities.java │ │ │ └── Seniority.java │ │ ├── risk │ │ ├── RiskConfiguration.java │ │ ├── RiskPeriodicCheckSaga.java │ │ ├── RiskPeriodicCheckSagaDispatcher.java │ │ ├── RiskPeriodicCheckSagaId.java │ │ ├── RiskPeriodicCheckSagaRepository.java │ │ ├── RiskPeriodicCheckSagaStep.java │ │ ├── RiskPushNotification.java │ │ ├── VerifyCriticalResourceAvailableDuringPlanning.java │ │ ├── VerifyEnoughDemandsDuringPlanning.java │ │ └── VerifyNeededResourcesAvailableInTimeSlot.java │ │ ├── shared │ │ ├── BaseRepository.java │ │ ├── CapabilitySelector.java │ │ ├── ClockConfiguration.java │ │ ├── EventingConfiguration.java │ │ ├── EventsPublisher.java │ │ ├── PrivateEvent.java │ │ ├── PublishedEvent.java │ │ ├── ResourceName.java │ │ ├── capability │ │ │ └── Capability.java │ │ └── timeslot │ │ │ └── TimeSlot.java │ │ ├── simulation │ │ ├── AdditionalPricedCapability.java │ │ ├── AvailableResourceCapability.java │ │ ├── Demand.java │ │ ├── Demands.java │ │ ├── ProjectId.java │ │ ├── SimulatedCapabilities.java │ │ ├── SimulatedProject.java │ │ └── SimulationFacade.java │ │ └── sorter │ │ ├── Edge.java │ │ ├── FeedbackArcSeOnGraph.java │ │ ├── GraphTopologicalSort.java │ │ ├── Node.java │ │ ├── Nodes.java │ │ └── SortedNodes.java └── resources │ ├── application.properties │ ├── schema-allocations.sql │ ├── schema-availability.sql │ ├── schema-capability-scheduling.sql │ ├── schema-cashflow.sql │ ├── schema-resources.sql │ └── schema-risk.sql └── test ├── java └── domaindrivers │ └── smartschedule │ ├── ArchitectureDependencyTest.java │ ├── MockedClockConfiguration.java │ ├── MockedEventPublisherConfiguration.java │ ├── TaskExecutorConfiguration.java │ ├── TestDbConfiguration.java │ ├── allocation │ ├── AllocationsToProjectTest.java │ ├── CapabilityAllocatingTest.java │ ├── CreateHourlyDemandsSummaryServiceTest.java │ ├── CreatingNewProjectTest.java │ ├── DemandSchedulingTest.java │ ├── InMemoryProjectAllocationsRepository.java │ ├── PotentialTransferScenarios.java │ ├── ResourceAllocatingTest.java │ ├── capabilityscheduling │ │ ├── CapabilitySchedulingTest.java │ │ └── legacyacl │ │ │ └── TranslateToCapabilitySelectorTest.java │ └── cashflow │ │ ├── CashFlowFacadeTest.java │ │ ├── CashFlowTestConfiguration.java │ │ └── EarningsTest.java │ ├── availability │ ├── AvailabilityCalendarTest.java │ ├── AvailabilityFacadeTest.java │ ├── ResourceAvailabilityLoadingTest.java │ ├── ResourceAvailabilityOptimisticLockingTest.java │ ├── ResourceAvailabilityTest.java │ ├── ResourceAvailabilityUniquenessTest.java │ ├── TakingRandomResourceTest.java │ └── segment │ │ ├── SegmentsTest.java │ │ └── SlotToNormalizedSlotTest.java │ ├── optimization │ ├── CapabilityCapacityDimension.java │ ├── OptimizationForTimedCapabilitiesTest.java │ └── OptimizationTest.java │ ├── planning │ ├── PlanningDbTestConfiguration.java │ ├── PlanningFacadeTest.java │ ├── PlanningTestConfiguration.java │ ├── RDTest.java │ ├── RedisRepositoryTest.java │ ├── SpecializedWaterfallTest.java │ ├── StandardWaterfallTest.java │ ├── TimeCriticalWaterfallTest.java │ ├── VisionTest.java │ ├── parallelization │ │ ├── DependencyRemovalSuggesting.java │ │ ├── DurationCalculatorTest.java │ │ └── ParallelizationTest.java │ └── schedule │ │ ├── ScheduleCalculationTest.java │ │ └── assertions │ │ ├── ScheduleAssert.java │ │ └── StageAssert.java │ ├── resource │ ├── device │ │ ├── CreatingDeviceTest.java │ │ └── ScheduleDeviceCapabilitiesTest.java │ └── employee │ │ ├── AllocationPoliciesTest.java │ │ ├── CreatingEmployeeTest.java │ │ └── ScheduleEmployeeCapabilitiesTest.java │ ├── risk │ ├── RiskPeriodicCheckSagaDispatcherE2ETest.java │ ├── RiskPeriodicCheckSagaTest.java │ └── VerifyEnoughDemandsDuringPlanningTest.java │ ├── shared │ ├── CapabilitySelectorTest.java │ └── timeslot │ │ └── TimeSlotTest.java │ ├── simulation │ ├── AvailableCapabilitiesBuilder.java │ ├── SimulatedProjectsBuilder.java │ └── SimulationScenarios.java │ └── sorter │ ├── FeedbackArcSetOnGraphTest.java │ └── GraphTopologicalSortTest.java └── resources └── application.properties /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DomainDrivers/dd-java/62b637474765a82ded04d29bb238e88f51e9e7a5/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 3 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/SmartScheduleApplication.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class SmartScheduleApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(SmartScheduleApplication.class, args); 11 | 12 | } 13 | } 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/AllocatedCapability.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | import domaindrivers.smartschedule.allocation.capabilityscheduling.AllocatableCapabilityId; 4 | import domaindrivers.smartschedule.allocation.capabilityscheduling.AllocatableResourceId; 5 | import domaindrivers.smartschedule.shared.CapabilitySelector; 6 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 7 | import domaindrivers.smartschedule.shared.capability.Capability; 8 | 9 | import java.util.Objects; 10 | import java.util.UUID; 11 | 12 | record AllocatedCapability(AllocatableCapabilityId allocatedCapabilityID, CapabilitySelector capability, TimeSlot timeSlot) { 13 | 14 | @Override 15 | public boolean equals(Object o) { 16 | if (this == o) return true; 17 | if (o == null || getClass() != o.getClass()) return false; 18 | AllocatedCapability that = (AllocatedCapability) o; 19 | return Objects.equals(allocatedCapabilityID, that.allocatedCapabilityID) && Objects.equals(capability, that.capability) && Objects.equals(timeSlot, that.timeSlot); 20 | } 21 | 22 | @Override 23 | public int hashCode() { 24 | return Objects.hash(allocatedCapabilityID, capability, timeSlot); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/AllocationConfiguration.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | 4 | import domaindrivers.smartschedule.allocation.capabilityscheduling.CapabilityFinder; 5 | import domaindrivers.smartschedule.allocation.cashflow.CashFlowFacade; 6 | import domaindrivers.smartschedule.availability.AvailabilityFacade; 7 | import domaindrivers.smartschedule.shared.EventsPublisher; 8 | import domaindrivers.smartschedule.simulation.SimulationFacade; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | import java.time.Clock; 13 | 14 | @Configuration 15 | class AllocationConfiguration { 16 | 17 | @Bean 18 | AllocationFacade allocationFacade( 19 | ProjectAllocationsRepository projectAllocationsRepository, 20 | AvailabilityFacade availabilityFacade, 21 | CapabilityFinder capabilityFinder, 22 | EventsPublisher eventsPublisher, 23 | Clock clock) { 24 | return new AllocationFacade(projectAllocationsRepository, availabilityFacade, capabilityFinder, eventsPublisher, clock); 25 | } 26 | 27 | @Bean 28 | PotentialTransfersService potentialTransfersService( 29 | CashFlowFacade cashFlowFacade, ProjectAllocationsRepository projectAllocationsRepository) { 30 | return new PotentialTransfersService(new SimulationFacade(), cashFlowFacade, projectAllocationsRepository); 31 | } 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/Allocations.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | import domaindrivers.smartschedule.allocation.capabilityscheduling.AllocatableCapabilityId; 4 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 5 | 6 | import java.util.HashSet; 7 | import java.util.Optional; 8 | import java.util.Set; 9 | import java.util.stream.Collectors; 10 | 11 | record Allocations(Set all) { 12 | 13 | static Allocations none() { 14 | return new Allocations(Set.of()); 15 | } 16 | 17 | Allocations add(AllocatedCapability newOne) { 18 | Set all = new HashSet<>(this.all); 19 | all.add(newOne); 20 | return new Allocations(all); 21 | } 22 | 23 | Allocations remove(AllocatableCapabilityId toRemove, TimeSlot slot) { 24 | return find(toRemove) 25 | .map(ar -> removeFromSlot(ar, slot)) 26 | .orElse(this); 27 | } 28 | private Allocations removeFromSlot(AllocatedCapability allocatedCapability, TimeSlot slot) { 29 | Set leftOvers = allocatedCapability 30 | .timeSlot() 31 | .leftoverAfterRemovingCommonWith(slot) 32 | .stream() 33 | .filter(leftOver -> leftOver.within(allocatedCapability.timeSlot())) 34 | .map(leftOver -> new AllocatedCapability(allocatedCapability.allocatedCapabilityID(), allocatedCapability.capability(), leftOver)) 35 | .collect(Collectors.toSet()); 36 | Set newSlots = new HashSet<>(this.all); 37 | newSlots.remove(allocatedCapability); 38 | newSlots.addAll(leftOvers); 39 | return new Allocations(newSlots); 40 | } 41 | 42 | Optional find(AllocatableCapabilityId allocatedCapabilityId) { 43 | return all.stream() 44 | .filter(ar -> ar.allocatedCapabilityID().equals(allocatedCapabilityId)) 45 | .findFirst(); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/CapabilitiesAllocated.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | 4 | import domaindrivers.smartschedule.shared.PrivateEvent; 5 | 6 | import java.time.Instant; 7 | import java.util.UUID; 8 | 9 | record CapabilitiesAllocated(UUID eventId, UUID allocatedCapabilityId, ProjectAllocationsId projectId, Demands missingDemands, Instant occurredAt) implements PrivateEvent { 10 | 11 | public CapabilitiesAllocated(UUID allocatedCapabilityId, ProjectAllocationsId projectId, Demands missingDemands, Instant occuredAt) { 12 | this(UUID.randomUUID(), allocatedCapabilityId, projectId, missingDemands, occuredAt); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/CapabilityReleased.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | 4 | import domaindrivers.smartschedule.shared.PrivateEvent; 5 | 6 | import java.time.Instant; 7 | import java.util.UUID; 8 | 9 | record CapabilityReleased(UUID eventId, ProjectAllocationsId projectId, Demands missingDemands, Instant occurredAt) implements PrivateEvent { 10 | 11 | public CapabilityReleased(ProjectAllocationsId projectId, Demands missingDemands, Instant occuredAt) { 12 | this(UUID.randomUUID(), projectId, missingDemands, occuredAt); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/Demand.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | import domaindrivers.smartschedule.shared.capability.Capability; 4 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 5 | 6 | public record Demand(Capability capability, TimeSlot slot) { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/Demands.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | 4 | import domaindrivers.smartschedule.shared.capability.Capability; 5 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.stream.Stream; 10 | 11 | public record Demands(List all) { 12 | 13 | public static Demands none() { 14 | return new Demands(List.of()); 15 | } 16 | 17 | public static Demands of(Demand... demands) { 18 | return new Demands(List.of(demands)); 19 | } 20 | 21 | static Demands allInSameTimeSlot(TimeSlot slot, Capability ... capabilities) { 22 | return new Demands(Stream 23 | .of(capabilities) 24 | .map(c -> new Demand(c, slot)) 25 | .toList()); 26 | } 27 | 28 | Demands missingDemands(Allocations allocations) { 29 | return new Demands(all 30 | .stream() 31 | .filter(d -> !satisfiedBy(d, allocations)) 32 | .toList()); 33 | } 34 | 35 | boolean satisfiedBy(Demand d, Allocations allocations) { 36 | return allocations 37 | .all() 38 | .stream() 39 | .anyMatch(ar -> ar.capability().canPerform(d.capability()) && d.slot().within(ar.timeSlot())); 40 | 41 | } 42 | 43 | Demands withNew(Demands newDemands) { 44 | List all = new ArrayList<>(this.all); 45 | all.addAll(newDemands.all); 46 | return new Demands(all); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/JpaProjectAllocationsRepository.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.Query; 5 | 6 | import java.time.Instant; 7 | import java.util.List; 8 | import java.util.Set; 9 | 10 | 11 | interface JpaProjectAllocationsRepository extends ProjectAllocationsRepository, JpaRepository { 12 | 13 | @Query(value = "SELECT * FROM project_allocations WHERE from_date <= :when AND to_date > :when", 14 | nativeQuery = true) 15 | List findAllContainingDate(Instant when); 16 | 17 | default List findAllById(Set projectIds) { 18 | return this.findAllById(projectIds); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/NotSatisfiedDemands.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | import domaindrivers.smartschedule.shared.PublishedEvent; 4 | 5 | import java.time.Instant; 6 | import java.util.Map; 7 | import java.util.UUID; 8 | 9 | public record NotSatisfiedDemands(UUID uuid, Map missingDemands, Instant occurredAt) implements PublishedEvent { 10 | 11 | public NotSatisfiedDemands(Map missingDemands, Instant occuredAt) { 12 | this(UUID.randomUUID(), missingDemands, occuredAt); 13 | } 14 | 15 | public static NotSatisfiedDemands forOneProject(ProjectAllocationsId projectId, Demands scheduledDemands, Instant occurredAt) { 16 | return new NotSatisfiedDemands(UUID.randomUUID(), Map.of(projectId, scheduledDemands), occurredAt); 17 | } 18 | 19 | public static NotSatisfiedDemands allSatisfied(ProjectAllocationsId projectId, Instant occurredAt) { 20 | return new NotSatisfiedDemands(UUID.randomUUID(), Map.of(projectId, Demands.none()), occurredAt); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/PotentialTransfers.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | import domaindrivers.smartschedule.allocation.capabilityscheduling.AllocatableCapabilityId; 4 | import domaindrivers.smartschedule.allocation.capabilityscheduling.AllocatableCapabilitySummary; 5 | import domaindrivers.smartschedule.allocation.cashflow.Earnings; 6 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 7 | import domaindrivers.smartschedule.simulation.ProjectId; 8 | import domaindrivers.smartschedule.simulation.SimulatedProject; 9 | 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | record PotentialTransfers(ProjectsAllocationsSummary summary, Map earnings) { 14 | 15 | PotentialTransfers transfer(ProjectAllocationsId projectFrom, ProjectAllocationsId projectTo, AllocatedCapability allocatedCapability, TimeSlot forSlot) { 16 | Allocations from = summary.projectAllocations().get(projectFrom); 17 | Allocations to = summary.projectAllocations().get(projectTo); 18 | if (from == null || to == null) { 19 | return this; 20 | } 21 | Allocations newAllocationsProjectFrom = from.remove(allocatedCapability.allocatedCapabilityID(), forSlot); 22 | if (newAllocationsProjectFrom.equals(from)) { 23 | return this; 24 | } 25 | summary.projectAllocations().put(projectFrom, newAllocationsProjectFrom); 26 | Allocations newAllocationsProjectTo = to.add(new AllocatedCapability(allocatedCapability.allocatedCapabilityID(), allocatedCapability.capability(), forSlot)); 27 | summary.projectAllocations().put(projectTo, newAllocationsProjectTo); 28 | return new PotentialTransfers(summary, earnings); 29 | } 30 | 31 | List toSimulatedProjects() { 32 | return summary.projectAllocations().keySet().stream().map(project -> new SimulatedProject(ProjectId.from(project.id()), () -> earnings.get(project).toBigDecimal(), getMissingDemands(project))).toList(); 33 | } 34 | 35 | domaindrivers.smartschedule.simulation.Demands getMissingDemands(ProjectAllocationsId projectAllocationsId) { 36 | Demands allDemands = summary.demands().get(projectAllocationsId).missingDemands(summary.projectAllocations().get(projectAllocationsId)); 37 | return new domaindrivers.smartschedule.simulation.Demands(allDemands.all().stream().map(demand -> new domaindrivers.smartschedule.simulation.Demand(demand.capability(), demand.slot())).toList()); 38 | } 39 | 40 | public PotentialTransfers transfer(ProjectAllocationsId projectTo, AllocatableCapabilitySummary capabilityToTransfer, TimeSlot forSlot) { 41 | ProjectAllocationsId projectToMoveFrom = findProjectToMoveFrom(capabilityToTransfer.id(), forSlot); 42 | if (projectToMoveFrom != null) { 43 | return transfer(projectToMoveFrom, projectTo, new AllocatedCapability(capabilityToTransfer.id(), capabilityToTransfer.capabilities(), capabilityToTransfer.timeSlot()), forSlot); 44 | } 45 | return this; 46 | } 47 | 48 | private ProjectAllocationsId findProjectToMoveFrom(AllocatableCapabilityId cap, TimeSlot inSlot) { 49 | return summary.projectAllocations().entrySet().stream().filter(entry -> entry.getValue().find(cap).isPresent()).map(Map.Entry::getKey).findFirst().orElse(null); 50 | } 51 | 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/PotentialTransfersService.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | import domaindrivers.smartschedule.allocation.capabilityscheduling.AllocatableCapabilitySummary; 4 | import domaindrivers.smartschedule.allocation.cashflow.CashFlowFacade; 5 | import domaindrivers.smartschedule.optimization.Result; 6 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 7 | import domaindrivers.smartschedule.simulation.SimulatedCapabilities; 8 | import domaindrivers.smartschedule.simulation.SimulationFacade; 9 | 10 | 11 | public class PotentialTransfersService { 12 | 13 | private final SimulationFacade simulationFacade; 14 | private final CashFlowFacade cashFlowFacade; 15 | private final ProjectAllocationsRepository projectAllocationsRepository; 16 | 17 | public PotentialTransfersService(SimulationFacade simulationFacade, CashFlowFacade cashFlowFacade, ProjectAllocationsRepository projectAllocationsRepository) { 18 | this.simulationFacade = simulationFacade; 19 | this.cashFlowFacade = cashFlowFacade; 20 | this.projectAllocationsRepository = projectAllocationsRepository; 21 | } 22 | 23 | public double profitAfterMovingCapabilities(ProjectAllocationsId projectId, AllocatableCapabilitySummary capabilityToMove, TimeSlot timeSlot) { 24 | //cached? 25 | PotentialTransfers potentialTransfers = new PotentialTransfers(ProjectsAllocationsSummary.of(projectAllocationsRepository.findAll()), cashFlowFacade.findAllEarnings()); 26 | return checkPotentialTransfer(potentialTransfers, projectId, capabilityToMove, timeSlot); 27 | } 28 | 29 | private double checkPotentialTransfer(PotentialTransfers transfers, ProjectAllocationsId projectTo, AllocatableCapabilitySummary capabilityToMove, TimeSlot forSlot) { 30 | Result resultBefore = 31 | simulationFacade.whatIsTheOptimalSetup(transfers.toSimulatedProjects(), SimulatedCapabilities.none()); 32 | transfers = transfers.transfer(projectTo, capabilityToMove, forSlot); 33 | Result resultAfter = 34 | simulationFacade.whatIsTheOptimalSetup(transfers.toSimulatedProjects(), SimulatedCapabilities.none()); 35 | return resultAfter.profit() - resultBefore.profit(); 36 | } 37 | 38 | double checkPotentialTransfer(PotentialTransfers transfers, ProjectAllocationsId projectFrom, ProjectAllocationsId projectTo, AllocatedCapability capability, TimeSlot forSlot) { 39 | Result resultBefore = 40 | simulationFacade.whatIsTheOptimalSetup(transfers.toSimulatedProjects(), SimulatedCapabilities.none()); 41 | transfers = transfers.transfer(projectFrom, projectTo, capability, forSlot); 42 | Result resultAfter = 43 | simulationFacade.whatIsTheOptimalSetup(transfers.toSimulatedProjects(), SimulatedCapabilities.none()); 44 | return resultAfter.profit() - resultBefore.profit(); 45 | } 46 | 47 | 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/ProjectAllocationScheduled.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | import domaindrivers.smartschedule.shared.PrivateEvent; 4 | import domaindrivers.smartschedule.shared.PublishedEvent; 5 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 6 | 7 | import java.time.Instant; 8 | import java.util.UUID; 9 | 10 | public record ProjectAllocationScheduled(UUID uuid, ProjectAllocationsId projectId, TimeSlot fromTo, 11 | Instant occurredAt) implements PublishedEvent, PrivateEvent { 12 | 13 | public ProjectAllocationScheduled(ProjectAllocationsId projectId, TimeSlot fromTo, Instant occuredAt) { 14 | this(UUID.randomUUID(), projectId, fromTo, occuredAt); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/ProjectAllocationsDemandsScheduled.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | 4 | import domaindrivers.smartschedule.shared.PrivateEvent; 5 | 6 | import java.time.Instant; 7 | import java.util.UUID; 8 | 9 | record ProjectAllocationsDemandsScheduled(UUID uuid, ProjectAllocationsId projectId, Demands missingDemands, Instant occurredAt) implements PrivateEvent { 10 | 11 | ProjectAllocationsDemandsScheduled(ProjectAllocationsId projectId, Demands missingDemands, Instant occuredAt) { 12 | this(UUID.randomUUID(), projectId, missingDemands, occuredAt); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/ProjectAllocationsId.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | import jakarta.persistence.Embeddable; 4 | 5 | import java.io.Serializable; 6 | import java.util.Objects; 7 | import java.util.UUID; 8 | 9 | @Embeddable 10 | public class ProjectAllocationsId implements Serializable { 11 | 12 | public static ProjectAllocationsId newOne() { 13 | return new ProjectAllocationsId(UUID.randomUUID()); 14 | } 15 | 16 | private UUID projectAllocationsId; 17 | 18 | public ProjectAllocationsId(UUID uuid) { 19 | this.projectAllocationsId = uuid; 20 | } 21 | 22 | public ProjectAllocationsId() { 23 | } 24 | 25 | public UUID id() { 26 | return projectAllocationsId; 27 | } 28 | 29 | @Override 30 | public boolean equals(Object o) { 31 | if (this == o) return true; 32 | if (o == null || getClass() != o.getClass()) return false; 33 | ProjectAllocationsId projectId1 = (ProjectAllocationsId) o; 34 | return Objects.equals(projectAllocationsId, projectId1.projectAllocationsId); 35 | } 36 | 37 | @Override 38 | public int hashCode() { 39 | return Objects.hash(projectAllocationsId); 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/ProjectAllocationsRepository.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | 4 | import java.time.Instant; 5 | import java.util.List; 6 | import java.util.Optional; 7 | import java.util.Set; 8 | 9 | 10 | interface ProjectAllocationsRepository { 11 | 12 | List findAllContainingDate(Instant when); 13 | 14 | Optional findById(ProjectAllocationsId projectId); 15 | 16 | ProjectAllocations save(ProjectAllocations project); 17 | 18 | List findAllById(Set projectIds); 19 | 20 | List findAll(); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/ProjectsAllocationsSummary.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 4 | 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.stream.Collectors; 8 | 9 | public record ProjectsAllocationsSummary( 10 | Map timeSlots, 11 | Map projectAllocations, 12 | Map demands) { 13 | 14 | static ProjectsAllocationsSummary of(List allProjectAllocations) { 15 | Map timeSlots = 16 | allProjectAllocations 17 | .stream() 18 | .filter(ProjectAllocations::hasTimeSlot) 19 | .collect(Collectors.toMap( 20 | ProjectAllocations::id, 21 | ProjectAllocations::timeSlot)); 22 | Map allocations = 23 | allProjectAllocations 24 | .stream() 25 | .collect(Collectors.toMap( 26 | ProjectAllocations::id, 27 | ProjectAllocations::allocations 28 | )); 29 | Map demands = 30 | allProjectAllocations 31 | .stream() 32 | .collect(Collectors.toMap( 33 | ProjectAllocations::id, 34 | ProjectAllocations::demands)); 35 | return new ProjectsAllocationsSummary(timeSlots, allocations, demands); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/PublishMissingDemandsService.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | import domaindrivers.smartschedule.shared.EventsPublisher; 4 | import org.springframework.scheduling.annotation.Scheduled; 5 | 6 | import java.time.Clock; 7 | import java.time.Instant; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | import static java.util.stream.Collectors.toMap; 12 | 13 | class PublishMissingDemandsService { 14 | 15 | private final ProjectAllocationsRepository projectAllocationsRepository; 16 | private final CreateHourlyDemandsSummaryService createHourlyDemandsSummaryService; 17 | private final EventsPublisher eventsPublisher; 18 | private final Clock clock; 19 | 20 | PublishMissingDemandsService(ProjectAllocationsRepository projectAllocationsRepository, CreateHourlyDemandsSummaryService createHourlyDemandsSummaryService, EventsPublisher eventsPublisher, Clock clock) { 21 | this.projectAllocationsRepository = projectAllocationsRepository; 22 | this.createHourlyDemandsSummaryService = createHourlyDemandsSummaryService; 23 | this.eventsPublisher = eventsPublisher; 24 | this.clock = clock; 25 | } 26 | 27 | @Scheduled(cron = "@hourly") 28 | void publish() { 29 | Instant when = clock.instant(); 30 | List projectAllocations = 31 | projectAllocationsRepository.findAllContainingDate(when); 32 | NotSatisfiedDemands missingDemands = createHourlyDemandsSummaryService.create(projectAllocations, when); 33 | //add metadata to event 34 | //if needed call EventStore and translate multiple private events to a new published event 35 | eventsPublisher.publish(missingDemands); 36 | } 37 | 38 | 39 | } 40 | 41 | class CreateHourlyDemandsSummaryService { 42 | 43 | NotSatisfiedDemands create(List projectAllocations, Instant when) { 44 | Map missingDemands = 45 | projectAllocations 46 | .stream() 47 | .filter(ProjectAllocations::hasTimeSlot) 48 | .collect(toMap(ProjectAllocations::id, ProjectAllocations::missingDemands)); 49 | return new NotSatisfiedDemands(missingDemands, when); 50 | } 51 | } 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/capabilityscheduling/AllocatableCapabilitiesSummary.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.capabilityscheduling; 2 | 3 | import java.util.List; 4 | 5 | public record AllocatableCapabilitiesSummary(List all) { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/capabilityscheduling/AllocatableCapability.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.capabilityscheduling; 2 | 3 | 4 | import domaindrivers.smartschedule.shared.CapabilitySelector; 5 | import domaindrivers.smartschedule.shared.capability.Capability; 6 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 7 | import io.hypersistence.utils.hibernate.type.json.JsonType; 8 | import jakarta.persistence.*; 9 | import org.hibernate.annotations.Type; 10 | 11 | import java.util.Set; 12 | 13 | 14 | @Entity(name = "allocatable_capabilities") 15 | class AllocatableCapability { 16 | 17 | @EmbeddedId 18 | private AllocatableCapabilityId id = AllocatableCapabilityId.newOne(); 19 | 20 | @Type(JsonType.class) 21 | @Column(columnDefinition = "jsonb") 22 | private CapabilitySelector possibleCapabilities; 23 | 24 | @Embedded 25 | private AllocatableResourceId resourceId; 26 | 27 | @Embedded 28 | @AttributeOverrides({ 29 | @AttributeOverride(name = "from", column = @Column(name = "from_date")), 30 | @AttributeOverride(name = "to", column = @Column(name = "to_date")) 31 | }) 32 | private TimeSlot timeSlot; 33 | 34 | AllocatableCapability(AllocatableResourceId resourceId, CapabilitySelector possibleCapabilities, TimeSlot timeSlot) { 35 | this.resourceId = resourceId; 36 | this.possibleCapabilities = possibleCapabilities; 37 | this.timeSlot = timeSlot; 38 | } 39 | 40 | public AllocatableCapability() { 41 | } 42 | 43 | AllocatableCapabilityId id() { 44 | return id; 45 | } 46 | 47 | boolean canPerform(Set capabilities) { 48 | return capabilities().canPerform(capabilities); 49 | } 50 | 51 | AllocatableResourceId resourceId() { 52 | return resourceId; 53 | } 54 | 55 | TimeSlot slot() { 56 | return timeSlot; 57 | } 58 | 59 | CapabilitySelector capabilities() { 60 | return possibleCapabilities; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/capabilityscheduling/AllocatableCapabilityId.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.capabilityscheduling; 2 | 3 | import domaindrivers.smartschedule.availability.ResourceId; 4 | import jakarta.persistence.Embeddable; 5 | 6 | import java.io.Serializable; 7 | import java.util.Objects; 8 | import java.util.UUID; 9 | 10 | @Embeddable 11 | public class AllocatableCapabilityId implements Serializable { 12 | 13 | public static AllocatableCapabilityId newOne() { 14 | return new AllocatableCapabilityId(UUID.randomUUID()); 15 | } 16 | 17 | private UUID id; 18 | 19 | public AllocatableCapabilityId(UUID uuid) { 20 | this.id = uuid; 21 | } 22 | 23 | public AllocatableCapabilityId() { 24 | } 25 | 26 | public static AllocatableCapabilityId none() { 27 | return new AllocatableCapabilityId(); 28 | } 29 | 30 | public UUID getId() { 31 | return id; 32 | } 33 | 34 | public ResourceId toAvailabilityResourceId() { 35 | return ResourceId.of(id.toString()); 36 | } 37 | 38 | public static AllocatableCapabilityId from(ResourceId resourceId) { 39 | return new AllocatableCapabilityId(resourceId.getId()); 40 | } 41 | 42 | @Override 43 | public boolean equals(Object o) { 44 | if (this == o) return true; 45 | if (o == null || getClass() != o.getClass()) return false; 46 | AllocatableCapabilityId that = (AllocatableCapabilityId) o; 47 | return Objects.equals(id, that.id); 48 | } 49 | 50 | @Override 51 | public int hashCode() { 52 | return Objects.hash(id); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/capabilityscheduling/AllocatableCapabilityRepository.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.capabilityscheduling; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.Query; 5 | 6 | import java.time.Instant; 7 | import java.util.Arrays; 8 | import java.util.List; 9 | import java.util.Optional; 10 | import java.util.UUID; 11 | 12 | interface AllocatableCapabilityRepository extends JpaRepository { 13 | 14 | @Query(value = "SELECT ac.*\n" + 15 | "FROM allocatable_capabilities ac \n" + 16 | "CROSS JOIN LATERAL jsonb_array_elements(ac.possible_capabilities -> 'capabilities') AS o(obj)\n" + 17 | "WHERE o.obj ->> 'name' = ?1 AND o.obj ->> 'type' = ?2 AND ac.from_date <= ?3 and ac.to_date >= ?4", nativeQuery = true) 18 | List findByCapabilityWithin(String name, String type, Instant from, Instant to); 19 | 20 | 21 | @Query(value = "SELECT ac.*\n" + 22 | "FROM allocatable_capabilities ac \n" + 23 | "CROSS JOIN LATERAL jsonb_array_elements(ac.possible_capabilities -> 'capabilities') AS o(obj)\n" + 24 | "WHERE ac.resource_id = ?1 AND o.obj ->> 'name' = ?2 AND o.obj ->> 'type' = ?3 AND ac.from_date = ?4 and ac.to_date = ?5", nativeQuery = true) 25 | Optional findByResourceIdAndCapabilityAndTimeSlot(UUID allocatableResourceId, String name, String type, Instant from, Instant to); 26 | 27 | @Query(value = "SELECT ac.*\n" + 28 | "FROM allocatable_capabilities ac \n" + 29 | "WHERE ac.resource_id = ?1 AND ac.from_date = ?2 and ac.to_date = ?3", nativeQuery = true) 30 | List findByResourceIdAndTimeSlot(UUID allocatableResourceId, Instant from, Instant to); 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/capabilityscheduling/AllocatableCapabilitySummary.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.capabilityscheduling; 2 | 3 | import domaindrivers.smartschedule.shared.CapabilitySelector; 4 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 5 | 6 | public record AllocatableCapabilitySummary(AllocatableCapabilityId id, AllocatableResourceId allocatableResourceId, 7 | CapabilitySelector capabilities, TimeSlot timeSlot) { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/capabilityscheduling/AllocatableResourceId.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.capabilityscheduling; 2 | 3 | import jakarta.persistence.Embeddable; 4 | 5 | import java.io.Serializable; 6 | import java.util.Objects; 7 | import java.util.UUID; 8 | 9 | @Embeddable 10 | public class AllocatableResourceId implements Serializable { 11 | 12 | private UUID resourceId; 13 | 14 | public AllocatableResourceId(UUID uuid) { 15 | this.resourceId = uuid; 16 | } 17 | 18 | public AllocatableResourceId() { 19 | 20 | } 21 | 22 | public static AllocatableResourceId newOne() { 23 | return new AllocatableResourceId(UUID.randomUUID()); 24 | } 25 | 26 | public UUID id() { 27 | return resourceId; 28 | } 29 | 30 | @Override 31 | public boolean equals(Object o) { 32 | if (this == o) return true; 33 | if (o == null || getClass() != o.getClass()) return false; 34 | AllocatableResourceId that = (AllocatableResourceId) o; 35 | return Objects.equals(resourceId, that.resourceId); 36 | } 37 | 38 | @Override 39 | public int hashCode() { 40 | return Objects.hash(resourceId); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/capabilityscheduling/CapabilityPlanningConfiguration.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.capabilityscheduling; 2 | 3 | import domaindrivers.smartschedule.availability.AvailabilityFacade; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | @Configuration 8 | class CapabilityPlanningConfiguration { 9 | 10 | @Bean 11 | CapabilityScheduler capabilityScheduler(AvailabilityFacade availabilityFacade, AllocatableCapabilityRepository allocatableResourceRepository) { 12 | return new CapabilityScheduler(availabilityFacade, allocatableResourceRepository); 13 | } 14 | 15 | @Bean 16 | CapabilityFinder capabilityFinder(AvailabilityFacade availabilityFacade, AllocatableCapabilityRepository allocatableResourceRepository) { 17 | return new CapabilityFinder(availabilityFacade, allocatableResourceRepository); 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/capabilityscheduling/legacyacl/EmployeeCreatedInLegacySystemMessageHandler.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.capabilityscheduling.legacyacl; 2 | 3 | import domaindrivers.smartschedule.allocation.capabilityscheduling.AllocatableResourceId; 4 | import domaindrivers.smartschedule.allocation.capabilityscheduling.CapabilityScheduler; 5 | import domaindrivers.smartschedule.shared.CapabilitySelector; 6 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 7 | 8 | import java.util.List; 9 | import java.util.UUID; 10 | 11 | public class EmployeeCreatedInLegacySystemMessageHandler { 12 | 13 | private final CapabilityScheduler capabilityScheduler; 14 | 15 | public EmployeeCreatedInLegacySystemMessageHandler(CapabilityScheduler capabilityScheduler) { 16 | this.capabilityScheduler = capabilityScheduler; 17 | } 18 | 19 | //subscribe to message bus 20 | //StreamListener to (message_bus) 21 | public void handle(EmployeeDataFromLegacyEsbMessage message) { 22 | AllocatableResourceId allocatableResourceId = new AllocatableResourceId(message.resourceId); 23 | List capabilitySelectors = new TranslateToCapabilitySelector().translate(message); 24 | capabilityScheduler.scheduleResourceCapabilitiesForPeriod(allocatableResourceId, capabilitySelectors, message.timeSlot); 25 | } 26 | 27 | } 28 | class EmployeeDataFromLegacyEsbMessage { 29 | UUID resourceId; 30 | List> skillsPerformedTogether; 31 | List exclusiveSkills; 32 | List permissions; 33 | TimeSlot timeSlot; 34 | 35 | EmployeeDataFromLegacyEsbMessage(UUID resourceId, List> skillsPerformedTogether, List exclusiveSkills, List permissions, TimeSlot timeSlot) { 36 | this.resourceId = resourceId; 37 | this.skillsPerformedTogether = skillsPerformedTogether; 38 | this.exclusiveSkills = exclusiveSkills; 39 | this.permissions = permissions; 40 | this.timeSlot = timeSlot; 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/capabilityscheduling/legacyacl/TranslateToCapabilitySelector.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.capabilityscheduling.legacyacl; 2 | 3 | import domaindrivers.smartschedule.shared.CapabilitySelector; 4 | import domaindrivers.smartschedule.shared.capability.Capability; 5 | 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.stream.Collectors; 9 | import java.util.stream.IntStream; 10 | import java.util.stream.Stream; 11 | 12 | import static domaindrivers.smartschedule.shared.capability.Capability.permission; 13 | 14 | class TranslateToCapabilitySelector { 15 | 16 | public List translate(EmployeeDataFromLegacyEsbMessage message) { 17 | List employeeSkills = message.skillsPerformedTogether 18 | .stream() 19 | .map(skills -> CapabilitySelector.canPerformAllAtTheTime( 20 | skills.stream().map(Capability::skill).collect(Collectors.toSet()))) 21 | .toList(); 22 | List employeeExclusiveSkills = message.exclusiveSkills 23 | .stream() 24 | .map(skill -> CapabilitySelector.canJustPerform( 25 | Capability.skill(skill))) 26 | .toList(); 27 | List employeePermissions = message.permissions 28 | .stream() 29 | .map(this::multiplePermission) 30 | .flatMap(List::stream) 31 | .toList(); 32 | //schedule or rewrite if exists; 33 | return Stream.concat(Stream.concat(employeeSkills.stream(), employeeExclusiveSkills.stream()), employeePermissions.stream()).toList(); 34 | } 35 | 36 | private List multiplePermission(String permissionLegacyCode) { 37 | List parts = Arrays 38 | .stream(permissionLegacyCode 39 | .split("<>")) 40 | .toList(); 41 | String permission = parts.get(0); 42 | int times = Integer.parseInt(parts.get(1)); 43 | return IntStream 44 | .range(0, times) 45 | .mapToObj(value -> CapabilitySelector.canJustPerform(permission(permission))) 46 | .toList(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/cashflow/CashFlowConfiguration.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.cashflow; 2 | 3 | 4 | import domaindrivers.smartschedule.shared.EventsPublisher; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | import java.time.Clock; 9 | 10 | @Configuration 11 | class CashFlowConfiguration { 12 | 13 | @Bean 14 | CashFlowFacade cashFlowFacade(CashflowRepository cashflowRepository, EventsPublisher eventsPublisher, Clock clock) { 15 | return new CashFlowFacade(cashflowRepository, eventsPublisher, clock); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/cashflow/CashFlowFacade.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.cashflow; 2 | 3 | import domaindrivers.smartschedule.allocation.ProjectAllocationsId; 4 | import domaindrivers.smartschedule.shared.EventsPublisher; 5 | 6 | import java.time.Clock; 7 | import java.util.Map; 8 | 9 | import static java.util.stream.Collectors.toMap; 10 | 11 | 12 | public class CashFlowFacade { 13 | 14 | private final CashflowRepository cashflowRepository; 15 | private final EventsPublisher eventsPublisher; 16 | private final Clock clock; 17 | 18 | public CashFlowFacade(CashflowRepository cashflowRepository, EventsPublisher eventsPublisher, Clock clock) { 19 | this.cashflowRepository = cashflowRepository; 20 | this.eventsPublisher = eventsPublisher; 21 | this.clock = clock; 22 | } 23 | 24 | public void addIncomeAndCost(ProjectAllocationsId projectId, Income income, Cost cost) { 25 | Cashflow cashflow = cashflowRepository.findById(projectId) 26 | .orElseGet(() -> new Cashflow(projectId)); 27 | cashflow.update(income, cost); 28 | eventsPublisher.publish(new EarningsRecalculated(projectId, cashflow.earnings(), clock.instant())); 29 | cashflowRepository.save(cashflow); 30 | } 31 | 32 | public Earnings find(ProjectAllocationsId projectId) { 33 | Cashflow byId = cashflowRepository.findById(projectId).orElseThrow(); 34 | return byId.earnings(); 35 | } 36 | 37 | public Map findAllEarnings() { 38 | return cashflowRepository 39 | .findAll() 40 | .stream() 41 | .collect(toMap(cashflow -> cashflow.projectId, Cashflow::earnings)); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/cashflow/Cashflow.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.cashflow; 2 | 3 | 4 | import domaindrivers.smartschedule.allocation.ProjectAllocationsId; 5 | import jakarta.persistence.Embedded; 6 | import jakarta.persistence.EmbeddedId; 7 | import jakarta.persistence.Entity; 8 | 9 | @Entity(name = "cashflows") 10 | class Cashflow { 11 | 12 | @EmbeddedId 13 | ProjectAllocationsId projectId; 14 | 15 | @Embedded 16 | Income income; 17 | 18 | @Embedded 19 | Cost cost; 20 | 21 | public Cashflow() { 22 | } 23 | 24 | Cashflow(ProjectAllocationsId projectId) { 25 | this.projectId = projectId; 26 | } 27 | 28 | Earnings earnings() { 29 | return income.minus(cost); 30 | } 31 | 32 | public void update(Income income, Cost cost) { 33 | this.income = income; 34 | this.cost = cost; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/cashflow/CashflowRepository.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.cashflow; 2 | 3 | 4 | import domaindrivers.smartschedule.allocation.ProjectAllocationsId; 5 | 6 | import java.util.List; 7 | import java.util.Optional; 8 | 9 | 10 | interface CashflowRepository { 11 | 12 | Optional findById(ProjectAllocationsId projectId); 13 | 14 | Cashflow save(Cashflow cashflow); 15 | 16 | List findAll(); 17 | } -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/cashflow/Cost.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.cashflow; 2 | 3 | import jakarta.persistence.Embeddable; 4 | 5 | import java.math.BigDecimal; 6 | import java.util.Objects; 7 | 8 | @Embeddable 9 | public class Cost { 10 | 11 | public static Cost of(int integer) { 12 | return new Cost(BigDecimal.valueOf(integer)); 13 | } 14 | 15 | private BigDecimal cost; 16 | 17 | Cost(BigDecimal value) { 18 | this.cost = value; 19 | } 20 | 21 | public Cost() { 22 | } 23 | 24 | @Override 25 | public boolean equals(Object o) { 26 | if (this == o) return true; 27 | if (o == null || getClass() != o.getClass()) return false; 28 | Cost cost1 = (Cost) o; 29 | return Objects.equals(cost, cost1.cost); 30 | } 31 | 32 | @Override 33 | public int hashCode() { 34 | return Objects.hash(cost); 35 | } 36 | 37 | BigDecimal value() { 38 | return cost; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/cashflow/Earnings.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.cashflow; 2 | 3 | import jakarta.persistence.Embeddable; 4 | 5 | import java.math.BigDecimal; 6 | import java.util.Objects; 7 | 8 | @Embeddable 9 | public class Earnings { 10 | 11 | public static Earnings of(int integer) { 12 | return new Earnings(BigDecimal.valueOf(integer)); 13 | } 14 | 15 | private BigDecimal earnings; 16 | 17 | Earnings(BigDecimal value) { 18 | this.earnings = value; 19 | } 20 | 21 | public Earnings() { 22 | } 23 | 24 | @Override 25 | public boolean equals(Object o) { 26 | if (this == o) return true; 27 | if (o == null || getClass() != o.getClass()) return false; 28 | Earnings earnings1 = (Earnings) o; 29 | return Objects.equals(earnings, earnings1.earnings); 30 | } 31 | 32 | @Override 33 | public int hashCode() { 34 | return Objects.hash(earnings); 35 | } 36 | 37 | public BigDecimal toBigDecimal() { 38 | return earnings; 39 | } 40 | 41 | public boolean greaterThan(Earnings value) { 42 | return earnings.compareTo(value.toBigDecimal()) > 0; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/cashflow/EarningsRecalculated.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.cashflow; 2 | 3 | import domaindrivers.smartschedule.allocation.ProjectAllocationsId; 4 | import domaindrivers.smartschedule.shared.PrivateEvent; 5 | import domaindrivers.smartschedule.shared.PublishedEvent; 6 | 7 | import java.time.Instant; 8 | import java.util.UUID; 9 | 10 | public record EarningsRecalculated(UUID uuid, ProjectAllocationsId projectId, Earnings earnings, Instant occurredAt) implements PublishedEvent { 11 | 12 | public EarningsRecalculated(ProjectAllocationsId projectId, Earnings earnings, Instant instant) { 13 | this(UUID.randomUUID(), projectId, earnings, instant); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/cashflow/Income.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.cashflow; 2 | 3 | import jakarta.persistence.Embeddable; 4 | 5 | import java.math.BigDecimal; 6 | import java.util.Objects; 7 | 8 | @Embeddable 9 | public class Income { 10 | static Income of(BigDecimal bigDecimal) { 11 | return new Income(bigDecimal); 12 | } 13 | 14 | public static Income of(int integer) { 15 | return new Income(BigDecimal.valueOf(integer)); 16 | } 17 | 18 | private BigDecimal income; 19 | 20 | Income(BigDecimal value) { 21 | this.income = value; 22 | } 23 | 24 | public Income() { 25 | } 26 | 27 | @Override 28 | public boolean equals(Object o) { 29 | if (this == o) return true; 30 | if (o == null || getClass() != o.getClass()) return false; 31 | Income earnings1 = (Income) o; 32 | return Objects.equals(income, earnings1.income); 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | return Objects.hash(income); 38 | } 39 | 40 | public Earnings minus(Cost estimatedCosts) { 41 | return new Earnings(income.subtract(estimatedCosts.value())); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/allocation/cashflow/JpaCashflowRepository.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.cashflow; 2 | 3 | 4 | import domaindrivers.smartschedule.allocation.ProjectAllocationsId; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | 8 | interface JpaCashflowRepository extends CashflowRepository, JpaRepository { 9 | 10 | 11 | } -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/availability/AvailabilityConfiguration.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.availability; 2 | 3 | 4 | import domaindrivers.smartschedule.shared.EventsPublisher; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.jdbc.core.JdbcTemplate; 8 | 9 | import java.time.Clock; 10 | 11 | @Configuration 12 | public class AvailabilityConfiguration { 13 | 14 | @Bean 15 | AvailabilityFacade availabilityFacade(JdbcTemplate jdbcTemplate, EventsPublisher eventsPublisher, Clock clock) { 16 | return new AvailabilityFacade(new ResourceAvailabilityRepository(jdbcTemplate), new ResourceAvailabilityReadModel(jdbcTemplate), eventsPublisher, clock); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/availability/Blockade.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.availability; 2 | 3 | record Blockade(Owner takenBy, boolean disabled) { 4 | 5 | static Blockade none() { 6 | return new Blockade(Owner.none(), false); 7 | } 8 | 9 | public static Blockade disabledBy(Owner owner) { 10 | return new Blockade(owner, true); 11 | } 12 | 13 | public static Blockade ownedBy(Owner owner) { 14 | return new Blockade(owner, false); 15 | } 16 | 17 | boolean canBeTakenBy(Owner requester) { 18 | return takenBy.byNone() || takenBy.equals(requester); 19 | } 20 | 21 | boolean isDisabledBy(Owner owner) { 22 | return disabled && owner.equals(this.takenBy); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/availability/Calendar.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.availability; 2 | 3 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 4 | 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | public record Calendar(ResourceId resourceId, 10 | Map> calendar) { 11 | 12 | public static Calendar withAvailableSlots(ResourceId resourceId, TimeSlot... availableSlots) { 13 | return new Calendar(resourceId, Map.of(Owner.none(), List.of(availableSlots))); 14 | } 15 | 16 | static Calendar empty(ResourceId resourceId) { 17 | return new Calendar(resourceId, new HashMap<>()); 18 | } 19 | 20 | public List availableSlots() { 21 | return calendar.getOrDefault(Owner.none(), List.of()); 22 | } 23 | 24 | public List takenBy(Owner requester) { 25 | return calendar.getOrDefault(requester, List.of()); 26 | } 27 | 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/availability/Calendars.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.availability; 2 | 3 | import java.util.*; 4 | import java.util.stream.Collectors; 5 | 6 | public record Calendars(Map calendars) { 7 | 8 | public static Calendars of(Calendar... calendars) { 9 | Map collect = 10 | Arrays.stream(calendars) 11 | .collect(Collectors.toMap(Calendar::resourceId, calendar -> calendar)); 12 | return new Calendars(collect); 13 | } 14 | 15 | public Calendar get(ResourceId resourceId) { 16 | return calendars.getOrDefault(resourceId, Calendar.empty(resourceId)); 17 | } 18 | } 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/availability/Owner.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.availability; 2 | 3 | import java.util.UUID; 4 | 5 | public record Owner(UUID owner) { 6 | 7 | static Owner none() { 8 | return new Owner(null); 9 | } 10 | 11 | public static Owner newOne() { 12 | return new Owner(UUID.randomUUID()); 13 | } 14 | 15 | public static Owner of(UUID id) { 16 | return new Owner(id); 17 | } 18 | 19 | public boolean byNone() { 20 | return none().equals(this); 21 | } 22 | 23 | public UUID id() { 24 | return owner; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/availability/ResourceAvailabilityId.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.availability; 2 | 3 | import java.util.UUID; 4 | 5 | public record ResourceAvailabilityId(UUID id) { 6 | 7 | static ResourceAvailabilityId none() { 8 | return new ResourceAvailabilityId(null); 9 | } 10 | 11 | public static ResourceAvailabilityId newOne() { 12 | return new ResourceAvailabilityId(UUID.randomUUID()); 13 | } 14 | 15 | public static ResourceAvailabilityId of(String id) { 16 | if (id == null) { 17 | return none(); 18 | } 19 | return new ResourceAvailabilityId(UUID.fromString(id)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/availability/ResourceId.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.availability; 2 | 3 | import jakarta.persistence.Embeddable; 4 | 5 | import java.io.Serializable; 6 | import java.util.Objects; 7 | import java.util.UUID; 8 | 9 | @Embeddable 10 | public class ResourceId implements Serializable { 11 | 12 | public static ResourceId newOne() { 13 | return new ResourceId(UUID.randomUUID()); 14 | } 15 | 16 | private UUID id; 17 | 18 | public ResourceId(UUID uuid) { 19 | this.id = uuid; 20 | } 21 | 22 | public ResourceId() { 23 | } 24 | 25 | public static ResourceId none() { 26 | return new ResourceId(null); 27 | } 28 | 29 | public static ResourceId of(String id) { 30 | if (id == null) { 31 | return none(); 32 | } 33 | return new ResourceId(UUID.fromString(id)); 34 | } 35 | 36 | public UUID getId() { 37 | return id; 38 | } 39 | 40 | @Override 41 | public boolean equals(Object o) { 42 | if (this == o) return true; 43 | if (o == null || getClass() != o.getClass()) return false; 44 | ResourceId resourceId = (ResourceId) o; 45 | return Objects.equals(resourceId.id, this.id); 46 | } 47 | 48 | @Override 49 | public int hashCode() { 50 | return Objects.hash(id); 51 | } 52 | 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/availability/ResourceTakenOver.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.availability; 2 | 3 | import domaindrivers.smartschedule.shared.PrivateEvent; 4 | import domaindrivers.smartschedule.shared.PublishedEvent; 5 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 6 | 7 | import java.time.Instant; 8 | import java.util.Set; 9 | import java.util.UUID; 10 | 11 | public record ResourceTakenOver(UUID eventId, ResourceId resourceId, Set previousOwners, TimeSlot slot, Instant occurredAt) implements PublishedEvent { 12 | 13 | public ResourceTakenOver(ResourceId resourceId, Set previousOwners, TimeSlot slot, Instant occuredAt) { 14 | this(UUID.randomUUID(), resourceId, previousOwners, slot, occuredAt); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/availability/segment/SegmentInMinutes.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.availability.segment; 2 | 3 | 4 | public record SegmentInMinutes(int value) { 5 | 6 | public static SegmentInMinutes of(int minutes, int slotDurationInMinutes) { 7 | if (minutes <= 0) { 8 | throw new IllegalArgumentException("SegmentInMinutesDuration must be positive"); 9 | } 10 | if (minutes < slotDurationInMinutes) { 11 | throw new IllegalArgumentException("SegmentInMinutesDuration must be at least " + slotDurationInMinutes + " minutes"); 12 | } 13 | if (minutes % slotDurationInMinutes != 0) { 14 | throw new IllegalArgumentException("SegmentInMinutesDuration must be a multiple of " + slotDurationInMinutes + " minutes"); 15 | } 16 | return new SegmentInMinutes(minutes); 17 | } 18 | 19 | public static SegmentInMinutes of(int minutes) { 20 | return of(minutes, Segments.DEFAULT_SEGMENT_DURATION_IN_MINUTES); 21 | } 22 | 23 | public static SegmentInMinutes defaultSegment() { 24 | return of(Segments.DEFAULT_SEGMENT_DURATION_IN_MINUTES); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/availability/segment/Segments.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.availability.segment; 2 | 3 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 4 | 5 | import java.util.List; 6 | 7 | public class Segments { 8 | 9 | public static final int DEFAULT_SEGMENT_DURATION_IN_MINUTES = 60; 10 | 11 | public static List split(TimeSlot timeSlot, SegmentInMinutes unit) { 12 | TimeSlot normalizedSlot = normalizeToSegmentBoundaries(timeSlot, unit); 13 | return new SlotToSegments().apply(normalizedSlot, unit); 14 | } 15 | 16 | public static TimeSlot normalizeToSegmentBoundaries(TimeSlot timeSlot, SegmentInMinutes unit) { 17 | return new SlotToNormalizedSlot().apply(timeSlot, unit); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/availability/segment/SlotToNormalizedSlot.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.availability.segment; 2 | 3 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 4 | 5 | import java.time.Instant; 6 | import java.time.temporal.ChronoUnit; 7 | import java.util.function.BiFunction; 8 | 9 | class SlotToNormalizedSlot implements BiFunction { 10 | 11 | @Override 12 | public TimeSlot apply(TimeSlot timeSlot, SegmentInMinutes segmentInMinutes) { 13 | 14 | int segmentInMinutesDuration = segmentInMinutes.value(); 15 | Instant segmentStart = normalizeStart(timeSlot.from(), segmentInMinutesDuration); 16 | Instant segmentEnd = normalizeEnd(timeSlot.to(), segmentInMinutesDuration); 17 | TimeSlot normalized = new TimeSlot(segmentStart, segmentEnd); 18 | TimeSlot minimalSegment = new TimeSlot(segmentStart, segmentStart.plus(segmentInMinutes.value(), ChronoUnit.MINUTES)); 19 | if (normalized.within(minimalSegment)) { 20 | return minimalSegment; 21 | } 22 | return normalized; 23 | } 24 | 25 | private Instant normalizeEnd(Instant initialEnd, int segmentInMinutesDuration) { 26 | Instant closestSegmentEnd = initialEnd.truncatedTo(ChronoUnit.HOURS); 27 | while (initialEnd.isAfter(closestSegmentEnd)) { 28 | closestSegmentEnd = closestSegmentEnd.plus(segmentInMinutesDuration, ChronoUnit.MINUTES); 29 | } 30 | return closestSegmentEnd; 31 | } 32 | 33 | private Instant normalizeStart(Instant initialStart, int segmentInMinutesDuration) { 34 | Instant closestSegmentStart = initialStart.truncatedTo(ChronoUnit.HOURS); 35 | if (closestSegmentStart.plus(segmentInMinutesDuration, ChronoUnit.MINUTES).isAfter(initialStart)) { 36 | return closestSegmentStart; 37 | } 38 | while (closestSegmentStart.isBefore(initialStart)) { 39 | closestSegmentStart = closestSegmentStart.plus(segmentInMinutesDuration, ChronoUnit.MINUTES); 40 | } 41 | return closestSegmentStart; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/availability/segment/SlotToSegments.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.availability.segment; 2 | 3 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 4 | 5 | import java.time.Duration; 6 | import java.time.Instant; 7 | import java.time.temporal.ChronoUnit; 8 | import java.util.List; 9 | import java.util.function.BiFunction; 10 | import java.util.stream.Stream; 11 | 12 | class SlotToSegments implements BiFunction> { 13 | 14 | @Override 15 | public List apply(TimeSlot timeSlot, SegmentInMinutes duration) { 16 | TimeSlot minimalSegment = new TimeSlot(timeSlot.from(), timeSlot.from().plus(duration.value(), ChronoUnit.MINUTES)); 17 | if (timeSlot.within(minimalSegment)) { 18 | return List.of(minimalSegment); 19 | } 20 | int segmentInMinutesDuration = duration.value(); 21 | long numberOfSegments = calculateNumberOfSegments(timeSlot, segmentInMinutesDuration); 22 | return Stream 23 | .iterate(timeSlot.from(), currentStart -> currentStart.plus(segmentInMinutesDuration, ChronoUnit.MINUTES)) 24 | .limit(numberOfSegments) 25 | .map(currentStart -> new TimeSlot(currentStart, calculateEnd(segmentInMinutesDuration, currentStart, timeSlot.to()))) 26 | .toList(); 27 | } 28 | 29 | private long calculateNumberOfSegments(TimeSlot timeSlot, int segmentInMinutesDuration) { 30 | return (long) Math.ceil((double) Duration.between(timeSlot.from(), timeSlot.to()).toMinutes() / segmentInMinutesDuration); 31 | } 32 | 33 | private Instant calculateEnd(int segmentInMinutesDuration, Instant currentStart, Instant initialEnd) { 34 | Instant segmentEnd = currentStart.plus(segmentInMinutesDuration, ChronoUnit.MINUTES); 35 | if (initialEnd.isBefore(segmentEnd)) { 36 | return initialEnd; 37 | } 38 | return segmentEnd; 39 | } 40 | 41 | 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/optimization/CapacityDimension.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.optimization; 2 | 3 | public interface CapacityDimension { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/optimization/Item.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.optimization; 2 | 3 | public record Item(String name, double value, TotalWeight totalWeight) { 4 | 5 | boolean isWeightZero() { 6 | return totalWeight().components().isEmpty(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/optimization/Result.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.optimization; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import java.util.Set; 6 | 7 | public record Result(Double profit, List chosenItems, Map> itemToCapacities) { 8 | @Override 9 | public String toString() { 10 | return "Result{" + 11 | "profit=" + profit + 12 | ", chosenItems=" + chosenItems + 13 | '}'; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/optimization/TotalCapacity.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.optimization; 2 | 3 | 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | 9 | public record TotalCapacity(List capacities) { 10 | 11 | public static TotalCapacity of(CapacityDimension... capacities) { 12 | return new TotalCapacity(Arrays.asList(capacities)); 13 | } 14 | 15 | public static TotalCapacity of(List capacities) { 16 | return new TotalCapacity(capacities); 17 | } 18 | 19 | static TotalCapacity zero() { 20 | return new TotalCapacity(List.of()); 21 | } 22 | 23 | int size() { 24 | return capacities.size(); 25 | } 26 | 27 | public List capacities() { 28 | return new ArrayList<>(capacities); 29 | } 30 | 31 | public TotalCapacity add(List capacities) { 32 | List newCapacities = new ArrayList<>(this.capacities); 33 | newCapacities.addAll(capacities); 34 | return new TotalCapacity(new ArrayList<>(newCapacities)); 35 | } 36 | } 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/optimization/TotalWeight.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.optimization; 2 | 3 | 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | public record TotalWeight(List components) { 9 | 10 | static TotalWeight zero() { 11 | return new TotalWeight(List.of()); 12 | } 13 | 14 | static TotalWeight of(WeightDimension... components) { 15 | return new TotalWeight(Arrays.asList(components)); 16 | } 17 | 18 | public List components() { 19 | return new ArrayList<>(components); 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/optimization/WeightDimension.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.optimization; 2 | 3 | public interface WeightDimension { 4 | boolean isSatisfiedBy(T capacityDimension); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/CapabilitiesDemanded.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning; 2 | 3 | import domaindrivers.smartschedule.shared.PublishedEvent; 4 | 5 | import java.time.Instant; 6 | import java.util.UUID; 7 | 8 | public record CapabilitiesDemanded(UUID uuid, ProjectId projectId, Demands demands, Instant occurredAt) implements PublishedEvent { 9 | 10 | public CapabilitiesDemanded(ProjectId projectId, Demands demands, Instant occuredAt) { 11 | this(UUID.randomUUID(), projectId, demands, occuredAt); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/ChosenResources.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning; 2 | 3 | import domaindrivers.smartschedule.availability.ResourceId; 4 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 5 | 6 | import java.util.Set; 7 | 8 | record ChosenResources(Set resources, TimeSlot timeSlot) { 9 | static ChosenResources none() { 10 | return new ChosenResources(Set.of(), TimeSlot.empty()); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/CreateProjectAllocations.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning; 2 | 3 | 4 | import domaindrivers.smartschedule.allocation.AllocationFacade; 5 | import domaindrivers.smartschedule.planning.schedule.Schedule; 6 | import jakarta.transaction.Transactional; 7 | 8 | public class CreateProjectAllocations { 9 | 10 | private final AllocationFacade allocationFacade; 11 | private final ProjectRepository projectRepository; 12 | 13 | public CreateProjectAllocations(AllocationFacade allocationFacade, ProjectRepository projectRepository) { 14 | this.allocationFacade = allocationFacade; 15 | this.projectRepository = projectRepository; 16 | } 17 | 18 | //can react to ScheduleCalculated event 19 | @Transactional 20 | public void createProjectAllocations(ProjectId projectId) { 21 | Project project = projectRepository.findById(projectId).orElseThrow(); 22 | Schedule schedule = project.getSchedule(); 23 | //for each stage in schedule 24 | //create allocation 25 | //allocate chosen resources (or find equivalents) 26 | //start risk analysis 27 | 28 | 29 | } 30 | 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/CriticalStagePlanned.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning; 2 | 3 | import domaindrivers.smartschedule.availability.ResourceId; 4 | import domaindrivers.smartschedule.shared.PrivateEvent; 5 | import domaindrivers.smartschedule.shared.PublishedEvent; 6 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 7 | 8 | import java.time.Instant; 9 | 10 | public record CriticalStagePlanned(ProjectId projectId, TimeSlot stageTimeSlot, ResourceId criticalResource, Instant occurredAt) implements PublishedEvent { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/Demand.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning; 2 | 3 | import domaindrivers.smartschedule.shared.capability.Capability; 4 | 5 | public record Demand(Capability capability) { 6 | 7 | public static Demand demandFor(Capability capability) { 8 | return new Demand(capability); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/Demands.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning; 2 | 3 | 4 | import java.util.List; 5 | import java.util.stream.Stream; 6 | 7 | public record Demands(List all) { 8 | static Demands none() { 9 | return new Demands(List.of()); 10 | } 11 | 12 | public static Demands of(Demand... demands) { 13 | return new Demands(List.of(demands)); 14 | } 15 | 16 | Demands add(Demands demands) { 17 | return new Demands(Stream.concat(all.stream(), demands.all.stream()).toList()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/DemandsPerStage.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning; 2 | 3 | 4 | import java.util.Map; 5 | 6 | record DemandsPerStage(Map demands) { 7 | 8 | static DemandsPerStage empty() { 9 | return new DemandsPerStage(Map.of()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/EditStageDateService.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning; 2 | 3 | 4 | import domaindrivers.smartschedule.allocation.AllocationFacade; 5 | import domaindrivers.smartschedule.planning.parallelization.Stage; 6 | import domaindrivers.smartschedule.planning.schedule.Schedule; 7 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 8 | import jakarta.transaction.Transactional; 9 | 10 | public class EditStageDateService { 11 | 12 | private final AllocationFacade allocationFacade; 13 | private final ProjectRepository projectRepository; 14 | 15 | public EditStageDateService(AllocationFacade allocationFacade, ProjectRepository projectRepository) { 16 | this.allocationFacade = allocationFacade; 17 | this.projectRepository = projectRepository; 18 | } 19 | 20 | @Transactional 21 | public void editStageDate(ProjectId projectId, Stage stage, TimeSlot newSlot) { 22 | Project project = projectRepository.findById(projectId).orElseThrow(); 23 | Schedule schedule = project.getSchedule(); 24 | //redefine schedule 25 | //for each stage in schedule 26 | //recreate allocation 27 | //reallocate chosen resources (or find equivalents) 28 | //start risk analysis 29 | 30 | 31 | } 32 | 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/NeededResourcesChosen.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning; 2 | 3 | import domaindrivers.smartschedule.availability.ResourceId; 4 | import domaindrivers.smartschedule.shared.PrivateEvent; 5 | import domaindrivers.smartschedule.shared.PublishedEvent; 6 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 7 | 8 | import java.time.Instant; 9 | import java.util.Set; 10 | 11 | public record NeededResourcesChosen(ProjectId projectId, Set neededResources, TimeSlot timeSlot, 12 | Instant occurredAt) implements PublishedEvent { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/PlanChosenResources.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning; 2 | 3 | 4 | import domaindrivers.smartschedule.availability.ResourceId; 5 | import domaindrivers.smartschedule.availability.AvailabilityFacade; 6 | import domaindrivers.smartschedule.availability.Calendars; 7 | import domaindrivers.smartschedule.planning.parallelization.Stage; 8 | import domaindrivers.smartschedule.planning.schedule.Schedule; 9 | import domaindrivers.smartschedule.shared.EventsPublisher; 10 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 11 | import jakarta.transaction.Transactional; 12 | 13 | import java.time.Clock; 14 | import java.util.Arrays; 15 | import java.util.Collection; 16 | import java.util.List; 17 | import java.util.Set; 18 | import java.util.stream.Collectors; 19 | 20 | 21 | public class PlanChosenResources { 22 | 23 | private final ProjectRepository projectRepository; 24 | private final AvailabilityFacade availabilityFacade; 25 | private final EventsPublisher eventsPublisher; 26 | private final Clock clock; 27 | 28 | public PlanChosenResources(ProjectRepository projectRepository, AvailabilityFacade availabilityFacade, EventsPublisher eventsPublisher, Clock clock) { 29 | this.projectRepository = projectRepository; 30 | this.availabilityFacade = availabilityFacade; 31 | this.eventsPublisher = eventsPublisher; 32 | this.clock = clock; 33 | } 34 | 35 | public void defineResourcesWithinDates(ProjectId projectId, Set chosenResources, TimeSlot timeBoundaries) { 36 | Project project = projectRepository.findById(projectId).orElseThrow(); 37 | project.addChosenResources(new ChosenResources(chosenResources, timeBoundaries)); 38 | projectRepository.save(project); 39 | eventsPublisher.publish(new NeededResourcesChosen(projectId, chosenResources, timeBoundaries, clock.instant())); 40 | } 41 | 42 | public void adjustStagesToResourceAvailability(ProjectId projectId, TimeSlot timeBoundaries, Stage... stages) { 43 | Set neededResources = neededResources(stages); 44 | Project project = projectRepository.findById(projectId).orElseThrow(); 45 | defineResourcesWithinDates(projectId, neededResources, timeBoundaries); 46 | Calendars neededResourcesCalendars = availabilityFacade.loadCalendars(neededResources, timeBoundaries); 47 | Schedule schedule = createScheduleAdjustingToCalendars(neededResourcesCalendars, List.of(stages)); 48 | project.addSchedule(schedule); 49 | projectRepository.save(project); 50 | } 51 | 52 | private Schedule createScheduleAdjustingToCalendars(Calendars neededResourcesCalendars, List stages) { 53 | return Schedule.basedOnChosenResourcesAvailability(neededResourcesCalendars, stages); 54 | } 55 | 56 | private Set neededResources(Stage[] stages) { 57 | return Arrays.stream(stages) 58 | .map(Stage::resources) 59 | .flatMap(Collection::stream) 60 | .collect(Collectors.toSet()); 61 | } 62 | 63 | 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/ProjectCard.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning; 2 | 3 | import domaindrivers.smartschedule.planning.parallelization.ParallelStagesList; 4 | import domaindrivers.smartschedule.planning.schedule.Schedule; 5 | 6 | 7 | public record ProjectCard(ProjectId projectId, String name, ParallelStagesList parallelizedStages, Demands demands, 8 | Schedule schedule, DemandsPerStage demandsPerStage, ChosenResources neededResources) { 9 | 10 | ProjectCard(ProjectId projectId, String name, ParallelStagesList parallelizedStages, Demands demands) { 11 | this(projectId, name, parallelizedStages, demands, Schedule.none(), DemandsPerStage.empty(), ChosenResources.none()); 12 | } 13 | 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/ProjectId.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning; 2 | 3 | import jakarta.persistence.Embeddable; 4 | 5 | import java.io.Serializable; 6 | import java.util.Objects; 7 | import java.util.UUID; 8 | 9 | @Embeddable 10 | public class ProjectId implements Serializable { 11 | 12 | public static ProjectId newOne() { 13 | return new ProjectId(UUID.randomUUID()); 14 | } 15 | 16 | private UUID projectId; 17 | 18 | ProjectId(UUID uuid) { 19 | this.projectId = uuid; 20 | } 21 | 22 | public ProjectId() { 23 | } 24 | 25 | public UUID id() { 26 | return projectId; 27 | } 28 | 29 | @Override 30 | public boolean equals(Object o) { 31 | if (this == o) return true; 32 | if (o == null || getClass() != o.getClass()) return false; 33 | ProjectId projectId1 = (ProjectId) o; 34 | return Objects.equals(projectId, projectId1.projectId); 35 | } 36 | 37 | @Override 38 | public int hashCode() { 39 | return Objects.hash(projectId); 40 | } 41 | 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/ProjectRepository.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning; 2 | 3 | 4 | 5 | import java.util.List; 6 | import java.util.Optional; 7 | import java.util.Set; 8 | 9 | 10 | interface ProjectRepository { 11 | 12 | Optional findById(ProjectId projectId); 13 | 14 | Project save(Project project); 15 | 16 | List findAllByIdIn(Set projectId); 17 | 18 | List findAll(); 19 | 20 | } -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/RedisProjectRepository.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning; 2 | 3 | 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.redis.core.RedisTemplate; 6 | 7 | import java.util.List; 8 | import java.util.Optional; 9 | import java.util.Set; 10 | import java.util.stream.Collectors; 11 | 12 | 13 | class RedisProjectRepository implements ProjectRepository { 14 | 15 | private final RedisTemplate redisTemplate; 16 | 17 | RedisProjectRepository(RedisTemplate redisTemplate) { 18 | this.redisTemplate = redisTemplate; 19 | } 20 | 21 | @Override 22 | public Optional findById(ProjectId projectId) { 23 | return Optional.ofNullable(redisTemplate.opsForHash().get("projects", projectId.id().toString())); 24 | } 25 | 26 | @Override 27 | public Project save(Project project) { 28 | redisTemplate.opsForHash().put("projects", project.id().id().toString(), project); 29 | return project; 30 | } 31 | 32 | @Override 33 | public List findAllByIdIn(Set projectId) { 34 | Set ids = projectId.stream().map(p -> p.id().toString()).collect(Collectors.toSet()); 35 | return redisTemplate.opsForHash().multiGet("projects", ids); 36 | } 37 | 38 | @Override 39 | public List findAll() { 40 | return redisTemplate.opsForHash().values("projects").stream().collect(Collectors.toList()); 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/parallelization/DurationCalculator.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning.parallelization; 2 | 3 | 4 | import java.time.Duration; 5 | import java.util.HashSet; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.function.Function; 9 | import java.util.stream.Collectors; 10 | 11 | public class DurationCalculator implements Function, Duration> { 12 | 13 | @Override 14 | public Duration apply(List stages) { 15 | ParallelStagesList parallelizedStages = new StageParallelization().of(new HashSet<>(stages)); 16 | Map durations = stages.stream() 17 | .collect(Collectors.toMap(identity -> identity, Stage::duration)); 18 | return parallelizedStages.allSorted().stream() 19 | .map(parallelStages -> parallelStages.stages().stream() 20 | .map(durations::get) 21 | .max(Duration::compareTo) 22 | .orElse(Duration.ZERO) 23 | ) 24 | .reduce(Duration.ZERO, Duration::plus); 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/parallelization/ParallelStages.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning.parallelization; 2 | 3 | import java.time.Duration; 4 | import java.util.Arrays; 5 | import java.util.HashSet; 6 | import java.util.List; 7 | import java.util.Set; 8 | import java.util.stream.Collectors; 9 | 10 | import static java.util.Arrays.*; 11 | 12 | public record ParallelStages(Set stages) { 13 | 14 | public String print() { 15 | return stages.stream() 16 | .map(Stage::name) 17 | .sorted() 18 | .collect(Collectors.joining(", ")); 19 | } 20 | 21 | 22 | public static ParallelStages of(Stage... stages) { 23 | return new ParallelStages(new HashSet<>(asList(stages))); 24 | } 25 | 26 | public Duration duration() { 27 | return stages 28 | .stream() 29 | .map(Stage::duration) 30 | .max(Duration::compareTo) 31 | .orElse(Duration.ZERO); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/parallelization/ParallelStagesList.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning.parallelization; 2 | 3 | import java.util.Comparator; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | import java.util.stream.Stream; 7 | 8 | import static java.util.Comparator.comparing; 9 | 10 | public record ParallelStagesList(List all) { 11 | 12 | public static ParallelStagesList empty() { 13 | return new ParallelStagesList(List.of()); 14 | } 15 | 16 | public static ParallelStagesList of(ParallelStages ... stages) { 17 | return new ParallelStagesList(List.of(stages)); 18 | } 19 | 20 | public String print() { 21 | return all.stream() 22 | .map(ParallelStages::print) 23 | .collect(Collectors.joining(" | ")); 24 | } 25 | 26 | public ParallelStagesList add(ParallelStages newParallelStages) { 27 | List result = 28 | Stream 29 | .concat(this.all.stream(), Stream.of(newParallelStages)) 30 | .collect(Collectors.toList()); 31 | return new ParallelStagesList(result); 32 | } 33 | 34 | public List allSorted(Comparator comparing) { 35 | return all 36 | .stream() 37 | .sorted(comparing) 38 | .collect(Collectors.toList()); 39 | } 40 | 41 | public List allSorted() { 42 | return allSorted(comparing(ParallelStages::print)); 43 | } 44 | 45 | 46 | 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/parallelization/SortedNodesToParallelizedStages.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning.parallelization; 2 | 3 | import domaindrivers.smartschedule.sorter.Node; 4 | import domaindrivers.smartschedule.sorter.SortedNodes; 5 | 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | 9 | 10 | class SortedNodesToParallelizedStages { 11 | 12 | ParallelStagesList calculate(SortedNodes sortedNodes) { 13 | List parallelized = sortedNodes 14 | .all() 15 | .stream() 16 | .map(nodes -> new ParallelStages(nodes 17 | .nodes() 18 | .stream() 19 | .map(Node::content) 20 | .collect(Collectors.toSet()))) 21 | .collect(Collectors.toList()); 22 | return new ParallelStagesList(parallelized); 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/parallelization/Stage.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning.parallelization; 2 | 3 | 4 | import domaindrivers.smartschedule.availability.ResourceId; 5 | 6 | import java.time.Duration; 7 | import java.util.Arrays; 8 | import java.util.HashSet; 9 | import java.util.Objects; 10 | import java.util.Set; 11 | import java.util.stream.Collectors; 12 | 13 | public record Stage(String stageName, Set dependencies, Set resources, 14 | Duration duration) { 15 | 16 | public Stage ofDuration(Duration duration) { 17 | return new Stage(stageName, dependencies, resources, duration); 18 | } 19 | 20 | public Stage(String name) { 21 | this(name, new HashSet<>(), new HashSet<>(), Duration.ZERO); 22 | } 23 | 24 | public Stage dependsOn(Stage stage) { 25 | Set newDependencies = new HashSet<>(dependencies); 26 | newDependencies.add(stage); 27 | this.dependencies.add(stage); 28 | return new Stage(stageName, newDependencies, resources, duration); 29 | } 30 | 31 | public String name() { 32 | return stageName; 33 | } 34 | 35 | public Stage withChosenResourceCapabilities(ResourceId... resources) { 36 | Set collect = Arrays.stream(resources).collect(Collectors.toSet()); 37 | return new Stage(stageName, dependencies, collect, duration); 38 | } 39 | 40 | @Override 41 | public boolean equals(Object o) { 42 | if (this == o) return true; 43 | if (o == null || getClass() != o.getClass()) return false; 44 | Stage stage = (Stage) o; 45 | return Objects.equals(stageName, stage.stageName) && Objects.equals(dependencies, stage.dependencies) && Objects.equals(resources, stage.resources) && Objects.equals(duration, stage.duration); 46 | } 47 | 48 | @Override 49 | public int hashCode() { 50 | return Objects.hash(stageName); 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | return stageName; 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/parallelization/StageParallelization.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning.parallelization; 2 | 3 | import domaindrivers.smartschedule.sorter.*; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Set; 8 | 9 | public class StageParallelization { 10 | 11 | public ParallelStagesList of(Set stages) { 12 | Nodes nodes = new StagesToNodes().calculate(new ArrayList<>(stages)); 13 | SortedNodes sortedNodes = new GraphTopologicalSort().sort(nodes); 14 | return new SortedNodesToParallelizedStages().calculate(sortedNodes); 15 | } 16 | 17 | public RemovalSuggestion whatToRemove(Set stages) { 18 | Nodes nodes = new StagesToNodes().calculate(new ArrayList<>(stages)); 19 | List result = new FeedbackArcSeOnGraph().calculate(new ArrayList<>(nodes.nodes())); 20 | return new RemovalSuggestion(result); 21 | } 22 | 23 | } 24 | 25 | record RemovalSuggestion(List edges) { 26 | @Override 27 | public String toString() { 28 | return edges.toString(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/parallelization/StagesToNodes.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning.parallelization; 2 | 3 | import domaindrivers.smartschedule.sorter.Node; 4 | import domaindrivers.smartschedule.sorter.Nodes; 5 | 6 | import java.util.*; 7 | import java.util.stream.Collectors; 8 | 9 | class StagesToNodes { 10 | 11 | Nodes calculate(List stages) { 12 | Map> result = stages.stream() 13 | .collect(Collectors.toMap(Stage::name, stage -> new Node<>(stage.name(), stage))); 14 | 15 | for (int i = 0; i < stages.size(); i++) { 16 | Stage stage = stages.get(i); 17 | result = explicitDependencies(stage, result); 18 | result = sharedResources(stage, stages.stream().skip(i + 1).collect(Collectors.toList()), result); 19 | } 20 | 21 | return new Nodes<>(new HashSet<>(result.values())); 22 | } 23 | 24 | private Map> sharedResources(Stage stage, List with, Map> result) { 25 | for (Stage other : with) { 26 | if (!stage.name().equals(other.name())) { 27 | if (!Collections.disjoint(stage.resources(), other.resources())) { 28 | if (other.resources().size() > stage.resources().size()) { 29 | Node node = result.get(stage.name()); 30 | node = node.dependsOn(result.get(other.name())); 31 | result.put(stage.name(), node); 32 | } else { 33 | Node node = result.get(other.name()); 34 | node = node.dependsOn(result.get(stage.name())); 35 | result.put(other.name(), node); 36 | } 37 | } 38 | } 39 | } 40 | return result; 41 | } 42 | 43 | private Map> explicitDependencies(Stage stage, Map> result) { 44 | Node nodeWithExplicitDeps = result.get(stage.name()); 45 | for(Stage explicitDependency: stage.dependencies()) { 46 | nodeWithExplicitDeps = nodeWithExplicitDeps.dependsOn(result.get(explicitDependency.name())); 47 | } 48 | result.put(stage.name(), nodeWithExplicitDeps); 49 | return result; 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/schedule/Schedule.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning.schedule; 2 | 3 | import domaindrivers.smartschedule.availability.Calendars; 4 | import domaindrivers.smartschedule.planning.parallelization.ParallelStages; 5 | import domaindrivers.smartschedule.planning.parallelization.ParallelStagesList; 6 | import domaindrivers.smartschedule.planning.parallelization.Stage; 7 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 8 | 9 | import java.time.Instant; 10 | import java.util.Comparator; 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | public record Schedule(Map dates) { 15 | 16 | public static Schedule none() { 17 | return new Schedule(Map.of()); 18 | } 19 | 20 | public static Schedule basedOnStartDay(Instant startDate, ParallelStagesList parallelizedStages) { 21 | Map scheduleMap = new ScheduleBasedOnStartDayCalculator().calculate(startDate, parallelizedStages, Comparator.comparing(ParallelStages::print)); 22 | return new Schedule(scheduleMap); 23 | } 24 | 25 | public static Schedule basedOnReferenceStageTimeSlot(Stage referenceStage, TimeSlot stageProposedTimeSlot, ParallelStagesList parallelizedStages) { 26 | Map scheduleMap = new ScheduleBasedOnReferenceStageCalculator().calculate(referenceStage, stageProposedTimeSlot, parallelizedStages, Comparator.comparing(ParallelStages::print)); 27 | return new Schedule(scheduleMap); 28 | } 29 | 30 | public static Schedule basedOnChosenResourcesAvailability(Calendars chosenResourcesCalendars, List stages) { 31 | Map schedule = new ScheduleBasedOnChosenResourcesAvailabilityCalculator().calculate(chosenResourcesCalendars, stages); 32 | return new Schedule(schedule); 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/schedule/ScheduleBasedOnChosenResourcesAvailabilityCalculator.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning.schedule; 2 | 3 | import domaindrivers.smartschedule.availability.Calendars; 4 | import domaindrivers.smartschedule.planning.parallelization.Stage; 5 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 6 | 7 | import java.time.Duration; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | import static java.util.Comparator.comparing; 13 | 14 | class ScheduleBasedOnChosenResourcesAvailabilityCalculator { 15 | 16 | Map calculate(Calendars chosenResourcesCalendars, List stages) { 17 | Map schedule = new HashMap<>(); 18 | for (Stage stage : stages) { 19 | TimeSlot proposedSlot = findSlotForStage(chosenResourcesCalendars, stage); 20 | if (proposedSlot.equals(TimeSlot.empty())) { 21 | return Map.of(); 22 | } 23 | schedule.put(stage.name(), proposedSlot); 24 | } 25 | return schedule; 26 | } 27 | 28 | private TimeSlot findSlotForStage(Calendars chosenResourcesCalendars, Stage stage) { 29 | List foundSlots = possibleSlots(chosenResourcesCalendars, stage); 30 | if (foundSlots.contains(TimeSlot.empty())) { 31 | return TimeSlot.empty(); 32 | } 33 | TimeSlot commonSlotForAllResources = findCommonPartOfSlots(foundSlots); 34 | while (!isSlotLongEnoughForStage(stage, commonSlotForAllResources)) { 35 | commonSlotForAllResources = commonSlotForAllResources.stretch(Duration.ofDays(1)); 36 | } 37 | return new TimeSlot(commonSlotForAllResources.from(), commonSlotForAllResources.from().plus(stage.duration())); 38 | } 39 | 40 | private boolean isSlotLongEnoughForStage(Stage stage, TimeSlot slot) { 41 | return slot.duration().compareTo(stage.duration()) >= 0; 42 | } 43 | 44 | private TimeSlot findCommonPartOfSlots(List foundSlots) { 45 | return foundSlots.stream() 46 | .reduce(TimeSlot::commonPartWith) 47 | .orElse(TimeSlot.empty()); 48 | } 49 | 50 | private List possibleSlots(Calendars chosenResourcesCalendars, Stage stage) { 51 | return stage.resources() 52 | .stream() 53 | .map(resource -> 54 | chosenResourcesCalendars 55 | .get(resource) 56 | .availableSlots() 57 | .stream() 58 | .sorted(comparing(TimeSlot::from)) 59 | .filter(slot -> isSlotLongEnoughForStage(stage, slot)) 60 | .findFirst() 61 | .orElse(TimeSlot.empty())) 62 | .toList(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/planning/schedule/ScheduleBasedOnStartDayCalculator.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning.schedule; 2 | 3 | import domaindrivers.smartschedule.planning.parallelization.ParallelStages; 4 | import domaindrivers.smartschedule.planning.parallelization.ParallelStagesList; 5 | import domaindrivers.smartschedule.planning.parallelization.Stage; 6 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 7 | 8 | import java.time.Instant; 9 | import java.util.Comparator; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | class ScheduleBasedOnStartDayCalculator { 15 | 16 | Map calculate(Instant startDate, ParallelStagesList parallelizedStages, Comparator comparing) { 17 | Map scheduleMap = new HashMap<>(); 18 | Instant currentStart = startDate; 19 | List allSorted = parallelizedStages.allSorted(comparing); 20 | for (ParallelStages stages : allSorted) { 21 | Instant parallelizedStagesEnd = currentStart; 22 | for (Stage stage : stages.stages()) { 23 | Instant stageEnd = currentStart.plus(stage.duration()); 24 | scheduleMap.put(stage.stageName(), new TimeSlot(currentStart, stageEnd)); 25 | if (stageEnd.isAfter(parallelizedStagesEnd)) { 26 | parallelizedStagesEnd = stageEnd; 27 | } 28 | } 29 | currentStart = parallelizedStagesEnd; 30 | 31 | } 32 | return scheduleMap; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/resource/ResourceConfiguration.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource; 2 | 3 | import domaindrivers.smartschedule.resource.device.DeviceFacade; 4 | import domaindrivers.smartschedule.resource.employee.EmployeeFacade; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | class ResourceConfiguration { 10 | 11 | @Bean 12 | ResourceFacade resourceFacade(EmployeeFacade employeeFacade, DeviceFacade deviceFacade) { 13 | return new ResourceFacade(employeeFacade, deviceFacade); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/resource/ResourceFacade.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource; 2 | 3 | import domaindrivers.smartschedule.resource.device.DeviceFacade; 4 | import domaindrivers.smartschedule.resource.employee.EmployeeFacade; 5 | import domaindrivers.smartschedule.shared.capability.Capability; 6 | 7 | import java.util.List; 8 | import java.util.stream.Stream; 9 | 10 | public class ResourceFacade { 11 | 12 | private final EmployeeFacade employeeFacade; 13 | private final DeviceFacade deviceFacade; 14 | 15 | ResourceFacade(EmployeeFacade employeeFacade, DeviceFacade deviceFacade) { 16 | this.employeeFacade = employeeFacade; 17 | this.deviceFacade = deviceFacade; 18 | } 19 | 20 | public List findAllCapabilities() { 21 | List employeeCapabilities = employeeFacade.findAllCapabilities(); 22 | List deviceCapabilities = deviceFacade.findAllCapabilities(); 23 | return Stream.concat(employeeCapabilities.stream(), deviceCapabilities.stream()).toList(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/resource/device/Device.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource.device; 2 | 3 | import domaindrivers.smartschedule.shared.capability.Capability; 4 | import io.hypersistence.utils.hibernate.type.json.JsonType; 5 | import jakarta.persistence.Column; 6 | import jakarta.persistence.EmbeddedId; 7 | import jakarta.persistence.Entity; 8 | import jakarta.persistence.Version; 9 | import org.hibernate.annotations.Type; 10 | 11 | import java.util.Set; 12 | 13 | @Entity(name = "devices") 14 | class Device { 15 | 16 | @EmbeddedId 17 | private DeviceId id = DeviceId.newOne(); 18 | 19 | @Version 20 | private int version; 21 | 22 | private String model; 23 | 24 | @Type(JsonType.class) 25 | @Column(columnDefinition = "jsonb") 26 | private Set capabilities; 27 | 28 | String model() { 29 | return model; 30 | } 31 | 32 | 33 | Set capabilities() { 34 | return capabilities; 35 | } 36 | 37 | Device(DeviceId id, String model, Set capabilities) { 38 | this.id = id; 39 | this.model = model; 40 | this.capabilities = capabilities; 41 | } 42 | 43 | public Device() { 44 | } 45 | 46 | public DeviceId id() { 47 | return id; 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/resource/device/DeviceConfiguration.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource.device; 2 | 3 | import domaindrivers.smartschedule.allocation.capabilityscheduling.CapabilityScheduler; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | @Configuration 8 | class DeviceConfiguration { 9 | @Bean 10 | DeviceFacade deviceFacade(DeviceRepository deviceRepository, CapabilityScheduler capabilityScheduler) { 11 | return new DeviceFacade(deviceRepository, new ScheduleDeviceCapabilities(deviceRepository, capabilityScheduler)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/resource/device/DeviceFacade.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource.device; 2 | 3 | import domaindrivers.smartschedule.allocation.capabilityscheduling.AllocatableCapabilityId; 4 | import domaindrivers.smartschedule.shared.capability.Capability; 5 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 6 | 7 | import java.util.List; 8 | import java.util.Set; 9 | 10 | public class DeviceFacade { 11 | private final DeviceRepository deviceRepository; 12 | private final ScheduleDeviceCapabilities scheduleDeviceCapabilities; 13 | 14 | public DeviceFacade(DeviceRepository deviceRepository, ScheduleDeviceCapabilities scheduleDeviceCapabilities) { 15 | this.deviceRepository = deviceRepository; 16 | this.scheduleDeviceCapabilities = scheduleDeviceCapabilities; 17 | } 18 | 19 | public DeviceSummary findDevice(DeviceId deviceId) { 20 | return deviceRepository.findSummary(deviceId); 21 | } 22 | 23 | public List findAllCapabilities() { 24 | return deviceRepository.findAllCapabilities(); 25 | } 26 | 27 | public DeviceId createDevice(String model, Set assets) { 28 | DeviceId deviceId = DeviceId.newOne(); 29 | Device device = new Device(deviceId, model, assets); 30 | return deviceRepository.save(device).id(); 31 | } 32 | 33 | public List scheduleCapabilities(DeviceId deviceId, TimeSlot oneDay) { 34 | return scheduleDeviceCapabilities.setupDeviceCapabilities(deviceId, oneDay); 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/resource/device/DeviceId.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource.device; 2 | 3 | 4 | import domaindrivers.smartschedule.allocation.capabilityscheduling.AllocatableResourceId; 5 | import domaindrivers.smartschedule.shared.CapabilitySelector; 6 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 7 | import jakarta.persistence.Embeddable; 8 | 9 | import java.io.Serializable; 10 | import java.util.List; 11 | import java.util.UUID; 12 | 13 | @Embeddable 14 | public class DeviceId implements Serializable { 15 | 16 | public static DeviceId newOne() { 17 | return new DeviceId(UUID.randomUUID()); 18 | } 19 | 20 | private UUID deviceId; 21 | 22 | DeviceId(UUID uuid) { 23 | this.deviceId = uuid; 24 | } 25 | 26 | public DeviceId() { 27 | } 28 | 29 | public UUID id() { 30 | return deviceId; 31 | } 32 | 33 | public AllocatableResourceId toAllocatableResourceId() { 34 | return new AllocatableResourceId(deviceId); 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/resource/device/DeviceRepository.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource.device; 2 | 3 | import domaindrivers.smartschedule.shared.capability.Capability; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.List; 7 | import java.util.Set; 8 | import java.util.stream.Collectors; 9 | 10 | interface DeviceRepository extends JpaRepository { 11 | 12 | default DeviceSummary findSummary(DeviceId deviceId) { 13 | Device device = this.findById(deviceId).orElseThrow(); 14 | Set assets = device.capabilities(); 15 | return new DeviceSummary(deviceId, device.model(), assets); 16 | } 17 | 18 | default List findAllCapabilities() { 19 | return this.findAll().stream().flatMap(device -> device.capabilities().stream()).collect(Collectors.toList()); 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/resource/device/DeviceSummary.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource.device; 2 | 3 | import domaindrivers.smartschedule.shared.capability.Capability; 4 | 5 | import java.util.Set; 6 | 7 | public record DeviceSummary(DeviceId id, String model, Set assets) { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/resource/device/ScheduleDeviceCapabilities.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource.device; 2 | 3 | import domaindrivers.smartschedule.allocation.capabilityscheduling.AllocatableCapabilityId; 4 | import domaindrivers.smartschedule.allocation.capabilityscheduling.CapabilityScheduler; 5 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 6 | 7 | import java.util.List; 8 | 9 | import static domaindrivers.smartschedule.shared.CapabilitySelector.canPerformAllAtTheTime; 10 | 11 | class ScheduleDeviceCapabilities { 12 | private final DeviceRepository deviceRepository; 13 | private final CapabilityScheduler capabilityScheduler; 14 | 15 | ScheduleDeviceCapabilities(DeviceRepository deviceRepository, CapabilityScheduler capabilityScheduler) { 16 | this.deviceRepository = deviceRepository; 17 | this.capabilityScheduler = capabilityScheduler; 18 | } 19 | 20 | public List setupDeviceCapabilities(DeviceId deviceId, TimeSlot timeSlot) { 21 | DeviceSummary summary = deviceRepository.findSummary(deviceId); 22 | return capabilityScheduler.scheduleResourceCapabilitiesForPeriod(deviceId.toAllocatableResourceId(), 23 | List.of(canPerformAllAtTheTime(summary.assets())), timeSlot); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/resource/employee/Employee.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource.employee; 2 | 3 | 4 | import domaindrivers.smartschedule.shared.capability.Capability; 5 | import io.hypersistence.utils.hibernate.type.json.JsonType; 6 | import jakarta.persistence.Column; 7 | import jakarta.persistence.EmbeddedId; 8 | import jakarta.persistence.Entity; 9 | import jakarta.persistence.Version; 10 | import org.hibernate.annotations.Type; 11 | 12 | import java.util.Set; 13 | 14 | @Entity(name = "employees") 15 | class Employee { 16 | 17 | @EmbeddedId 18 | private EmployeeId id = EmployeeId.newOne(); 19 | 20 | @Version 21 | private int version; 22 | 23 | private String name; 24 | 25 | private String lastName; 26 | 27 | private Seniority seniority; 28 | 29 | @Type(JsonType.class) 30 | @Column(columnDefinition = "jsonb") 31 | private Set capabilities; 32 | 33 | String name() { 34 | return name; 35 | } 36 | 37 | String lastName() { 38 | return lastName; 39 | } 40 | 41 | Seniority seniority() { 42 | return seniority; 43 | } 44 | 45 | Set capabilities() { 46 | return capabilities; 47 | } 48 | 49 | Employee(EmployeeId id, String name, String lastName, Seniority status, Set capabilities) { 50 | this.id = id; 51 | this.name = name; 52 | this.lastName = lastName; 53 | this.seniority = status; 54 | this.capabilities = capabilities; 55 | } 56 | 57 | public Employee() { 58 | } 59 | 60 | public EmployeeId id() { 61 | return id; 62 | } 63 | 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/resource/employee/EmployeeAllocationPolicy.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource.employee; 2 | 3 | import domaindrivers.smartschedule.shared.CapabilitySelector; 4 | import domaindrivers.smartschedule.shared.capability.Capability; 5 | 6 | import java.util.*; 7 | import java.util.stream.IntStream; 8 | 9 | public interface EmployeeAllocationPolicy { 10 | 11 | List simultaneousCapabilitiesOf(EmployeeSummary employee); 12 | 13 | static EmployeeAllocationPolicy defaultPolicy() { 14 | return employee -> { 15 | Set all = new HashSet<>(); 16 | all.addAll(employee.skills()); 17 | all.addAll(employee.permissions()); 18 | return List.of(CapabilitySelector.canPerformOneOf(all)); 19 | }; 20 | } 21 | 22 | static EmployeeAllocationPolicy permissionsInMultipleProjects(int howMany) { 23 | return employee -> employee 24 | .permissions() 25 | .stream() 26 | .flatMap(permission -> IntStream.range(0, howMany).mapToObj(i -> permission)) 27 | .map(CapabilitySelector::canJustPerform) 28 | .toList(); 29 | } 30 | 31 | static EmployeeAllocationPolicy oneOfSkills() { 32 | return employee -> List.of(CapabilitySelector.canPerformOneOf(employee.skills())); 33 | } 34 | 35 | static CompositePolicy simultaneous(EmployeeAllocationPolicy... policies) { 36 | return new CompositePolicy(Arrays.asList(policies)); 37 | } 38 | } 39 | 40 | class CompositePolicy implements EmployeeAllocationPolicy { 41 | 42 | final List policies; 43 | 44 | CompositePolicy(List policies) { 45 | this.policies = policies; 46 | } 47 | 48 | @Override 49 | public List simultaneousCapabilitiesOf(EmployeeSummary employee) { 50 | return policies 51 | .stream() 52 | .flatMap(p -> p.simultaneousCapabilitiesOf(employee).stream()) 53 | .toList(); 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/resource/employee/EmployeeConfiguration.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource.employee; 2 | 3 | 4 | import domaindrivers.smartschedule.allocation.capabilityscheduling.CapabilityScheduler; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | class EmployeeConfiguration { 10 | 11 | @Bean 12 | EmployeeFacade employeeFacade(EmployeeRepository employeeRepository, CapabilityScheduler capabilityScheduler) { 13 | return new EmployeeFacade(employeeRepository, new ScheduleEmployeeCapabilities(employeeRepository, capabilityScheduler)); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/resource/employee/EmployeeFacade.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource.employee; 2 | 3 | 4 | import domaindrivers.smartschedule.allocation.capabilityscheduling.AllocatableCapabilityId; 5 | import domaindrivers.smartschedule.allocation.capabilityscheduling.CapabilityScheduler; 6 | import domaindrivers.smartschedule.shared.capability.Capability; 7 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 8 | 9 | import java.util.List; 10 | import java.util.Set; 11 | import java.util.stream.Collectors; 12 | import java.util.stream.Stream; 13 | 14 | 15 | public class EmployeeFacade { 16 | 17 | private final EmployeeRepository employeeRepository; 18 | private final ScheduleEmployeeCapabilities scheduleEmployeeCapabilities; 19 | 20 | public EmployeeFacade(EmployeeRepository employeeRepository, ScheduleEmployeeCapabilities scheduleEmployeeCapabilities) { 21 | this.employeeRepository = employeeRepository; 22 | this.scheduleEmployeeCapabilities = scheduleEmployeeCapabilities; 23 | } 24 | 25 | public EmployeeSummary findEmployee(EmployeeId employeeId) { 26 | return employeeRepository.findSummary(employeeId); 27 | } 28 | 29 | public List findAllCapabilities() { 30 | return employeeRepository.findAllCapabilities(); 31 | } 32 | 33 | public EmployeeId addEmployee(String name, String lastName, Seniority seniority, Set skills, Set permissions) { 34 | EmployeeId employeeId = EmployeeId.newOne(); 35 | Set capabilities = Stream.concat(skills.stream(), permissions.stream()).collect(Collectors.toSet()); 36 | Employee employee = new Employee(employeeId, name, lastName, seniority, capabilities); 37 | return employeeRepository.save(employee).id(); 38 | } 39 | 40 | public List scheduleCapabilities(EmployeeId employeeId, TimeSlot oneDay) { 41 | return scheduleEmployeeCapabilities.setupEmployeeCapabilities(employeeId, oneDay); 42 | } 43 | 44 | //add vacation 45 | // calls availability 46 | //add sick leave 47 | // calls availability 48 | //change skills 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/resource/employee/EmployeeId.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource.employee; 2 | 3 | import domaindrivers.smartschedule.allocation.capabilityscheduling.AllocatableResourceId; 4 | import jakarta.persistence.Embeddable; 5 | 6 | import java.io.Serializable; 7 | import java.util.UUID; 8 | 9 | @Embeddable 10 | public class EmployeeId implements Serializable { 11 | 12 | public static EmployeeId newOne() { 13 | return new EmployeeId(UUID.randomUUID()); 14 | } 15 | 16 | private UUID employeeId; 17 | 18 | EmployeeId(UUID uuid) { 19 | this.employeeId = uuid; 20 | } 21 | 22 | public EmployeeId() { 23 | } 24 | 25 | public UUID id() { 26 | return employeeId; 27 | } 28 | 29 | public AllocatableResourceId toAllocatableResourceId() { 30 | return new AllocatableResourceId(employeeId); 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/resource/employee/EmployeeRepository.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource.employee; 2 | 3 | 4 | import domaindrivers.smartschedule.shared.capability.Capability; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | import java.util.List; 8 | import java.util.Set; 9 | import java.util.function.Predicate; 10 | import java.util.stream.Collectors; 11 | 12 | 13 | 14 | interface EmployeeRepository extends JpaRepository { 15 | 16 | default EmployeeSummary findSummary(EmployeeId employeeId) { 17 | Employee employee = this.findById(employeeId).orElseThrow(); 18 | Set skills = filterCapabilities(employee.capabilities(), cap -> cap.isOfType("SKILL")); 19 | Set permissions = filterCapabilities(employee.capabilities(), cap -> cap.isOfType("PERMISSION")); 20 | return new EmployeeSummary(employeeId, employee.name(), employee.lastName(), employee.seniority(), skills, permissions); 21 | } 22 | 23 | default List findAllCapabilities() { 24 | return this.findAll().stream().flatMap(employee -> employee.capabilities().stream()).collect(Collectors.toList()); 25 | } 26 | 27 | private Set filterCapabilities(Set capabilities, Predicate p) { 28 | return capabilities.stream().filter(p).collect(Collectors.toSet()); 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/resource/employee/EmployeeSummary.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource.employee; 2 | 3 | 4 | import domaindrivers.smartschedule.shared.capability.Capability; 5 | 6 | import java.util.Set; 7 | 8 | public record EmployeeSummary(EmployeeId id, String name, String lastName, Seniority seniority, Set skills, Set permissions) { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/resource/employee/ScheduleEmployeeCapabilities.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource.employee; 2 | 3 | import domaindrivers.smartschedule.allocation.AllocationFacade; 4 | import domaindrivers.smartschedule.allocation.capabilityscheduling.AllocatableCapabilityId; 5 | import domaindrivers.smartschedule.allocation.capabilityscheduling.CapabilityScheduler; 6 | import domaindrivers.smartschedule.shared.CapabilitySelector; 7 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 8 | 9 | import java.util.List; 10 | 11 | class ScheduleEmployeeCapabilities { 12 | 13 | private final EmployeeRepository employeeRepository; 14 | private final CapabilityScheduler capabilityScheduler; 15 | 16 | ScheduleEmployeeCapabilities(EmployeeRepository employeeRepository, CapabilityScheduler capabilityScheduler) { 17 | this.employeeRepository = employeeRepository; 18 | this.capabilityScheduler = capabilityScheduler; 19 | } 20 | 21 | public List setupEmployeeCapabilities(EmployeeId employeeId, TimeSlot timeSlot) { 22 | EmployeeSummary summary = employeeRepository.findSummary(employeeId); 23 | EmployeeAllocationPolicy policy = findAllocationPolicy(summary); 24 | List capabilities = policy.simultaneousCapabilitiesOf(summary); 25 | return capabilityScheduler.scheduleResourceCapabilitiesForPeriod(employeeId.toAllocatableResourceId(), capabilities, timeSlot); 26 | } 27 | 28 | private EmployeeAllocationPolicy findAllocationPolicy(EmployeeSummary employee) { 29 | if (employee.seniority().equals(Seniority.LEAD)) { 30 | return EmployeeAllocationPolicy.simultaneous(EmployeeAllocationPolicy.oneOfSkills(), EmployeeAllocationPolicy.permissionsInMultipleProjects(3)); 31 | } 32 | return EmployeeAllocationPolicy.defaultPolicy(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/resource/employee/Seniority.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource.employee; 2 | 3 | public enum Seniority { 4 | JUNIOR, MID, SENIOR, LEAD 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/risk/RiskConfiguration.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.risk; 2 | 3 | 4 | import domaindrivers.smartschedule.allocation.AllocationFacade; 5 | import domaindrivers.smartschedule.allocation.PotentialTransfersService; 6 | import domaindrivers.smartschedule.allocation.capabilityscheduling.CapabilityFinder; 7 | import domaindrivers.smartschedule.allocation.cashflow.CashFlowFacade; 8 | import domaindrivers.smartschedule.availability.AvailabilityFacade; 9 | import domaindrivers.smartschedule.planning.PlanningFacade; 10 | import domaindrivers.smartschedule.resource.ResourceFacade; 11 | import domaindrivers.smartschedule.simulation.SimulationFacade; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | import org.springframework.scheduling.annotation.EnableScheduling; 15 | 16 | import java.time.Clock; 17 | 18 | @Configuration 19 | @EnableScheduling 20 | class RiskConfiguration { 21 | 22 | @Bean 23 | RiskPeriodicCheckSagaDispatcher riskSagaDispatcher(RiskPeriodicCheckSagaRepository stateRepository, PotentialTransfersService potentialTransfersService, CapabilityFinder capabilityFinder, RiskPushNotification riskPushNotification, Clock clock) { 24 | return new RiskPeriodicCheckSagaDispatcher(stateRepository, potentialTransfersService, capabilityFinder, riskPushNotification, clock); 25 | } 26 | 27 | @Bean 28 | RiskPushNotification riskPushNotification() { 29 | return new RiskPushNotification(); 30 | } 31 | 32 | @Bean 33 | VerifyEnoughDemandsDuringPlanning verifyEnoughDemandsDuringPlanning(PlanningFacade planningFacade, ResourceFacade resourceFacade, RiskPushNotification riskPushNotification) { 34 | return new VerifyEnoughDemandsDuringPlanning(planningFacade, new SimulationFacade(), resourceFacade, riskPushNotification); 35 | } 36 | 37 | @Bean 38 | VerifyCriticalResourceAvailableDuringPlanning verifyCriticalResourceAvailableDuringPlanning(AvailabilityFacade availabilityFacade, RiskPushNotification riskPushNotification) { 39 | return new VerifyCriticalResourceAvailableDuringPlanning(availabilityFacade, riskPushNotification); 40 | } 41 | 42 | @Bean 43 | VerifyNeededResourcesAvailableInTimeSlot verifyNeededResourcesAvailableInTimeSlot(AvailabilityFacade availabilityFacade, RiskPushNotification riskPushNotification) { 44 | return new VerifyNeededResourcesAvailableInTimeSlot(availabilityFacade, riskPushNotification); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/risk/RiskPeriodicCheckSagaId.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.risk; 2 | 3 | import jakarta.persistence.Embeddable; 4 | 5 | import java.io.Serializable; 6 | import java.util.Objects; 7 | import java.util.UUID; 8 | 9 | @Embeddable 10 | public class RiskPeriodicCheckSagaId implements Serializable { 11 | 12 | public static RiskPeriodicCheckSagaId newOne() { 13 | return new RiskPeriodicCheckSagaId(UUID.randomUUID()); 14 | } 15 | 16 | private UUID projectRiskSagaId; 17 | 18 | RiskPeriodicCheckSagaId(UUID uuid) { 19 | this.projectRiskSagaId = uuid; 20 | } 21 | 22 | public RiskPeriodicCheckSagaId() { 23 | } 24 | 25 | public UUID id() { 26 | return projectRiskSagaId; 27 | } 28 | 29 | @Override 30 | public boolean equals(Object o) { 31 | if (this == o) return true; 32 | if (o == null || getClass() != o.getClass()) return false; 33 | RiskPeriodicCheckSagaId projectId1 = (RiskPeriodicCheckSagaId) o; 34 | return Objects.equals(projectRiskSagaId, projectId1.projectRiskSagaId); 35 | } 36 | 37 | @Override 38 | public int hashCode() { 39 | return Objects.hash(projectRiskSagaId); 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/risk/RiskPeriodicCheckSagaRepository.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.risk; 2 | 3 | 4 | import domaindrivers.smartschedule.allocation.ProjectAllocationsId; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | import java.util.List; 8 | 9 | 10 | interface RiskPeriodicCheckSagaRepository extends JpaRepository { 11 | 12 | RiskPeriodicCheckSaga findByProjectId(ProjectAllocationsId projectId); 13 | 14 | List findByProjectIdIn(List interested); 15 | 16 | default RiskPeriodicCheckSaga findByProjectIdOrCreate(ProjectAllocationsId projectId) { 17 | RiskPeriodicCheckSaga found = findByProjectId(projectId); 18 | if (found == null) { 19 | found = save(new RiskPeriodicCheckSaga(projectId)); 20 | } 21 | return found; 22 | } 23 | 24 | default List findByProjectIdInOrElseCreate(List interested) { 25 | List found = findByProjectIdIn(interested); 26 | List foundIds = found 27 | .stream() 28 | .map(RiskPeriodicCheckSaga::projectId). 29 | toList(); 30 | List missing = 31 | interested 32 | .stream() 33 | .filter(projectId -> !foundIds.contains(projectId)) 34 | .map(RiskPeriodicCheckSaga::new).toList(); 35 | missing = saveAll(missing); 36 | found.addAll(missing); 37 | return found; 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/risk/RiskPeriodicCheckSagaStep.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.risk; 2 | 3 | public enum RiskPeriodicCheckSagaStep { 4 | FIND_AVAILABLE, 5 | DO_NOTHING, 6 | SUGGEST_REPLACEMENT, 7 | NOTIFY_ABOUT_POSSIBLE_RISK, 8 | NOTIFY_ABOUT_DEMANDS_SATISFIED 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/risk/RiskPushNotification.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.risk; 2 | 3 | import domaindrivers.smartschedule.allocation.Demand; 4 | import domaindrivers.smartschedule.allocation.ProjectAllocationsId; 5 | import domaindrivers.smartschedule.allocation.capabilityscheduling.AllocatableCapabilitiesSummary; 6 | import domaindrivers.smartschedule.allocation.capabilityscheduling.AllocatableCapabilityId; 7 | import domaindrivers.smartschedule.availability.ResourceId; 8 | import domaindrivers.smartschedule.planning.ProjectId; 9 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 10 | 11 | import java.util.Map; 12 | import java.util.Set; 13 | 14 | public class RiskPushNotification { 15 | 16 | public void notifyDemandsSatisfied(ProjectAllocationsId projectId) { 17 | } 18 | 19 | public void notifyAboutAvailability(ProjectAllocationsId projectId, Map available) { 20 | } 21 | 22 | public void notifyProfitableRelocationFound(ProjectAllocationsId projectId, AllocatableCapabilityId allocatableCapabilityId) { 23 | } 24 | 25 | public void notifyAboutPossibleRisk(ProjectAllocationsId projectId) { 26 | } 27 | 28 | public void notifyAboutPossibleRiskDuringPlanning(ProjectId cause, domaindrivers.smartschedule.planning.Demands demands) { 29 | } 30 | 31 | public void notifyAboutCriticalResourceNotAvailable(ProjectId cause, ResourceId criticalResource, TimeSlot timeSlot) { 32 | } 33 | 34 | public void notifyAboutResourcesNotAvailable(ProjectId projectId, Set notAvailable) { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/risk/VerifyCriticalResourceAvailableDuringPlanning.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.risk; 2 | 3 | import domaindrivers.smartschedule.availability.AvailabilityFacade; 4 | import domaindrivers.smartschedule.availability.Calendar; 5 | import domaindrivers.smartschedule.planning.CriticalStagePlanned; 6 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 7 | import org.springframework.context.event.EventListener; 8 | import org.springframework.scheduling.annotation.Async; 9 | 10 | class VerifyCriticalResourceAvailableDuringPlanning { 11 | 12 | private final AvailabilityFacade availabilityFacade; 13 | private final RiskPushNotification riskPushNotification; 14 | 15 | VerifyCriticalResourceAvailableDuringPlanning(AvailabilityFacade availabilityFacade, RiskPushNotification riskPushNotification) { 16 | this.availabilityFacade = availabilityFacade; 17 | this.riskPushNotification = riskPushNotification; 18 | } 19 | 20 | @Async 21 | @EventListener 22 | void handle(CriticalStagePlanned criticalStagePlanned) { 23 | if (criticalStagePlanned.criticalResource() == null) { 24 | return; 25 | } 26 | Calendar calendar = availabilityFacade.loadCalendar(criticalStagePlanned.criticalResource(), criticalStagePlanned.stageTimeSlot()); 27 | if (!resourceIsAvailable(criticalStagePlanned.stageTimeSlot(), calendar)) { 28 | riskPushNotification.notifyAboutCriticalResourceNotAvailable(criticalStagePlanned.projectId(), criticalStagePlanned.criticalResource(), criticalStagePlanned.stageTimeSlot()); 29 | } 30 | } 31 | 32 | private boolean resourceIsAvailable(TimeSlot timeSlot, Calendar calendar) { 33 | return calendar.availableSlots().stream().anyMatch(slot -> slot.equals(timeSlot)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/risk/VerifyNeededResourcesAvailableInTimeSlot.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.risk; 2 | 3 | import domaindrivers.smartschedule.availability.AvailabilityFacade; 4 | import domaindrivers.smartschedule.availability.Calendars; 5 | import domaindrivers.smartschedule.availability.ResourceId; 6 | import domaindrivers.smartschedule.planning.NeededResourcesChosen; 7 | import domaindrivers.smartschedule.planning.ProjectId; 8 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 9 | import org.springframework.context.event.EventListener; 10 | import org.springframework.scheduling.annotation.Async; 11 | 12 | import java.util.HashSet; 13 | import java.util.Set; 14 | 15 | 16 | class VerifyNeededResourcesAvailableInTimeSlot { 17 | 18 | private final AvailabilityFacade availabilityFacade; 19 | private final RiskPushNotification riskPushNotification; 20 | 21 | VerifyNeededResourcesAvailableInTimeSlot(AvailabilityFacade availabilityFacade, RiskPushNotification riskPushNotification) { 22 | this.availabilityFacade = availabilityFacade; 23 | this.riskPushNotification = riskPushNotification; 24 | } 25 | 26 | @Async 27 | @EventListener 28 | void handle(NeededResourcesChosen resourcesNeeded) { 29 | notifyAboutNotAvailableResources(resourcesNeeded.neededResources(), resourcesNeeded.timeSlot(), resourcesNeeded.projectId()); 30 | } 31 | 32 | private void notifyAboutNotAvailableResources(Set resourcedIds, TimeSlot timeSlot, ProjectId projectId) { 33 | Set notAvailable = new HashSet<>(); 34 | Calendars calendars = availabilityFacade.loadCalendars(resourcedIds, timeSlot); 35 | for (ResourceId resourceId : resourcedIds) { 36 | if (calendars.get(resourceId).availableSlots().stream().noneMatch(timeSlot::within)) { 37 | notAvailable.add(resourceId); 38 | } 39 | } 40 | if (!notAvailable.isEmpty()) { 41 | riskPushNotification.notifyAboutResourcesNotAvailable(projectId, notAvailable); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/shared/BaseRepository.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.shared; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | 7 | //to use if we don't want to expose findAll() method from spring-data-jpa for entities with possible large number of records 8 | public interface BaseRepository { 9 | //extends Repository { 10 | 11 | Optional findById(ID id); 12 | 13 | T getReferenceById(ID id); 14 | 15 | boolean existsById(ID id); 16 | 17 | void deleteById(ID id); 18 | 19 | void delete(T entity); 20 | 21 | long count(); 22 | 23 | void save(T entity); 24 | 25 | List findAllById(List ids); 26 | 27 | void saveAll(List entities); 28 | } -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/shared/CapabilitySelector.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.shared; 2 | 3 | 4 | 5 | import domaindrivers.smartschedule.shared.capability.Capability; 6 | 7 | import java.io.Serializable; 8 | import java.util.*; 9 | 10 | public record CapabilitySelector(Set capabilities, SelectingPolicy selectingPolicy) implements Serializable { 11 | 12 | 13 | public enum SelectingPolicy { 14 | ALL_SIMULTANEOUSLY, ONE_OF_ALL 15 | } 16 | 17 | public static CapabilitySelector canPerformAllAtTheTime(Set capabilities) { 18 | return new CapabilitySelector(capabilities, SelectingPolicy.ALL_SIMULTANEOUSLY); 19 | } 20 | 21 | public static CapabilitySelector canPerformOneOf(Set capabilities) { 22 | return new CapabilitySelector(capabilities, SelectingPolicy.ONE_OF_ALL); 23 | } 24 | 25 | public static CapabilitySelector canJustPerform(Capability capability) { 26 | return new CapabilitySelector(Set.of(capability), SelectingPolicy.ONE_OF_ALL); 27 | } 28 | 29 | public boolean canPerform(Capability capability) { 30 | return capabilities.contains(capability); 31 | } 32 | 33 | public boolean canPerform(Set capabilities) { 34 | if (capabilities.size() == 1) { 35 | return new HashSet<>(this.capabilities).containsAll(capabilities); 36 | } 37 | return selectingPolicy.equals(SelectingPolicy.ALL_SIMULTANEOUSLY) && 38 | new HashSet<>(this.capabilities).containsAll(capabilities); 39 | } 40 | 41 | 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/shared/ClockConfiguration.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.shared; 2 | 3 | 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | import java.time.Clock; 8 | 9 | @Configuration 10 | public class ClockConfiguration { 11 | 12 | @Bean 13 | Clock clock() { 14 | return Clock.systemDefaultZone(); 15 | } 16 | } 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/shared/EventingConfiguration.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.shared; 2 | 3 | 4 | import org.springframework.context.ApplicationEventPublisher; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.scheduling.annotation.EnableAsync; 8 | 9 | @Configuration 10 | @EnableAsync 11 | public class EventingConfiguration { 12 | 13 | @Bean 14 | EventsPublisher eventsPublisher(ApplicationEventPublisher applicationEventPublisher) { 15 | return applicationEventPublisher::publishEvent; 16 | } 17 | } 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/shared/EventsPublisher.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.shared; 2 | 3 | 4 | public interface EventsPublisher { 5 | //remember about transactions scope 6 | void publish(PublishedEvent event); 7 | } 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/shared/PrivateEvent.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.shared; 2 | 3 | import java.time.Instant; 4 | 5 | //metadata: 6 | //correlationId 7 | //potential aggregate's id 8 | //causationId - id of a message that caused this message 9 | //messageId - unique id of the 10 | //user - if there is any (might be a system event) 11 | public interface PrivateEvent { 12 | 13 | Instant occurredAt(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/shared/PublishedEvent.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.shared; 2 | 3 | import java.time.Instant; 4 | 5 | //metadata: 6 | //correlationId 7 | //potential aggregate's id 8 | //causationId - id of a message that caused this message 9 | //messageId - unique id of the 10 | //user - if there is any (might be a system event) 11 | public interface PublishedEvent { 12 | 13 | Instant occurredAt(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/shared/ResourceName.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.shared; 2 | 3 | import java.util.Objects; 4 | 5 | public record ResourceName(String name) { 6 | 7 | @Override 8 | public boolean equals(Object o) { 9 | if (this == o) return true; 10 | if (o == null || getClass() != o.getClass()) return false; 11 | ResourceName that = (ResourceName) o; 12 | return Objects.equals(name, that.name); 13 | } 14 | 15 | @Override 16 | public int hashCode() { 17 | return Objects.hash(name); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/shared/capability/Capability.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.shared.capability; 2 | 3 | 4 | import java.io.Serializable; 5 | import java.util.List; 6 | import java.util.Set; 7 | import java.util.stream.Collectors; 8 | import java.util.stream.Stream; 9 | 10 | public record Capability(String name, String type) implements Serializable { 11 | 12 | public static Capability skill(String name) { 13 | return new Capability(name, "SKILL"); 14 | } 15 | 16 | public static Capability permission(String name) { 17 | return new Capability(name, "PERMISSION"); 18 | } 19 | 20 | public static Capability asset(String asset) { 21 | return new Capability(asset, "ASSET"); 22 | } 23 | 24 | public static Set skills(String ... skills) { 25 | return Stream.of(skills).map(Capability::skill).collect(Collectors.toSet()); 26 | } 27 | 28 | public static Set assets(String ... assets) { 29 | return Stream.of(assets).map(Capability::asset).collect(Collectors.toSet()); 30 | } 31 | 32 | public static Set permissions(String ... permissions) { 33 | return Stream.of(permissions).map(Capability::permission).collect(Collectors.toSet()); 34 | } 35 | 36 | public boolean isOfType(String type) { 37 | return this.type.equals(type); 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/simulation/AdditionalPricedCapability.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.simulation; 2 | 3 | import java.math.BigDecimal; 4 | 5 | record AdditionalPricedCapability(BigDecimal value, AvailableResourceCapability availableResourceCapability) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/simulation/AvailableResourceCapability.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.simulation; 2 | 3 | 4 | import domaindrivers.smartschedule.optimization.CapacityDimension; 5 | import domaindrivers.smartschedule.shared.CapabilitySelector; 6 | import domaindrivers.smartschedule.shared.capability.Capability; 7 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 8 | 9 | import java.util.Set; 10 | import java.util.UUID; 11 | 12 | public record AvailableResourceCapability(UUID resourceId, CapabilitySelector capabilitySelector, TimeSlot timeSlot) implements CapacityDimension { 13 | 14 | AvailableResourceCapability(UUID resourceId, Capability capability, TimeSlot timeSlot) { 15 | this(resourceId, CapabilitySelector.canJustPerform(capability), timeSlot); 16 | } 17 | boolean performs(Capability capability) { 18 | return capabilitySelector.canPerform(Set.of(capability)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/simulation/Demand.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.simulation; 2 | 3 | import domaindrivers.smartschedule.optimization.WeightDimension; 4 | import domaindrivers.smartschedule.shared.capability.Capability; 5 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 6 | 7 | public record Demand(Capability capability, TimeSlot slot) implements WeightDimension { 8 | 9 | static Demand demandFor(Capability capability, TimeSlot slot) { 10 | return new Demand(capability, slot); 11 | } 12 | 13 | @Override 14 | public boolean isSatisfiedBy(AvailableResourceCapability availableCapability) { 15 | return availableCapability.performs(this.capability()) && 16 | this.slot().within(availableCapability.timeSlot()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/simulation/Demands.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.simulation; 2 | 3 | 4 | import java.util.List; 5 | 6 | public record Demands(List all) { 7 | 8 | static Demands of(Demand... demands) { 9 | return new Demands(List.of(demands)); 10 | } 11 | 12 | } 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/simulation/ProjectId.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.simulation; 2 | 3 | import java.io.Serializable; 4 | import java.util.Objects; 5 | import java.util.UUID; 6 | 7 | public class ProjectId implements Serializable { 8 | 9 | public static ProjectId newOne() { 10 | return new ProjectId(UUID.randomUUID()); 11 | } 12 | 13 | private UUID projectId; 14 | 15 | ProjectId(UUID uuid) { 16 | this.projectId = uuid; 17 | } 18 | 19 | public static ProjectId from(UUID key) { 20 | return new ProjectId(key); 21 | } 22 | 23 | public UUID id() { 24 | return projectId; 25 | } 26 | 27 | @Override 28 | public boolean equals(Object o) { 29 | if (this == o) return true; 30 | if (o == null || getClass() != o.getClass()) return false; 31 | ProjectId projectId1 = (ProjectId) o; 32 | return Objects.equals(projectId, projectId1.projectId); 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | return Objects.hash(projectId); 38 | } 39 | 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/simulation/SimulatedCapabilities.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.simulation; 2 | 3 | 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | public record SimulatedCapabilities(List capabilities) { 8 | 9 | public static SimulatedCapabilities none() { 10 | return new SimulatedCapabilities(List.of()); 11 | } 12 | 13 | SimulatedCapabilities add(List newCapabilities) { 14 | List newAvailabilities = new ArrayList<>(capabilities); 15 | newAvailabilities.addAll(newCapabilities); 16 | return new SimulatedCapabilities(newAvailabilities); 17 | } 18 | 19 | SimulatedCapabilities add(AvailableResourceCapability newCapability) { 20 | return add(List.of(newCapability)); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/simulation/SimulatedProject.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.simulation; 2 | 3 | 4 | import java.math.BigDecimal; 5 | import java.util.function.Supplier; 6 | 7 | public record SimulatedProject(ProjectId projectId, Supplier value, Demands missingDemands) { 8 | 9 | BigDecimal calculateValue() { 10 | return value.get(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/simulation/SimulationFacade.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.simulation; 2 | 3 | import domaindrivers.smartschedule.optimization.*; 4 | 5 | import java.util.*; 6 | 7 | 8 | public class SimulationFacade { 9 | 10 | private final OptimizationFacade optimizationFacade; 11 | 12 | public SimulationFacade(OptimizationFacade optimizationFacade) { 13 | this.optimizationFacade = optimizationFacade; 14 | } 15 | 16 | public SimulationFacade() { 17 | this.optimizationFacade = new OptimizationFacade(); 18 | } 19 | 20 | public double profitAfterBuyingNewCapability(List projectsSimulations, SimulatedCapabilities capabilitiesWithoutNewOne, AdditionalPricedCapability newPricedCapability) { 21 | SimulatedCapabilities capabilitiesWithNewResource = capabilitiesWithoutNewOne.add(newPricedCapability.availableResourceCapability()); 22 | Result resultWithout = optimizationFacade.calculate(toItems(projectsSimulations), toCapacity(capabilitiesWithoutNewOne), Comparator.comparing(Item::value).reversed()); 23 | Result resultWith = optimizationFacade.calculate(toItems(projectsSimulations), toCapacity(capabilitiesWithNewResource), Comparator.comparing(Item::value).reversed()); 24 | return (resultWith.profit() - newPricedCapability.value().doubleValue()) - resultWithout.profit(); 25 | } 26 | 27 | public Result whatIsTheOptimalSetup(List projectsSimulations, SimulatedCapabilities totalCapability) { 28 | return optimizationFacade.calculate(toItems(projectsSimulations), toCapacity(totalCapability), Comparator.comparing(Item::value).reversed()); 29 | } 30 | 31 | private TotalCapacity toCapacity(SimulatedCapabilities simulatedCapabilities) { 32 | List capabilities = simulatedCapabilities.capabilities(); 33 | List capacityDimensions = new ArrayList<>(capabilities); 34 | return new TotalCapacity(capacityDimensions); 35 | } 36 | 37 | private List toItems(List projectsSimulations) { 38 | return projectsSimulations 39 | .stream() 40 | .map(this::toItem) 41 | .toList(); 42 | } 43 | 44 | private Item toItem(SimulatedProject simulatedProject) { 45 | List missingDemands = simulatedProject.missingDemands().all(); 46 | List weights = new ArrayList<>(missingDemands); 47 | return new Item(simulatedProject.projectId().toString(), simulatedProject.calculateValue().doubleValue(), new TotalWeight(weights)); 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/sorter/Edge.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.sorter; 2 | 3 | public record Edge(int source, int target) { 4 | @Override 5 | public String toString() { 6 | return "(" + source + " -> " + target + ")"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/sorter/FeedbackArcSeOnGraph.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.sorter; 2 | 3 | import java.util.*; 4 | 5 | public class FeedbackArcSeOnGraph { 6 | 7 | public List calculate(List> initialNodes) { 8 | Map> adjacencyList = createAdjacencyList(initialNodes); 9 | int v = adjacencyList.size(); 10 | List feedbackEdges = new ArrayList<>(); 11 | int[] visited = new int[v + 1]; 12 | Iterator nodes = adjacencyList.keySet().iterator(); 13 | while (nodes.hasNext()) { 14 | Integer i = nodes.next(); 15 | List neighbours = adjacencyList.get(i); 16 | if (neighbours.size() != 0) { 17 | visited[i] = 1; 18 | for (int j = 0; j < neighbours.size(); j++) { 19 | if (visited[neighbours.get(j)] == 1) { 20 | feedbackEdges.add(new Edge(i, neighbours.get(j))); 21 | } else { 22 | visited[neighbours.get(j)] = 1; 23 | } 24 | } 25 | } 26 | } 27 | return feedbackEdges; 28 | } 29 | 30 | private Map> createAdjacencyList(List> initialNodes) { 31 | Map> adjacencyList = new HashMap<>(); 32 | 33 | for (int i = 1; i <= initialNodes.size(); i++) { 34 | adjacencyList.put(i, new LinkedList<>()); 35 | } 36 | 37 | for (int i = 0; i < initialNodes.size(); i++) { 38 | List dependencies = new ArrayList<>(); 39 | for (Node dependency : initialNodes.get(i).dependencies().nodes()) { 40 | dependencies.add(initialNodes.indexOf(dependency) + 1); 41 | } 42 | adjacencyList.put(i + 1, dependencies); 43 | } 44 | return adjacencyList; 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/sorter/GraphTopologicalSort.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.sorter; 2 | 3 | import java.util.List; 4 | 5 | 6 | public class GraphTopologicalSort { 7 | 8 | public SortedNodes sort(Nodes nodes) { 9 | return createSortedNodesRecursively(nodes, SortedNodes.empty()); 10 | } 11 | 12 | private SortedNodes createSortedNodesRecursively(Nodes remainingNodes, SortedNodes accumulatedSortedNodes) { 13 | List> alreadyProcessedNodes = accumulatedSortedNodes.all() 14 | .stream() 15 | .flatMap(n -> n.all().stream()) 16 | .toList(); 17 | Nodes nodesWithoutDependencies = 18 | remainingNodes 19 | .withAllDependenciesPresentIn(alreadyProcessedNodes); 20 | 21 | if (nodesWithoutDependencies.all().isEmpty()) { 22 | return accumulatedSortedNodes; 23 | } 24 | SortedNodes newSortedNodes = accumulatedSortedNodes.add(nodesWithoutDependencies); 25 | remainingNodes = remainingNodes.removeAll(nodesWithoutDependencies.all()); 26 | return createSortedNodesRecursively(remainingNodes, newSortedNodes); 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/sorter/Node.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.sorter; 2 | 3 | 4 | import java.util.HashSet; 5 | import java.util.Objects; 6 | 7 | public record Node(String name, Nodes dependencies, T content) { 8 | public Node(String name) { 9 | this(name, new Nodes<>(new HashSet<>()), null); 10 | } 11 | 12 | public Node(String name, T content) { 13 | this(name, new Nodes<>(new HashSet<>()), content); 14 | } 15 | 16 | public Node dependsOn(Node node) { 17 | return new Node<>(this.name, this.dependencies.add(node), content); 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return name; 23 | } 24 | 25 | @Override 26 | public boolean equals(Object o) { 27 | if (this == o) return true; 28 | if (o == null || getClass() != o.getClass()) return false; 29 | Node node = (Node) o; 30 | return name.equals(node.name); 31 | } 32 | 33 | @Override 34 | public int hashCode() { 35 | return Objects.hash(name); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/sorter/Nodes.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.sorter; 2 | 3 | import java.util.*; 4 | 5 | import static java.util.stream.Collectors.toSet; 6 | import static java.util.stream.Stream.concat; 7 | import static java.util.stream.Stream.of; 8 | 9 | 10 | public record Nodes(Set> nodes) { 11 | 12 | public Nodes(Node... nodes) { 13 | this(new HashSet<>(Arrays.asList(nodes))); 14 | } 15 | 16 | Set> all() { 17 | return Collections.unmodifiableSet(nodes); 18 | } 19 | 20 | Nodes add(Node node) { 21 | Set > newNode = concat(this.nodes.stream(), of(node)).collect(toSet()); 22 | return new Nodes<>(newNode); 23 | } 24 | 25 | Nodes withAllDependenciesPresentIn(Collection> nodes) { 26 | return new Nodes<>( 27 | all() 28 | .stream() 29 | .filter(n -> nodes.containsAll(n.dependencies().all())) 30 | .collect(toSet())); 31 | } 32 | 33 | Nodes removeAll(Collection> nodes) { 34 | return new Nodes<>( 35 | all() 36 | .stream() 37 | .filter(s -> !nodes.contains(s)) 38 | .collect(toSet())); 39 | } 40 | 41 | @Override 42 | public String toString() { 43 | return "Nodes{" + 44 | "node=" + nodes + 45 | '}'; 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/main/java/domaindrivers/smartschedule/sorter/SortedNodes.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.sorter; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | import java.util.stream.Stream; 7 | 8 | public record SortedNodes(List> all) { 9 | 10 | public static SortedNodes empty() { 11 | return new SortedNodes<>(new ArrayList<>()); 12 | } 13 | 14 | public SortedNodes add(Nodes newNodes) { 15 | List> result = 16 | Stream 17 | .concat(this.all.stream(), Stream.of(newNodes)) 18 | .collect(Collectors.toList()); 19 | return new SortedNodes<>(result); 20 | } 21 | 22 | @Override 23 | public String toString() { 24 | return "SortedNodes: " + all; 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.sql.init.mode=always 2 | spring.datasource.hikari.data-source-properties.reWriteBatchedInserts=true 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/schema-allocations.sql: -------------------------------------------------------------------------------- 1 | create table if not exists project_allocations ( 2 | project_allocations_id uuid not null, 3 | allocations jsonb not null, 4 | demands jsonb not null, 5 | from_date timestamp, 6 | to_date timestamp, 7 | primary key (project_allocations_id)); 8 | 9 | create table if not exists allocatable_capabilities ( 10 | id uuid not null, 11 | resource_id uuid not null, 12 | possible_capabilities jsonb not null, 13 | from_date timestamp not null, 14 | to_date timestamp not null, 15 | primary key (id)); 16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/schema-availability.sql: -------------------------------------------------------------------------------- 1 | create table if not exists availabilities ( 2 | id uuid not null, 3 | resource_id uuid not null, 4 | resource_parent_id uuid, 5 | version bigserial not null, 6 | from_date timestamp not null, 7 | to_date timestamp not null, 8 | taken_by uuid, 9 | disabled boolean not null, 10 | primary key (id), 11 | unique(resource_id, from_date, to_date)); 12 | 13 | -------------------------------------------------------------------------------- /src/main/resources/schema-capability-scheduling.sql: -------------------------------------------------------------------------------- 1 | create table if not exists allocatable_capabilities ( 2 | id uuid not null, 3 | resource_id uuid not null, 4 | possible_capabilities jsonb not null, 5 | from_date timestamp not null, 6 | to_date timestamp not null, 7 | primary key (id)); 8 | -------------------------------------------------------------------------------- /src/main/resources/schema-cashflow.sql: -------------------------------------------------------------------------------- 1 | create table if not exists cashflows ( 2 | project_allocations_id uuid not null, 3 | version bigserial not null, 4 | cost bigint, 5 | income bigint, 6 | primary key (project_allocations_id)); 7 | -------------------------------------------------------------------------------- /src/main/resources/schema-resources.sql: -------------------------------------------------------------------------------- 1 | create table if not exists employees 2 | ( 3 | employee_id uuid not null, 4 | version bigserial not null, 5 | name varchar not null, 6 | seniority varchar not null, 7 | last_name varchar not null, 8 | capabilities jsonb not null, 9 | primary key (employee_id) 10 | ); 11 | 12 | create table if not exists devices 13 | ( 14 | device_id uuid not null, 15 | version bigserial not null, 16 | model varchar not null, 17 | capabilities jsonb not null, 18 | primary key (device_id) 19 | ); 20 | -------------------------------------------------------------------------------- /src/main/resources/schema-risk.sql: -------------------------------------------------------------------------------- 1 | create table if not exists project_risk_sagas ( 2 | project_risk_saga_id uuid not null, 3 | project_allocations_id uuid not null, 4 | earnings bigint, 5 | missing_demands jsonb, 6 | deadline timestamp, 7 | version bigserial not null, 8 | primary key (project_risk_saga_id)); 9 | 10 | -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/ArchitectureDependencyTest.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule; 2 | 3 | 4 | import com.tngtech.archunit.core.domain.JavaClasses; 5 | import com.tngtech.archunit.core.importer.ClassFileImporter; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static com.tngtech.archunit.library.Architectures.layeredArchitecture; 9 | 10 | 11 | public class ArchitectureDependencyTest { 12 | 13 | private final JavaClasses classes = new ClassFileImporter().importPackages("domaindrivers.smartschedule"); 14 | 15 | @Test 16 | void checkDependencies() { 17 | layeredArchitecture().consideringOnlyDependenciesInLayers() 18 | .layer("availability").definedBy("domaindrivers.smartschedule.availability..") 19 | .layer("allocation").definedBy("domaindrivers.smartschedule.allocation..") 20 | .layer("capabilityscheduling").definedBy("domaindrivers.smartschedule.allocation.capabilityscheduling..") 21 | .layer("capabilityscheduling-acl").definedBy("domaindrivers.smartschedule.allocation.capabilityscheduling.legacyacl..") 22 | .layer("cashflow").definedBy("domaindrivers.smartschedule.allocation.cashflow..") 23 | .layer("parallelization").definedBy("domaindrivers.smartschedule.planning.parallelization..") 24 | .layer("sorter").definedBy("domaindrivers.smartschedule.sorter..") 25 | .layer("simulation").definedBy("domaindrivers.smartschedule.simulation..") 26 | .layer("optimization").definedBy("domaindrivers.smartschedule.optimization..") 27 | .layer("resource").definedBy("domaindrivers.smartschedule.resource..") 28 | .layer("employee").definedBy("domaindrivers.smartschedule.resource.employee..") 29 | .layer("device").definedBy("domaindrivers.smartschedule.resource.device..") 30 | .layer("shared").definedBy("domaindrivers.smartschedule.shared..") 31 | .whereLayer("availability").mayOnlyAccessLayers("shared") 32 | .whereLayer("allocation").mayOnlyAccessLayers("shared", "availability", "simulation", "optimization", "capabilityscheduling") 33 | .whereLayer("parallelization").mayOnlyAccessLayers("sorter", "shared", "availability") 34 | .whereLayer("sorter").mayNotAccessAnyLayer() 35 | .whereLayer("simulation").mayOnlyAccessLayers("optimization", "shared") 36 | .whereLayer("optimization").mayOnlyAccessLayers("shared") 37 | .whereLayer("cashflow").mayOnlyAccessLayers("shared", "allocation") 38 | .whereLayer("capabilityscheduling").mayOnlyAccessLayers("shared", "availability") 39 | .whereLayer("employee").mayOnlyAccessLayers("capabilityscheduling", "shared") 40 | .whereLayer("device").mayOnlyAccessLayers("capabilityscheduling", "shared") 41 | .whereLayer("capabilityscheduling-acl").mayOnlyAccessLayers("shared", "capabilityscheduling") 42 | .whereLayer("shared").mayNotAccessAnyLayer() 43 | .check(classes); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/MockedClockConfiguration.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule; 2 | 3 | import org.springframework.boot.test.context.TestConfiguration; 4 | import org.springframework.boot.test.mock.mockito.MockBean; 5 | 6 | import java.time.Clock; 7 | 8 | @TestConfiguration(proxyBeanMethods = false) 9 | public class MockedClockConfiguration { 10 | 11 | @MockBean 12 | Clock clock; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/MockedEventPublisherConfiguration.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule; 2 | 3 | import domaindrivers.smartschedule.shared.EventsPublisher; 4 | import org.springframework.boot.test.context.TestConfiguration; 5 | import org.springframework.boot.test.mock.mockito.MockBean; 6 | 7 | @TestConfiguration(proxyBeanMethods = false) 8 | public class MockedEventPublisherConfiguration { 9 | 10 | @MockBean 11 | EventsPublisher eventsPublisher; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/TaskExecutorConfiguration.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.Primary; 6 | import org.springframework.core.task.SyncTaskExecutor; 7 | import org.springframework.core.task.TaskExecutor; 8 | 9 | @Configuration 10 | public class TaskExecutorConfiguration { 11 | 12 | @Bean 13 | @Primary 14 | public TaskExecutor taskExecutor() { 15 | return new SyncTaskExecutor(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/TestDbConfiguration.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule; 2 | 3 | import domaindrivers.smartschedule.planning.PlanningDbTestConfiguration; 4 | import org.springframework.boot.jdbc.DataSourceBuilder; 5 | import org.springframework.boot.test.context.TestConfiguration; 6 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Import; 9 | import org.springframework.jdbc.core.JdbcTemplate; 10 | import org.testcontainers.containers.PostgreSQLContainer; 11 | 12 | import javax.sql.DataSource; 13 | 14 | @TestConfiguration(proxyBeanMethods = false) 15 | @Import(PlanningDbTestConfiguration.class) 16 | public class TestDbConfiguration { 17 | 18 | @Bean 19 | @ServiceConnection 20 | public PostgreSQLContainer postgreSQLContainer() { 21 | return new PostgreSQLContainer("postgres:15-alpine"); 22 | } 23 | 24 | @Bean 25 | JdbcTemplate jdbcTemplate(DataSource dataSource) { 26 | return new JdbcTemplate(dataSource); 27 | } 28 | 29 | @Bean 30 | public DataSource dataSource(PostgreSQLContainer postgres) { 31 | DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create(); 32 | dataSourceBuilder.username(postgres.getUsername()); 33 | dataSourceBuilder.password(postgres.getPassword()); 34 | dataSourceBuilder.driverClassName(postgres.getDriverClassName()); 35 | dataSourceBuilder.url(postgres.getJdbcUrl()); 36 | return dataSourceBuilder.build(); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/allocation/CreateHourlyDemandsSummaryServiceTest.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.time.Instant; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import static domaindrivers.smartschedule.shared.capability.Capability.skill; 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | 14 | class CreateHourlyDemandsSummaryServiceTest { 15 | 16 | static Instant NOW = Instant.now(); 17 | static TimeSlot JAN = TimeSlot.createMonthlyTimeSlotAtUTC(2021, 1); 18 | static Demands CSHARP = Demands.of(new Demand(skill("CSHARP"), JAN)); 19 | static Demands JAVA = Demands.of(new Demand(skill("JAVA"), JAN)); 20 | 21 | CreateHourlyDemandsSummaryService service = new CreateHourlyDemandsSummaryService(); 22 | 23 | @Test 24 | void createsMissingDemandsSummaryForAllGivenProjects() { 25 | //given 26 | ProjectAllocationsId csharpProjectId = ProjectAllocationsId.newOne(); 27 | ProjectAllocationsId javaProjectId = ProjectAllocationsId.newOne(); 28 | ProjectAllocations csharpProject = new ProjectAllocations(csharpProjectId, Allocations.none(), CSHARP, JAN); 29 | ProjectAllocations javaProject = new ProjectAllocations(javaProjectId, Allocations.none(), JAVA, JAN); 30 | 31 | //when 32 | NotSatisfiedDemands result = service.create(List.of(csharpProject, javaProject), NOW); 33 | 34 | //then 35 | assertEquals(NOW, result.occurredAt()); 36 | Map expectedMissingDemands = 37 | Map.of(javaProjectId, JAVA, csharpProjectId, CSHARP); 38 | assertThat(result.missingDemands()).containsExactlyInAnyOrderEntriesOf(expectedMissingDemands); 39 | } 40 | 41 | @Test 42 | void takesIntoAccountOnlyProjectsWithTimeSlot() { 43 | //given 44 | ProjectAllocationsId withTimeSlotId = ProjectAllocationsId.newOne(); 45 | ProjectAllocationsId withoutTimeSlotId = ProjectAllocationsId.newOne(); 46 | ProjectAllocations withTimeSlot = new ProjectAllocations(withTimeSlotId, Allocations.none(), CSHARP, JAN); 47 | ProjectAllocations withoutTimeSlot = new ProjectAllocations(withoutTimeSlotId, Allocations.none(), JAVA); 48 | 49 | //when 50 | NotSatisfiedDemands result = service.create(List.of(withTimeSlot, withoutTimeSlot), NOW); 51 | 52 | //then 53 | assertEquals(NOW, result.occurredAt()); 54 | Map expectedMissingDemands = 55 | Map.of(withTimeSlotId, CSHARP); 56 | assertThat(result.missingDemands()).containsExactlyInAnyOrderEntriesOf(expectedMissingDemands); 57 | } 58 | 59 | 60 | } -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/allocation/CreatingNewProjectTest.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | import domaindrivers.smartschedule.allocation.capabilityscheduling.CapabilityFinder; 4 | import domaindrivers.smartschedule.availability.AvailabilityFacade; 5 | import domaindrivers.smartschedule.shared.EventsPublisher; 6 | import domaindrivers.smartschedule.shared.capability.Capability; 7 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 8 | import org.junit.jupiter.api.Test; 9 | import org.mockito.ArgumentMatcher; 10 | import org.mockito.Mockito; 11 | 12 | import java.time.Clock; 13 | import java.util.Set; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | import static org.mockito.Mockito.mock; 17 | 18 | 19 | class CreatingNewProjectTest { 20 | 21 | EventsPublisher eventsPublisher = mock(EventsPublisher.class); 22 | 23 | AllocationFacade allocationFacade = 24 | new AllocationFacade(new InMemoryProjectAllocationsRepository(), 25 | mock(AvailabilityFacade.class), 26 | mock(CapabilityFinder.class), 27 | eventsPublisher, 28 | Clock.systemDefaultZone()); 29 | 30 | 31 | static TimeSlot JAN = TimeSlot.createDailyTimeSlotAtUTC(2021, 1, 1); 32 | static TimeSlot FEB = TimeSlot.createDailyTimeSlotAtUTC(2021, 2, 1); 33 | 34 | @Test 35 | void canCreateNewProject() { 36 | //given 37 | Demand demand = new Demand(Capability.skill("JAVA"), JAN); 38 | 39 | //when 40 | Demands demands = Demands.of(demand); 41 | ProjectAllocationsId newProject = allocationFacade.createAllocation(JAN, demands); 42 | 43 | //then 44 | ProjectsAllocationsSummary summary = allocationFacade.findAllProjectsAllocations(Set.of(newProject)); 45 | assertThat(summary.demands().get(newProject)).isEqualTo(demands); 46 | assertThat(summary.timeSlots().get(newProject)).isEqualTo(JAN); 47 | Mockito.verify(eventsPublisher).publish(Mockito.argThat(isProjectAllocationsScheduledEvent(newProject, JAN))); 48 | } 49 | 50 | @Test 51 | void canRedefineProjectDeadline() { 52 | //given 53 | Demand demand = new Demand(Capability.skill("JAVA"), JAN); 54 | //and 55 | Demands demands = Demands.of(demand); 56 | ProjectAllocationsId newProject = allocationFacade.createAllocation(JAN, demands); 57 | 58 | //when 59 | allocationFacade.editProjectDates(newProject, FEB); 60 | 61 | //then 62 | ProjectsAllocationsSummary summary = allocationFacade.findAllProjectsAllocations(Set.of(newProject)); 63 | assertThat(summary.timeSlots().get(newProject)).isEqualTo(FEB); 64 | Mockito.verify(eventsPublisher).publish(Mockito.argThat(isProjectAllocationsScheduledEvent(newProject, FEB))); 65 | } 66 | 67 | ArgumentMatcher isProjectAllocationsScheduledEvent(ProjectAllocationsId projectId, TimeSlot timeSlot) { 68 | return event -> 69 | event.uuid() != null && 70 | event.projectId().equals(projectId) && 71 | event.fromTo().equals(timeSlot) && 72 | event.occurredAt() != null; 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/allocation/DemandSchedulingTest.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | import domaindrivers.smartschedule.allocation.capabilityscheduling.CapabilityFinder; 4 | import domaindrivers.smartschedule.availability.AvailabilityFacade; 5 | import domaindrivers.smartschedule.shared.EventsPublisher; 6 | import domaindrivers.smartschedule.shared.capability.Capability; 7 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.time.Clock; 11 | import java.time.Instant; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | import static org.mockito.Mockito.mock; 15 | 16 | 17 | class DemandSchedulingTest { 18 | 19 | static final Demand JAVA = new Demand( 20 | Capability.skill("JAVA"), 21 | TimeSlot.createDailyTimeSlotAtUTC(2022, 2, 2)); 22 | static final TimeSlot PROJECT_DATES = new TimeSlot( 23 | Instant.parse("2021-01-01T00:00:00.00Z"), 24 | Instant.parse("2021-01-06T00:00:00.00Z")); 25 | 26 | AllocationFacade allocationFacade = 27 | new AllocationFacade(new InMemoryProjectAllocationsRepository(), 28 | mock(AvailabilityFacade.class), 29 | mock(CapabilityFinder.class), 30 | mock(EventsPublisher.class), 31 | Clock.systemDefaultZone()); 32 | 33 | @Test 34 | void canScheduleProjectDemands() { 35 | //given 36 | ProjectAllocationsId projectId = ProjectAllocationsId.newOne(); 37 | 38 | //when 39 | allocationFacade.scheduleProjectAllocationDemands(projectId, Demands.of(JAVA)); 40 | 41 | //then 42 | ProjectsAllocationsSummary summary = allocationFacade.findAllProjectsAllocations(); 43 | assertThat(summary.projectAllocations()).containsKey(projectId); 44 | assertThat(summary.projectAllocations().get(projectId).all()).hasSize(0); 45 | assertThat(summary.demands().get(projectId).all()).containsOnlyOnceElementsOf(Demands.of(JAVA).all()); 46 | } 47 | 48 | 49 | } -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/allocation/InMemoryProjectAllocationsRepository.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation; 2 | 3 | 4 | import java.time.Instant; 5 | import java.util.*; 6 | 7 | 8 | class InMemoryProjectAllocationsRepository implements ProjectAllocationsRepository { 9 | 10 | private final Map projects = new HashMap<>(); 11 | 12 | 13 | @Override 14 | public Optional findById(ProjectAllocationsId projectId) { 15 | return Optional.ofNullable(projects.get(projectId)); 16 | } 17 | 18 | @Override 19 | public ProjectAllocations save(ProjectAllocations project) { 20 | return projects.put(project.id(), project); 21 | } 22 | 23 | @Override 24 | public List findAllById(Set projectIds) { 25 | return projects 26 | .values() 27 | .stream() 28 | .filter(project -> projectIds.contains(project.id())) 29 | .toList(); 30 | } 31 | 32 | @Override 33 | public List findAll() { 34 | return new ArrayList<>(projects.values()); 35 | } 36 | 37 | @Override 38 | public List findAllContainingDate(Instant when) { 39 | return projects 40 | .values() 41 | .stream() 42 | .filter(project -> project.timeSlot() != null) 43 | .toList(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/allocation/capabilityscheduling/legacyacl/TranslateToCapabilitySelectorTest.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.capabilityscheduling.legacyacl; 2 | 3 | import domaindrivers.smartschedule.shared.CapabilitySelector; 4 | import domaindrivers.smartschedule.shared.capability.Capability; 5 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.List; 9 | import java.util.Set; 10 | import java.util.UUID; 11 | 12 | import static domaindrivers.smartschedule.shared.CapabilitySelector.canPerformOneOf; 13 | import static domaindrivers.smartschedule.shared.capability.Capability.permission; 14 | import static domaindrivers.smartschedule.shared.capability.Capability.skill; 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | 17 | 18 | class TranslateToCapabilitySelectorTest { 19 | 20 | @Test 21 | void translateLegacyEsbMessageToCapabilitySelectorModel() { 22 | //given 23 | List legacyPermissions = 24 | List.of("ADMIN<>2", "ROOT<>1"); 25 | List> legacySkillsPerformedTogether = List.of( 26 | List.of("JAVA", "CSHARP", "PYTHON"), 27 | List.of("RUST", "CSHARP", "PYTHON") 28 | ); 29 | List legacyExclusiveSkills = List.of("YT DRAMA COMMENTS"); 30 | 31 | //when 32 | List result = translate(legacySkillsPerformedTogether, legacyExclusiveSkills, legacyPermissions); 33 | 34 | //then 35 | assertThat(result) 36 | .containsExactlyInAnyOrder( 37 | canPerformOneOf(Set.of(skill("YT DRAMA COMMENTS"))), 38 | CapabilitySelector.canPerformAllAtTheTime(Capability.skills("JAVA", "CSHARP", "PYTHON")), 39 | CapabilitySelector.canPerformAllAtTheTime(Capability.skills("RUST", "CSHARP", "PYTHON")), 40 | canPerformOneOf(Set.of(permission("ADMIN"))), 41 | canPerformOneOf(Set.of(permission("ADMIN"))), 42 | canPerformOneOf(Set.of(permission("ROOT"))) 43 | ); 44 | 45 | } 46 | 47 | @Test 48 | void zeroMeansNoPermissionNowhere() { 49 | List legacyPermissions = 50 | List.of("ADMIN<>0"); 51 | 52 | //when 53 | List< CapabilitySelector> result = translate(List.of(), List.of(), legacyPermissions); 54 | 55 | //then 56 | assertThat(result) 57 | .isEmpty(); 58 | } 59 | 60 | List translate(List> legacySkillsPerformedTogether, List legacyExclusiveSkills, List legacyPermissions) { 61 | return new TranslateToCapabilitySelector().translate(new EmployeeDataFromLegacyEsbMessage(UUID.randomUUID(), legacySkillsPerformedTogether, legacyExclusiveSkills, legacyPermissions, TimeSlot.empty())); 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/allocation/cashflow/CashFlowFacadeTest.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.cashflow; 2 | 3 | import domaindrivers.smartschedule.allocation.ProjectAllocationsId; 4 | import domaindrivers.smartschedule.shared.EventsPublisher; 5 | import org.junit.jupiter.api.Test; 6 | import org.mockito.ArgumentMatcher; 7 | import org.mockito.Mockito; 8 | 9 | import java.time.Clock; 10 | import java.time.Instant; 11 | import java.time.ZoneId; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | import static org.mockito.ArgumentMatchers.argThat; 15 | 16 | 17 | class CashFlowFacadeTest { 18 | 19 | static final Instant NOW = Instant.now(); 20 | 21 | EventsPublisher eventsPublisher = Mockito.mock(EventsPublisher.class); 22 | Clock clock = Clock.fixed(NOW, ZoneId.systemDefault()); 23 | CashFlowFacade cashFlowFacade = CashFlowTestConfiguration.cashFlowFacade(eventsPublisher, clock); 24 | 25 | @Test 26 | void canSaveCashFlow() { 27 | //given 28 | ProjectAllocationsId projectId = ProjectAllocationsId.newOne(); 29 | 30 | //when 31 | cashFlowFacade.addIncomeAndCost(projectId, Income.of(100), Cost.of(50)); 32 | 33 | //then 34 | assertEquals(Earnings.of(50), cashFlowFacade.find(projectId)); 35 | } 36 | 37 | @Test 38 | void updatingCashFlowEmitsAnEvent() { 39 | //given 40 | ProjectAllocationsId projectId = ProjectAllocationsId.newOne(); 41 | Income income = Income.of(100); 42 | Cost cost = Cost.of(50); 43 | 44 | //when 45 | cashFlowFacade.addIncomeAndCost(projectId, income, cost); 46 | 47 | //then 48 | Mockito.verify(eventsPublisher).publish(argThat(isEarningsRecalculatedEvent(projectId, Earnings.of(50)))); 49 | } 50 | 51 | ArgumentMatcher isEarningsRecalculatedEvent(ProjectAllocationsId projectId, Earnings earnings) { 52 | return event -> event.projectId().equals(projectId) && 53 | event.earnings().equals(earnings) && 54 | event.occurredAt() != null; 55 | } 56 | 57 | 58 | } -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/allocation/cashflow/CashFlowTestConfiguration.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.cashflow; 2 | 3 | 4 | import domaindrivers.smartschedule.allocation.ProjectAllocationsId; 5 | import domaindrivers.smartschedule.shared.EventsPublisher; 6 | 7 | import java.time.Clock; 8 | import java.util.*; 9 | 10 | class CashFlowTestConfiguration { 11 | 12 | static CashFlowFacade cashFlowFacade(EventsPublisher eventsPublisher, Clock clock) { 13 | return new CashFlowFacade(new InMemoryCashflowRepository(), eventsPublisher, clock); 14 | } 15 | 16 | } 17 | 18 | class InMemoryCashflowRepository implements CashflowRepository { 19 | 20 | private final Map cashflows = new HashMap<>(); 21 | 22 | @Override 23 | public Cashflow save(Cashflow cashflow) { 24 | return cashflows.put(cashflow.projectId, cashflow); 25 | } 26 | 27 | @Override 28 | public List findAll() { 29 | return new ArrayList<>(cashflows.values()); 30 | } 31 | 32 | @Override 33 | public Optional findById(ProjectAllocationsId projectId) { 34 | return Optional.ofNullable(cashflows.get(projectId)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/allocation/cashflow/EarningsTest.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.allocation.cashflow; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static java.math.BigDecimal.TEN; 6 | import static org.junit.jupiter.api.Assertions.*; 7 | 8 | class EarningsTest { 9 | 10 | @Test 11 | void incomeMinusCostTest() { 12 | //expect 13 | assertEquals(Earnings.of(9), Income.of(TEN).minus(Cost.of(1))); 14 | assertEquals(Earnings.of(8), Income.of(TEN).minus(Cost.of(2))); 15 | assertEquals(Earnings.of(7), Income.of(TEN).minus(Cost.of(3))); 16 | assertEquals(Earnings.of(-70), Income.of(100).minus(Cost.of(170))); 17 | } 18 | 19 | @Test 20 | void greaterThanTest() { 21 | assertTrue(Earnings.of(10).greaterThan(Earnings.of(9))); 22 | assertTrue(Earnings.of(10).greaterThan(Earnings.of(0))); 23 | assertTrue(Earnings.of(10).greaterThan(Earnings.of(-1))); 24 | assertFalse(Earnings.of(10).greaterThan(Earnings.of(10))); 25 | assertFalse(Earnings.of(10).greaterThan(Earnings.of(11))); 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/availability/ResourceAvailabilityLoadingTest.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.availability; 2 | 3 | import domaindrivers.smartschedule.MockedEventPublisherConfiguration; 4 | import domaindrivers.smartschedule.TestDbConfiguration; 5 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.jdbc.core.JdbcTemplate; 11 | import org.springframework.test.context.jdbc.Sql; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | import static org.junit.jupiter.api.Assertions.assertThrows; 15 | 16 | @SpringBootTest(classes = {TestDbConfiguration.class, MockedEventPublisherConfiguration.class}) 17 | @Sql(scripts = "classpath:schema-availability.sql") 18 | public class ResourceAvailabilityLoadingTest { 19 | 20 | static final TimeSlot ONE_MONTH = TimeSlot.createDailyTimeSlotAtUTC(2021, 1, 1); 21 | 22 | @Autowired 23 | JdbcTemplate jdbcTemplate; 24 | 25 | ResourceAvailabilityRepository resourceAvailabilityRepository; 26 | 27 | @BeforeEach 28 | void setup() { 29 | resourceAvailabilityRepository = new ResourceAvailabilityRepository(jdbcTemplate); 30 | } 31 | 32 | @Test 33 | void canSaveAndLoadById() { 34 | //given 35 | ResourceAvailabilityId resourceAvailabilityId = ResourceAvailabilityId.newOne(); 36 | ResourceId resourceId = ResourceId.newOne(); 37 | ResourceAvailability resourceAvailability = new ResourceAvailability(resourceAvailabilityId, resourceId, ONE_MONTH); 38 | 39 | //when 40 | resourceAvailabilityRepository.saveNew(resourceAvailability); 41 | 42 | //then 43 | ResourceAvailability loaded = resourceAvailabilityRepository.loadById(resourceAvailability.id()); 44 | assertEquals(resourceAvailability, loaded); 45 | assertEquals(resourceAvailability.segment(), loaded.segment()); 46 | assertEquals(resourceAvailability.resourceId(), loaded.resourceId()); 47 | assertEquals(resourceAvailability.blockedBy(), loaded.blockedBy()); 48 | } 49 | } -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/availability/ResourceAvailabilityUniquenessTest.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.availability; 2 | 3 | import domaindrivers.smartschedule.MockedEventPublisherConfiguration; 4 | import domaindrivers.smartschedule.TestDbConfiguration; 5 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.dao.DuplicateKeyException; 11 | import org.springframework.jdbc.core.JdbcTemplate; 12 | import org.springframework.test.context.jdbc.Sql; 13 | 14 | import static org.junit.jupiter.api.Assertions.assertEquals; 15 | import static org.junit.jupiter.api.Assertions.assertThrows; 16 | 17 | @SpringBootTest(classes = {TestDbConfiguration.class, MockedEventPublisherConfiguration.class}) 18 | @Sql(scripts = "classpath:schema-availability.sql") 19 | class ResourceAvailabilityUniquenessTest { 20 | 21 | static final TimeSlot ONE_MONTH = TimeSlot.createDailyTimeSlotAtUTC(2021, 1, 1); 22 | 23 | @Autowired 24 | JdbcTemplate jdbcTemplate; 25 | 26 | ResourceAvailabilityRepository resourceAvailabilityRepository; 27 | 28 | @BeforeEach 29 | void setup() { 30 | resourceAvailabilityRepository = new ResourceAvailabilityRepository(jdbcTemplate); 31 | } 32 | 33 | @Test 34 | void cantSaveTwoAvailabilitiesWithSameResourceIdAndSegment() { 35 | //given 36 | ResourceId resourceId = ResourceId.newOne(); 37 | ResourceId anotherResourceId = ResourceId.newOne(); 38 | ResourceAvailabilityId resourceAvailabilityId = ResourceAvailabilityId.newOne(); 39 | 40 | //when 41 | resourceAvailabilityRepository.saveNew(new ResourceAvailability(resourceAvailabilityId, resourceId, ONE_MONTH)); 42 | 43 | //expect 44 | assertThrows(DuplicateKeyException.class, () -> resourceAvailabilityRepository.saveNew(new ResourceAvailability(resourceAvailabilityId, anotherResourceId, ONE_MONTH))); 45 | } 46 | 47 | 48 | 49 | } -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/optimization/CapabilityCapacityDimension.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.optimization; 2 | 3 | 4 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 5 | 6 | import java.util.UUID; 7 | 8 | record CapabilityCapacityDimension(UUID uuid, String id, String capacityName, String capacityType) implements CapacityDimension { 9 | 10 | public CapabilityCapacityDimension(String id, String capacityName, String capacityType) { 11 | this(UUID.randomUUID(), id, capacityName, capacityType); 12 | } 13 | } 14 | 15 | record CapabilityWeightDimension(String name, String type) implements WeightDimension { 16 | 17 | @Override 18 | public boolean isSatisfiedBy(CapabilityCapacityDimension capacityDimension) { 19 | return capacityDimension.capacityName().equals(name) && 20 | capacityDimension.capacityType().equals(type); 21 | } 22 | } 23 | 24 | record CapabilityTimedCapacityDimension(UUID uuid, String id, String capacityName, String capacityType, TimeSlot timeSlot) implements CapacityDimension { 25 | 26 | public CapabilityTimedCapacityDimension(String id, String capacityName, String capacityType, TimeSlot timeSlot) { 27 | this(UUID.randomUUID(), id, capacityName, capacityType, timeSlot); 28 | } 29 | } 30 | 31 | record CapabilityTimedWeightDimension(String name, String type, TimeSlot timeSlot) implements WeightDimension { 32 | 33 | @Override 34 | public boolean isSatisfiedBy(CapabilityTimedCapacityDimension capacityTimedDimension) { 35 | return capacityTimedDimension.capacityName().equals(name) && 36 | capacityTimedDimension.capacityType().equals(type) && 37 | this.timeSlot.within(capacityTimedDimension.timeSlot()); 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/optimization/OptimizationForTimedCapabilitiesTest.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.optimization; 2 | 3 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.List; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | class OptimizationForTimedCapabilitiesTest { 11 | 12 | OptimizationFacade facade = new OptimizationFacade(); 13 | 14 | @Test 15 | void nothingIsChosenWhenNoCapacitiesInTimeSlot() { 16 | //given 17 | TimeSlot june = TimeSlot.createMonthlyTimeSlotAtUTC(2020, 6); 18 | TimeSlot october = TimeSlot.createMonthlyTimeSlotAtUTC(2020, 10); 19 | 20 | List items = List.of( 21 | new Item("Item1", 100, 22 | TotalWeight.of(new CapabilityTimedWeightDimension("COMMON SENSE", "Skill", june))), 23 | new Item("Item2", 100, 24 | TotalWeight.of(new CapabilityTimedWeightDimension("THINKING", "Skill", june)))); 25 | 26 | //when 27 | Result result = facade.calculate(items, TotalCapacity.of( 28 | new CapabilityTimedCapacityDimension("anna", "COMMON SENSE", "Skill", october) 29 | )); 30 | 31 | //then 32 | assertEquals(0, result.profit(), 0.0d); 33 | assertEquals(0, result.chosenItems().size()); 34 | } 35 | 36 | @Test 37 | void mostProfitableItemIsChosen() { 38 | //given 39 | TimeSlot june = TimeSlot.createMonthlyTimeSlotAtUTC(2020, 6); 40 | 41 | List items = List.of( 42 | new Item("Item1", 200, 43 | TotalWeight.of(new CapabilityTimedWeightDimension("COMMON SENSE", "Skill", june))), 44 | new Item("Item2", 100, 45 | TotalWeight.of(new CapabilityTimedWeightDimension("THINKING", "Skill", june)))); 46 | 47 | //when 48 | Result result = facade.calculate(items, TotalCapacity.of( 49 | new CapabilityTimedCapacityDimension("anna", "COMMON SENSE", "Skill", june) 50 | )); 51 | 52 | //then 53 | assertEquals(200, result.profit(), 0.0d); 54 | assertEquals(1, result.chosenItems().size()); 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/planning/PlanningDbTestConfiguration.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.Primary; 6 | 7 | import java.util.*; 8 | 9 | @Configuration 10 | public class PlanningDbTestConfiguration { 11 | 12 | static ProjectRepository inMemoryProjectDb() { 13 | return new InMemoryProjectRepository(); 14 | } 15 | 16 | @Bean 17 | @Primary 18 | ProjectRepository inMemoryProjectDbBean() { 19 | return inMemoryProjectDb(); 20 | } 21 | 22 | } 23 | 24 | class InMemoryProjectRepository implements ProjectRepository { 25 | 26 | private final Map projects = new HashMap<>(); 27 | 28 | @Override 29 | public Optional findById(ProjectId projectId) { 30 | return Optional.of(projects.get(projectId)); 31 | } 32 | 33 | @Override 34 | public Project save(Project project) { 35 | return projects.put(project.id(), project); 36 | } 37 | 38 | @Override 39 | public List findAllByIdIn(Set projectIds) { 40 | return projects 41 | .entrySet() 42 | .stream() 43 | .filter(entry -> projectIds.contains(entry.getKey())) 44 | .map(Map.Entry::getValue) 45 | .toList(); 46 | } 47 | 48 | @Override 49 | public List findAll() { 50 | return new ArrayList<>(projects.values()); 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/planning/PlanningTestConfiguration.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning; 2 | 3 | import domaindrivers.smartschedule.availability.AvailabilityFacade; 4 | import domaindrivers.smartschedule.planning.parallelization.StageParallelization; 5 | import domaindrivers.smartschedule.shared.EventsPublisher; 6 | import org.mockito.Mockito; 7 | 8 | import java.time.Clock; 9 | import java.time.Instant; 10 | import java.time.ZoneId; 11 | import java.util.*; 12 | 13 | 14 | 15 | class PlanningTestConfiguration { 16 | 17 | static PlanningFacade planningFacadeWithInMemoryDb(EventsPublisher eventsPublisher) { 18 | Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); 19 | ProjectRepository projectRepository = PlanningDbTestConfiguration.inMemoryProjectDb(); 20 | PlanChosenResources planChosenResources = new PlanChosenResources(projectRepository, Mockito.mock(AvailabilityFacade.class), eventsPublisher, clock); 21 | return new PlanningFacade(projectRepository, new StageParallelization(), planChosenResources, eventsPublisher, clock); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/planning/TimeCriticalWaterfallTest.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning; 2 | 3 | import domaindrivers.smartschedule.TaskExecutorConfiguration; 4 | import domaindrivers.smartschedule.TestDbConfiguration; 5 | import domaindrivers.smartschedule.planning.parallelization.Stage; 6 | import domaindrivers.smartschedule.planning.schedule.Schedule; 7 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 8 | import org.junit.Ignore; 9 | import org.junit.jupiter.api.Disabled; 10 | import org.junit.jupiter.api.Test; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.context.annotation.Import; 14 | import org.springframework.test.context.jdbc.Sql; 15 | 16 | import java.time.Duration; 17 | import java.time.Instant; 18 | 19 | import static domaindrivers.smartschedule.planning.schedule.assertions.ScheduleAssert.assertThat; 20 | 21 | 22 | @SpringBootTest 23 | @Import({TestDbConfiguration.class, TaskExecutorConfiguration.class}) 24 | @Sql(scripts = {"classpath:schema-risk.sql", "classpath:schema-availability.sql", "classpath:schema-resources.sql", "classpath:schema-allocations.sql"}) 25 | 26 | @Ignore 27 | class TimeCriticalWaterfallTest { 28 | 29 | static final TimeSlot JAN_1_5 = new TimeSlot(Instant.parse("2020-01-01T00:00:00.00Z"), Instant.parse("2020-01-05T00:00:00.00Z")); 30 | static final TimeSlot JAN_1_3 = new TimeSlot(Instant.parse("2020-01-01T00:00:00.00Z"), Instant.parse("2020-01-03T00:00:00Z")); 31 | static final TimeSlot JAN_1_4 = new TimeSlot(Instant.parse("2020-01-01T00:00:00.00Z"), Instant.parse("2020-01-04T00:00:00Z")); 32 | 33 | @Autowired 34 | PlanningFacade projectFacade; 35 | 36 | @Test 37 | void timeCriticalWaterfallProjectProcess() { 38 | //given 39 | ProjectId projectId = 40 | projectFacade.addNewProject("waterfall"); 41 | //and 42 | Stage stageBeforeCritical = new Stage("stage1") 43 | .ofDuration(Duration.ofDays(2)); 44 | Stage criticalStage = new Stage("stage2") 45 | .ofDuration(JAN_1_5.duration()); 46 | Stage stageAfterCritical = new Stage("stage3") 47 | .ofDuration(Duration.ofDays(3)); 48 | projectFacade.defineProjectStages(projectId, stageBeforeCritical, criticalStage, stageAfterCritical); 49 | 50 | //when 51 | projectFacade.planCriticalStage(projectId, criticalStage, JAN_1_5); 52 | 53 | //then 54 | Schedule schedule = projectFacade.load(projectId).schedule(); 55 | assertThat(schedule) 56 | .hasStage("stage1").withSlot(JAN_1_3) 57 | .and() 58 | .hasStage("stage2").withSlot(JAN_1_5) 59 | .and() 60 | .hasStage("stage3").withSlot(JAN_1_4); 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/planning/parallelization/DependencyRemovalSuggesting.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning.parallelization; 2 | 3 | import domaindrivers.smartschedule.sorter.Edge; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.List; 7 | import java.util.Set; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | class DependencyRemovalSuggesting { 12 | 13 | static final StageParallelization stageParallelization = new StageParallelization(); 14 | 15 | 16 | @Test 17 | void suggestingBreaksTheCycleInSchedule() { 18 | //given 19 | Stage stage1 = new Stage("Stage1"); 20 | Stage stage2 = new Stage("Stage2"); 21 | Stage stage3 = new Stage("Stage3"); 22 | Stage stage4 = new Stage("Stage4"); 23 | stage1 = stage1.dependsOn(stage2); 24 | stage2 = stage2.dependsOn(stage3); 25 | stage4 = stage4.dependsOn(stage3); 26 | stage1 = stage1.dependsOn(stage4); 27 | stage3 = stage3.dependsOn(stage1); 28 | 29 | //when 30 | RemovalSuggestion suggestion = stageParallelization.whatToRemove(Set.of(stage1, stage2, stage3, stage4)); 31 | 32 | //then 33 | assertThat(suggestion.toString()).isEqualTo("[(3 -> 1), (4 -> 3)]"); 34 | } 35 | } -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/planning/parallelization/DurationCalculatorTest.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning.parallelization; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.time.Duration; 6 | import java.util.List; 7 | 8 | import static java.time.Duration.*; 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | class DurationCalculatorTest { 12 | 13 | private static final DurationCalculator durationCalculator = new DurationCalculator(); 14 | 15 | @Test 16 | void longestStageIsTakenIntoAccount() { 17 | //given 18 | Stage stage1 = new Stage("Stage1").ofDuration(ZERO); 19 | Stage stage2 = new Stage("Stage2").ofDuration(ofDays(3)); 20 | Stage stage3 = new Stage("Stage3").ofDuration(ofDays(2)); 21 | Stage stage4 = new Stage("Stage4").ofDuration(ofDays(5)); 22 | 23 | //when 24 | Duration duration = durationCalculator.apply(List.of(stage1, stage2, stage3, stage4)); 25 | 26 | //then 27 | assertThat(duration).hasDays(5); 28 | } 29 | 30 | @Test 31 | void sumIsTakenIntoAccountWhenNothingIsParallel() { 32 | //given 33 | Stage stage1 = new Stage( 34 | "Stage1").ofDuration(ofHours(10)); 35 | Stage stage2 = new Stage( 36 | "Stage2").ofDuration(ofHours(24)); 37 | Stage stage3 = new Stage( 38 | "Stage3").ofDuration(ofDays(2)); 39 | Stage stage4 = new Stage( 40 | "Stage4").ofDuration(ofDays(1)); 41 | stage4.dependsOn(stage3); 42 | stage3.dependsOn(stage2); 43 | stage2.dependsOn(stage1); 44 | 45 | //when 46 | Duration duration = durationCalculator.apply(List.of(stage1, stage2, stage3, stage4)); 47 | 48 | //then 49 | assertThat(duration).hasHours(106); 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/planning/schedule/assertions/ScheduleAssert.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning.schedule.assertions; 2 | 3 | import domaindrivers.smartschedule.planning.schedule.Schedule; 4 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 5 | import org.assertj.core.api.AbstractAssert; 6 | import org.assertj.core.api.Assertions; 7 | 8 | public class ScheduleAssert extends AbstractAssert { 9 | 10 | public ScheduleAssert(Schedule actual) { 11 | super(actual, ScheduleAssert.class); 12 | } 13 | 14 | public static ScheduleAssert assertThat(Schedule actual) { 15 | return new ScheduleAssert(actual); 16 | } 17 | 18 | public ScheduleAssert hasStages(int number) { 19 | Assertions.assertThat(actual.dates().keySet()).hasSize(number); 20 | return this; 21 | } 22 | 23 | public StageAssert hasStage(String name) { 24 | TimeSlot stageTimeSlot = actual.dates().get(name); 25 | Assertions.assertThat(stageTimeSlot).isNotNull(); 26 | return new StageAssert(stageTimeSlot, this); 27 | } 28 | 29 | public void isEmpty() { 30 | Assertions.assertThat(actual).isEqualTo(Schedule.none()); 31 | } 32 | 33 | public Schedule schedule() { 34 | return actual; 35 | } 36 | } 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/planning/schedule/assertions/StageAssert.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.planning.schedule.assertions; 2 | 3 | import domaindrivers.smartschedule.planning.schedule.Schedule; 4 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 5 | import org.assertj.core.api.AbstractAssert; 6 | import org.assertj.core.api.Assertions; 7 | 8 | import java.time.Instant; 9 | 10 | public class StageAssert extends AbstractAssert { 11 | 12 | private final ScheduleAssert scheduleAssert; 13 | 14 | public StageAssert(TimeSlot actual) { 15 | super(actual, StageAssert.class); 16 | this.scheduleAssert = null; 17 | } 18 | 19 | public StageAssert(TimeSlot actual, ScheduleAssert scheduleAssert) { 20 | super(actual, StageAssert.class); 21 | this.scheduleAssert = scheduleAssert; 22 | } 23 | 24 | public StageAssert thatStarts(String start) { 25 | Assertions.assertThat(actual.from()).isEqualTo(Instant.parse(start)); 26 | return this; 27 | } 28 | 29 | public StageAssert withSlot(TimeSlot slot) { 30 | Assertions.assertThat(actual).isEqualTo(slot); 31 | return this; 32 | } 33 | 34 | public StageAssert thatEnds(String end) { 35 | Assertions.assertThat(actual.to()).isEqualTo(Instant.parse(end)); 36 | return this; 37 | } 38 | 39 | public ScheduleAssert and() { 40 | return scheduleAssert; 41 | } 42 | 43 | public StageAssert isBefore(String stage) { 44 | Schedule schedule = scheduleAssert.schedule(); 45 | Assertions.assertThat(actual.to()).isBeforeOrEqualTo(schedule.dates().get(stage).from()); 46 | return this; 47 | } 48 | 49 | public StageAssert startsTogetherWith(String stage) { 50 | Schedule schedule = scheduleAssert.schedule(); 51 | Assertions.assertThat(actual.from()).isEqualTo(schedule.dates().get(stage).from()); 52 | return this; 53 | } 54 | 55 | public StageAssert isAfter(String stage) { 56 | Schedule schedule = scheduleAssert.schedule(); 57 | Assertions.assertThat(actual.from()).isAfterOrEqualTo(schedule.dates().get(stage).to()); 58 | return this; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/resource/device/CreatingDeviceTest.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource.device; 2 | 3 | import domaindrivers.smartschedule.TestDbConfiguration; 4 | import domaindrivers.smartschedule.shared.capability.Capability; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.context.annotation.Import; 9 | import org.springframework.test.context.jdbc.Sql; 10 | 11 | import java.util.List; 12 | 13 | import static domaindrivers.smartschedule.shared.capability.Capability.assets; 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | 17 | 18 | @SpringBootTest 19 | @Import({TestDbConfiguration.class}) 20 | @Sql(scripts = {"classpath:schema-resources.sql"}) 21 | class CreatingDeviceTest { 22 | 23 | @Autowired 24 | DeviceFacade deviceFacade; 25 | 26 | @Test 27 | void canCreateAndLoadDevices() { 28 | //given 29 | DeviceId device = deviceFacade.createDevice("super-excavator-1000", assets("BULLDOZER", "EXCAVATOR")); 30 | 31 | //when 32 | DeviceSummary loaded = deviceFacade.findDevice(device); 33 | 34 | //then 35 | assertThat(loaded.assets()).containsExactlyElementsOf(assets("BULLDOZER", "EXCAVATOR")); 36 | assertEquals("super-excavator-1000", loaded.model()); 37 | } 38 | 39 | @Test 40 | void canFindAllCapabilities() { 41 | //given 42 | deviceFacade.createDevice("super-excavator-1000", assets("SMALL-EXCAVATOR", "BULLDOZER")); 43 | deviceFacade.createDevice("super-excavator-2000", assets("MEDIUM-EXCAVATOR", "UBER-BULLDOZER")); 44 | deviceFacade.createDevice("super-excavator-3000", assets("BIG-EXCAVATOR")); 45 | 46 | //when 47 | List loaded = deviceFacade.findAllCapabilities(); 48 | 49 | //then 50 | assertThat(loaded).contains( 51 | Capability.asset("SMALL-EXCAVATOR"), 52 | Capability.asset("BULLDOZER"), 53 | Capability.asset("MEDIUM-EXCAVATOR"), 54 | Capability.asset("UBER-BULLDOZER"), 55 | Capability.asset("BIG-EXCAVATOR") 56 | ); 57 | } 58 | } -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/resource/device/ScheduleDeviceCapabilitiesTest.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource.device; 2 | 3 | import domaindrivers.smartschedule.TestDbConfiguration; 4 | import domaindrivers.smartschedule.allocation.AllocationFacade; 5 | import domaindrivers.smartschedule.allocation.capabilityscheduling.AllocatableCapabilitiesSummary; 6 | import domaindrivers.smartschedule.allocation.capabilityscheduling.AllocatableCapabilityId; 7 | import domaindrivers.smartschedule.allocation.capabilityscheduling.CapabilityFinder; 8 | import domaindrivers.smartschedule.resource.device.DeviceFacade; 9 | import domaindrivers.smartschedule.resource.device.DeviceId; 10 | import domaindrivers.smartschedule.shared.capability.Capability; 11 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 12 | import org.junit.jupiter.api.Test; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.context.SpringBootTest; 15 | import org.springframework.context.annotation.Import; 16 | import org.springframework.test.context.jdbc.Sql; 17 | 18 | import java.util.List; 19 | 20 | import static org.junit.jupiter.api.Assertions.assertEquals; 21 | 22 | @SpringBootTest 23 | @Import({TestDbConfiguration.class}) 24 | @Sql(scripts = {"classpath:schema-resources.sql", "classpath:schema-availability.sql", "classpath:schema-capability-scheduling.sql"}) 25 | class ScheduleDeviceCapabilitiesTest { 26 | 27 | @Autowired 28 | DeviceFacade deviceFacade; 29 | 30 | @Autowired 31 | CapabilityFinder capabilityFinder; 32 | 33 | @Test 34 | void canSetupCapabilitiesAccordingToPolicy() { 35 | //given 36 | DeviceId device = deviceFacade.createDevice( 37 | "super-bulldozer-3000", 38 | Capability.assets("EXCAVATOR", "BULLDOZER")); 39 | //when 40 | TimeSlot oneDay = TimeSlot.createDailyTimeSlotAtUTC(2021, 1, 1); 41 | List allocations = deviceFacade.scheduleCapabilities(device, oneDay); 42 | 43 | //then 44 | AllocatableCapabilitiesSummary loaded = capabilityFinder.findById(allocations); 45 | assertEquals(allocations.size(), loaded.all().size()); 46 | } 47 | } -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/resource/employee/CreatingEmployeeTest.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource.employee; 2 | 3 | import domaindrivers.smartschedule.TestDbConfiguration; 4 | import domaindrivers.smartschedule.shared.capability.Capability; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.context.annotation.Import; 9 | import org.springframework.test.context.jdbc.Sql; 10 | 11 | import java.util.List; 12 | 13 | import static domaindrivers.smartschedule.resource.employee.Seniority.SENIOR; 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | 17 | 18 | @SpringBootTest 19 | @Import({TestDbConfiguration.class}) 20 | @Sql(scripts = {"classpath:schema-resources.sql"}) 21 | class CreatingEmployeeTest { 22 | 23 | @Autowired 24 | EmployeeFacade employeeFacade; 25 | 26 | @Test 27 | void canCreateAndLoadEmployee() { 28 | //given 29 | EmployeeId employee = 30 | employeeFacade.addEmployee("resourceName", "lastName", SENIOR, Capability.skills("JAVA, PYTHON"), Capability.permissions("ADMIN, COURT")); 31 | 32 | //when 33 | EmployeeSummary loaded = employeeFacade.findEmployee(employee); 34 | 35 | //then 36 | assertThat(loaded.skills()).containsOnlyOnceElementsOf(Capability.skills("JAVA, PYTHON")); 37 | assertEquals(Capability.permissions("ADMIN, COURT"), loaded.permissions()); 38 | assertEquals("resourceName", loaded.name()); 39 | assertEquals("lastName", loaded.lastName()); 40 | assertEquals(SENIOR, loaded.seniority()); 41 | 42 | } 43 | 44 | @Test 45 | void canFindAllCapabilities() { 46 | //given 47 | employeeFacade.addEmployee("staszek", "lastName", SENIOR, Capability.skills("JAVA12", "PYTHON21"), Capability.permissions("ADMIN1", "COURT1")); 48 | employeeFacade.addEmployee("leon", "lastName", SENIOR, Capability.skills("JAVA12", "PYTHON21"), Capability.permissions("ADMIN2", "COURT2")); 49 | employeeFacade.addEmployee("sławek", "lastName", SENIOR, Capability.skills("JAVA12", "PYTHON21"), Capability.permissions("ADMIN3", "COURT3")); 50 | 51 | //when 52 | List loaded = employeeFacade.findAllCapabilities(); 53 | 54 | //then 55 | assertThat(loaded).contains( 56 | Capability.permission("ADMIN1"), 57 | Capability.permission("ADMIN2"), 58 | Capability.permission("ADMIN3"), 59 | Capability.permission("COURT1"), 60 | Capability.permission("COURT2"), 61 | Capability.permission("COURT3"), 62 | Capability.skill("JAVA12"), 63 | Capability.skill("JAVA12"), 64 | Capability.skill("JAVA12"), 65 | Capability.skill("PYTHON21"), 66 | Capability.skill("PYTHON21"), 67 | Capability.skill("PYTHON21") 68 | ); 69 | 70 | } 71 | } -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/resource/employee/ScheduleEmployeeCapabilitiesTest.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.resource.employee; 2 | 3 | import domaindrivers.smartschedule.TestDbConfiguration; 4 | import domaindrivers.smartschedule.allocation.AllocationFacade; 5 | import domaindrivers.smartschedule.allocation.capabilityscheduling.AllocatableCapabilitiesSummary; 6 | import domaindrivers.smartschedule.allocation.capabilityscheduling.AllocatableCapabilityId; 7 | import domaindrivers.smartschedule.allocation.capabilityscheduling.CapabilityFinder; 8 | import domaindrivers.smartschedule.allocation.capabilityscheduling.CapabilityScheduler; 9 | import domaindrivers.smartschedule.resource.employee.EmployeeFacade; 10 | import domaindrivers.smartschedule.resource.employee.EmployeeId; 11 | import domaindrivers.smartschedule.resource.employee.Seniority; 12 | import domaindrivers.smartschedule.shared.capability.Capability; 13 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 14 | import org.junit.jupiter.api.Test; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.test.context.SpringBootTest; 17 | import org.springframework.context.annotation.Import; 18 | import org.springframework.test.context.jdbc.Sql; 19 | 20 | import java.util.List; 21 | 22 | import static org.junit.jupiter.api.Assertions.assertEquals; 23 | 24 | @SpringBootTest 25 | @Import({TestDbConfiguration.class}) 26 | @Sql(scripts = {"classpath:schema-resources.sql", "classpath:schema-availability.sql", "classpath:schema-capability-scheduling.sql"}) 27 | class ScheduleEmployeeCapabilitiesTest { 28 | 29 | @Autowired 30 | CapabilityFinder capabilityFinder; 31 | 32 | @Autowired 33 | EmployeeFacade employeeFacade; 34 | 35 | @Test 36 | void canSetupCapabilitiesAccordingToPolicy() { 37 | //given 38 | EmployeeId employee = employeeFacade.addEmployee( 39 | "resourceName", "lastName", Seniority.LEAD, 40 | Capability.skills("JAVA, PYTHON"), 41 | Capability.permissions("ADMIN")); 42 | //when 43 | TimeSlot oneDay = TimeSlot.createDailyTimeSlotAtUTC(2021, 1, 1); 44 | List allocations = employeeFacade.scheduleCapabilities(employee, oneDay); 45 | 46 | //then 47 | AllocatableCapabilitiesSummary loaded = capabilityFinder.findById(allocations); 48 | assertEquals(allocations.size(), loaded.all().size()); 49 | 50 | } 51 | } -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/shared/CapabilitySelectorTest.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.shared; 2 | 3 | import domaindrivers.smartschedule.shared.capability.Capability; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.Set; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertFalse; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | 12 | class CapabilitySelectorTest { 13 | 14 | static final Capability RUST = new Capability("RUST", "SKILL"); 15 | static final Capability BEING_AN_ADMIN = new Capability("ADMIN", "PERMISSION"); 16 | static final Capability JAVA = new Capability("JAVA", "SKILL"); 17 | 18 | @Test 19 | void allocatableResourceCanPerformOnlyOneOfPresentCapabilities() { 20 | //given 21 | CapabilitySelector adminOrRust = CapabilitySelector.canPerformOneOf(Set.of(BEING_AN_ADMIN, RUST)); 22 | 23 | //expect 24 | assertTrue(adminOrRust.canPerform(BEING_AN_ADMIN)); 25 | assertTrue(adminOrRust.canPerform(RUST)); 26 | assertFalse(adminOrRust.canPerform(Set.of(RUST, BEING_AN_ADMIN))); 27 | assertFalse(adminOrRust.canPerform(new Capability("JAVA", "SKILL"))); 28 | assertFalse(adminOrRust.canPerform(new Capability("LAWYER", "PERMISSION"))); 29 | } 30 | 31 | @Test 32 | void allocatableResourceCanPerformSimultaneousCapabilities() { 33 | //given 34 | CapabilitySelector adminAndRust = CapabilitySelector.canPerformAllAtTheTime(Set.of(BEING_AN_ADMIN, RUST)); 35 | 36 | //expect 37 | assertTrue(adminAndRust.canPerform(BEING_AN_ADMIN)); 38 | assertTrue(adminAndRust.canPerform(RUST)); 39 | assertTrue(adminAndRust.canPerform(Set.of(RUST, BEING_AN_ADMIN))); 40 | assertFalse(adminAndRust.canPerform(Set.of(RUST, BEING_AN_ADMIN, JAVA))); 41 | assertFalse(adminAndRust.canPerform(JAVA)); 42 | assertFalse(adminAndRust.canPerform(new Capability("LAWYER", "PERMISSION"))); 43 | } 44 | } -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/simulation/AvailableCapabilitiesBuilder.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.simulation; 2 | 3 | 4 | import domaindrivers.smartschedule.shared.CapabilitySelector; 5 | import domaindrivers.smartschedule.shared.capability.Capability; 6 | import domaindrivers.smartschedule.shared.timeslot.TimeSlot; 7 | 8 | import java.util.*; 9 | 10 | class AvailableCapabilitiesBuilder { 11 | private final List availabilities = new ArrayList<>(); 12 | private UUID currentResourceId; 13 | private Set capabilities; 14 | private TimeSlot timeSlot; 15 | private CapabilitySelector.SelectingPolicy selectingPolicy; 16 | 17 | AvailableCapabilitiesBuilder withEmployee(UUID id) { 18 | if (currentResourceId != null) { 19 | this.availabilities.add(new AvailableResourceCapability(currentResourceId, new CapabilitySelector(capabilities, selectingPolicy), timeSlot)); 20 | } 21 | this.currentResourceId = id; 22 | return this; 23 | } 24 | 25 | AvailableCapabilitiesBuilder thatBrings(Capability capability) { 26 | this.capabilities = Set.of(capability); 27 | this.selectingPolicy = CapabilitySelector.SelectingPolicy.ONE_OF_ALL; 28 | return this; 29 | } 30 | 31 | AvailableCapabilitiesBuilder thatIsAvailableAt(TimeSlot timeSlot) { 32 | this.timeSlot = timeSlot; 33 | return this; 34 | } 35 | 36 | SimulatedCapabilities build() { 37 | if (currentResourceId != null) { 38 | this.availabilities.add(new AvailableResourceCapability(currentResourceId, new CapabilitySelector(capabilities, selectingPolicy), timeSlot)); 39 | } 40 | return new SimulatedCapabilities(availabilities); 41 | } 42 | 43 | public AvailableCapabilitiesBuilder thatBringsSimultaneously(Capability ... skills) { 44 | this.capabilities = new HashSet<>(Arrays.asList(skills)); 45 | this.selectingPolicy = CapabilitySelector.SelectingPolicy.ALL_SIMULTANEOUSLY; 46 | return this; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/simulation/SimulatedProjectsBuilder.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.simulation; 2 | 3 | 4 | import java.math.BigDecimal; 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.function.Supplier; 10 | 11 | class SimulatedProjectsBuilder { 12 | 13 | private ProjectId currentId; 14 | private final List simulatedProjects = new ArrayList<>(); 15 | private final Map simulatedDemands = new HashMap<>(); 16 | private final Map> values = new HashMap<>(); 17 | 18 | SimulatedProjectsBuilder withProject(ProjectId id) { 19 | this.currentId = id; 20 | simulatedProjects.add(id); 21 | return this; 22 | } 23 | 24 | SimulatedProjectsBuilder thatRequires(Demand... demands) { 25 | simulatedDemands.put(currentId, Demands.of(demands)); 26 | return this; 27 | } 28 | 29 | SimulatedProjectsBuilder thatCanEarn(BigDecimal earnings) { 30 | this.values.put(currentId, () -> earnings); 31 | return this; 32 | } 33 | 34 | SimulatedProjectsBuilder thatCanGenerateReputationLoss(int factor) { 35 | this.values.put(currentId, () -> new BigDecimal(factor)); 36 | return this; 37 | } 38 | 39 | List build() { 40 | return simulatedProjects 41 | .stream() 42 | .map(id -> new SimulatedProject(id, values.get(id), simulatedDemands.get(id))) 43 | .toList(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/domaindrivers/smartschedule/sorter/FeedbackArcSetOnGraphTest.java: -------------------------------------------------------------------------------- 1 | package domaindrivers.smartschedule.sorter; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | 10 | class FeedbackArcSetOnGraphTest { 11 | 12 | @Test 13 | void canFindMinimumNumberOfEdgesToRemoveToMakeTheGraphAcyclic() { 14 | //given 15 | Node node1 = new Node<>("1"); 16 | Node node2 = new Node<>("2"); 17 | Node node3 = new Node<>("3"); 18 | Node node4 = new Node<>("4"); 19 | node1 = node1.dependsOn(node2); 20 | node2 = node2.dependsOn(node3); 21 | node4 = node4.dependsOn(node3); 22 | node1 = node1.dependsOn(node4); 23 | node3 = node3.dependsOn(node1); 24 | 25 | //when 26 | List toRemove = new FeedbackArcSeOnGraph().calculate(List.of(node1, node2, node3, node4)); 27 | 28 | //then 29 | assertThat(toRemove).containsExactlyInAnyOrder( 30 | new Edge(3, 1), 31 | new Edge(4, 3) 32 | ); 33 | } 34 | 35 | @Test 36 | void whenGraphIsAcyclicThereIsNothingToRemove() { 37 | //given 38 | Node node1 = new Node<>("1"); 39 | Node node2 = new Node<>("2"); 40 | Node node3 = new Node<>("3"); 41 | Node node4 = new Node<>("4"); 42 | node1 = node1.dependsOn(node2); 43 | node2 = node2.dependsOn(node3); 44 | node1 = node1.dependsOn(node4); 45 | 46 | //when 47 | List toRemove = new FeedbackArcSeOnGraph().calculate(List.of(node1, node2, node3, node4)); 48 | 49 | //then 50 | assertThat(toRemove).isEmpty(); 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.sql.init.mode=always 2 | spring.datasource.hikari.data-source-properties.reWriteBatchedInserts=true --------------------------------------------------------------------------------