├── .gitignore ├── DomainDrivers.SmartSchedule.Tests ├── Allocation │ ├── AllocationsToProjectTest.cs │ ├── CapabilityAllocatingTest.cs │ ├── CapabilityScheduling │ │ ├── CapabilitySchedulingTest.cs │ │ └── LegacyAcl │ │ │ └── TranslateToCapabilitySelectorTest.cs │ ├── Cashflow │ │ ├── CashFlowFacadeTest.cs │ │ ├── CashFlowTestConfiguration.cs │ │ └── EarningsTest.cs │ ├── CreateHourlyDemandsSummaryServiceTest.cs │ ├── CreatingNewProjectTest.cs │ ├── DemandSchedulingTest.cs │ ├── InMemoryProjectAllocationsRepository.cs │ ├── PotentialTransferScenarios.cs │ ├── ResourceAllocatingTest.cs │ └── TakingRandomResourceTest.cs ├── ArchitectureDependencyTest.cs ├── Availability │ ├── AvailabilityCalendarTest.cs │ ├── AvailabilityFacadeTest.cs │ ├── ResourceAvailabilityLoadingTest.cs │ ├── ResourceAvailabilityOptimisticLockingTest.cs │ ├── ResourceAvailabilityTest.cs │ ├── ResourceAvailabilityUniquenessTest.cs │ └── Segment │ │ ├── SegmentsTest.cs │ │ └── SlotToNormalizedSlotTest.cs ├── DomainDrivers.SmartSchedule.Tests.csproj ├── GlobalUsings.cs ├── InMemoryUnitOfWork.cs ├── IntegrationTest.cs ├── IntegrationTestApp.cs ├── Optimization │ ├── CapabilityCapacityDimension.cs │ ├── OptimizationForTimedCapabilitiesTest.cs │ └── OptimizationTest.cs ├── Planning │ ├── Parallelization │ │ ├── DependencyRemovalSuggesting.cs │ │ ├── DurationCalculatorTest.cs │ │ └── ParallelizationTest.cs │ ├── PlanningFacadeTest.cs │ ├── PlanningTestConfiguration.cs │ ├── RDTest.cs │ ├── RedisRepositoryTest.cs │ ├── Scheduling │ │ ├── Assertions │ │ │ ├── ScheduleAssert.cs │ │ │ └── StageAssert.cs │ │ └── ScheduleCalculationTest.cs │ ├── SpecializedWaterfallTest.cs │ ├── StandardWaterfallTest.cs │ ├── TimeCriticalWaterfallTest.cs │ └── VisionTest.cs ├── Resource │ ├── Device │ │ ├── CreatingDeviceTest.cs │ │ └── ScheduleDeviceCapabilitiesTest.cs │ └── Employee │ │ ├── AllocationPoliciesTest.cs │ │ ├── CreatingEmployeeTest.cs │ │ └── ScheduleEmployeeCapabilitiesTest.cs ├── Risk │ ├── RiskPeriodicCheckSagaDispatcherE2ETest.cs │ ├── RiskPeriodicCheckSagaTest.cs │ └── VerifyEnoughDemandsDuringPlanningTest.cs ├── Shared │ ├── CapabilitySelectorTest.cs │ └── TimeSlotTest.cs ├── Simulation │ ├── AvailableCapabilitiesBuilder.cs │ ├── SimulatedProjectsBuilder.cs │ └── SimulationScenarios.cs └── Sorter │ ├── FeedbackArcSetOnGraphTest.cs │ └── GraphTopologicalSortTest.cs ├── DomainDrivers.SmartSchedule.sln ├── DomainDrivers.SmartSchedule ├── Allocation │ ├── AllocatedCapability.cs │ ├── AllocationConfiguration.cs │ ├── AllocationFacade.cs │ ├── Allocations.cs │ ├── CapabilitiesAllocated.cs │ ├── CapabilityReleased.cs │ ├── CapabilityScheduling │ │ ├── AllocatableCapabilitiesSummary.cs │ │ ├── AllocatableCapability.cs │ │ ├── AllocatableCapabilityId.cs │ │ ├── AllocatableCapabilityRepository.cs │ │ ├── AllocatableCapabilitySummary.cs │ │ ├── AllocatableResourceId.cs │ │ ├── CapabilityFinder.cs │ │ ├── CapabilityPlanningConfiguration.cs │ │ ├── CapabilityScheduler.cs │ │ ├── ICapabilitySchedulingDbContext.cs │ │ └── LegacyAcl │ │ │ ├── EmployeeCreatedInLegacySystemMessageHandler.cs │ │ │ └── TranslateToCapabilitySelector.cs │ ├── Cashflow │ │ ├── CashFlowConfiguration.cs │ │ ├── CashFlowFacade.cs │ │ ├── Cashflow.cs │ │ ├── CashflowRepository.cs │ │ ├── Cost.cs │ │ ├── Earnings.cs │ │ ├── EarningsRecalculated.cs │ │ ├── ICashflowDbContext.cs │ │ ├── ICashflowRepository.cs │ │ └── Income.cs │ ├── Demand.cs │ ├── Demands.cs │ ├── IAllocationDbContext.cs │ ├── IProjectAllocationsRepository.cs │ ├── NotSatisfiedDemands.cs │ ├── PotentialTransfers.cs │ ├── PotentialTransfersService.cs │ ├── ProjectAllocationScheduled.cs │ ├── ProjectAllocations.cs │ ├── ProjectAllocationsDemandsScheduled.cs │ ├── ProjectAllocationsId.cs │ ├── ProjectAllocationsRepository.cs │ ├── ProjectAllocationsSummary.cs │ ├── PublishMissingDemandsJob.cs │ └── PublishMissingDemandsService.cs ├── Availability │ ├── AvailabilityConfiguration.cs │ ├── AvailabilityFacade.cs │ ├── Blockade.cs │ ├── Calendar.cs │ ├── Calendars.cs │ ├── Owner.cs │ ├── ResourceAvailability.cs │ ├── ResourceAvailabilityId.cs │ ├── ResourceAvailabilityReadModel.cs │ ├── ResourceAvailabilityRepository.cs │ ├── ResourceGroupedAvailability.cs │ ├── ResourceId.cs │ ├── ResourceTakenOver.cs │ └── Segment │ │ ├── SegmentInMinutes.cs │ │ ├── Segments.cs │ │ ├── SlotToNormalizedSlot.cs │ │ └── SlotToSegments.cs ├── DomainDrivers.SmartSchedule.csproj ├── Optimization │ ├── ICapacityDimension.cs │ ├── IWeightDimension.cs │ ├── Item.cs │ ├── OptimizationConfiguration.cs │ ├── OptimizationFacade.cs │ ├── Result.cs │ ├── TotalCapacity.cs │ └── TotalWeight.cs ├── Planning │ ├── CapabilitiesDemanded.cs │ ├── ChosenResources.cs │ ├── CreateProjectAllocations.cs │ ├── CriticalStagePlanned.cs │ ├── Demand.cs │ ├── Demands.cs │ ├── DemandsPerStage.cs │ ├── EditStageDateService.cs │ ├── IProjectRepository.cs │ ├── NeededResourcesChosen.cs │ ├── Parallelization │ │ ├── DurationCalculator.cs │ │ ├── ParallelStages.cs │ │ ├── ParallelStagesList.cs │ │ ├── SortedNodesToParallelizedStages.cs │ │ ├── Stage.cs │ │ ├── StageParallelization.cs │ │ └── StageToNodes.cs │ ├── PlanChosenResources.cs │ ├── PlanningConfiguration.cs │ ├── PlanningFacade.cs │ ├── Project.cs │ ├── ProjectCard.cs │ ├── ProjectId.cs │ ├── RedisProjectRepository.cs │ └── Scheduling │ │ ├── Schedule.cs │ │ ├── ScheduleBasedOnChosenResourcesAvailabilityCalculator.cs │ │ ├── ScheduleBasedOnReferenceStageCalculator.cs │ │ └── ScheduleBasedOnStartDayCalculator.cs ├── Program.cs ├── Resource │ ├── Device │ │ ├── Device.cs │ │ ├── DeviceConfiguration.cs │ │ ├── DeviceFacade.cs │ │ ├── DeviceId.cs │ │ ├── DeviceRepository.cs │ │ ├── DeviceSummary.cs │ │ ├── IDeviceDbContext.cs │ │ └── ScheduleDeviceCapabilities.cs │ ├── Employee │ │ ├── Employee.cs │ │ ├── EmployeeAllocationPolicy.cs │ │ ├── EmployeeConfiguration.cs │ │ ├── EmployeeFacade.cs │ │ ├── EmployeeId.cs │ │ ├── EmployeeRepository.cs │ │ ├── EmployeeSummary.cs │ │ ├── IEmployeeDbContext.cs │ │ ├── ScheduleEmployeeCapabilities.cs │ │ └── Seniority.cs │ ├── ResourceConfiguration.cs │ └── ResourceFacade.cs ├── Resources │ ├── schema-allocations.sql │ ├── schema-availability.sql │ ├── schema-capability-scheduling.sql │ ├── schema-cashflow.sql │ ├── schema-resources.sql │ └── schema-risk.sql ├── Risk │ ├── IRiskDbContext.cs │ ├── RiskConfiguration.cs │ ├── RiskPeriodicCheckSaga.cs │ ├── RiskPeriodicCheckSagaDispatcher.cs │ ├── RiskPeriodicCheckSagaId.cs │ ├── RiskPeriodicCheckSagaRepository.cs │ ├── RiskPeriodicCheckSagaStep.cs │ ├── RiskPeriodicCheckSagaWeeklyCheckJob.cs │ ├── RiskPushNotification.cs │ ├── VerifyCriticalResourceAvailableDuringPlanning.cs │ ├── VerifyEnoughDemandsDuringPlanning.cs │ └── VerifyNeededResourcesAvailableInTimeSlot.cs ├── Shared │ ├── Capability.cs │ ├── CapabilitySelector.cs │ ├── CollectionExtensions.cs │ ├── IBaseRepository.cs │ ├── IEventsPublisher.cs │ ├── IPrivateEvent.cs │ ├── IPublishedEvent.cs │ ├── IUnitOfWork.cs │ ├── ResourceName.cs │ ├── SharedConfiguration.cs │ └── TimeSlot.cs ├── Simulation │ ├── AdditionalPricedCapability.cs │ ├── AvailableResourceCapability.cs │ ├── Demand.cs │ ├── Demands.cs │ ├── ProjectId.cs │ ├── SimulatedCapabilities.cs │ ├── SimulatedProject.cs │ ├── SimulationConfiguration.cs │ └── SimulationFacade.cs ├── SmartScheduleDbContext.cs ├── Sorter │ ├── Edge.cs │ ├── FeedbackArcSetOnGraph.cs │ ├── GraphTopologicalSort.cs │ ├── Node.cs │ ├── Nodes.cs │ └── SortedNodes.cs ├── UnitOfWork.cs ├── appsettings.Development.json └── appsettings.json └── LICENSE /DomainDrivers.SmartSchedule.Tests/Allocation/CapabilityScheduling/LegacyAcl/TranslateToCapabilitySelectorTest.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling.LegacyAcl; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | using NUnit.Framework.Legacy; 4 | using static DomainDrivers.SmartSchedule.Shared.CapabilitySelector; 5 | using static DomainDrivers.SmartSchedule.Shared.Capability; 6 | 7 | namespace DomainDrivers.SmartSchedule.Tests.Allocation.CapabilityScheduling.LegacyAcl; 8 | 9 | public class TranslateToCapabilitySelectorTest 10 | { 11 | [Fact] 12 | public void TranslateLegacyEsbMessageToCapabilitySelectorModel() 13 | { 14 | //given 15 | var legacyPermissions = new List { "ADMIN<>2", "ROOT<>1" }; 16 | var legacySkillsPerformedTogether = new List> 17 | { 18 | new List { "JAVA", "CSHARP", "PYTHON" }, 19 | new List { "RUST", "CSHARP", "PYTHON" } 20 | }; 21 | var legacyExclusiveSkills = new List { "YT DRAMA COMMENTS" }; 22 | 23 | //when 24 | var result = Translate(legacySkillsPerformedTogether, legacyExclusiveSkills, legacyPermissions); 25 | 26 | //then 27 | CollectionAssert.AreEquivalent(new List 28 | { 29 | CanPerformOneOf(new HashSet { Skill("YT DRAMA COMMENTS") }), 30 | CapabilitySelector.CanPerformAllAtTheTime(Capability.Skills("JAVA", "CSHARP", "PYTHON")), 31 | CapabilitySelector.CanPerformAllAtTheTime(Capability.Skills("RUST", "CSHARP", "PYTHON")), 32 | CanPerformOneOf(new HashSet { Permission("ADMIN") }), 33 | CanPerformOneOf(new HashSet { Permission("ADMIN") }), 34 | CanPerformOneOf(new HashSet { Permission("ROOT") }) 35 | }, 36 | result 37 | ); 38 | } 39 | 40 | [Fact] 41 | public void ZeroMeansNoPermissionNowhere() 42 | { 43 | var legacyPermissions = new List { "ADMIN<>0" }; 44 | 45 | //when 46 | var result = Translate(new List>(), new List(), legacyPermissions); 47 | 48 | //then 49 | Assert.Empty(result); 50 | } 51 | 52 | private IList Translate(IList> legacySkillsPerformedTogether, 53 | IList legacyExclusiveSkills, IList legacyPermissions) 54 | { 55 | return new TranslateToCapabilitySelector().Translate(new EmployeeDataFromLegacyEsbMessage(Guid.NewGuid(), 56 | legacySkillsPerformedTogether, legacyExclusiveSkills, legacyPermissions, TimeSlot.Empty())); 57 | } 58 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Allocation/Cashflow/CashFlowFacadeTest.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using DomainDrivers.SmartSchedule.Allocation; 3 | using DomainDrivers.SmartSchedule.Allocation.Cashflow; 4 | using DomainDrivers.SmartSchedule.Shared; 5 | using NSubstitute; 6 | 7 | namespace DomainDrivers.SmartSchedule.Tests.Allocation.Cashflow; 8 | 9 | public class CashFlowFacadeTest 10 | { 11 | private static readonly DateTime Now = DateTime.UtcNow; 12 | private readonly CashFlowFacade _cashFlowFacade; 13 | private readonly IEventsPublisher _eventsPublisher; 14 | 15 | public CashFlowFacadeTest() 16 | { 17 | _eventsPublisher = Substitute.For(); 18 | var timeProvider = Substitute.For(); 19 | timeProvider.GetUtcNow().Returns(new DateTimeOffset(Now)); 20 | _cashFlowFacade = CashFlowTestConfiguration.CashFlowFacade(_eventsPublisher,timeProvider); 21 | } 22 | 23 | [Fact] 24 | public async Task CanSaveCashFlow() 25 | { 26 | //given 27 | var projectId = ProjectAllocationsId.NewOne(); 28 | 29 | //when 30 | await _cashFlowFacade.AddIncomeAndCost(projectId, Income.Of(100), Cost.Of(50)); 31 | 32 | //then 33 | Assert.Equal(Earnings.Of(50), await _cashFlowFacade.Find(projectId)); 34 | } 35 | 36 | [Fact] 37 | public async Task UpdatingCashFlowEmitsAnEvent() 38 | { 39 | //given 40 | var projectId = ProjectAllocationsId.NewOne(); 41 | var income = Income.Of(100); 42 | var cost = Cost.Of(50); 43 | 44 | //when 45 | await _cashFlowFacade.AddIncomeAndCost(projectId, income, cost); 46 | 47 | //then 48 | await _eventsPublisher.Received(1) 49 | .Publish(Arg.Is(IsEarningsRecalculatedEvent(projectId, Earnings.Of(50)))); 50 | } 51 | 52 | private static Expression> IsEarningsRecalculatedEvent( 53 | ProjectAllocationsId projectId, Earnings earnings) 54 | { 55 | return @event => @event.ProjectId == projectId 56 | && @event.Earnings == earnings 57 | && @event.OccurredAt != default; 58 | } 59 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Allocation/Cashflow/CashFlowTestConfiguration.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Allocation; 2 | using DomainDrivers.SmartSchedule.Allocation.Cashflow; 3 | using DomainDrivers.SmartSchedule.Shared; 4 | 5 | namespace DomainDrivers.SmartSchedule.Tests.Allocation.Cashflow; 6 | 7 | public static class CashFlowTestConfiguration 8 | { 9 | public static CashFlowFacade CashFlowFacade(IEventsPublisher eventsPublisher, TimeProvider timeProvider) 10 | { 11 | return new CashFlowFacade(new InMemoryCashflowRepository(), eventsPublisher, timeProvider, new InMemoryUnitOfWork()); 12 | } 13 | } 14 | 15 | public class InMemoryCashflowRepository : ICashflowRepository 16 | { 17 | private readonly Dictionary _cashflows = new(); 18 | 19 | public Task FindById(ProjectAllocationsId projectId) 20 | { 21 | return Task.FromResult(_cashflows.GetValueOrDefault(projectId)); 22 | } 23 | 24 | public Task GetById(ProjectAllocationsId projectId) 25 | { 26 | return Task.FromResult(_cashflows[projectId]); 27 | } 28 | 29 | public Task Add(SmartSchedule.Allocation.Cashflow.Cashflow cashflow) 30 | { 31 | _cashflows.Add(cashflow.ProjectId, cashflow); 32 | return Task.FromResult(cashflow); 33 | } 34 | 35 | public Task Update(SmartSchedule.Allocation.Cashflow.Cashflow cashflow) 36 | { 37 | _cashflows[cashflow.ProjectId] = cashflow; 38 | return Task.FromResult(cashflow); 39 | } 40 | 41 | public Task> FindAll() 42 | { 43 | return Task.FromResult>(_cashflows.Values.ToList()); 44 | } 45 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Allocation/Cashflow/EarningsTest.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Allocation.Cashflow; 2 | 3 | namespace DomainDrivers.SmartSchedule.Tests.Allocation.Cashflow; 4 | 5 | public class EarningsTest 6 | { 7 | [Fact] 8 | public void IncomeMinusCostTest() 9 | { 10 | Assert.Equal(Earnings.Of(9), Income.Of(10.0m).Minus(Cost.Of(1))); 11 | Assert.Equal(Earnings.Of(8), Income.Of(10.0m).Minus(Cost.Of(2))); 12 | Assert.Equal(Earnings.Of(7), Income.Of(10.0m).Minus(Cost.Of(3))); 13 | Assert.Equal(Earnings.Of(-70), Income.Of(100).Minus(Cost.Of(170))); 14 | } 15 | 16 | [Fact] 17 | public void GreaterThanTest() 18 | { 19 | //expect 20 | Assert.True(Earnings.Of(10).GreaterThan(Earnings.Of(9))); 21 | Assert.True(Earnings.Of(10).GreaterThan(Earnings.Of(0))); 22 | Assert.True(Earnings.Of(10).GreaterThan(Earnings.Of(-1))); 23 | Assert.False(Earnings.Of(10).GreaterThan(Earnings.Of(10))); 24 | Assert.False(Earnings.Of(10).GreaterThan(Earnings.Of(11))); 25 | } 26 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Allocation/CreateHourlyDemandsSummaryServiceTest.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Allocation; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | using NUnit.Framework.Legacy; 4 | using static DomainDrivers.SmartSchedule.Shared.Capability; 5 | 6 | namespace DomainDrivers.SmartSchedule.Tests.Allocation; 7 | 8 | public class CreateHourlyDemandsSummaryServiceTest 9 | { 10 | private static readonly DateTime Now = DateTime.UtcNow; 11 | private static readonly TimeSlot Jan = TimeSlot.CreateMonthlyTimeSlotAtUtc(2021, 1); 12 | private static readonly Demands Csharp = Demands.Of(new Demand(Skill("CSHARP"), Jan)); 13 | private static readonly Demands Java = Demands.Of(new Demand(Skill("JAVA"), Jan)); 14 | 15 | private readonly CreateHourlyDemandsSummaryService _service = new CreateHourlyDemandsSummaryService(); 16 | 17 | [Fact] 18 | public void CreatesMissingDemandsSummaryForAllGivenProjects() 19 | { 20 | //given 21 | var csharpProjectId = ProjectAllocationsId.NewOne(); 22 | var javaProjectId = ProjectAllocationsId.NewOne(); 23 | var csharpProject = new ProjectAllocations(csharpProjectId, Allocations.None(), Csharp, Jan); 24 | var javaProject = new ProjectAllocations(javaProjectId, Allocations.None(), Java, Jan); 25 | 26 | //when 27 | var result = _service.Create(new List() { csharpProject, javaProject }, Now); 28 | 29 | //then 30 | Assert.Equal(Now, result.OccurredAt); 31 | var expectedMissingDemands = new Dictionary() 32 | { 33 | { javaProjectId, Java }, 34 | { csharpProjectId, Csharp } 35 | }; 36 | CollectionAssert.AreEquivalent(expectedMissingDemands, result.MissingDemands); 37 | } 38 | 39 | [Fact] 40 | public void TakesIntoAccountOnlyProjectsWithTimeSlot() 41 | { 42 | //given 43 | var withTimeSlotId = ProjectAllocationsId.NewOne(); 44 | var withoutTimeSlotId = ProjectAllocationsId.NewOne(); 45 | var withTimeSlot = new ProjectAllocations(withTimeSlotId, Allocations.None(), Csharp, Jan); 46 | var withoutTimeSlot = new ProjectAllocations(withoutTimeSlotId, Allocations.None(), Java); 47 | 48 | //when 49 | var result = _service.Create(new List() { withTimeSlot, withoutTimeSlot }, Now); 50 | 51 | //then 52 | Assert.Equal(Now, result.OccurredAt); 53 | var expectedMissingDemands = new Dictionary() { { withTimeSlotId, Csharp } }; 54 | CollectionAssert.AreEquivalent(expectedMissingDemands, result.MissingDemands); 55 | } 56 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Allocation/DemandSchedulingTest.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using DomainDrivers.SmartSchedule.Allocation; 3 | using DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 4 | using DomainDrivers.SmartSchedule.Availability; 5 | using DomainDrivers.SmartSchedule.Shared; 6 | using NSubstitute; 7 | 8 | namespace DomainDrivers.SmartSchedule.Tests.Allocation; 9 | 10 | public class DemandSchedulingTest 11 | { 12 | static readonly Demand Java = new Demand(Capability.Skill("JAVA"), TimeSlot.CreateDailyTimeSlotAtUtc(2022, 2, 2)); 13 | 14 | static readonly TimeSlot ProjectDates = new TimeSlot(DateTime.Parse("2021-01-01T00:00:00.00Z"), 15 | DateTime.Parse("2021-01-06T00:00:00.00Z")); 16 | 17 | private readonly AllocationFacade _allocationFacade; 18 | 19 | public DemandSchedulingTest() 20 | { 21 | _allocationFacade = new AllocationFacade(new InMemoryProjectAllocationsRepository(), 22 | Substitute.For(), Substitute.For(), 23 | Substitute.For(), TimeProvider.System, new InMemoryUnitOfWork()); 24 | } 25 | 26 | [Fact] 27 | public async Task CanScheduleProjectDemands() 28 | { 29 | //given 30 | var projectId = ProjectAllocationsId.NewOne(); 31 | 32 | //when 33 | await _allocationFacade.ScheduleProjectAllocationDemands(projectId, Demands.Of(Java)); 34 | 35 | //then 36 | var summary = await _allocationFacade.FindAllProjectsAllocations(); 37 | Assert.True(summary.ProjectAllocations.ContainsKey(projectId)); 38 | Assert.Empty(summary.ProjectAllocations[projectId].All); 39 | Assert.Equal(Demands.Of(Java).All, summary.Demands[projectId].All); 40 | } 41 | 42 | private static Expression> IsProjectDemandsScheduledEvent(ProjectAllocationsId projectId, Demands demands) 43 | { 44 | return @event => 45 | @event.Uuid != default 46 | && @event.ProjectAllocationsId == projectId 47 | && @event.MissingDemands == demands 48 | && @event.OccurredAt != default; 49 | } 50 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Allocation/InMemoryProjectAllocationsRepository.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Allocation; 2 | 3 | namespace DomainDrivers.SmartSchedule.Tests.Allocation; 4 | 5 | public class InMemoryProjectAllocationsRepository : IProjectAllocationsRepository 6 | { 7 | private readonly Dictionary _projects = new(); 8 | 9 | public Task> FindAllContainingDate(DateTime when) 10 | { 11 | return Task.FromResult>(_projects.Values.Where(x => x.TimeSlot != null).ToList()); 12 | } 13 | 14 | public Task FindById(ProjectAllocationsId projectId) 15 | { 16 | return Task.FromResult(_projects.GetValueOrDefault(projectId)); 17 | } 18 | 19 | public Task GetById(ProjectAllocationsId projectId) 20 | { 21 | return Task.FromResult(_projects[projectId]); 22 | } 23 | 24 | public Task Add(ProjectAllocations project) 25 | { 26 | _projects.Add(project.ProjectId, project); 27 | return Task.FromResult(project); 28 | } 29 | 30 | public Task Update(ProjectAllocations project) 31 | { 32 | _projects[project.ProjectId] = project; 33 | return Task.FromResult(project); 34 | } 35 | 36 | public Task> FindAllById(ISet projectIds) 37 | { 38 | var projects = _projects 39 | .Where(x => projectIds.Contains(x.Key)) 40 | .Select(x => x.Value) 41 | .ToList(); 42 | return Task.FromResult>(projects); 43 | } 44 | 45 | public Task> FindAll() 46 | { 47 | return Task.FromResult>(_projects.Values.ToList()); 48 | } 49 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Availability/ResourceAvailabilityLoadingTest.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Availability; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | 4 | namespace DomainDrivers.SmartSchedule.Tests.Availability; 5 | 6 | public class ResourceAvailabilityLoadingTest : IntegrationTestWithSharedApp 7 | { 8 | private static readonly TimeSlot OneMonth = TimeSlot.CreateDailyTimeSlotAtUtc(2021, 1, 1); 9 | private readonly ResourceAvailabilityRepository _resourceAvailabilityRepository; 10 | 11 | public ResourceAvailabilityLoadingTest(IntegrationTestApp testApp) : base(testApp) 12 | { 13 | _resourceAvailabilityRepository = Scope.ServiceProvider.GetRequiredService(); 14 | } 15 | 16 | [Fact] 17 | public async Task CanSaveAndLoadById() 18 | { 19 | //given 20 | var resourceAvailabilityId = ResourceAvailabilityId.NewOne(); 21 | var resourceId = ResourceId.NewOne(); 22 | var resourceAvailability = new ResourceAvailability(resourceAvailabilityId, resourceId, OneMonth); 23 | 24 | //when 25 | await _resourceAvailabilityRepository.SaveNew(resourceAvailability); 26 | 27 | //then 28 | var loaded = await _resourceAvailabilityRepository.LoadById(resourceAvailability.Id); 29 | Assert.Equal(resourceAvailability, loaded); 30 | Assert.Equal(resourceAvailability.Segment, loaded.Segment); 31 | Assert.Equal(resourceAvailability.ResourceId, loaded.ResourceId); 32 | Assert.Equal(resourceAvailability.BlockedBy, loaded.BlockedBy); 33 | } 34 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Availability/ResourceAvailabilityUniquenessTest.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Availability; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | using Npgsql; 4 | 5 | namespace DomainDrivers.SmartSchedule.Tests.Availability; 6 | 7 | public class ResourceAvailabilityUniquenessTest : IntegrationTestWithSharedApp 8 | { 9 | static readonly TimeSlot OneMonth = TimeSlot.CreateDailyTimeSlotAtUtc(2021, 1, 1); 10 | 11 | private readonly ResourceAvailabilityRepository _resourceAvailabilityRepository; 12 | 13 | public ResourceAvailabilityUniquenessTest(IntegrationTestApp testApp) : base(testApp) 14 | { 15 | _resourceAvailabilityRepository = Scope.ServiceProvider.GetRequiredService(); 16 | } 17 | 18 | [Fact] 19 | public async Task CantSaveTwoAvailabilitiesWithSameResourceIdAndSegment() { 20 | //given 21 | var resourceId = ResourceId.NewOne(); 22 | var anotherResourceId = ResourceId.NewOne(); 23 | var resourceAvailabilityId = ResourceAvailabilityId.NewOne(); 24 | 25 | //when 26 | await _resourceAvailabilityRepository.SaveNew(new ResourceAvailability(resourceAvailabilityId, resourceId, OneMonth)); 27 | 28 | //expect 29 | var exception = await Assert.ThrowsAsync(async () => 30 | { 31 | await _resourceAvailabilityRepository.SaveNew(new ResourceAvailability(resourceAvailabilityId, anotherResourceId, OneMonth)); 32 | }); 33 | Assert.Contains("duplicate key", exception.Message, StringComparison.InvariantCultureIgnoreCase); 34 | } 35 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/DomainDrivers.SmartSchedule.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | Nullable 8 | 9 | false 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | runtime; build; native; contentfiles; analyzers; buildtransitive 29 | all 30 | 31 | 32 | runtime; build; native; contentfiles; analyzers; buildtransitive 33 | all 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Microsoft.Extensions.DependencyInjection; 2 | global using Xunit; -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/InMemoryUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Tests; 4 | 5 | public class InMemoryUnitOfWork : IUnitOfWork 6 | { 7 | public async Task InTransaction(Func> operation) 8 | { 9 | return await operation(); 10 | } 11 | 12 | public async Task InTransaction(Func operation) 13 | { 14 | await operation(); 15 | } 16 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/IntegrationTest.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Tests; 2 | 3 | [Collection(nameof(SharedIntegrationTestAppCollection))] 4 | public abstract class IntegrationTestWithSharedApp : IntegrationTest 5 | { 6 | protected IntegrationTestWithSharedApp(IntegrationTestAppBase app) : base(app) 7 | { 8 | } 9 | } 10 | 11 | public abstract class IntegrationTest : IDisposable 12 | { 13 | protected IntegrationTest(IntegrationTestAppBase app) 14 | { 15 | // One scope per test to match java's behavior. 16 | Scope = app.Services.CreateScope(); 17 | } 18 | 19 | protected IServiceScope Scope { get; } 20 | 21 | public void Dispose() 22 | { 23 | Scope.Dispose(); 24 | } 25 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Optimization/CapabilityCapacityDimension.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Optimization; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | 4 | namespace DomainDrivers.SmartSchedule.Tests.Optimization; 5 | 6 | public record CapabilityCapacityDimension 7 | (Guid Uuid, string Id, string CapacityName, string CapacityType) : ICapacityDimension 8 | { 9 | public CapabilityCapacityDimension(string id, string capacityName, string capacityType) 10 | : this(Guid.NewGuid(), id, capacityName, capacityType) 11 | { 12 | } 13 | } 14 | 15 | public record CapabilityWeightDimension(string Name, string Type) : IWeightDimension 16 | { 17 | public bool IsSatisfiedBy(CapabilityCapacityDimension capacityDimension) 18 | { 19 | return capacityDimension.CapacityName == Name && capacityDimension.CapacityType == Type; 20 | } 21 | 22 | public bool IsSatisfiedBy(ICapacityDimension capacityDimension) 23 | { 24 | return IsSatisfiedBy((CapabilityCapacityDimension)capacityDimension); 25 | } 26 | } 27 | 28 | public record CapabilityTimedCapacityDimension(Guid Uuid, string Id, string CapacityName, string CapacityType, 29 | TimeSlot TimeSlot) : ICapacityDimension 30 | { 31 | public CapabilityTimedCapacityDimension(string id, string capacityName, string capacityType, TimeSlot timeSlot) 32 | : this(Guid.NewGuid(), id, capacityName, capacityType, timeSlot) 33 | { 34 | } 35 | } 36 | 37 | public record CapabilityTimedWeightDimension 38 | (string Name, string Type, TimeSlot TimeSlot) : IWeightDimension 39 | { 40 | public bool IsSatisfiedBy(CapabilityTimedCapacityDimension capacityTimedDimension) 41 | { 42 | return capacityTimedDimension.CapacityName == Name && 43 | capacityTimedDimension.CapacityType == Type && 44 | TimeSlot.Within(capacityTimedDimension.TimeSlot); 45 | } 46 | 47 | public bool IsSatisfiedBy(ICapacityDimension capacityTimedDimension) 48 | { 49 | return IsSatisfiedBy((CapabilityTimedCapacityDimension)capacityTimedDimension); 50 | } 51 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Optimization/OptimizationForTimedCapabilitiesTest.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Optimization; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | 4 | namespace DomainDrivers.SmartSchedule.Tests.Optimization; 5 | 6 | public class OptimizationForTimedCapabilitiesTest 7 | { 8 | private readonly OptimizationFacade facade = new OptimizationFacade(); 9 | 10 | [Fact] 11 | public void NothingIsChosenWhenNoCapacitiesInTimeSlot() 12 | { 13 | //given 14 | var june = TimeSlot.CreateMonthlyTimeSlotAtUtc(2020, 6); 15 | var october = TimeSlot.CreateMonthlyTimeSlotAtUtc(2020, 10); 16 | 17 | var items = new List 18 | { 19 | new Item("Item1", 100, 20 | TotalWeight.Of(new CapabilityTimedWeightDimension("COMMON SENSE", "Skill", june))), 21 | new Item("Item2", 100, 22 | TotalWeight.Of(new CapabilityTimedWeightDimension("THINKING", "Skill", june))) 23 | }; 24 | 25 | //when 26 | var result = facade.Calculate(items, TotalCapacity.Of( 27 | new CapabilityTimedCapacityDimension("anna", "COMMON SENSE", "Skill", october) 28 | )); 29 | 30 | //then 31 | Assert.Equal(0, result.Profit); 32 | Assert.Empty(result.ChosenItems); 33 | } 34 | 35 | [Fact] 36 | public void MostProfitableItemIsChosen() 37 | { 38 | //given 39 | var june = TimeSlot.CreateMonthlyTimeSlotAtUtc(2020, 6); 40 | 41 | var items = new List 42 | { 43 | new Item("Item1", 200, 44 | TotalWeight.Of(new CapabilityTimedWeightDimension("COMMON SENSE", "Skill", june))), 45 | new Item("Item2", 100, 46 | TotalWeight.Of(new CapabilityTimedWeightDimension("THINKING", "Skill", june))) 47 | }; 48 | 49 | //when 50 | var result = facade.Calculate(items, TotalCapacity.Of( 51 | new CapabilityTimedCapacityDimension("anna", "COMMON SENSE", "Skill", june) 52 | )); 53 | 54 | //then 55 | Assert.Equal(200, result.Profit); 56 | Assert.Single(result.ChosenItems); 57 | } 58 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Planning/Parallelization/DependencyRemovalSuggesting.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Planning.Parallelization; 2 | 3 | namespace DomainDrivers.SmartSchedule.Tests.Planning.Parallelization; 4 | 5 | public class DependencyRemovalSuggesting 6 | { 7 | private static readonly StageParallelization StageParallelization = new StageParallelization(); 8 | 9 | [Fact] 10 | public void SuggestingBreaksTheCycleInSchedule() 11 | { 12 | //given 13 | var stage1 = new Stage("Stage1"); 14 | var stage2 = new Stage("Stage2"); 15 | var stage3 = new Stage("Stage3"); 16 | var stage4 = new Stage("Stage4"); 17 | stage1 = stage1.DependsOn(stage2); 18 | stage2 = stage2.DependsOn(stage3); 19 | stage4 = stage4.DependsOn(stage3); 20 | stage1 = stage1.DependsOn(stage4); 21 | stage3 = stage3.DependsOn(stage1); 22 | 23 | //when 24 | var suggestion = 25 | StageParallelization.WhatToRemove(new HashSet() { stage1, stage2, stage3, stage4 }); 26 | 27 | //then 28 | Assert.Equal("[(3 -> 1), (4 -> 3)]", suggestion.ToString()); 29 | } 30 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Planning/Parallelization/DurationCalculatorTest.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Planning.Parallelization; 2 | 3 | namespace DomainDrivers.SmartSchedule.Tests.Planning.Parallelization; 4 | 5 | public class DurationCalculatorTest 6 | { 7 | [Fact] 8 | public void LongestStageIsTakenIntoAccount() 9 | { 10 | //given 11 | var stage1 = new Stage("Stage1").OfDuration(TimeSpan.Zero); 12 | var stage2 = new Stage("Stage2").OfDuration(TimeSpan.FromDays(3)); 13 | var stage3 = new Stage("Stage3").OfDuration(TimeSpan.FromDays(2)); 14 | var stage4 = new Stage("Stage4").OfDuration(TimeSpan.FromDays(5)); 15 | 16 | //when 17 | var duration = DurationCalculator.Calculate(new List { stage1, stage2, stage3, stage4 }); 18 | 19 | //then 20 | Assert.Equal(5, duration.Days); 21 | } 22 | 23 | [Fact] 24 | public void SumIsTakenIntoAccountWhenNothingIsParallel() 25 | { 26 | //given 27 | var stage1 = new Stage("Stage1").OfDuration(TimeSpan.FromHours(10)); 28 | var stage2 = new Stage("Stage2").OfDuration(TimeSpan.FromHours(24)); 29 | var stage3 = new Stage("Stage3").OfDuration(TimeSpan.FromDays(2)); 30 | var stage4 = new Stage("Stage4").OfDuration(TimeSpan.FromDays(1)); 31 | stage4.DependsOn(stage3); 32 | stage3.DependsOn(stage2); 33 | stage2.DependsOn(stage1); 34 | 35 | //when 36 | var duration = DurationCalculator.Calculate(new List { stage1, stage2, stage3, stage4 }); 37 | 38 | //then 39 | Assert.Equal(106, duration.TotalHours); 40 | } 41 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Planning/PlanningTestConfiguration.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Availability; 2 | using DomainDrivers.SmartSchedule.Planning; 3 | using DomainDrivers.SmartSchedule.Planning.Parallelization; 4 | using DomainDrivers.SmartSchedule.Shared; 5 | using NSubstitute; 6 | 7 | namespace DomainDrivers.SmartSchedule.Tests.Planning; 8 | 9 | public static class PlanningTestConfiguration 10 | { 11 | public static PlanningFacade PlanningFacadeWithInMemoryDb(IEventsPublisher eventsPublisher) 12 | { 13 | var clock = Substitute.For(); 14 | clock.GetUtcNow().Returns(DateTimeOffset.UtcNow); 15 | var projectRepository = new InMemoryProjectRepository(); 16 | var planChosenResources = new PlanChosenResources(projectRepository, Substitute.For(), 17 | eventsPublisher, clock); 18 | return new PlanningFacade(projectRepository, new StageParallelization(), planChosenResources, eventsPublisher, 19 | clock); 20 | } 21 | } 22 | 23 | public class InMemoryProjectRepository : IProjectRepository 24 | { 25 | private readonly Dictionary _projects = new Dictionary(); 26 | 27 | public Task GetById(ProjectId projectId) 28 | { 29 | return Task.FromResult(_projects[projectId]); 30 | } 31 | 32 | public Task Save(Project project) 33 | { 34 | _projects[project.Id] = project; 35 | return Task.FromResult(project); 36 | } 37 | 38 | public Task> FindAllByIdIn(ISet projectIds) 39 | { 40 | var projects = _projects 41 | .Where(x => projectIds.Contains(x.Key)) 42 | .Select(x => x.Value) 43 | .ToList(); 44 | return Task.FromResult>(projects); 45 | } 46 | 47 | public Task> FindAll() 48 | { 49 | return Task.FromResult>(_projects.Values.ToList()); 50 | } 51 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Planning/Scheduling/Assertions/ScheduleAssert.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Planning.Scheduling; 2 | 3 | namespace DomainDrivers.SmartSchedule.Tests.Planning.Scheduling.Assertions; 4 | 5 | public class ScheduleAssert 6 | { 7 | public ScheduleAssert(Schedule actual) 8 | { 9 | Schedule = actual; 10 | } 11 | 12 | public Schedule Schedule { get; } 13 | 14 | public static ScheduleAssert AssertThat(Schedule actual) 15 | { 16 | return new ScheduleAssert(actual); 17 | } 18 | 19 | public ScheduleAssert HasStages(int number) 20 | { 21 | Assert.Equal(number, Schedule.Dates.Count); 22 | return this; 23 | } 24 | 25 | public StageAssert HasStage(string name) 26 | { 27 | Schedule.Dates.TryGetValue(name, out var stageTimeSlot); 28 | Assert.NotNull(stageTimeSlot); 29 | return new StageAssert(stageTimeSlot, this); 30 | } 31 | 32 | public void IsEmpty() 33 | { 34 | Assert.True(Schedule.None() == Schedule); 35 | } 36 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Planning/Scheduling/Assertions/StageAssert.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Tests.Planning.Scheduling.Assertions; 4 | 5 | public class StageAssert 6 | { 7 | private readonly TimeSlot _actual; 8 | private readonly ScheduleAssert _scheduleAssert; 9 | 10 | public StageAssert(TimeSlot actual, ScheduleAssert scheduleAssert) 11 | { 12 | _actual = actual; 13 | _scheduleAssert = scheduleAssert; 14 | } 15 | 16 | public StageAssert ThatStarts(string start) 17 | { 18 | Assert.Equal(DateTime.Parse(start), _actual.From); 19 | return this; 20 | } 21 | 22 | public StageAssert WithSlot(TimeSlot slot) 23 | { 24 | Assert.Equal(slot, _actual); 25 | return this; 26 | } 27 | 28 | public StageAssert ThatEnds(string end) 29 | { 30 | Assert.Equal(DateTime.Parse(end), _actual.To); 31 | return this; 32 | } 33 | 34 | public ScheduleAssert And() 35 | { 36 | return _scheduleAssert; 37 | } 38 | 39 | public StageAssert IsBefore(string stage) 40 | { 41 | var schedule = _scheduleAssert.Schedule; 42 | Assert.True(_actual.To <= schedule.Dates[stage].From); 43 | return this; 44 | } 45 | 46 | public StageAssert StartsTogetherWith(string stage) 47 | { 48 | var schedule = _scheduleAssert.Schedule; 49 | Assert.Equal(_actual.From, schedule.Dates[stage].From); 50 | return this; 51 | } 52 | 53 | public StageAssert IsAfter(string stage) 54 | { 55 | var schedule = _scheduleAssert.Schedule; 56 | Assert.True(_actual.From >= schedule.Dates[stage].To); 57 | return this; 58 | } 59 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Planning/TimeCriticalWaterfallTest.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Planning; 2 | using DomainDrivers.SmartSchedule.Planning.Parallelization; 3 | using DomainDrivers.SmartSchedule.Shared; 4 | using static DomainDrivers.SmartSchedule.Tests.Planning.Scheduling.Assertions.ScheduleAssert; 5 | 6 | namespace DomainDrivers.SmartSchedule.Tests.Planning; 7 | 8 | public class TimeCriticalWaterfallTest : IntegrationTestWithSharedApp 9 | { 10 | static readonly TimeSlot Jan1_5 = new TimeSlot(DateTime.Parse("2020-01-01T00:00:00.00Z"), 11 | DateTime.Parse("2020-01-05T00:00:00.00Z")); 12 | 13 | static readonly TimeSlot Jan1_3 = 14 | new TimeSlot(DateTime.Parse("2020-01-01T00:00:00.00Z"), DateTime.Parse("2020-01-03T00:00:00Z")); 15 | 16 | static readonly TimeSlot Jan1_4 = 17 | new TimeSlot(DateTime.Parse("2020-01-01T00:00:00.00Z"), DateTime.Parse("2020-01-04T00:00:00Z")); 18 | 19 | private readonly PlanningFacade _projectFacade; 20 | 21 | public TimeCriticalWaterfallTest(IntegrationTestApp testApp) : base(testApp) 22 | { 23 | _projectFacade = Scope.ServiceProvider.GetRequiredService(); 24 | } 25 | 26 | [Fact] 27 | public async Task TimeCriticalWaterfallProjectProcess() 28 | { 29 | //given 30 | var projectId = await _projectFacade.AddNewProject("waterfall"); 31 | 32 | //and 33 | var stageBeforeCritical = new Stage("stage1") 34 | .OfDuration(TimeSpan.FromDays(2)); 35 | var criticalStage = new Stage("stage2") 36 | .OfDuration(Jan1_5.Duration); 37 | var stageAfterCritical = new Stage("stage3") 38 | .OfDuration(TimeSpan.FromDays(3)); 39 | await _projectFacade.DefineProjectStages(projectId, stageBeforeCritical, criticalStage, stageAfterCritical); 40 | 41 | //when 42 | await _projectFacade.PlanCriticalStage(projectId, criticalStage, Jan1_5); 43 | 44 | //then 45 | var schedule = (await _projectFacade.Load(projectId)).Schedule; 46 | AssertThat(schedule) 47 | .HasStage("stage1").WithSlot(Jan1_3) 48 | .And() 49 | .HasStage("stage2").WithSlot(Jan1_5) 50 | .And() 51 | .HasStage("stage3").WithSlot(Jan1_4); 52 | } 53 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Resource/Device/CreatingDeviceTest.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Resource.Device; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | using static DomainDrivers.SmartSchedule.Shared.Capability; 4 | 5 | namespace DomainDrivers.SmartSchedule.Tests.Resource.Device; 6 | 7 | public class CreatingDeviceTest : IntegrationTestWithSharedApp 8 | { 9 | private readonly DeviceFacade _deviceFacade; 10 | 11 | public CreatingDeviceTest(IntegrationTestApp testApp) : base(testApp) 12 | { 13 | _deviceFacade = Scope.ServiceProvider.GetRequiredService(); 14 | } 15 | 16 | [Fact] 17 | public async Task CanCreateAndLoadDevices() 18 | { 19 | //given 20 | var device = await _deviceFacade.CreateDevice("super-excavator-1000", Assets("BULLDOZER", "EXCAVATOR")); 21 | 22 | //when 23 | var loaded = await _deviceFacade.FindDevice(device); 24 | 25 | //then 26 | Assert.Equal(Assets("BULLDOZER", "EXCAVATOR"), loaded.Assets); 27 | Assert.Equal("super-excavator-1000", loaded.Model); 28 | } 29 | 30 | [Fact] 31 | public async Task CanFindAllCapabilities() 32 | { 33 | //given 34 | await _deviceFacade.CreateDevice("super-excavator-1000", Assets("SMALL-EXCAVATOR", "BULLDOZER")); 35 | await _deviceFacade.CreateDevice("super-excavator-2000", Assets("MEDIUM-EXCAVATOR", "UBER-BULLDOZER")); 36 | await _deviceFacade.CreateDevice("super-excavator-3000", Assets("BIG-EXCAVATOR")); 37 | 38 | //when 39 | var loaded = await _deviceFacade.FindAllCapabilities(); 40 | 41 | //then 42 | Assert.Contains(Capability.Asset("SMALL-EXCAVATOR"), loaded); 43 | Assert.Contains(Capability.Asset("BULLDOZER"), loaded); 44 | Assert.Contains(Capability.Asset("MEDIUM-EXCAVATOR"), loaded); 45 | Assert.Contains(Capability.Asset("UBER-BULLDOZER"), loaded); 46 | Assert.Contains(Capability.Asset("BIG-EXCAVATOR"), loaded); 47 | } 48 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Resource/Device/ScheduleDeviceCapabilitiesTest.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 2 | using DomainDrivers.SmartSchedule.Resource.Device; 3 | using DomainDrivers.SmartSchedule.Shared; 4 | 5 | namespace DomainDrivers.SmartSchedule.Tests.Resource.Device; 6 | 7 | public class ScheduleDeviceCapabilitiesTest : IntegrationTestWithSharedApp 8 | { 9 | private readonly DeviceFacade _deviceFacade; 10 | private readonly ICapabilityFinder _capabilityFinder; 11 | 12 | public ScheduleDeviceCapabilitiesTest(IntegrationTestApp testApp) : base(testApp) 13 | { 14 | _deviceFacade = Scope.ServiceProvider.GetRequiredService(); 15 | _capabilityFinder = Scope.ServiceProvider.GetRequiredService(); 16 | } 17 | 18 | [Fact] 19 | public async Task CanSetupCapabilitiesAccordingToPolicy() 20 | { 21 | //given 22 | var device = await _deviceFacade.CreateDevice("super-bulldozer-3000", 23 | Capability.Assets("EXCAVATOR", "BULLDOZER")); 24 | //when 25 | var oneDay = TimeSlot.CreateDailyTimeSlotAtUtc(2021, 1, 1); 26 | var allocations = await _deviceFacade.ScheduleCapabilities(device, oneDay); 27 | 28 | //then 29 | var loaded = await _capabilityFinder.FindById(allocations); 30 | Assert.Equal(allocations.Count, loaded.All.Count); 31 | } 32 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Resource/Employee/AllocationPoliciesTest.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Resource.Employee; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | using NUnit.Framework.Legacy; 4 | using static DomainDrivers.SmartSchedule.Shared.Capability; 5 | using static DomainDrivers.SmartSchedule.Resource.Employee.IEmployeeAllocationPolicy; 6 | 7 | namespace DomainDrivers.SmartSchedule.Tests.Resource.Employee; 8 | 9 | public class AllocationPoliciesTest 10 | { 11 | [Fact] 12 | public void DefaultPolicyShouldReturnJustOneSkillAtOnce() 13 | { 14 | //given 15 | var employee = new EmployeeSummary(EmployeeId.NewOne(), "resourceName", "lastName", Seniority.LEAD, 16 | Capability.Skills("JAVA"), Capability.Permissions("ADMIN")); 17 | 18 | //when 19 | var capabilities = IEmployeeAllocationPolicy.DefaultPolicy().SimultaneousCapabilitiesOf(employee); 20 | 21 | //then 22 | Assert.Single(capabilities); 23 | CollectionAssert.AreEquivalent(new HashSet() { Permission("ADMIN"), Skill("JAVA") }, 24 | capabilities[0].Capabilities); 25 | } 26 | 27 | [Fact] 28 | public void PermissionsCanBeSharedBetweenProjects() 29 | { 30 | //given 31 | var policy = PermissionsInMultipleProjects(3); 32 | var employee = new EmployeeSummary(EmployeeId.NewOne(), "resourceName", "lastName", Seniority.LEAD, 33 | Capability.Skills("JAVA"), Capability.Permissions("ADMIN")); 34 | 35 | //when 36 | var capabilities = policy.SimultaneousCapabilitiesOf(employee); 37 | 38 | //then 39 | Assert.Equal(3, capabilities.Count); 40 | 41 | CollectionAssert.AreEquivalent(new List() { Permission("ADMIN"), Permission("ADMIN"), Permission("ADMIN") }, 42 | capabilities.SelectMany(cap => cap.Capabilities)); 43 | } 44 | 45 | [Fact] 46 | public void CanCreateCompositePolicy() 47 | { 48 | //given 49 | var policy = Simultaneous(PermissionsInMultipleProjects(3), IEmployeeAllocationPolicy.OneOfSkills()); 50 | var employee = new EmployeeSummary(EmployeeId.NewOne(), "resourceName", "lastName", Seniority.LEAD, 51 | Capability.Skills("JAVA", "PYTHON"), Capability.Permissions("ADMIN")); 52 | 53 | //when 54 | var capabilities = policy.SimultaneousCapabilitiesOf(employee); 55 | 56 | //then 57 | Assert.Equal(4, capabilities.Count); 58 | CollectionAssert.AreEquivalent( 59 | new List() 60 | { 61 | CapabilitySelector.CanPerformOneOf(Capability.Skills("JAVA", "PYTHON")), 62 | CapabilitySelector.CanJustPerform(Permission("ADMIN")), 63 | CapabilitySelector.CanJustPerform(Permission("ADMIN")), 64 | CapabilitySelector.CanJustPerform(Permission("ADMIN")) 65 | }, capabilities); 66 | } 67 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Resource/Employee/ScheduleEmployeeCapabilitiesTest.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 2 | using DomainDrivers.SmartSchedule.Resource.Employee; 3 | using DomainDrivers.SmartSchedule.Shared; 4 | 5 | namespace DomainDrivers.SmartSchedule.Tests.Resource.Employee; 6 | 7 | public class ScheduleEmployeeCapabilitiesTest : IntegrationTestWithSharedApp 8 | { 9 | private readonly ICapabilityFinder _capabilityFinder; 10 | private readonly EmployeeFacade _employeeFacade; 11 | 12 | public ScheduleEmployeeCapabilitiesTest(IntegrationTestApp testApp) : base(testApp) 13 | { 14 | _capabilityFinder = Scope.ServiceProvider.GetRequiredService(); 15 | _employeeFacade = Scope.ServiceProvider.GetRequiredService(); 16 | } 17 | 18 | [Fact] 19 | public async Task CanSetupCapabilitiesAccordingToPolicy() 20 | { 21 | var employee = await _employeeFacade.AddEmployee("resourceName", "lastName", Seniority.LEAD, 22 | Capability.Skills("JAVA, PYTHON"), 23 | Capability.Permissions("ADMIN")); 24 | //when 25 | var oneDay = TimeSlot.CreateDailyTimeSlotAtUtc(2021, 1, 1); 26 | var allocations = await _employeeFacade.ScheduleCapabilities(employee, oneDay); 27 | 28 | //then 29 | var loaded = await _capabilityFinder.FindById(allocations); 30 | Assert.Equal(allocations.Count, loaded.All.Count); 31 | } 32 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Shared/CapabilitySelectorTest.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Tests.Shared; 4 | 5 | public class CapabilitySelectorTest 6 | { 7 | static readonly Capability Rust = new Capability("RUST", "SKILL"); 8 | static readonly Capability BeingAnAdmin = new Capability("ADMIN", "PERMISSION"); 9 | static readonly Capability Java = new Capability("JAVA", "SKILL"); 10 | 11 | [Fact] 12 | public void AllocatableResourceCanPerformOnlyOneOfPresentCapabilities() 13 | { 14 | //given 15 | var adminOrRust = CapabilitySelector.CanPerformOneOf(new HashSet() { BeingAnAdmin, Rust }); 16 | 17 | //expect 18 | Assert.True(adminOrRust.CanPerform(BeingAnAdmin)); 19 | Assert.True(adminOrRust.CanPerform(Rust)); 20 | Assert.False(adminOrRust.CanPerform(new HashSet() { Rust, BeingAnAdmin })); 21 | Assert.False(adminOrRust.CanPerform(new Capability("JAVA", "SKILL"))); 22 | Assert.False(adminOrRust.CanPerform(new Capability("LAWYER", "PERMISSION"))); 23 | } 24 | 25 | [Fact] 26 | public void AllocatableResourceCanPerformSimultaneousCapabilities() 27 | { 28 | //given 29 | var adminAndRust = CapabilitySelector.CanPerformAllAtTheTime(new HashSet() { BeingAnAdmin, Rust }); 30 | 31 | //expect 32 | Assert.True(adminAndRust.CanPerform(BeingAnAdmin)); 33 | Assert.True(adminAndRust.CanPerform(Rust)); 34 | Assert.True(adminAndRust.CanPerform(new HashSet() { Rust, BeingAnAdmin })); 35 | Assert.False(adminAndRust.CanPerform(new HashSet() { Rust, BeingAnAdmin, Java })); 36 | Assert.False(adminAndRust.CanPerform(Java)); 37 | Assert.False(adminAndRust.CanPerform(new Capability("LAWYER", "PERMISSION"))); 38 | } 39 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Simulation/AvailableCapabilitiesBuilder.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | using DomainDrivers.SmartSchedule.Simulation; 3 | 4 | namespace DomainDrivers.SmartSchedule.Tests.Simulation; 5 | 6 | public class AvailableCapabilitiesBuilder 7 | { 8 | private readonly IList _availabilities = new List(); 9 | private Guid? _currentResourceId; 10 | private ISet? _capabilities; 11 | private TimeSlot? _timeSlot; 12 | private SelectingPolicy _selectingPolicy; 13 | 14 | public AvailableCapabilitiesBuilder WithEmployee(Guid id) 15 | { 16 | if (_currentResourceId.HasValue) 17 | { 18 | _availabilities.Add(new AvailableResourceCapability(_currentResourceId.Value, 19 | new CapabilitySelector(_capabilities!, _selectingPolicy), _timeSlot!)); 20 | } 21 | 22 | _currentResourceId = id; 23 | return this; 24 | } 25 | 26 | public AvailableCapabilitiesBuilder ThatBrings(Capability capability) 27 | { 28 | _capabilities = new HashSet() { capability }; 29 | _selectingPolicy = SelectingPolicy.OneOfAll; 30 | return this; 31 | } 32 | 33 | public AvailableCapabilitiesBuilder ThatIsAvailableAt(TimeSlot timeSlot) 34 | { 35 | _timeSlot = timeSlot; 36 | return this; 37 | } 38 | 39 | public SimulatedCapabilities Build() 40 | { 41 | if (_currentResourceId.HasValue) 42 | { 43 | _availabilities.Add(new AvailableResourceCapability(_currentResourceId.Value, 44 | new CapabilitySelector(_capabilities!, _selectingPolicy), _timeSlot!)); 45 | } 46 | 47 | return new SimulatedCapabilities(_availabilities); 48 | } 49 | 50 | public AvailableCapabilitiesBuilder ThatBringsSimultaneously(params Capability[] skills) 51 | { 52 | _capabilities = new HashSet(skills); 53 | _selectingPolicy = SelectingPolicy.AllSimultaneously; 54 | return this; 55 | } 56 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Simulation/SimulatedProjectsBuilder.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Simulation; 2 | 3 | namespace DomainDrivers.SmartSchedule.Tests.Simulation; 4 | 5 | public class SimulatedProjectsBuilder 6 | { 7 | private ProjectId? _currentId; 8 | private readonly IList _simulatedProjects = new List(); 9 | private readonly IDictionary _simulatedDemands = new Dictionary(); 10 | private readonly IDictionary> _values = new Dictionary>(); 11 | 12 | public SimulatedProjectsBuilder WithProject(ProjectId id) 13 | { 14 | _currentId = id; 15 | _simulatedProjects.Add(id); 16 | return this; 17 | } 18 | 19 | public SimulatedProjectsBuilder ThatRequires(params Demand[] demands) 20 | { 21 | _simulatedDemands[_currentId!] = Demands.Of(demands); 22 | return this; 23 | } 24 | 25 | public SimulatedProjectsBuilder ThatCanEarn(decimal earnings) 26 | { 27 | _values[_currentId!] = () => earnings; 28 | return this; 29 | } 30 | 31 | public SimulatedProjectsBuilder ThatCanGenerateReputationLoss(int factor) 32 | { 33 | _values[_currentId!] = () => factor; 34 | return this; 35 | } 36 | 37 | public List Build() 38 | { 39 | return _simulatedProjects 40 | .Select(id => new SimulatedProject(id, _values[id], _simulatedDemands[id])) 41 | .ToList(); 42 | } 43 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.Tests/Sorter/FeedbackArcSetOnGraphTest.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Sorter; 2 | using NUnit.Framework.Legacy; 3 | using Assert = Xunit.Assert; 4 | 5 | namespace DomainDrivers.SmartSchedule.Tests.Sorter; 6 | 7 | public class FeedbackArcSetOnGraphTest 8 | { 9 | [Fact] 10 | public void CanFindMinimumNumberOfEdgesToRemoveToMakeTheGraphAcyclic() 11 | { 12 | //given 13 | var node1 = new Node("1"); 14 | var node2 = new Node("2"); 15 | var node3 = new Node("3"); 16 | var node4 = new Node("4"); 17 | node1 = node1.DependsOn(node2); 18 | node2 = node2.DependsOn(node3); 19 | node4 = node4.DependsOn(node3); 20 | node1 = node1.DependsOn(node4); 21 | node3 = node3.DependsOn(node1); 22 | 23 | //when 24 | var toRemove = new FeedbackArcSetOnGraph().Calculate(new List> {node1, node2, node3, node4}); 25 | 26 | //then 27 | CollectionAssert.AreEquivalent(new[] { new Edge(3, 1), new Edge(4, 3) }, toRemove); 28 | } 29 | 30 | [Fact] 31 | public void WhenGraphIsAcyclicThereIsNothingToRemove() 32 | { 33 | //given 34 | var node1 = new Node("1"); 35 | var node2 = new Node("2"); 36 | var node3 = new Node("3"); 37 | var node4 = new Node("4"); 38 | node1 = node1.DependsOn(node2); 39 | node2 = node2.DependsOn(node3); 40 | node3 = node3.DependsOn(node4); 41 | 42 | //when 43 | var toRemove = new FeedbackArcSetOnGraph().Calculate(new List> {node1, node2, node3, node4}); 44 | 45 | //then 46 | Assert.Empty(toRemove); 47 | } 48 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DomainDrivers.SmartSchedule", "DomainDrivers.SmartSchedule\DomainDrivers.SmartSchedule.csproj", "{1E891947-6444-427A-861A-293F223C7ED4}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DomainDrivers.SmartSchedule.Tests", "DomainDrivers.SmartSchedule.Tests\DomainDrivers.SmartSchedule.Tests.csproj", "{4B23B6E7-AD51-4C93-A336-DB2B0DAE0228}" 6 | EndProject 7 | Global 8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 9 | Debug|Any CPU = Debug|Any CPU 10 | Release|Any CPU = Release|Any CPU 11 | EndGlobalSection 12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 13 | {1E891947-6444-427A-861A-293F223C7ED4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 14 | {1E891947-6444-427A-861A-293F223C7ED4}.Debug|Any CPU.Build.0 = Debug|Any CPU 15 | {1E891947-6444-427A-861A-293F223C7ED4}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | {1E891947-6444-427A-861A-293F223C7ED4}.Release|Any CPU.Build.0 = Release|Any CPU 17 | {4B23B6E7-AD51-4C93-A336-DB2B0DAE0228}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {4B23B6E7-AD51-4C93-A336-DB2B0DAE0228}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {4B23B6E7-AD51-4C93-A336-DB2B0DAE0228}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {4B23B6E7-AD51-4C93-A336-DB2B0DAE0228}.Release|Any CPU.Build.0 = Release|Any CPU 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/AllocatedCapability.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 3 | using DomainDrivers.SmartSchedule.Shared; 4 | 5 | namespace DomainDrivers.SmartSchedule.Allocation; 6 | 7 | [method: JsonConstructor] 8 | public record AllocatedCapability(AllocatableCapabilityId AllocatedCapabilityId, CapabilitySelector Capability, TimeSlot TimeSlot) 9 | { 10 | public virtual bool Equals(AllocatedCapability? other) 11 | { 12 | if (ReferenceEquals(null, other)) return false; 13 | if (ReferenceEquals(this, other)) return true; 14 | return AllocatedCapabilityId == other.AllocatedCapabilityId && Capability == other.Capability && TimeSlot == other.TimeSlot; 15 | } 16 | 17 | public override int GetHashCode() 18 | { 19 | return HashCode.Combine(AllocatedCapabilityId, Capability, TimeSlot); 20 | } 21 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/AllocationConfiguration.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | using Quartz; 5 | 6 | namespace DomainDrivers.SmartSchedule.Allocation; 7 | 8 | public static class AllocationConfiguration 9 | { 10 | public static IServiceCollection AddAllocation(this IServiceCollection serviceCollection) 11 | { 12 | serviceCollection.AddScoped( 13 | sp => sp.GetRequiredService()); 14 | serviceCollection.AddScoped(); 15 | serviceCollection.AddTransient(); 16 | serviceCollection.AddTransient(); 17 | serviceCollection.AddQuartz(q => 18 | { 19 | var jobKey = new JobKey("PublishMissingDemandsJob"); 20 | q.AddJob(opts => opts.WithIdentity(jobKey)); 21 | 22 | q.AddTrigger(opts => opts 23 | .ForJob(jobKey) 24 | .WithIdentity("PublishMissingDemandsJob-trigger") 25 | .WithCronSchedule("0 0 * ? * *")); 26 | }); 27 | return serviceCollection; 28 | } 29 | } 30 | 31 | public class ProjectAllocationsEntityTypeConfiguration : IEntityTypeConfiguration 32 | { 33 | public void Configure(EntityTypeBuilder builder) 34 | { 35 | builder.ToTable("project_allocations"); 36 | 37 | builder.Property(p => p.ProjectId) 38 | .HasConversion( 39 | projectId => projectId.Id, 40 | projectId => new ProjectAllocationsId(projectId)) 41 | .HasColumnName("project_allocations_id"); 42 | builder.HasKey(p => p.ProjectId); 43 | 44 | builder.Property(p => p.Allocations) 45 | .HasColumnName("allocations") 46 | .HasColumnType("jsonb"); 47 | 48 | builder.Property(p => p.Demands) 49 | .HasColumnName("demands") 50 | .HasColumnType("jsonb"); 51 | 52 | builder.OwnsOne(p => p.TimeSlot, ts => 53 | { 54 | ts.Property(t => t.From).HasColumnName("from_date"); 55 | ts.Property(t => t.To).HasColumnName("to_date"); 56 | }); 57 | } 58 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/Allocations.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | 4 | namespace DomainDrivers.SmartSchedule.Allocation; 5 | 6 | public record Allocations(ISet All) 7 | { 8 | public static Allocations None() 9 | { 10 | return new Allocations(new HashSet()); 11 | } 12 | 13 | public Allocations Add(AllocatedCapability newOne) 14 | { 15 | var all = new HashSet(All) { newOne }; 16 | return new Allocations(all); 17 | } 18 | 19 | public Allocations Remove(AllocatableCapabilityId toRemove, TimeSlot slot) 20 | { 21 | var allocatedCapability = Find(toRemove); 22 | 23 | if (allocatedCapability == null) 24 | { 25 | return this; 26 | } 27 | 28 | return RemoveFromSlot(allocatedCapability, slot); 29 | } 30 | 31 | private Allocations RemoveFromSlot(AllocatedCapability allocatedCapability, TimeSlot slot) 32 | { 33 | var leftOvers = allocatedCapability.TimeSlot 34 | .LeftoverAfterRemovingCommonWith(slot) 35 | .Where(leftOver => leftOver.Within(allocatedCapability.TimeSlot)) 36 | .Select(leftOver => new AllocatedCapability(allocatedCapability.AllocatedCapabilityId, allocatedCapability.Capability, leftOver)) 37 | .ToHashSet(); 38 | var newSlots = new HashSet(All); 39 | newSlots.Remove(allocatedCapability); 40 | newSlots.UnionWith(leftOvers); 41 | return new Allocations(newSlots); 42 | } 43 | 44 | public AllocatedCapability? Find(AllocatableCapabilityId allocatedCapabilityId) 45 | { 46 | return All.FirstOrDefault(ar => ar.AllocatedCapabilityId == allocatedCapabilityId); 47 | } 48 | 49 | public virtual bool Equals(Allocations? other) 50 | { 51 | if (ReferenceEquals(null, other)) return false; 52 | if (ReferenceEquals(this, other)) return true; 53 | return All.SetEquals(other.All); 54 | } 55 | 56 | public override int GetHashCode() 57 | { 58 | return All.CalculateHashCode(); 59 | } 60 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/CapabilitiesAllocated.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation; 4 | 5 | public record CapabilitiesAllocated( 6 | Guid EventId, 7 | Guid AllocatedCapabilityId, 8 | ProjectAllocationsId ProjectId, 9 | Demands MissingDemands, 10 | DateTime OccurredAt) : IPrivateEvent 11 | { 12 | public CapabilitiesAllocated(Guid allocatedCapabilityId, ProjectAllocationsId projectId, Demands missingDemands, 13 | DateTime occuredAt) 14 | : this(Guid.NewGuid(), allocatedCapabilityId, projectId, missingDemands, occuredAt) 15 | { 16 | } 17 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/CapabilityReleased.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation; 4 | 5 | public record CapabilityReleased( 6 | Guid EventId, 7 | ProjectAllocationsId ProjectId, 8 | Demands MissingDemands, 9 | DateTime OccurredAt) : IPrivateEvent 10 | { 11 | public CapabilityReleased(ProjectAllocationsId projectId, Demands missingDemands, DateTime occurredAt) 12 | : this(Guid.NewGuid(), projectId, missingDemands, occurredAt) 13 | { 14 | } 15 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/CapabilityScheduling/AllocatableCapabilitiesSummary.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 4 | 5 | public record AllocatableCapabilitiesSummary(IList All) 6 | { 7 | public override int GetHashCode() 8 | { 9 | return All.CalculateHashCode(); 10 | } 11 | 12 | public virtual bool Equals(AllocatableCapabilitiesSummary? other) 13 | { 14 | if (ReferenceEquals(null, other)) return false; 15 | if (ReferenceEquals(this, other)) return true; 16 | return All.SequenceEqual(other.All); 17 | } 18 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/CapabilityScheduling/AllocatableCapability.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 4 | 5 | public class AllocatableCapability 6 | { 7 | public AllocatableCapabilityId Id { get; } = AllocatableCapabilityId.NewOne(); 8 | 9 | public CapabilitySelector Capabilities { get; } 10 | 11 | public AllocatableResourceId ResourceId { get; } 12 | 13 | public TimeSlot TimeSlot { get; } 14 | 15 | public AllocatableCapability(AllocatableResourceId resourceId, CapabilitySelector capabilities, TimeSlot timeSlot) 16 | { 17 | ResourceId = resourceId; 18 | Capabilities = capabilities; 19 | //https://learn.microsoft.com/en-us/ef/core/modeling/owned-entities#by-design-restrictions 20 | TimeSlot = new TimeSlot(timeSlot.From, timeSlot.To); 21 | } 22 | 23 | #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 24 | private AllocatableCapability(){} 25 | #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 26 | 27 | public bool CanPerform(ISet capabilities) 28 | { 29 | return Capabilities.CanPerform(capabilities); 30 | } 31 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/CapabilityScheduling/AllocatableCapabilityId.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Availability; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 4 | 5 | public record AllocatableCapabilityId(Guid? Id) 6 | { 7 | public static AllocatableCapabilityId NewOne() 8 | { 9 | return new AllocatableCapabilityId(Guid.NewGuid()); 10 | } 11 | 12 | public static AllocatableCapabilityId None() 13 | { 14 | return new AllocatableCapabilityId((Guid?)null); 15 | } 16 | 17 | public ResourceId ToAvailabilityResourceId() 18 | { 19 | return ResourceId.Of(Id.ToString()); 20 | } 21 | 22 | public static AllocatableCapabilityId From(ResourceId resourceId) { 23 | return new AllocatableCapabilityId(resourceId.Id!.Value); 24 | } 25 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/CapabilityScheduling/AllocatableCapabilitySummary.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 4 | 5 | public record AllocatableCapabilitySummary(AllocatableCapabilityId Id, AllocatableResourceId AllocatableResourceId, 6 | CapabilitySelector Capabilities, TimeSlot TimeSlot); -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/CapabilityScheduling/AllocatableResourceId.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 2 | 3 | public record AllocatableResourceId(Guid Id) 4 | { 5 | public static AllocatableResourceId NewOne() 6 | { 7 | return new AllocatableResourceId(Guid.NewGuid()); 8 | } 9 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/CapabilityScheduling/CapabilityPlanningConfiguration.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 6 | 7 | public static class CapabilityPlanningConfiguration 8 | { 9 | public static IServiceCollection AddCapabilityPlanning(this IServiceCollection serviceCollection) 10 | { 11 | serviceCollection.AddScoped( 12 | sp => sp.GetRequiredService()); 13 | serviceCollection.AddTransient(); 14 | serviceCollection.AddTransient(); 15 | serviceCollection.AddTransient(); 16 | return serviceCollection; 17 | } 18 | } 19 | 20 | public class AllocatableCapabilityEntityTypeConfiguration : IEntityTypeConfiguration 21 | { 22 | public void Configure(EntityTypeBuilder builder) 23 | { 24 | builder.ToTable("allocatable_capabilities"); 25 | 26 | builder.Property(x => x.Id) 27 | .HasConversion( 28 | allocatableCapabilityId => allocatableCapabilityId.Id, 29 | allocatableCapabilityId => new AllocatableCapabilityId(allocatableCapabilityId!.Value)) 30 | .HasColumnName("id"); 31 | builder.HasKey(x => x.Id); 32 | 33 | builder.Property(p => p.Capabilities) 34 | .HasColumnName("possible_capabilities") 35 | .HasColumnType("jsonb"); 36 | 37 | builder.Property(x => x.ResourceId) 38 | .HasConversion( 39 | resourceId => resourceId.Id, 40 | resourceId => new AllocatableResourceId(resourceId)) 41 | .HasColumnName("resource_id"); 42 | 43 | builder.Ignore(x => x.TimeSlot); 44 | builder.OwnsOne(x => x.TimeSlot, ts => 45 | { 46 | ts.Property(t => t.From).HasColumnName("from_date"); 47 | ts.Property(t => t.To).HasColumnName("to_date"); 48 | }); 49 | } 50 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/CapabilityScheduling/ICapabilitySchedulingDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 4 | 5 | public interface ICapabilitySchedulingDbContext 6 | { 7 | public DbSet AllocatableCapabilities { get; } 8 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/CapabilityScheduling/LegacyAcl/EmployeeCreatedInLegacySystemMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling.LegacyAcl; 4 | 5 | public class EmployeeCreatedInLegacySystemMessageHandler 6 | { 7 | private readonly CapabilityScheduler _capabilityScheduler; 8 | 9 | public EmployeeCreatedInLegacySystemMessageHandler(CapabilityScheduler capabilityScheduler) 10 | { 11 | _capabilityScheduler = capabilityScheduler; 12 | } 13 | 14 | //subscribe to message bus 15 | //StreamListener to (message_bus) 16 | public async Task Handle(EmployeeDataFromLegacyEsbMessage message) 17 | { 18 | var allocatableResourceId = new AllocatableResourceId(message.ResourceId); 19 | var capabilitySelectors = new TranslateToCapabilitySelector().Translate(message); 20 | await _capabilityScheduler.ScheduleResourceCapabilitiesForPeriod(allocatableResourceId, capabilitySelectors, 21 | message.TimeSlot); 22 | } 23 | } 24 | 25 | public record EmployeeDataFromLegacyEsbMessage( 26 | Guid ResourceId, 27 | IList> SkillsPerformedTogether, 28 | IList ExclusiveSkills, 29 | IList Permissions, 30 | TimeSlot TimeSlot) 31 | { 32 | public override int GetHashCode() 33 | { 34 | return HashCode.Combine(ResourceId, 35 | SkillsPerformedTogether.Aggregate(0, (hash, pair) => HashCode.Combine(hash, pair.CalculateHashCode())), 36 | ExclusiveSkills.CalculateHashCode(), 37 | Permissions.CalculateHashCode(), TimeSlot); 38 | } 39 | 40 | public virtual bool Equals(EmployeeDataFromLegacyEsbMessage? other) 41 | { 42 | if (ReferenceEquals(null, other)) return false; 43 | if (ReferenceEquals(this, other)) return true; 44 | 45 | return ResourceId == other.ResourceId && 46 | SkillsPerformedTogether.SequenceEqual(other.SkillsPerformedTogether, 47 | EqualityComparer>.Create((x, y) => x!.SequenceEqual(y!))) && 48 | ExclusiveSkills.SequenceEqual(other.ExclusiveSkills) && Permissions.SequenceEqual(other.Permissions) && 49 | TimeSlot == other.TimeSlot; 50 | } 51 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/CapabilityScheduling/LegacyAcl/TranslateToCapabilitySelector.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling.LegacyAcl; 4 | 5 | public class TranslateToCapabilitySelector 6 | { 7 | public IList Translate(EmployeeDataFromLegacyEsbMessage message) 8 | { 9 | var employeeSkills = message.SkillsPerformedTogether 10 | .Select(skills => CapabilitySelector.CanPerformAllAtTheTime( 11 | skills.Select(x => Capability.Skill(x)).ToHashSet())) 12 | .ToList(); 13 | var employeeExclusiveSkills = message.ExclusiveSkills 14 | .Select(skill => CapabilitySelector.CanJustPerform(Capability.Skill(skill))) 15 | .ToList(); 16 | var employeePermissions = message.Permissions 17 | .SelectMany(MultiplePermission) 18 | .ToList(); 19 | //schedule or rewrite if exists; 20 | return employeeSkills.Concat(employeeExclusiveSkills).Concat(employeePermissions).ToList(); 21 | } 22 | 23 | private IList MultiplePermission(string permissionLegacyCode) 24 | { 25 | var parts = permissionLegacyCode.Split("<>").ToList(); 26 | var permission = parts[0]; 27 | var times = int.Parse(parts[1]); 28 | return Enumerable 29 | .Range(0, times) 30 | .Select(_ => CapabilitySelector.CanJustPerform(Capability.Permission(permission))) 31 | .ToList(); 32 | } 33 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/Cashflow/CashFlowConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | 4 | namespace DomainDrivers.SmartSchedule.Allocation.Cashflow; 5 | 6 | public static class CashFlowConfiguration 7 | { 8 | public static IServiceCollection AddCashFlow(this IServiceCollection serviceCollection) 9 | { 10 | serviceCollection.AddScoped( 11 | sp => sp.GetRequiredService()); 12 | serviceCollection.AddScoped(); 13 | serviceCollection.AddTransient(); 14 | return serviceCollection; 15 | } 16 | } 17 | 18 | public class CashflowEntityTypeConfiguration : IEntityTypeConfiguration 19 | { 20 | public void Configure(EntityTypeBuilder builder) 21 | { 22 | builder.ToTable("cashflows"); 23 | 24 | builder.Property(x => x.ProjectId) 25 | .HasConversion( 26 | projectId => projectId.Id, 27 | projectId => new ProjectAllocationsId(projectId)) 28 | .HasColumnName("project_allocations_id"); 29 | builder.HasKey(x => x.ProjectId); 30 | 31 | builder.OwnsOne("_cost", ts => 32 | { 33 | ts.Property(t => t.Value).HasColumnName("cost"); 34 | }); 35 | 36 | builder.OwnsOne("_income", ts => 37 | { 38 | ts.Property(t => t.Value).HasColumnName("income"); 39 | }); 40 | } 41 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/Cashflow/CashFlowFacade.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation.Cashflow; 4 | 5 | public class CashFlowFacade 6 | { 7 | private readonly ICashflowRepository _cashflowRepository; 8 | private readonly IEventsPublisher _eventsPublisher; 9 | private readonly TimeProvider _timeProvider; 10 | private readonly IUnitOfWork _unitOfWork; 11 | 12 | public CashFlowFacade(ICashflowRepository cashflowRepository, IEventsPublisher eventsPublisher, 13 | TimeProvider timeProvider, IUnitOfWork unitOfWork) 14 | { 15 | _cashflowRepository = cashflowRepository; 16 | _eventsPublisher = eventsPublisher; 17 | _timeProvider = timeProvider; 18 | _unitOfWork = unitOfWork; 19 | } 20 | 21 | public async Task AddIncomeAndCost(ProjectAllocationsId projectId, Income income, Cost cost) 22 | { 23 | await _unitOfWork.InTransaction(async () => 24 | { 25 | var cashflow = await _cashflowRepository.FindById(projectId); 26 | if (cashflow == null) 27 | { 28 | cashflow = new Cashflow(projectId); 29 | await _cashflowRepository.Add(cashflow); 30 | } 31 | 32 | cashflow.Update(income, cost); 33 | await _cashflowRepository.Update(cashflow); 34 | await _eventsPublisher.Publish(new EarningsRecalculated(projectId, cashflow.Earnings(), 35 | _timeProvider.GetUtcNow().DateTime)); 36 | }); 37 | } 38 | 39 | public async Task Find(ProjectAllocationsId projectId) 40 | { 41 | var byId = await _cashflowRepository.GetById(projectId); 42 | return byId.Earnings(); 43 | } 44 | 45 | public async Task> FindAllEarnings() 46 | { 47 | return (await _cashflowRepository.FindAll()) 48 | .ToDictionary(x => x.ProjectId, x => x.Earnings()); 49 | } 50 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/Cashflow/Cashflow.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Allocation.Cashflow; 2 | 3 | public class Cashflow 4 | { 5 | public ProjectAllocationsId ProjectId { get; private set; } 6 | private Income? _income; 7 | private Cost? _cost; 8 | 9 | public Cashflow(ProjectAllocationsId projectId) 10 | { 11 | ProjectId = projectId; 12 | } 13 | 14 | public Earnings Earnings() 15 | { 16 | return _income!.Minus(_cost!); 17 | } 18 | 19 | public void Update(Income income, Cost cost) 20 | { 21 | _income = income; 22 | _cost = cost; 23 | } 24 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/Cashflow/CashflowRepository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation.Cashflow; 4 | 5 | public class CashflowRepository : ICashflowRepository 6 | { 7 | private readonly ICashflowDbContext _cashflowDbContext; 8 | 9 | public CashflowRepository(ICashflowDbContext cashflowDbContext) 10 | { 11 | _cashflowDbContext = cashflowDbContext; 12 | } 13 | 14 | public async Task FindById(ProjectAllocationsId projectId) 15 | { 16 | return await _cashflowDbContext.Cashflows 17 | .SingleOrDefaultAsync(x => x.ProjectId == projectId); 18 | } 19 | 20 | public async Task GetById(ProjectAllocationsId projectId) 21 | { 22 | return await _cashflowDbContext.Cashflows 23 | .SingleAsync(x => x.ProjectId == projectId); 24 | } 25 | 26 | public async Task Add(Cashflow cashflow) 27 | { 28 | return (await _cashflowDbContext.Cashflows.AddAsync(cashflow)).Entity; 29 | } 30 | 31 | public Task Update(Cashflow cashflow) 32 | { 33 | return Task.FromResult(_cashflowDbContext.Cashflows.Update(cashflow).Entity); 34 | } 35 | 36 | public async Task> FindAll() 37 | { 38 | return await _cashflowDbContext.Cashflows.ToListAsync(); 39 | } 40 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/Cashflow/Cost.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Allocation.Cashflow; 2 | 3 | public record Cost(decimal Value) 4 | { 5 | public static Cost Of(int integer) 6 | { 7 | return new Cost(integer); 8 | } 9 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/Cashflow/Earnings.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Allocation.Cashflow; 2 | 3 | public record Earnings(decimal Value) 4 | { 5 | public static Earnings Of(int integer) 6 | { 7 | return new Earnings(integer); 8 | } 9 | 10 | public bool GreaterThan(Earnings value) 11 | { 12 | return Value > value.Value; 13 | } 14 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/Cashflow/EarningsRecalculated.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation.Cashflow; 4 | 5 | public record EarningsRecalculated(Guid Uuid, ProjectAllocationsId ProjectId, Earnings Earnings, DateTime OccurredAt) 6 | : IPublishedEvent 7 | { 8 | public EarningsRecalculated(ProjectAllocationsId projectId, Earnings earnings, DateTime occurredAt) 9 | : this(Guid.NewGuid(), projectId, earnings, occurredAt) { } 10 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/Cashflow/ICashflowDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation.Cashflow; 4 | 5 | public interface ICashflowDbContext 6 | { 7 | public DbSet Cashflows { get; } 8 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/Cashflow/ICashflowRepository.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Allocation.Cashflow; 2 | 3 | public interface ICashflowRepository 4 | { 5 | Task FindById(ProjectAllocationsId projectId); 6 | 7 | Task GetById(ProjectAllocationsId projectId); 8 | 9 | Task Add(Cashflow cashflow); 10 | 11 | Task Update(Cashflow cashflow); 12 | 13 | Task> FindAll(); 14 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/Cashflow/Income.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Allocation.Cashflow; 2 | 3 | public record Income(decimal Value) 4 | { 5 | public static Income Of(decimal @decimal) 6 | { 7 | return new Income(@decimal); 8 | } 9 | 10 | public static Income Of(int integer) 11 | { 12 | return new Income(integer); 13 | } 14 | 15 | public Earnings Minus(Cost estimatedCosts) 16 | { 17 | return new Earnings(Value - estimatedCosts.Value); 18 | } 19 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/Demand.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation; 4 | 5 | public record Demand(Capability Capability, TimeSlot Slot); -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/Demands.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation; 4 | 5 | public record Demands(IList All) 6 | { 7 | public static Demands None() 8 | { 9 | return new Demands(new List()); 10 | } 11 | 12 | public static Demands Of(params Demand[] demands) 13 | { 14 | return new Demands(demands); 15 | } 16 | 17 | public static Demands AllInSameTimeSlot(TimeSlot slot, params Capability[] capabilities) 18 | { 19 | return new Demands(capabilities.Select(c => new Demand(c, slot)).ToList()); 20 | } 21 | 22 | public Demands MissingDemands(Allocations allocations) 23 | { 24 | return new Demands(All.Where(d => !SatisfiedBy(d, allocations)).ToList()); 25 | } 26 | 27 | private static bool SatisfiedBy(Demand d, Allocations allocations) 28 | { 29 | return allocations.All.Any(ar => ar.Capability.CanPerform(d.Capability) && d.Slot.Within(ar.TimeSlot)); 30 | } 31 | 32 | public Demands WithNew(Demands newDemands) { 33 | var all = new List(All); 34 | all.AddRange(newDemands.All); 35 | return new Demands(all); 36 | } 37 | 38 | public virtual bool Equals(Demands? other) 39 | { 40 | if (ReferenceEquals(null, other)) return false; 41 | if (ReferenceEquals(this, other)) return true; 42 | return All.SequenceEqual(other.All); 43 | } 44 | 45 | public override int GetHashCode() 46 | { 47 | return All.CalculateHashCode(); 48 | } 49 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/IAllocationDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation; 4 | 5 | public interface IAllocationDbContext 6 | { 7 | public DbSet ProjectAllocations { get; } 8 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/IProjectAllocationsRepository.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Allocation; 2 | 3 | public interface IProjectAllocationsRepository 4 | { 5 | Task> FindAllContainingDate(DateTime when); 6 | 7 | Task FindById(ProjectAllocationsId projectId); 8 | 9 | Task GetById(ProjectAllocationsId projectId); 10 | 11 | Task Add(ProjectAllocations project); 12 | 13 | Task Update(ProjectAllocations project); 14 | 15 | Task> FindAllById(ISet projectIds); 16 | 17 | Task> FindAll(); 18 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/NotSatisfiedDemands.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation; 4 | 5 | public record NotSatisfiedDemands( 6 | Guid Uuid, 7 | IDictionary MissingDemands, 8 | DateTime OccurredAt) : IPublishedEvent 9 | { 10 | public NotSatisfiedDemands(IDictionary missingDemands, DateTime occuredAt) : 11 | this(Guid.NewGuid(), missingDemands, occuredAt) 12 | { 13 | } 14 | 15 | public static NotSatisfiedDemands ForOneProject(ProjectAllocationsId projectId, Demands scheduledDemands, 16 | DateTime occurredAt) 17 | { 18 | return new NotSatisfiedDemands(Guid.NewGuid(), new Dictionary() 19 | { 20 | { projectId, scheduledDemands } 21 | }, occurredAt); 22 | } 23 | 24 | public static NotSatisfiedDemands AllSatisfied(ProjectAllocationsId projectId, DateTime occurredAt) 25 | { 26 | return new NotSatisfiedDemands(Guid.NewGuid(), new Dictionary() 27 | { 28 | { projectId, Demands.None() } 29 | }, occurredAt); 30 | } 31 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/PotentialTransfersService.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 2 | using DomainDrivers.SmartSchedule.Allocation.Cashflow; 3 | using DomainDrivers.SmartSchedule.Shared; 4 | using DomainDrivers.SmartSchedule.Simulation; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace DomainDrivers.SmartSchedule.Allocation; 8 | 9 | public class PotentialTransfersService 10 | { 11 | private readonly SimulationFacade _simulationFacade; 12 | private readonly CashFlowFacade _cashFlowFacade; 13 | private readonly IAllocationDbContext _allocationDbContext; 14 | 15 | public PotentialTransfersService(SimulationFacade simulationFacade, CashFlowFacade cashFlowFacade, 16 | IAllocationDbContext allocationDbContext) 17 | { 18 | _simulationFacade = simulationFacade; 19 | _cashFlowFacade = cashFlowFacade; 20 | _allocationDbContext = allocationDbContext; 21 | } 22 | 23 | public async Task ProfitAfterMovingCapabilities(ProjectAllocationsId projectId, 24 | AllocatableCapabilitySummary capabilityToMove, TimeSlot timeSlot) 25 | { 26 | //cached? 27 | var potentialTransfers = 28 | new PotentialTransfers( 29 | ProjectsAllocationsSummary.Of(await _allocationDbContext.ProjectAllocations.ToListAsync()), 30 | await _cashFlowFacade.FindAllEarnings()); 31 | return CheckPotentialTransfer(potentialTransfers, projectId, capabilityToMove, timeSlot); 32 | } 33 | 34 | private double CheckPotentialTransfer(PotentialTransfers transfers, ProjectAllocationsId projectTo, 35 | AllocatableCapabilitySummary capabilityToMove, TimeSlot forSlot) 36 | { 37 | var resultBefore = 38 | _simulationFacade.WhatIsTheOptimalSetup(transfers.ToSimulatedProjects(), SimulatedCapabilities.None()); 39 | transfers = transfers.Transfer(projectTo, capabilityToMove, forSlot); 40 | var resultAfter = 41 | _simulationFacade.WhatIsTheOptimalSetup(transfers.ToSimulatedProjects(), SimulatedCapabilities.None()); 42 | return resultAfter.Profit - resultBefore.Profit; 43 | } 44 | 45 | public double CheckPotentialTransfer(PotentialTransfers transfers, ProjectAllocationsId projectFrom, 46 | ProjectAllocationsId projectTo, AllocatedCapability capability, TimeSlot forSlot) 47 | { 48 | var resultBefore = 49 | _simulationFacade.WhatIsTheOptimalSetup(transfers.ToSimulatedProjects(), SimulatedCapabilities.None()); 50 | transfers = transfers.Transfer(projectFrom, projectTo, capability, forSlot); 51 | var resultAfter = 52 | _simulationFacade.WhatIsTheOptimalSetup(transfers.ToSimulatedProjects(), SimulatedCapabilities.None()); 53 | return resultAfter.Profit - resultBefore.Profit; 54 | } 55 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/ProjectAllocationScheduled.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation; 4 | 5 | public record ProjectAllocationScheduled( 6 | Guid Uuid, 7 | ProjectAllocationsId ProjectAllocationsId, 8 | TimeSlot FromTo, 9 | DateTime OccurredAt) : IPrivateEvent, IPublishedEvent 10 | { 11 | public ProjectAllocationScheduled(ProjectAllocationsId projectId, TimeSlot FromTo, 12 | DateTime occurredAt) 13 | : this(Guid.NewGuid(), projectId, FromTo, occurredAt) 14 | { 15 | } 16 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/ProjectAllocationsDemandsScheduled.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation; 4 | 5 | public record ProjectAllocationsDemandsScheduled( 6 | Guid Uuid, 7 | ProjectAllocationsId ProjectAllocationsId, 8 | Demands MissingDemands, 9 | DateTime OccurredAt) : IPrivateEvent 10 | { 11 | public ProjectAllocationsDemandsScheduled(ProjectAllocationsId projectId, Demands missingDemands, 12 | DateTime occurredAt) 13 | : this(Guid.NewGuid(), projectId, missingDemands, occurredAt) 14 | { 15 | } 16 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/ProjectAllocationsId.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Allocation; 2 | 3 | public record ProjectAllocationsId(Guid Id) 4 | { 5 | public static ProjectAllocationsId NewOne() 6 | { 7 | return new ProjectAllocationsId(Guid.NewGuid()); 8 | } 9 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/ProjectAllocationsRepository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation; 4 | 5 | public class ProjectAllocationsRepository : IProjectAllocationsRepository 6 | { 7 | private readonly IAllocationDbContext _allocationDbContext; 8 | 9 | public ProjectAllocationsRepository(IAllocationDbContext allocationDbContext) 10 | { 11 | _allocationDbContext = allocationDbContext; 12 | } 13 | 14 | public async Task> FindAllContainingDate(DateTime when) 15 | { 16 | return await _allocationDbContext.ProjectAllocations.FromSql( 17 | $"SELECT * FROM project_allocations WHERE from_date <= {when} AND to_date > {when}") 18 | .ToListAsync(); 19 | } 20 | 21 | public async Task FindById(ProjectAllocationsId projectId) 22 | { 23 | return await _allocationDbContext.ProjectAllocations 24 | .SingleOrDefaultAsync(x => x.ProjectId == projectId); 25 | } 26 | 27 | public async Task GetById(ProjectAllocationsId projectId) 28 | { 29 | return await _allocationDbContext.ProjectAllocations 30 | .SingleAsync(x => x.ProjectId == projectId); 31 | } 32 | 33 | public async Task Add(ProjectAllocations project) 34 | { 35 | return (await _allocationDbContext.ProjectAllocations.AddAsync(project)).Entity; 36 | } 37 | 38 | public Task Update(ProjectAllocations project) 39 | { 40 | return Task.FromResult(_allocationDbContext.ProjectAllocations.Update(project).Entity); 41 | } 42 | 43 | public async Task> FindAllById(ISet projectIds) 44 | { 45 | //ToArray cast is needed for query to be compiled properly 46 | return await _allocationDbContext.ProjectAllocations 47 | .Where(x => projectIds.ToArray().Contains(x.ProjectId)) 48 | .ToListAsync(); 49 | } 50 | 51 | public async Task> FindAll() 52 | { 53 | return await _allocationDbContext.ProjectAllocations.ToListAsync(); 54 | } 55 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/ProjectAllocationsSummary.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation; 4 | 5 | public record ProjectsAllocationsSummary( 6 | IDictionary TimeSlots, 7 | IDictionary ProjectAllocations, 8 | IDictionary Demands) 9 | { 10 | public static ProjectsAllocationsSummary Of(IList allProjectAllocations) 11 | { 12 | var timeSlots = allProjectAllocations 13 | .Where(pa => pa.HasTimeSlot) 14 | .ToDictionary( 15 | pa => pa.ProjectId, 16 | pa => pa.TimeSlot!); 17 | 18 | var allocations = allProjectAllocations 19 | .ToDictionary( 20 | pa => pa.ProjectId, 21 | pa => pa.Allocations); 22 | 23 | var demands = allProjectAllocations 24 | .ToDictionary( 25 | pa => pa.ProjectId, 26 | pa => pa.Demands); 27 | 28 | return new ProjectsAllocationsSummary(timeSlots, allocations, demands); 29 | } 30 | 31 | public virtual bool Equals(ProjectsAllocationsSummary? other) 32 | { 33 | if (ReferenceEquals(null, other)) return false; 34 | if (ReferenceEquals(this, other)) return true; 35 | return TimeSlots.DictionaryEqual(other.TimeSlots) 36 | && ProjectAllocations.DictionaryEqual(other.ProjectAllocations) 37 | && Demands.DictionaryEqual(other.Demands); 38 | } 39 | 40 | public override int GetHashCode() 41 | { 42 | return HashCode.Combine(TimeSlots.CalculateHashCode(), 43 | ProjectAllocations.CalculateHashCode(), 44 | Demands.CalculateHashCode()); 45 | } 46 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/PublishMissingDemandsJob.cs: -------------------------------------------------------------------------------- 1 | using Quartz; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation; 4 | 5 | public class PublishMissingDemandsJob : IJob 6 | { 7 | private readonly PublishMissingDemandsService _publishMissingDemandsService; 8 | 9 | public PublishMissingDemandsJob(PublishMissingDemandsService publishMissingDemandsService) 10 | { 11 | _publishMissingDemandsService = publishMissingDemandsService; 12 | } 13 | 14 | public async Task Execute(IJobExecutionContext context) 15 | { 16 | await _publishMissingDemandsService.Publish(); 17 | } 18 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Allocation/PublishMissingDemandsService.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Allocation; 4 | 5 | public class PublishMissingDemandsService 6 | { 7 | private readonly ProjectAllocationsRepository _projectAllocationsRepository; 8 | private readonly CreateHourlyDemandsSummaryService _createHourlyDemandsSummaryService; 9 | private readonly IEventsPublisher _eventsPublisher; 10 | private readonly TimeProvider _timeProvider; 11 | 12 | public PublishMissingDemandsService(ProjectAllocationsRepository projectAllocationsRepository, 13 | CreateHourlyDemandsSummaryService createHourlyDemandsSummaryService, IEventsPublisher eventsPublisher, 14 | TimeProvider timeProvider) 15 | { 16 | _projectAllocationsRepository = projectAllocationsRepository; 17 | _createHourlyDemandsSummaryService = createHourlyDemandsSummaryService; 18 | _eventsPublisher = eventsPublisher; 19 | _timeProvider = timeProvider; 20 | } 21 | 22 | public async Task Publish() 23 | { 24 | var when = _timeProvider.GetUtcNow().DateTime; 25 | var projectAllocations = 26 | await _projectAllocationsRepository.FindAllContainingDate(when); 27 | var missingDemands = _createHourlyDemandsSummaryService.Create(projectAllocations, when); 28 | //add metadata to event 29 | //if needed call EventStore and translate multiple private events to a new published event 30 | await _eventsPublisher.Publish(missingDemands); 31 | } 32 | } 33 | 34 | public class CreateHourlyDemandsSummaryService 35 | { 36 | public NotSatisfiedDemands Create(IList projectAllocations, DateTime when) 37 | { 38 | var missingDemands = 39 | projectAllocations 40 | .Where(x => x.HasTimeSlot) 41 | .ToDictionary(x => x.ProjectId, x => x.MissingDemands()); 42 | return new NotSatisfiedDemands(missingDemands, when); 43 | } 44 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Availability/AvailabilityConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Availability; 2 | 3 | public static class AvailabilityConfiguration 4 | { 5 | public static IServiceCollection AddAvailability(this IServiceCollection serviceCollection) 6 | { 7 | serviceCollection.AddTransient(); 8 | serviceCollection.AddTransient(); 9 | serviceCollection.AddTransient(); 10 | return serviceCollection; 11 | } 12 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Availability/Blockade.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Availability; 2 | 3 | public record Blockade(Owner TakenBy, bool Disabled) 4 | { 5 | public static Blockade None() 6 | { 7 | return new Blockade(Owner.None(), false); 8 | } 9 | 10 | public static Blockade DisabledBy(Owner owner) 11 | { 12 | return new Blockade(owner, true); 13 | } 14 | 15 | public static Blockade OwnedBy(Owner owner) 16 | { 17 | return new Blockade(owner, false); 18 | } 19 | 20 | public bool CanBeTakenBy(Owner requester) 21 | { 22 | return TakenBy.ByNone || TakenBy == requester; 23 | } 24 | 25 | public bool IsDisabledBy(Owner owner) 26 | { 27 | return Disabled && TakenBy == owner; 28 | } 29 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Availability/Calendar.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Availability; 4 | 5 | public record Calendar(ResourceId ResourceId, IDictionary> CalendarEntries) 6 | { 7 | public static Calendar WithAvailableSlots(ResourceId resourceId, params TimeSlot[] availableSlots) 8 | { 9 | return new Calendar(resourceId, 10 | new Dictionary> { { Owner.None(), new List(availableSlots) } }); 11 | } 12 | 13 | public static Calendar Empty(ResourceId resourceId) 14 | { 15 | return new Calendar(resourceId, new Dictionary>()); 16 | } 17 | 18 | public IList AvailableSlots() 19 | { 20 | if (CalendarEntries.TryGetValue(Owner.None(), out var availableSlots)) 21 | { 22 | return availableSlots; 23 | } 24 | 25 | return new List(); 26 | } 27 | 28 | public IList TakenBy(Owner requester) 29 | { 30 | if (CalendarEntries.TryGetValue(requester, out var slots)) 31 | { 32 | return slots; 33 | } 34 | 35 | return new List(); 36 | } 37 | 38 | public virtual bool Equals(Calendar? other) 39 | { 40 | if (ReferenceEquals(null, other)) return false; 41 | if (ReferenceEquals(this, other)) return true; 42 | return ResourceId == other.ResourceId 43 | && CalendarEntries.DictionaryEqual(other.CalendarEntries, 44 | EqualityComparer>.Create((x, y) => x!.SequenceEqual(y!))); 45 | } 46 | 47 | public override int GetHashCode() 48 | { 49 | return HashCode.Combine(ResourceId, 50 | CalendarEntries.CalculateHashCode(x => x.CalculateHashCode())); 51 | } 52 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Availability/Calendars.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Availability; 4 | 5 | public record Calendars(IDictionary CalendarsDictionary) 6 | { 7 | public static Calendars Of(params Calendar[] calendars) 8 | { 9 | var collect = calendars 10 | .ToDictionary(calendar => calendar.ResourceId, calendar => calendar); 11 | return new Calendars(collect); 12 | } 13 | 14 | public Calendar Get(ResourceId resourceId) 15 | { 16 | if (CalendarsDictionary.TryGetValue(resourceId, out var calendar)) 17 | { 18 | return calendar; 19 | } 20 | 21 | return Calendar.Empty(resourceId); 22 | } 23 | 24 | public virtual bool Equals(Calendars? other) 25 | { 26 | if (ReferenceEquals(null, other)) return false; 27 | if (ReferenceEquals(this, other)) return true; 28 | return CalendarsDictionary.DictionaryEqual(other.CalendarsDictionary); 29 | } 30 | 31 | public override int GetHashCode() 32 | { 33 | return CalendarsDictionary.CalculateHashCode(); 34 | } 35 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Availability/Owner.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Availability; 2 | 3 | public record Owner(Guid? OwnerId) 4 | { 5 | public bool ByNone 6 | { 7 | get { return this == None(); } 8 | } 9 | 10 | public static Owner None() 11 | { 12 | return new Owner((Guid?)null); 13 | } 14 | 15 | public static Owner NewOne() 16 | { 17 | return new Owner(Guid.NewGuid()); 18 | } 19 | 20 | public static Owner Of(Guid id) 21 | { 22 | return new Owner(id); 23 | } 24 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Availability/ResourceAvailabilityId.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Availability; 2 | 3 | public record ResourceAvailabilityId(Guid? Id) 4 | { 5 | public static ResourceAvailabilityId None() { 6 | return new ResourceAvailabilityId((Guid?)null); 7 | } 8 | 9 | public static ResourceAvailabilityId NewOne() 10 | { 11 | return new ResourceAvailabilityId(Guid.NewGuid()); 12 | } 13 | 14 | public static ResourceAvailabilityId Of(string? id) { 15 | if (id == null) { 16 | return None(); 17 | } 18 | return new ResourceAvailabilityId(Guid.Parse(id)); 19 | } 20 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Availability/ResourceId.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Availability; 2 | 3 | public record ResourceId(Guid? Id) 4 | { 5 | public static ResourceId NewOne() 6 | { 7 | return new ResourceId(Guid.NewGuid()); 8 | } 9 | 10 | public static ResourceId None() { 11 | return new ResourceId((Guid?)null); 12 | } 13 | 14 | public static ResourceId Of(string? id) { 15 | if (id == null) { 16 | return None(); 17 | } 18 | return new ResourceId(Guid.Parse(id)); 19 | } 20 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Availability/ResourceTakenOver.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Availability; 4 | 5 | public record ResourceTakenOver( 6 | Guid EventId, 7 | ResourceId ResourceId, 8 | ISet PreviousOwners, 9 | TimeSlot Slot, 10 | DateTime OccurredAt) : IPublishedEvent 11 | { 12 | public ResourceTakenOver(ResourceId resourceId, 13 | ISet previousOwners, 14 | TimeSlot slot, 15 | DateTime occurredAt) : this(Guid.NewGuid(), resourceId, previousOwners, slot, occurredAt) { } 16 | 17 | public override int GetHashCode() 18 | { 19 | return HashCode.Combine(EventId, ResourceId, PreviousOwners.CalculateHashCode(), Slot, OccurredAt); 20 | } 21 | 22 | public virtual bool Equals(ResourceTakenOver? other) 23 | { 24 | if (ReferenceEquals(null, other)) return false; 25 | if (ReferenceEquals(this, other)) return true; 26 | return EventId == other.EventId && ResourceId == other.ResourceId && 27 | PreviousOwners.SetEquals(other.PreviousOwners) 28 | && Slot == other.Slot && OccurredAt == other.OccurredAt; 29 | } 30 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Availability/Segment/SegmentInMinutes.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Availability.Segment; 2 | 3 | public record SegmentInMinutes(int Value) 4 | { 5 | public static SegmentInMinutes Of(int minutes, int slotDurationInMinutes) 6 | { 7 | if (minutes <= 0) 8 | { 9 | throw new ArgumentException("SegmentInMinutesDuration must be positive"); 10 | } 11 | 12 | if (minutes < slotDurationInMinutes) 13 | { 14 | throw new ArgumentException($"SegmentInMinutesDuration must be at least {slotDurationInMinutes} minutes"); 15 | } 16 | 17 | if (minutes % slotDurationInMinutes != 0) 18 | { 19 | throw new ArgumentException($"SegmentInMinutesDuration must be a multiple of {slotDurationInMinutes} minutes"); 20 | } 21 | 22 | return new SegmentInMinutes(minutes); 23 | } 24 | 25 | public static SegmentInMinutes Of(int minutes) 26 | { 27 | return Of(minutes, Segments.DefaultSegmentDurationInMinutes); 28 | } 29 | 30 | public static SegmentInMinutes DefaultSegment() 31 | { 32 | return Of(Segments.DefaultSegmentDurationInMinutes); 33 | } 34 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Availability/Segment/Segments.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Availability.Segment; 4 | 5 | public static class Segments 6 | { 7 | public const int DefaultSegmentDurationInMinutes = 60; 8 | 9 | public static IList Split(TimeSlot timeSlot, SegmentInMinutes unit) 10 | { 11 | var normalizedSlot = NormalizeToSegmentBoundaries(timeSlot, unit); 12 | return SlotToSegments.Apply(normalizedSlot, unit); 13 | } 14 | 15 | public static TimeSlot NormalizeToSegmentBoundaries(TimeSlot timeSlot, SegmentInMinutes unit) 16 | { 17 | return SlotToNormalizedSlot.Apply(timeSlot, unit); 18 | } 19 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Availability/Segment/SlotToNormalizedSlot.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Availability.Segment; 4 | 5 | public static class SlotToNormalizedSlot 6 | { 7 | public static TimeSlot Apply(TimeSlot timeSlot, SegmentInMinutes segmentInMinutes) 8 | { 9 | var segmentInMinutesDuration = segmentInMinutes.Value; 10 | var segmentStart = NormalizeStart(timeSlot.From, segmentInMinutesDuration); 11 | var segmentEnd = NormalizeEnd(timeSlot.To, segmentInMinutesDuration); 12 | var normalized = new TimeSlot(segmentStart, segmentEnd); 13 | var minimalSegment = new TimeSlot(segmentStart, segmentStart.AddMinutes(segmentInMinutes.Value)); 14 | 15 | if (normalized.Within(minimalSegment)) 16 | { 17 | return minimalSegment; 18 | } 19 | 20 | return normalized; 21 | } 22 | 23 | private static DateTime NormalizeEnd(DateTime initialEnd, int segmentInMinutesDuration) 24 | { 25 | var closestSegmentEnd = new DateTime(initialEnd.Year, initialEnd.Month, initialEnd.Day, initialEnd.Hour, 0, 0, 26 | initialEnd.Kind); 27 | 28 | while (initialEnd > closestSegmentEnd) 29 | { 30 | closestSegmentEnd = closestSegmentEnd.AddMinutes(segmentInMinutesDuration); 31 | } 32 | 33 | return closestSegmentEnd; 34 | } 35 | 36 | private static DateTime NormalizeStart(DateTime initialStart, int segmentInMinutesDuration) 37 | { 38 | var closestSegmentStart = new DateTime(initialStart.Year, initialStart.Month, initialStart.Day, 39 | initialStart.Hour, 0, 0, initialStart.Kind); 40 | 41 | if (closestSegmentStart.AddMinutes(segmentInMinutesDuration) > initialStart) 42 | { 43 | return closestSegmentStart; 44 | } 45 | 46 | while (closestSegmentStart < initialStart) 47 | { 48 | closestSegmentStart = closestSegmentStart.AddMinutes(segmentInMinutesDuration); 49 | } 50 | 51 | return closestSegmentStart; 52 | } 53 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Availability/Segment/SlotToSegments.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Availability.Segment; 4 | 5 | public static class SlotToSegments 6 | { 7 | public static IList Apply(TimeSlot timeSlot, SegmentInMinutes duration) 8 | { 9 | var minimalSegment = new TimeSlot(timeSlot.From, timeSlot.From.AddMinutes(duration.Value)); 10 | 11 | if (timeSlot.Within(minimalSegment)) 12 | { 13 | return new List { minimalSegment }; 14 | } 15 | 16 | var segmentInMinutesDuration = duration.Value; 17 | var numberOfSegments = CalculateNumberOfSegments(timeSlot, segmentInMinutesDuration); 18 | 19 | var segments = new List(); 20 | 21 | for (long i = 0; i < numberOfSegments; i++) 22 | { 23 | var currentStart = timeSlot.From.AddMinutes(i * segmentInMinutesDuration); 24 | segments.Add(new TimeSlot(currentStart, 25 | CalculateEnd(segmentInMinutesDuration, currentStart, timeSlot.To))); 26 | } 27 | 28 | return segments; 29 | } 30 | 31 | private static long CalculateNumberOfSegments(TimeSlot timeSlot, int segmentInMinutesDuration) 32 | { 33 | return (long)Math.Ceiling((timeSlot.To - timeSlot.From).TotalMinutes / segmentInMinutesDuration); 34 | } 35 | 36 | private static DateTime CalculateEnd(int segmentInMinutesDuration, DateTime currentStart, DateTime initialEnd) 37 | { 38 | var segmentEnd = currentStart.AddMinutes(segmentInMinutesDuration); 39 | 40 | if (initialEnd < segmentEnd) 41 | { 42 | return initialEnd; 43 | } 44 | 45 | return segmentEnd; 46 | } 47 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/DomainDrivers.SmartSchedule.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | Nullable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | PreserveNewest 29 | 30 | 31 | PreserveNewest 32 | 33 | 34 | PreserveNewest 35 | 36 | 37 | PreserveNewest 38 | 39 | 40 | PreserveNewest 41 | 42 | 43 | PreserveNewest 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Optimization/ICapacityDimension.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Optimization; 2 | 3 | public interface ICapacityDimension; -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Optimization/IWeightDimension.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Optimization; 2 | 3 | public interface IWeightDimension 4 | { 5 | bool IsSatisfiedBy(ICapacityDimension capacityDimension); 6 | } 7 | 8 | public interface IWeightDimension : IWeightDimension where T : ICapacityDimension 9 | { 10 | bool IsSatisfiedBy(T capacityDimension); 11 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Optimization/Item.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Optimization; 2 | 3 | public record Item(string Name, double Value, TotalWeight TotalWeight) 4 | { 5 | public bool IsWeightZero 6 | { 7 | get { return TotalWeight.Components().Count == 0; } 8 | } 9 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Optimization/OptimizationConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Optimization; 2 | 3 | public static class OptimizationConfiguration 4 | { 5 | public static IServiceCollection AddOptimization(this IServiceCollection serviceCollection) 6 | { 7 | serviceCollection.AddTransient(); 8 | return serviceCollection; 9 | } 10 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Optimization/Result.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Optimization; 4 | 5 | public record Result(double Profit, IList ChosenItems, IDictionary> ItemToCapacities) 6 | { 7 | public virtual bool Equals(Result? other) 8 | { 9 | if (ReferenceEquals(null, other)) return false; 10 | if (ReferenceEquals(this, other)) return true; 11 | return Profit == other.Profit 12 | && ChosenItems.SequenceEqual(other.ChosenItems) 13 | && ItemToCapacities.DictionaryEqual(other.ItemToCapacities, 14 | EqualityComparer>.Create((x, y) => x!.SetEquals(y!))); 15 | } 16 | 17 | public override int GetHashCode() 18 | { 19 | return HashCode.Combine(Profit, ChosenItems.CalculateHashCode(), ItemToCapacities.CalculateHashCode()); 20 | } 21 | 22 | public override string ToString() 23 | { 24 | return $"Result{{profit={Profit}, chosenItems={ChosenItems.ToCollectionString()}}}"; 25 | } 26 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Optimization/TotalCapacity.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Optimization; 4 | 5 | public record TotalCapacity(IList CapacitiesList) 6 | { 7 | public static TotalCapacity Of(params ICapacityDimension[] capacities) 8 | { 9 | return new TotalCapacity(capacities.ToList()); 10 | } 11 | 12 | public static TotalCapacity Of(IList capacities) 13 | { 14 | return new TotalCapacity(capacities); 15 | } 16 | 17 | public static TotalCapacity Zero() 18 | { 19 | return new TotalCapacity(new List()); 20 | } 21 | 22 | public int Size 23 | { 24 | get { return CapacitiesList.Count; } 25 | } 26 | 27 | public IList Capacities() 28 | { 29 | return new List(CapacitiesList); 30 | } 31 | 32 | public TotalCapacity Add(IList capacities) 33 | { 34 | var newCapacities = new List(CapacitiesList); 35 | newCapacities.AddRange(capacities); 36 | return new TotalCapacity(newCapacities); 37 | } 38 | 39 | public virtual bool Equals(TotalCapacity? other) 40 | { 41 | if (ReferenceEquals(null, other)) return false; 42 | if (ReferenceEquals(this, other)) return true; 43 | return CapacitiesList.SequenceEqual(other.CapacitiesList); 44 | } 45 | 46 | public override int GetHashCode() 47 | { 48 | return CapacitiesList.CalculateHashCode(); 49 | } 50 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Optimization/TotalWeight.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Optimization; 4 | 5 | public record TotalWeight(IList ComponentsList) 6 | { 7 | public static TotalWeight Zero() 8 | { 9 | return new TotalWeight(new List()); 10 | } 11 | 12 | public static TotalWeight Of(params IWeightDimension[] components) 13 | { 14 | return new TotalWeight(components.ToList()); 15 | } 16 | 17 | public IList Components() 18 | { 19 | return new List(ComponentsList); 20 | } 21 | 22 | public virtual bool Equals(TotalWeight? other) 23 | { 24 | if (ReferenceEquals(null, other)) return false; 25 | if (ReferenceEquals(this, other)) return true; 26 | return ComponentsList.SequenceEqual(other.ComponentsList); 27 | } 28 | 29 | public override int GetHashCode() 30 | { 31 | return ComponentsList.CalculateHashCode(); 32 | } 33 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/CapabilitiesDemanded.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Planning; 4 | 5 | public record CapabilitiesDemanded(Guid Uuid, ProjectId ProjectId, Demands Demands, DateTime OccurredAt) : IPublishedEvent 6 | { 7 | public CapabilitiesDemanded(ProjectId projectId, Demands demands, DateTime occurredAt) : this(Guid.NewGuid(), 8 | projectId, demands, occurredAt) { } 9 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/ChosenResources.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Availability; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | 4 | namespace DomainDrivers.SmartSchedule.Planning; 5 | 6 | public record ChosenResources(ISet Resources, TimeSlot TimeSlot) 7 | { 8 | public static ChosenResources None() 9 | { 10 | return new ChosenResources(new HashSet(), TimeSlot.Empty()); 11 | } 12 | 13 | public virtual bool Equals(ChosenResources? other) 14 | { 15 | if (ReferenceEquals(null, other)) return false; 16 | if (ReferenceEquals(this, other)) return true; 17 | return Resources.SetEquals(other.Resources) 18 | && TimeSlot == other.TimeSlot; 19 | } 20 | 21 | public override int GetHashCode() 22 | { 23 | return HashCode.Combine(Resources.CalculateHashCode(), TimeSlot); 24 | } 25 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/CreateProjectAllocations.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Allocation; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | 4 | namespace DomainDrivers.SmartSchedule.Planning; 5 | 6 | public class CreateProjectAllocations 7 | { 8 | private readonly AllocationFacade _allocationFacade; 9 | private readonly IProjectRepository _projectRepository; 10 | private readonly IUnitOfWork _unitOfWork; 11 | 12 | public CreateProjectAllocations(AllocationFacade allocationFacade, IProjectRepository projectRepository, IUnitOfWork unitOfWork) 13 | { 14 | _allocationFacade = allocationFacade; 15 | _projectRepository = projectRepository; 16 | _unitOfWork = unitOfWork; 17 | } 18 | 19 | //can react to ScheduleCalculated event 20 | public async Task Create(ProjectId projectId) 21 | { 22 | await _unitOfWork.InTransaction(async () => 23 | { 24 | var project = await _projectRepository.GetById(projectId); 25 | var schedule = project.Schedule; 26 | //for each stage in schedule 27 | //create allocation 28 | //allocate chosen resources (or find equivalents) 29 | //start risk analysis 30 | }); 31 | } 32 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/CriticalStagePlanned.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Availability; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | 4 | namespace DomainDrivers.SmartSchedule.Planning; 5 | 6 | public record CriticalStagePlanned( 7 | ProjectId ProjectId, 8 | TimeSlot StageTimeSlot, 9 | ResourceId? CriticalResource, 10 | DateTime OccurredAt) : IPublishedEvent; -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/Demand.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Planning; 4 | 5 | public record Demand(Capability Capability) 6 | { 7 | public static Demand DemandFor(Capability capability) 8 | { 9 | return new Demand(capability); 10 | } 11 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/Demands.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Planning; 4 | 5 | public record Demands(IList All) 6 | { 7 | public static Demands None() 8 | { 9 | return new Demands(new List()); 10 | } 11 | 12 | public static Demands Of(params Demand[] demands) 13 | { 14 | return new Demands(demands.ToList()); 15 | } 16 | 17 | public Demands Add(Demands demands) 18 | { 19 | return new Demands(All.Concat(demands.All).ToList()); 20 | } 21 | 22 | public virtual bool Equals(Demands? other) 23 | { 24 | if (ReferenceEquals(null, other)) return false; 25 | if (ReferenceEquals(this, other)) return true; 26 | return All.SequenceEqual(other.All); 27 | } 28 | 29 | public override int GetHashCode() 30 | { 31 | return All.CalculateHashCode(); 32 | } 33 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/DemandsPerStage.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Planning; 4 | 5 | public record DemandsPerStage(IDictionary Demands) 6 | { 7 | public static DemandsPerStage Empty() 8 | { 9 | return new DemandsPerStage(new Dictionary()); 10 | } 11 | 12 | public virtual bool Equals(DemandsPerStage? other) 13 | { 14 | if (ReferenceEquals(null, other)) return false; 15 | if (ReferenceEquals(this, other)) return true; 16 | return Demands.DictionaryEqual(other.Demands); 17 | } 18 | 19 | public override int GetHashCode() 20 | { 21 | return Demands.CalculateHashCode(); 22 | } 23 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/EditStageDateService.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Allocation; 2 | using DomainDrivers.SmartSchedule.Planning.Parallelization; 3 | using DomainDrivers.SmartSchedule.Shared; 4 | 5 | namespace DomainDrivers.SmartSchedule.Planning; 6 | 7 | public class EditStageDateService 8 | { 9 | private readonly AllocationFacade _allocationFacade; 10 | private readonly IProjectRepository _projectRepository; 11 | private readonly IUnitOfWork _unitOfWork; 12 | 13 | public EditStageDateService(AllocationFacade allocationFacade, IProjectRepository projectRepository, 14 | IUnitOfWork unitOfWork) 15 | { 16 | _allocationFacade = allocationFacade; 17 | _projectRepository = projectRepository; 18 | _unitOfWork = unitOfWork; 19 | } 20 | 21 | public async Task EditStageDate(ProjectId projectId, Stage stage, TimeSlot timeSlot) 22 | { 23 | await _unitOfWork.InTransaction(async () => 24 | { 25 | var project = await _projectRepository.GetById(projectId); 26 | var schedule = project.Schedule; 27 | //redefine schedule 28 | //for each stage in schedule 29 | //recreate allocation 30 | //reallocate chosen resources (or find equivalents) 31 | //start risk analysis 32 | }); 33 | } 34 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/IProjectRepository.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Planning; 2 | 3 | public interface IProjectRepository 4 | { 5 | Task GetById(ProjectId projectId); 6 | 7 | Task Save(Project project); 8 | 9 | Task> FindAllByIdIn(ISet projectIds); 10 | 11 | Task> FindAll(); 12 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/NeededResourcesChosen.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Availability; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | 4 | namespace DomainDrivers.SmartSchedule.Planning; 5 | 6 | public record NeededResourcesChosen( 7 | ProjectId ProjectId, 8 | ISet NeededResources, 9 | TimeSlot TimeSlot, 10 | DateTime OccurredAt) : IPublishedEvent 11 | { 12 | public override int GetHashCode() 13 | { 14 | return HashCode.Combine(ProjectId, NeededResources.CalculateHashCode(), TimeSlot, OccurredAt); 15 | } 16 | 17 | public virtual bool Equals(NeededResourcesChosen? other) 18 | { 19 | if (ReferenceEquals(null, other)) return false; 20 | if (ReferenceEquals(this, other)) return true; 21 | return ProjectId == other.ProjectId && NeededResources.SetEquals(other.NeededResources) && 22 | TimeSlot == other.TimeSlot && OccurredAt == other.OccurredAt; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/Parallelization/DurationCalculator.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Planning.Parallelization; 2 | 3 | public static class DurationCalculator 4 | { 5 | public static TimeSpan Calculate(IList stages) 6 | { 7 | var parallelizedStages = new StageParallelization().Of(new HashSet(stages)); 8 | var durations = stages.ToDictionary(identity => identity, stage => stage.Duration); 9 | return parallelizedStages 10 | .AllSorted() 11 | .Aggregate(TimeSpan.Zero, (current, parallelStages) => 12 | current.Add(parallelStages.Stages 13 | .Select(stage => durations[stage]) 14 | .DefaultIfEmpty(TimeSpan.Zero) 15 | .Max())); 16 | } 17 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/Parallelization/ParallelStages.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Planning.Parallelization; 4 | 5 | public record ParallelStages(ISet Stages) 6 | { 7 | public string Print() 8 | { 9 | return string.Join(", ", Stages.Select(stage => stage.StageName).OrderBy(name => name)); 10 | } 11 | 12 | public static ParallelStages Of(params Stage[] stages) 13 | { 14 | return new ParallelStages(stages.ToHashSet()); 15 | } 16 | 17 | public TimeSpan Duration 18 | { 19 | get 20 | { 21 | return Stages 22 | .Select(x => x.Duration) 23 | .DefaultIfEmpty(TimeSpan.Zero) 24 | .Max(); 25 | } 26 | } 27 | 28 | public virtual bool Equals(ParallelStages? other) 29 | { 30 | if (ReferenceEquals(null, other)) return false; 31 | if (ReferenceEquals(this, other)) return true; 32 | return Stages.SetEquals(other.Stages); 33 | } 34 | 35 | public override int GetHashCode() 36 | { 37 | return Stages.CalculateHashCode(); 38 | } 39 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/Parallelization/ParallelStagesList.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Planning.Parallelization; 4 | 5 | public record ParallelStagesList(IList All) 6 | { 7 | public static ParallelStagesList Empty() 8 | { 9 | return new ParallelStagesList(new List()); 10 | } 11 | 12 | public static ParallelStagesList Of(params ParallelStages[] stages) 13 | { 14 | return new ParallelStagesList(stages.ToList()); 15 | } 16 | 17 | public string Print() 18 | { 19 | return string.Join(" | ", All.Select(parallelStages => parallelStages.Print())); 20 | } 21 | 22 | public ParallelStagesList Add(ParallelStages newParallelStages) 23 | { 24 | var result = new List(All) { newParallelStages }; 25 | return new ParallelStagesList(result); 26 | } 27 | 28 | public IList AllSorted(IComparer comparing) 29 | { 30 | return All 31 | .Order(comparing) 32 | .ToList(); 33 | } 34 | 35 | public IList AllSorted() 36 | { 37 | return AllSorted(Comparer.Create((x, y) => x.Print().CompareTo(y.Print()))); 38 | } 39 | 40 | public virtual bool Equals(ParallelStagesList? other) 41 | { 42 | if (ReferenceEquals(null, other)) return false; 43 | if (ReferenceEquals(this, other)) return true; 44 | return All.SequenceEqual(other.All); 45 | } 46 | 47 | public override int GetHashCode() 48 | { 49 | return All.CalculateHashCode(); 50 | } 51 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/Parallelization/SortedNodesToParallelizedStages.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Sorter; 2 | 3 | namespace DomainDrivers.SmartSchedule.Planning.Parallelization; 4 | 5 | public class SortedNodesToParallelizedStages 6 | { 7 | public ParallelStagesList Calculate(SortedNodes sortedNodes) 8 | { 9 | var parallelized = sortedNodes.All 10 | .Select(nodes => new ParallelStages(nodes.NodesCollection 11 | .Where(node => node.Content != null) 12 | .Select(node => node.Content!) 13 | .ToHashSet())) 14 | .ToList(); 15 | return new ParallelStagesList(parallelized); 16 | } 17 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/Parallelization/Stage.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using DomainDrivers.SmartSchedule.Availability; 3 | 4 | namespace DomainDrivers.SmartSchedule.Planning.Parallelization; 5 | 6 | [method: JsonConstructor] 7 | public record Stage(string StageName, ISet Dependencies, ISet Resources, TimeSpan Duration) 8 | { 9 | public Stage OfDuration(TimeSpan duration) 10 | { 11 | return new Stage(StageName, Dependencies, Resources, duration); 12 | } 13 | 14 | public Stage(string name) : this(name, new HashSet(), new HashSet(), TimeSpan.Zero) 15 | { 16 | } 17 | 18 | public Stage DependsOn(Stage stage) 19 | { 20 | var newDependencies = new HashSet(Dependencies) { stage }; 21 | Dependencies.Add(stage); 22 | return new Stage(StageName, newDependencies, Resources, Duration); 23 | } 24 | 25 | public string Name 26 | { 27 | get { return StageName; } 28 | } 29 | 30 | public Stage WithChosenResourceCapabilities(params ResourceId[] resources) 31 | { 32 | var collect = new HashSet(resources); 33 | return new Stage(StageName, Dependencies, collect, Duration); 34 | } 35 | 36 | public virtual bool Equals(Stage? other) 37 | { 38 | if (ReferenceEquals(null, other)) return false; 39 | if (ReferenceEquals(this, other)) return true; 40 | return StageName == other.StageName 41 | && Dependencies.SetEquals(other.Dependencies) 42 | && Resources.SetEquals(other.Resources) 43 | && Duration == other.Duration; 44 | } 45 | 46 | public override int GetHashCode() 47 | { 48 | return StageName.GetHashCode(); 49 | } 50 | 51 | public override string ToString() 52 | { 53 | return StageName; 54 | } 55 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/Parallelization/StageParallelization.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | using DomainDrivers.SmartSchedule.Sorter; 3 | 4 | namespace DomainDrivers.SmartSchedule.Planning.Parallelization; 5 | 6 | public class StageParallelization 7 | { 8 | public ParallelStagesList Of(ISet stages) 9 | { 10 | var nodes = new StagesToNodes().Calculate(new List(stages)); 11 | var sortedNodes = new GraphTopologicalSort().Sort(nodes); 12 | return new SortedNodesToParallelizedStages().Calculate(sortedNodes); 13 | } 14 | 15 | public RemovalSuggestion WhatToRemove(ISet stages) 16 | { 17 | var nodes = new StagesToNodes().Calculate(new List(stages)); 18 | var result = new FeedbackArcSetOnGraph().Calculate(new List>(nodes.NodesCollection)); 19 | return new RemovalSuggestion(result); 20 | } 21 | } 22 | 23 | public record RemovalSuggestion(IList Edges) 24 | { 25 | public override int GetHashCode() 26 | { 27 | return Edges.CalculateHashCode(); 28 | } 29 | 30 | public virtual bool Equals(RemovalSuggestion? other) 31 | { 32 | if (ReferenceEquals(null, other)) return false; 33 | if (ReferenceEquals(this, other)) return true; 34 | return Edges.SequenceEqual(other.Edges); 35 | } 36 | 37 | public override string ToString() 38 | { 39 | return Edges.ToCollectionString(); 40 | } 41 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/Parallelization/StageToNodes.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Sorter; 2 | 3 | namespace DomainDrivers.SmartSchedule.Planning.Parallelization; 4 | 5 | public class StagesToNodes 6 | { 7 | public Nodes Calculate(IList stages) 8 | { 9 | IDictionary> result = stages.ToDictionary(stage => stage.Name, stage => new Node(stage.Name, stage)); 10 | 11 | for (var i = 0; i < stages.Count; i++) 12 | { 13 | var stage = stages[i]; 14 | result = ExplicitDependencies(stage, result); 15 | result = SharedResources(stage, stages.Skip(i + 1).ToList(), result); 16 | } 17 | 18 | return new Nodes(new HashSet>(result.Values)); 19 | } 20 | 21 | private IDictionary> SharedResources(Stage stage, IList with, IDictionary> result) 22 | { 23 | foreach (var other in with) 24 | { 25 | if (stage.Name != other.Name) 26 | { 27 | if (stage.Resources.Intersect(other.Resources).Any()) 28 | { 29 | if (other.Resources.Count > stage.Resources.Count) 30 | { 31 | var node = result[stage.Name]; 32 | node = node.DependsOn(result[other.Name]); 33 | result[stage.Name] = node; 34 | } 35 | else 36 | { 37 | var node = result[other.Name]; 38 | node = node.DependsOn(result[stage.Name]); 39 | result[other.Name] = node; 40 | } 41 | } 42 | } 43 | } 44 | return result; 45 | } 46 | 47 | private IDictionary> ExplicitDependencies(Stage stage, IDictionary> result) 48 | { 49 | var nodeWithExplicitDeps = result[stage.Name]; 50 | foreach (var explicitDependency in stage.Dependencies) 51 | { 52 | nodeWithExplicitDeps = nodeWithExplicitDeps.DependsOn(result[explicitDependency.Name]); 53 | } 54 | result[stage.Name] = nodeWithExplicitDeps; 55 | return result; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/PlanChosenResources.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Availability; 2 | using DomainDrivers.SmartSchedule.Planning.Parallelization; 3 | using DomainDrivers.SmartSchedule.Planning.Scheduling; 4 | using DomainDrivers.SmartSchedule.Shared; 5 | 6 | namespace DomainDrivers.SmartSchedule.Planning; 7 | 8 | public class PlanChosenResources 9 | { 10 | private readonly IProjectRepository _projectRepository; 11 | private readonly IAvailabilityFacade _availabilityFacade; 12 | private readonly IEventsPublisher _eventsPublisher; 13 | private readonly TimeProvider _timeProvider; 14 | 15 | public PlanChosenResources(IProjectRepository projectRepository, IAvailabilityFacade availabilityFacade, 16 | IEventsPublisher eventsPublisher, TimeProvider timeProvider) 17 | { 18 | _projectRepository = projectRepository; 19 | _availabilityFacade = availabilityFacade; 20 | _eventsPublisher = eventsPublisher; 21 | _timeProvider = timeProvider; 22 | } 23 | 24 | public async Task DefineResourcesWithinDates(ProjectId projectId, ISet chosenResources, 25 | TimeSlot timeBoundaries) 26 | { 27 | var project = await _projectRepository.GetById(projectId); 28 | project.AddChosenResources(new ChosenResources(chosenResources, timeBoundaries)); 29 | await _projectRepository.Save(project); 30 | await _eventsPublisher.Publish(new NeededResourcesChosen(projectId, chosenResources, timeBoundaries, 31 | _timeProvider.GetUtcNow().DateTime)); 32 | } 33 | 34 | public async Task AdjustStagesToResourceAvailability(ProjectId projectId, TimeSlot timeBoundaries, 35 | params Stage[] stages) 36 | { 37 | var neededResources = NeededResources(stages); 38 | var project = await _projectRepository.GetById(projectId); 39 | await DefineResourcesWithinDates(projectId, neededResources, timeBoundaries); 40 | var neededResourcesCalendars = await _availabilityFacade.LoadCalendars(neededResources, timeBoundaries); 41 | var schedule = CreateScheduleAdjustingToCalendars(neededResourcesCalendars, stages.ToList()); 42 | project.AddSchedule(schedule); 43 | await _projectRepository.Save(project); 44 | } 45 | 46 | private Schedule CreateScheduleAdjustingToCalendars(Calendars neededResourcesCalendars, 47 | IList stages) 48 | { 49 | return Schedule.BasedOnChosenResourcesAvailability(neededResourcesCalendars, stages); 50 | } 51 | 52 | private ISet NeededResources(Stage[] stages) 53 | { 54 | return stages.SelectMany(stage => stage.Resources).ToHashSet(); 55 | } 56 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/PlanningConfiguration.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Planning.Parallelization; 2 | 3 | namespace DomainDrivers.SmartSchedule.Planning; 4 | 5 | public static class PlanningConfiguration 6 | { 7 | public static IServiceCollection AddPlanning(this IServiceCollection serviceCollection) 8 | { 9 | serviceCollection.AddScoped(); 10 | serviceCollection.AddTransient(); 11 | serviceCollection.AddTransient(); 12 | serviceCollection.AddTransient(); 13 | return serviceCollection; 14 | } 15 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/Project.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using DomainDrivers.SmartSchedule.Planning.Parallelization; 3 | using DomainDrivers.SmartSchedule.Planning.Scheduling; 4 | using DomainDrivers.SmartSchedule.Shared; 5 | 6 | namespace DomainDrivers.SmartSchedule.Planning; 7 | 8 | public class Project 9 | { 10 | public ProjectId Id { get; private set; } = ProjectId.NewOne(); 11 | 12 | public string Name { get; private set; } 13 | 14 | public ParallelStagesList ParallelizedStages { get; private set; } 15 | 16 | public DemandsPerStage DemandsPerStage { get; private set; } 17 | 18 | public Demands AllDemands { get; private set; } 19 | 20 | public ChosenResources ChosenResources { get; private set; } 21 | 22 | public Schedule Schedule { get; private set; } 23 | 24 | [JsonConstructor] 25 | public Project(string name, ParallelStagesList parallelizedStages, DemandsPerStage demandsPerStage, 26 | Demands allDemands, ChosenResources chosenResources, Schedule schedule, ProjectId id) 27 | { 28 | Name = name; 29 | ParallelizedStages = parallelizedStages; 30 | DemandsPerStage = demandsPerStage; 31 | AllDemands = allDemands; 32 | ChosenResources = chosenResources; 33 | Schedule = schedule; 34 | Id = id; 35 | } 36 | 37 | public Project(string name, ParallelStagesList parallelizedStages) 38 | { 39 | Name = name; 40 | ParallelizedStages = parallelizedStages; 41 | AllDemands = Demands.None(); 42 | Schedule = Schedule.None(); 43 | ChosenResources = ChosenResources.None(); 44 | DemandsPerStage = DemandsPerStage.Empty(); 45 | } 46 | 47 | public void AddDemands(Demands demands) 48 | { 49 | AllDemands = AllDemands.Add(demands); 50 | } 51 | 52 | public void AddSchedule(DateTime possibleStartDate) 53 | { 54 | Schedule = Schedule.BasedOnStartDay(possibleStartDate, ParallelizedStages); 55 | } 56 | 57 | public void AddChosenResources(ChosenResources neededResources) 58 | { 59 | ChosenResources = neededResources; 60 | } 61 | 62 | public void AddDemandsPerStage(DemandsPerStage demandsPerStage) 63 | { 64 | DemandsPerStage = demandsPerStage; 65 | var uniqueDemands = demandsPerStage.Demands.Values 66 | .SelectMany(demands => demands.All) 67 | .ToHashSet(); 68 | AddDemands(new Demands(uniqueDemands.ToList())); 69 | } 70 | 71 | public void AddSchedule(Stage criticalStage, TimeSlot stageTimeSlot) 72 | { 73 | Schedule = Schedule.BasedOnReferenceStageTimeSlot(criticalStage, stageTimeSlot, ParallelizedStages); 74 | } 75 | 76 | public void AddSchedule(Schedule schedule) 77 | { 78 | Schedule = schedule; 79 | } 80 | 81 | public void DefineStages(ParallelStagesList parallelizedStages) 82 | { 83 | ParallelizedStages = parallelizedStages; 84 | } 85 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/ProjectCard.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Planning.Parallelization; 2 | using DomainDrivers.SmartSchedule.Planning.Scheduling; 3 | 4 | namespace DomainDrivers.SmartSchedule.Planning; 5 | 6 | public record ProjectCard( 7 | ProjectId ProjectId, 8 | string Name, 9 | ParallelStagesList ParallelizedStages, 10 | Demands Demands, 11 | Schedule Schedule, 12 | DemandsPerStage DemandsPerStage, 13 | ChosenResources NeededResources) 14 | { 15 | public ProjectCard(ProjectId projectId, string name, ParallelStagesList parallelizedStages, Demands demands) 16 | : this(projectId, name, parallelizedStages, demands, Schedule.None(), DemandsPerStage.Empty(), 17 | ChosenResources.None()) 18 | { 19 | } 20 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/ProjectId.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Planning; 2 | 3 | public record ProjectId(Guid Id) 4 | { 5 | public static ProjectId NewOne() 6 | { 7 | return new ProjectId(Guid.NewGuid()); 8 | } 9 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/RedisProjectRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using StackExchange.Redis; 3 | 4 | namespace DomainDrivers.SmartSchedule.Planning; 5 | 6 | public class RedisProjectRepository : IProjectRepository 7 | { 8 | private readonly IDatabase _database; 9 | 10 | public RedisProjectRepository(IConnectionMultiplexer redisConnection) 11 | { 12 | _database = redisConnection.GetDatabase(); 13 | } 14 | 15 | public async Task GetById(ProjectId projectId) 16 | { 17 | var redisValue = await _database.HashGetAsync("projects", projectId.Id.ToString()); 18 | 19 | if (redisValue.IsNull) 20 | { 21 | return null!; 22 | } 23 | 24 | return JsonSerializer.Deserialize(redisValue.ToString())!; 25 | } 26 | 27 | public async Task Save(Project project) 28 | { 29 | await _database.HashSetAsync("projects", project.Id.Id.ToString(), JsonSerializer.Serialize(project)); 30 | return project; 31 | } 32 | 33 | public async Task> FindAllByIdIn(ISet projectIds) 34 | { 35 | var ids = projectIds.Select(p => (RedisValue)p.Id.ToString()).ToArray(); 36 | var redisValues = await _database.HashGetAsync("projects", ids); 37 | return redisValues.Select(redisValue => JsonSerializer.Deserialize(redisValue.ToString())!).ToList(); 38 | } 39 | 40 | public async Task> FindAll() 41 | { 42 | var redisValues = await _database.HashValuesAsync("projects"); 43 | return redisValues.Select(redisValue => JsonSerializer.Deserialize(redisValue.ToString())!).ToList(); 44 | } 45 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/Scheduling/Schedule.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Availability; 2 | using DomainDrivers.SmartSchedule.Planning.Parallelization; 3 | using DomainDrivers.SmartSchedule.Shared; 4 | 5 | namespace DomainDrivers.SmartSchedule.Planning.Scheduling; 6 | 7 | public record Schedule(IDictionary Dates) 8 | { 9 | public static Schedule None() 10 | { 11 | return new Schedule(new Dictionary()); 12 | } 13 | 14 | public static Schedule BasedOnStartDay(DateTime startDate, ParallelStagesList parallelizedStages) 15 | { 16 | var scheduleMap = new ScheduleBasedOnStartDayCalculator().Calculate(startDate, parallelizedStages, 17 | Comparer.Create((x, y) => x.Print().CompareTo(y.Print()))); 18 | return new Schedule(scheduleMap); 19 | } 20 | 21 | public static Schedule BasedOnReferenceStageTimeSlot(Stage referenceStage, TimeSlot stageProposedTimeSlot, 22 | ParallelStagesList parallelizedStages) 23 | { 24 | var scheduleMap = new ScheduleBasedOnReferenceStageCalculator().Calculate(referenceStage, stageProposedTimeSlot, 25 | parallelizedStages, Comparer.Create((x, y) => x.Print().CompareTo(y.Print()))); 26 | return new Schedule(scheduleMap); 27 | } 28 | 29 | public static Schedule BasedOnChosenResourcesAvailability(Calendars chosenResourcesCalendars, IList stages) 30 | { 31 | var schedule = 32 | new ScheduleBasedOnChosenResourcesAvailabilityCalculator().Calculate(chosenResourcesCalendars, stages); 33 | return new Schedule(schedule); 34 | } 35 | 36 | public virtual bool Equals(Schedule? other) 37 | { 38 | if (ReferenceEquals(null, other)) return false; 39 | if (ReferenceEquals(this, other)) return true; 40 | return Dates.DictionaryEqual(other.Dates); 41 | } 42 | 43 | public override int GetHashCode() 44 | { 45 | return Dates.CalculateHashCode(); 46 | } 47 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/Scheduling/ScheduleBasedOnChosenResourcesAvailabilityCalculator.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Availability; 2 | using DomainDrivers.SmartSchedule.Planning.Parallelization; 3 | using DomainDrivers.SmartSchedule.Shared; 4 | 5 | namespace DomainDrivers.SmartSchedule.Planning.Scheduling; 6 | 7 | public class ScheduleBasedOnChosenResourcesAvailabilityCalculator 8 | { 9 | public IDictionary Calculate(Calendars chosenResourcesCalendars, IList stages) 10 | { 11 | var schedule = new Dictionary(); 12 | 13 | foreach (var stage in stages) 14 | { 15 | var proposedSlot = FindSlotForStage(chosenResourcesCalendars, stage); 16 | 17 | if (proposedSlot == TimeSlot.Empty()) 18 | { 19 | return new Dictionary(); 20 | } 21 | 22 | schedule[stage.Name] = proposedSlot; 23 | } 24 | 25 | return schedule; 26 | } 27 | 28 | private TimeSlot FindSlotForStage(Calendars chosenResourcesCalendars, Stage stage) 29 | { 30 | var foundSlots = PossibleSlots(chosenResourcesCalendars, stage); 31 | 32 | if (foundSlots.Contains(TimeSlot.Empty())) 33 | { 34 | return TimeSlot.Empty(); 35 | } 36 | 37 | var commonSlotForAllResources = FindCommonPartOfSlots(foundSlots); 38 | 39 | while (!IsSlotLongEnoughForStage(stage, commonSlotForAllResources)) 40 | { 41 | commonSlotForAllResources = commonSlotForAllResources.Stretch(TimeSpan.FromDays(1)); 42 | } 43 | 44 | return new TimeSlot(commonSlotForAllResources.From, commonSlotForAllResources.From.Add(stage.Duration)); 45 | } 46 | 47 | private bool IsSlotLongEnoughForStage(Stage stage, TimeSlot slot) 48 | { 49 | return slot.Duration.CompareTo(stage.Duration) >= 0; 50 | } 51 | 52 | private TimeSlot FindCommonPartOfSlots(IList foundSlots) 53 | { 54 | return foundSlots 55 | .DefaultIfEmpty(TimeSlot.Empty()) 56 | .Aggregate((a, b) => a.CommonPartWith(b)); 57 | } 58 | 59 | private List PossibleSlots(Calendars chosenResourcesCalendars, Stage stage) 60 | { 61 | return stage.Resources 62 | .Select(resource => 63 | chosenResourcesCalendars 64 | .Get(resource) 65 | .AvailableSlots() 66 | .OrderBy(slot => slot.From) 67 | .FirstOrDefault(slot => IsSlotLongEnoughForStage(stage, slot)) 68 | ?? TimeSlot.Empty()) 69 | .ToList(); 70 | } 71 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Planning/Scheduling/ScheduleBasedOnStartDayCalculator.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Planning.Parallelization; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | 4 | namespace DomainDrivers.SmartSchedule.Planning.Scheduling; 5 | 6 | public class ScheduleBasedOnStartDayCalculator 7 | { 8 | public IDictionary Calculate(DateTime startDate, ParallelStagesList parallelizedStages, IComparer comparing) 9 | { 10 | var scheduleMap = new Dictionary(); 11 | var currentStart = startDate; 12 | var allSorted = parallelizedStages.AllSorted(comparing); 13 | 14 | foreach (var stages in allSorted) 15 | { 16 | var parallelizedStagesEnd = currentStart; 17 | 18 | foreach (var stage in stages.Stages) 19 | { 20 | var stageEnd = currentStart.Add(stage.Duration); 21 | scheduleMap.Add(stage.StageName, new TimeSlot(currentStart, stageEnd)); 22 | 23 | if (stageEnd > parallelizedStagesEnd) 24 | { 25 | parallelizedStagesEnd = stageEnd; 26 | } 27 | } 28 | 29 | currentStart = parallelizedStagesEnd; 30 | } 31 | 32 | return scheduleMap; 33 | } 34 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using System.Text.Json; 3 | using DomainDrivers.SmartSchedule; 4 | using DomainDrivers.SmartSchedule.Allocation; 5 | using DomainDrivers.SmartSchedule.Availability; 6 | using DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 7 | using DomainDrivers.SmartSchedule.Allocation.Cashflow; 8 | using DomainDrivers.SmartSchedule.Optimization; 9 | using DomainDrivers.SmartSchedule.Planning; 10 | using DomainDrivers.SmartSchedule.Resource; 11 | using DomainDrivers.SmartSchedule.Resource.Device; 12 | using DomainDrivers.SmartSchedule.Resource.Employee; 13 | using DomainDrivers.SmartSchedule.Risk; 14 | using DomainDrivers.SmartSchedule.Shared; 15 | using DomainDrivers.SmartSchedule.Simulation; 16 | using Microsoft.EntityFrameworkCore; 17 | using Npgsql; 18 | using Quartz; 19 | using StackExchange.Redis; 20 | 21 | var builder = WebApplication.CreateBuilder(args); 22 | var postgresConnectionString = builder.Configuration.GetConnectionString("Postgres"); 23 | var redisConnectionString = builder.Configuration.GetConnectionString("Redis"); 24 | builder.Services.AddSingleton(_ => ConnectionMultiplexer.Connect(redisConnectionString!)); 25 | 26 | var dataSource = new NpgsqlDataSourceBuilder(postgresConnectionString) 27 | .ConfigureJsonOptions(new JsonSerializerOptions() { IgnoreReadOnlyProperties = true, IgnoreReadOnlyFields = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase}) 28 | .EnableDynamicJson() 29 | .Build(); 30 | builder.Services.AddDbContext(options => { options.UseNpgsql(dataSource); }); 31 | builder.Services.AddScoped(sp => sp.GetRequiredService().Database.GetDbConnection()); 32 | builder.Services.AddShared(); 33 | builder.Services.AddPlanning(); 34 | builder.Services.AddAvailability(); 35 | builder.Services.AddAllocation(); 36 | builder.Services.AddCashFlow(); 37 | builder.Services.AddEmployee(); 38 | builder.Services.AddDevice(); 39 | builder.Services.AddResource(); 40 | builder.Services.AddCapabilityPlanning(); 41 | builder.Services.AddOptimization(); 42 | builder.Services.AddSimulation(); 43 | builder.Services.AddRisk(); 44 | builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); 45 | 46 | var app = builder.Build(); 47 | 48 | app.Run(); 49 | 50 | public partial class Program; -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Resource/Device/Device.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Resource.Device; 4 | 5 | public class Device 6 | { 7 | public DeviceId Id { get; private set; } = DeviceId.NewOne(); 8 | 9 | private int _version; 10 | 11 | public string Model { get; private set; } 12 | 13 | public ISet Capabilities { get; private set; } 14 | 15 | public Device(DeviceId id, string model, ISet capabilities) 16 | { 17 | Id = id; 18 | Model = model; 19 | Capabilities = capabilities; 20 | } 21 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Resource/Device/DeviceConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | 4 | namespace DomainDrivers.SmartSchedule.Resource.Device; 5 | 6 | public static class DeviceConfiguration 7 | { 8 | public static IServiceCollection AddDevice(this IServiceCollection serviceCollection) 9 | { 10 | serviceCollection.AddScoped( 11 | sp => sp.GetRequiredService()); 12 | serviceCollection.AddTransient(); 13 | serviceCollection.AddTransient(); 14 | serviceCollection.AddTransient(); 15 | return serviceCollection; 16 | } 17 | } 18 | 19 | public class DeviceEntityTypeConfiguration : IEntityTypeConfiguration 20 | { 21 | public void Configure(EntityTypeBuilder builder) 22 | { 23 | builder.ToTable("devices"); 24 | 25 | builder.Property(x => x.Id) 26 | .HasConversion( 27 | projectId => projectId.Id, 28 | projectId => new DeviceId(projectId)) 29 | .HasColumnName("device_id"); 30 | builder.HasKey(x => x.Id); 31 | 32 | builder.Property("_version") 33 | .HasColumnName("version") 34 | .IsConcurrencyToken(); 35 | 36 | builder.Property(p => p.Model) 37 | .HasColumnName("model"); 38 | 39 | builder.Property(x => x.Capabilities) 40 | .HasColumnName("capabilities") 41 | .HasColumnType("jsonb"); 42 | } 43 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Resource/Device/DeviceFacade.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | 4 | namespace DomainDrivers.SmartSchedule.Resource.Device; 5 | 6 | public class DeviceFacade 7 | { 8 | private readonly DeviceRepository _deviceRepository; 9 | private readonly ScheduleDeviceCapabilities _scheduleDeviceCapabilities; 10 | private readonly IUnitOfWork _unitOfWork; 11 | 12 | public DeviceFacade(DeviceRepository deviceRepository, ScheduleDeviceCapabilities scheduleDeviceCapabilities, 13 | IUnitOfWork unitOfWork) 14 | { 15 | _deviceRepository = deviceRepository; 16 | _scheduleDeviceCapabilities = scheduleDeviceCapabilities; 17 | _unitOfWork = unitOfWork; 18 | } 19 | 20 | public async Task FindDevice(DeviceId deviceId) 21 | { 22 | return await _deviceRepository.FindSummary(deviceId); 23 | } 24 | 25 | public async Task> FindAllCapabilities() 26 | { 27 | return await _deviceRepository.FindAllCapabilities(); 28 | } 29 | 30 | public async Task CreateDevice(string model, ISet assets) 31 | { 32 | return await _unitOfWork.InTransaction(async () => 33 | { 34 | var deviceId = DeviceId.NewOne(); 35 | var device = new Device(deviceId, model, assets); 36 | await _deviceRepository.Add(device); 37 | return deviceId; 38 | }); 39 | } 40 | 41 | public async Task> ScheduleCapabilities(DeviceId deviceId, TimeSlot oneDay) 42 | { 43 | return await _scheduleDeviceCapabilities.SetupDeviceCapabilities(deviceId, oneDay); 44 | } 45 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Resource/Device/DeviceId.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 2 | 3 | namespace DomainDrivers.SmartSchedule.Resource.Device; 4 | 5 | public record DeviceId(Guid Id) 6 | { 7 | public static DeviceId NewOne() 8 | { 9 | return new DeviceId(Guid.NewGuid()); 10 | } 11 | 12 | public AllocatableResourceId ToAllocatableResourceId() 13 | { 14 | return new AllocatableResourceId(Id); 15 | } 16 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Resource/Device/DeviceRepository.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace DomainDrivers.SmartSchedule.Resource.Device; 5 | 6 | public class DeviceRepository 7 | { 8 | private readonly IDeviceDbContext _deviceDbContext; 9 | 10 | public DeviceRepository(IDeviceDbContext deviceDbContext) 11 | { 12 | _deviceDbContext = deviceDbContext; 13 | } 14 | 15 | public async Task FindSummary(DeviceId deviceId) 16 | { 17 | var device = await _deviceDbContext.Devices.SingleAsync(x => x.Id == deviceId); 18 | var assets = device.Capabilities; 19 | return new DeviceSummary(deviceId, device.Model, assets); 20 | } 21 | 22 | public async Task> FindAllCapabilities() 23 | { 24 | return (await _deviceDbContext.Devices.ToListAsync()) 25 | .SelectMany(device => device.Capabilities) 26 | .ToList(); 27 | } 28 | 29 | public async Task Add(Device device) 30 | { 31 | await _deviceDbContext.Devices.AddAsync(device); 32 | } 33 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Resource/Device/DeviceSummary.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Resource.Device; 4 | 5 | public record DeviceSummary(DeviceId Id, string Model, ISet Assets) 6 | { 7 | public override int GetHashCode() 8 | { 9 | return HashCode.Combine(Id, Model, Assets.CalculateHashCode()); 10 | } 11 | 12 | public virtual bool Equals(DeviceSummary? other) 13 | { 14 | if (ReferenceEquals(null, other)) return false; 15 | if (ReferenceEquals(this, other)) return true; 16 | return Id == other.Id && Model == other.Model && Assets.SetEquals(other.Assets); 17 | } 18 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Resource/Device/IDeviceDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace DomainDrivers.SmartSchedule.Resource.Device; 4 | 5 | public interface IDeviceDbContext 6 | { 7 | public DbSet Devices { get; } 8 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Resource/Device/ScheduleDeviceCapabilities.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | using static DomainDrivers.SmartSchedule.Shared.CapabilitySelector; 4 | 5 | namespace DomainDrivers.SmartSchedule.Resource.Device; 6 | 7 | public class ScheduleDeviceCapabilities 8 | { 9 | private readonly DeviceRepository _deviceRepository; 10 | private readonly CapabilityScheduler _capabilityScheduler; 11 | 12 | public ScheduleDeviceCapabilities(DeviceRepository deviceRepository, CapabilityScheduler capabilityScheduler) 13 | { 14 | _deviceRepository = deviceRepository; 15 | _capabilityScheduler = capabilityScheduler; 16 | } 17 | 18 | public async Task> SetupDeviceCapabilities(DeviceId deviceId, TimeSlot timeSlot) 19 | { 20 | var summary = await _deviceRepository.FindSummary(deviceId); 21 | return await _capabilityScheduler.ScheduleResourceCapabilitiesForPeriod(deviceId.ToAllocatableResourceId(), 22 | new List() { CanPerformAllAtTheTime(summary.Assets) }, timeSlot); 23 | } 24 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Resource/Employee/Employee.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Resource.Employee; 4 | 5 | public class Employee 6 | { 7 | public EmployeeId Id { get; private set; } = EmployeeId.NewOne(); 8 | 9 | private int _version; 10 | 11 | public string Name { get; private set; } 12 | 13 | public string LastName { get; private set; } 14 | 15 | public Seniority Seniority { get; private set; } 16 | 17 | public ISet Capabilities { get; private set; } 18 | 19 | public Employee(EmployeeId id, string name, string lastName, Seniority seniority, ISet capabilities) 20 | { 21 | Id = id; 22 | Name = name; 23 | LastName = lastName; 24 | Seniority = seniority; 25 | Capabilities = capabilities; 26 | } 27 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Resource/Employee/EmployeeAllocationPolicy.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Resource.Employee; 4 | 5 | public interface IEmployeeAllocationPolicy 6 | { 7 | IList SimultaneousCapabilitiesOf(EmployeeSummary employee); 8 | 9 | public static IEmployeeAllocationPolicy DefaultPolicy() 10 | { 11 | return new DefaultPolicy(); 12 | } 13 | 14 | public static IEmployeeAllocationPolicy PermissionsInMultipleProjects(int howMany) 15 | { 16 | return new PermissionsInMultipleProjectsPolicy(howMany); 17 | } 18 | 19 | public static IEmployeeAllocationPolicy OneOfSkills() 20 | { 21 | return new OneOfSkillsPolicy(); 22 | } 23 | 24 | public static CompositePolicy Simultaneous(params IEmployeeAllocationPolicy[] policies) 25 | { 26 | return new CompositePolicy(policies.ToList()); 27 | } 28 | } 29 | 30 | file class DefaultPolicy : IEmployeeAllocationPolicy 31 | { 32 | public IList SimultaneousCapabilitiesOf(EmployeeSummary employee) 33 | { 34 | var all = new HashSet(); 35 | all.UnionWith(employee.Skills); 36 | all.UnionWith(employee.Permissions); 37 | return new List { CapabilitySelector.CanPerformOneOf(all) }; 38 | } 39 | } 40 | 41 | file class PermissionsInMultipleProjectsPolicy : IEmployeeAllocationPolicy 42 | { 43 | private readonly int _howMany; 44 | 45 | public PermissionsInMultipleProjectsPolicy(int howMany) 46 | { 47 | _howMany = howMany; 48 | } 49 | 50 | public IList SimultaneousCapabilitiesOf(EmployeeSummary employee) 51 | { 52 | return employee.Permissions 53 | .SelectMany(permission => Enumerable.Range(0, _howMany).Select(_ => permission)) 54 | .Select(CapabilitySelector.CanJustPerform) 55 | .ToList(); 56 | } 57 | } 58 | 59 | file class OneOfSkillsPolicy : IEmployeeAllocationPolicy 60 | { 61 | public IList SimultaneousCapabilitiesOf(EmployeeSummary employee) 62 | { 63 | return new List { CapabilitySelector.CanPerformOneOf(employee.Skills) }; 64 | } 65 | } 66 | 67 | public class CompositePolicy : IEmployeeAllocationPolicy 68 | { 69 | private readonly IList _policies; 70 | 71 | public CompositePolicy(IList policies) 72 | { 73 | _policies = policies; 74 | } 75 | 76 | public IList SimultaneousCapabilitiesOf(EmployeeSummary employee) 77 | { 78 | return _policies 79 | .SelectMany(p => p.SimultaneousCapabilitiesOf(employee)) 80 | .ToList(); 81 | } 82 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Resource/Employee/EmployeeConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | 4 | namespace DomainDrivers.SmartSchedule.Resource.Employee; 5 | 6 | public static class EmployeeConfiguration 7 | { 8 | public static IServiceCollection AddEmployee(this IServiceCollection serviceCollection) 9 | { 10 | serviceCollection.AddScoped( 11 | sp => sp.GetRequiredService()); 12 | serviceCollection.AddTransient(); 13 | serviceCollection.AddTransient(); 14 | serviceCollection.AddTransient(); 15 | return serviceCollection; 16 | } 17 | } 18 | 19 | public class EmployeeEntityTypeConfiguration : IEntityTypeConfiguration 20 | { 21 | public void Configure(EntityTypeBuilder builder) 22 | { 23 | builder.ToTable("employees"); 24 | 25 | builder.Property(x => x.Id) 26 | .HasConversion( 27 | projectId => projectId.Id, 28 | projectId => new EmployeeId(projectId)) 29 | .HasColumnName("employee_id"); 30 | builder.HasKey(x => x.Id); 31 | 32 | builder.Property("_version") 33 | .HasColumnName("version") 34 | .IsConcurrencyToken(); 35 | 36 | builder.Property(p => p.Name) 37 | .HasColumnName("name"); 38 | 39 | builder.Property(p => p.LastName) 40 | .HasColumnName("last_name"); 41 | 42 | builder.Property(p => p.Seniority) 43 | .HasConversion( 44 | v => v.ToString(), // Convert enum to string 45 | v => (Seniority)Enum.Parse(typeof(Seniority), v)) // Convert string back to enum 46 | .HasColumnName("seniority") 47 | .HasColumnType("varchar"); 48 | 49 | builder.Property(x => x.Capabilities) 50 | .HasColumnName("capabilities") 51 | .HasColumnType("jsonb"); 52 | } 53 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Resource/Employee/EmployeeFacade.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | 4 | namespace DomainDrivers.SmartSchedule.Resource.Employee; 5 | 6 | public class EmployeeFacade 7 | { 8 | private readonly EmployeeRepository _employeeRepository; 9 | private readonly ScheduleEmployeeCapabilities _scheduleEmployeeCapabilities; 10 | private readonly IUnitOfWork _unitOfWork; 11 | 12 | public EmployeeFacade(EmployeeRepository employeeRepository, 13 | ScheduleEmployeeCapabilities scheduleEmployeeCapabilities, IUnitOfWork unitOfWork) 14 | { 15 | _employeeRepository = employeeRepository; 16 | _scheduleEmployeeCapabilities = scheduleEmployeeCapabilities; 17 | _unitOfWork = unitOfWork; 18 | } 19 | 20 | public async Task FindEmployee(EmployeeId employeeId) 21 | { 22 | return await _employeeRepository.FindSummary(employeeId); 23 | } 24 | 25 | public async Task> FindAllCapabilities() 26 | { 27 | return await _employeeRepository.FindAllCapabilities(); 28 | } 29 | 30 | public async Task AddEmployee(string name, string lastName, Seniority seniority, 31 | ISet skills, ISet permissions) 32 | { 33 | return await _unitOfWork.InTransaction(async () => 34 | { 35 | var employeeId = EmployeeId.NewOne(); 36 | var capabilities = skills.Concat(permissions).ToHashSet(); 37 | var employee = new Employee(employeeId, name, lastName, seniority, capabilities); 38 | await _employeeRepository.Add(employee); 39 | return employeeId; 40 | }); 41 | } 42 | 43 | public async Task> ScheduleCapabilities(EmployeeId employeeId, TimeSlot oneDay) 44 | { 45 | return await _scheduleEmployeeCapabilities.SetupEmployeeCapabilities(employeeId, oneDay); 46 | } 47 | 48 | //add vacation 49 | // calls availability 50 | //add sick leave 51 | // calls availability 52 | //change skills 53 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Resource/Employee/EmployeeId.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 2 | 3 | namespace DomainDrivers.SmartSchedule.Resource.Employee; 4 | 5 | public record EmployeeId(Guid Id) 6 | { 7 | public static EmployeeId NewOne() 8 | { 9 | return new EmployeeId(Guid.NewGuid()); 10 | } 11 | 12 | public AllocatableResourceId ToAllocatableResourceId() 13 | { 14 | return new AllocatableResourceId(Id); 15 | } 16 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Resource/Employee/EmployeeRepository.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace DomainDrivers.SmartSchedule.Resource.Employee; 5 | 6 | public class EmployeeRepository 7 | { 8 | private readonly IEmployeeDbContext _employeeDbContext; 9 | 10 | public EmployeeRepository(IEmployeeDbContext employeeDbContext) 11 | { 12 | _employeeDbContext = employeeDbContext; 13 | } 14 | 15 | public async Task FindSummary(EmployeeId employeeId) 16 | { 17 | var employee = await _employeeDbContext.Employees.SingleAsync(x => x.Id == employeeId); 18 | 19 | var skills = FilterCapabilities(employee.Capabilities, cap => cap.IsOfType("SKILL")); 20 | var permissions = FilterCapabilities(employee.Capabilities, cap => cap.IsOfType("PERMISSION")); 21 | return new EmployeeSummary(employeeId, employee.Name, employee.LastName, employee.Seniority, skills, permissions); 22 | } 23 | 24 | public async Task> FindAllCapabilities() 25 | { 26 | return (await _employeeDbContext.Employees.ToListAsync()) 27 | .SelectMany(employee => employee.Capabilities) 28 | .ToList(); 29 | } 30 | 31 | private ISet FilterCapabilities(ISet capabilities, Predicate predicate) 32 | { 33 | return capabilities.Where(capability => predicate(capability)).ToHashSet(); 34 | } 35 | 36 | public async Task Add(Employee employee) 37 | { 38 | await _employeeDbContext.Employees.AddAsync(employee); 39 | } 40 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Resource/Employee/EmployeeSummary.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Resource.Employee; 4 | 5 | public record EmployeeSummary(EmployeeId Id, string Name, string LastName, Seniority Seniority, ISet Skills, ISet Permissions) 6 | { 7 | public override int GetHashCode() 8 | { 9 | return HashCode.Combine(Id, Name, LastName, Seniority, Skills.CalculateHashCode(), Permissions.CalculateHashCode()); 10 | } 11 | 12 | public virtual bool Equals(EmployeeSummary? other) 13 | { 14 | if (ReferenceEquals(null, other)) return false; 15 | if (ReferenceEquals(this, other)) return true; 16 | return Id == other.Id && Name == other.Name && LastName == other.LastName && Seniority == other.Seniority && 17 | Skills.SetEquals(other.Skills) && Permissions.SetEquals(other.Permissions); 18 | } 19 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Resource/Employee/IEmployeeDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace DomainDrivers.SmartSchedule.Resource.Employee; 4 | 5 | public interface IEmployeeDbContext 6 | { 7 | public DbSet Employees { get; } 8 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Resource/Employee/ScheduleEmployeeCapabilities.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | 4 | namespace DomainDrivers.SmartSchedule.Resource.Employee; 5 | 6 | public class ScheduleEmployeeCapabilities 7 | { 8 | private readonly EmployeeRepository _employeeRepository; 9 | private readonly CapabilityScheduler _capabilityScheduler; 10 | 11 | public ScheduleEmployeeCapabilities(EmployeeRepository employeeRepository, CapabilityScheduler capabilityScheduler) 12 | { 13 | _employeeRepository = employeeRepository; 14 | _capabilityScheduler = capabilityScheduler; 15 | } 16 | 17 | public async Task> SetupEmployeeCapabilities(EmployeeId employeeId, 18 | TimeSlot timeSlot) 19 | { 20 | var summary = await _employeeRepository.FindSummary(employeeId); 21 | var policy = FindAllocationPolicy(summary); 22 | var capabilities = policy.SimultaneousCapabilitiesOf(summary); 23 | return await _capabilityScheduler.ScheduleResourceCapabilitiesForPeriod(employeeId.ToAllocatableResourceId(), 24 | capabilities, timeSlot); 25 | } 26 | 27 | private IEmployeeAllocationPolicy FindAllocationPolicy(EmployeeSummary employee) 28 | { 29 | if (employee.Seniority == Seniority.LEAD) 30 | { 31 | return IEmployeeAllocationPolicy.Simultaneous(IEmployeeAllocationPolicy.OneOfSkills(), 32 | IEmployeeAllocationPolicy.PermissionsInMultipleProjects(3)); 33 | } 34 | 35 | return IEmployeeAllocationPolicy.DefaultPolicy(); 36 | } 37 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Resource/Employee/Seniority.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Resource.Employee; 2 | 3 | public enum Seniority 4 | { 5 | JUNIOR, 6 | MID, 7 | SENIOR, 8 | LEAD 9 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Resource/ResourceConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Resource; 2 | 3 | public static class ResourceConfiguration 4 | { 5 | public static IServiceCollection AddResource(this IServiceCollection serviceCollection) 6 | { 7 | serviceCollection.AddTransient(); 8 | return serviceCollection; 9 | } 10 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Resource/ResourceFacade.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Resource.Device; 2 | using DomainDrivers.SmartSchedule.Resource.Employee; 3 | using DomainDrivers.SmartSchedule.Shared; 4 | 5 | namespace DomainDrivers.SmartSchedule.Resource; 6 | 7 | public class ResourceFacade 8 | { 9 | private readonly EmployeeFacade _employeeFacade; 10 | private readonly DeviceFacade _deviceFacade; 11 | 12 | public ResourceFacade(EmployeeFacade employeeFacade, DeviceFacade deviceFacade) 13 | { 14 | _employeeFacade = employeeFacade; 15 | _deviceFacade = deviceFacade; 16 | } 17 | 18 | public async Task> FindAllCapabilities() 19 | { 20 | var employeeCapabilities = await _employeeFacade.FindAllCapabilities(); 21 | var deviceCapabilities = await _deviceFacade.FindAllCapabilities(); 22 | return deviceCapabilities.Concat(employeeCapabilities).ToList(); 23 | } 24 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/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 | -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/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)); -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/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 | -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/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)); -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/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 | ); -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/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 | -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Risk/IRiskDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace DomainDrivers.SmartSchedule.Risk; 4 | 5 | public interface IRiskDbContext 6 | { 7 | public DbSet RiskPeriodicCheckSagas { get; } 8 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Risk/RiskPeriodicCheckSagaId.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Risk; 2 | 3 | public record RiskPeriodicCheckSagaId(Guid Id) 4 | { 5 | public static RiskPeriodicCheckSagaId NewOne() 6 | { 7 | return new RiskPeriodicCheckSagaId(Guid.NewGuid()); 8 | } 9 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Risk/RiskPeriodicCheckSagaRepository.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Allocation; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace DomainDrivers.SmartSchedule.Risk; 5 | 6 | public class RiskPeriodicCheckSagaRepository 7 | { 8 | private readonly IRiskDbContext _riskDbContext; 9 | 10 | public RiskPeriodicCheckSagaRepository(IRiskDbContext riskDbContext) 11 | { 12 | _riskDbContext = riskDbContext; 13 | } 14 | 15 | public async Task FindByProjectId(ProjectAllocationsId projectId) 16 | { 17 | return await _riskDbContext.RiskPeriodicCheckSagas.SingleOrDefaultAsync(x => x.ProjectId == projectId); 18 | } 19 | 20 | public async Task> FindByProjectIdIn(List interested) 21 | { 22 | return await _riskDbContext.RiskPeriodicCheckSagas 23 | .Where(x => interested.Contains(x.ProjectId)) 24 | .ToListAsync(); 25 | } 26 | 27 | public async Task FindByProjectIdOrCreate(ProjectAllocationsId projectId) 28 | { 29 | var found = await FindByProjectId(projectId); 30 | if (found == null) { 31 | found = await Add(new RiskPeriodicCheckSaga(projectId)); 32 | } 33 | return found; 34 | } 35 | 36 | public async Task> FindByProjectIdInOrElseCreate(List interested) 37 | { 38 | var found = await FindByProjectIdIn(interested); 39 | var foundIds = found.Select(x => x.ProjectId).ToList(); 40 | var missing = interested.Where(projectId => !foundIds.Contains(projectId)) 41 | .Select(x => new RiskPeriodicCheckSaga(x)).ToList(); 42 | foreach (var missingSaga in missing) 43 | { 44 | found.Add(await Add(missingSaga)); 45 | } 46 | return found; 47 | } 48 | 49 | public async Task> FindAll() 50 | { 51 | return await _riskDbContext.RiskPeriodicCheckSagas.ToListAsync(); 52 | } 53 | 54 | public async Task Add(RiskPeriodicCheckSaga riskPeriodicCheckSaga) 55 | { 56 | return (await _riskDbContext.RiskPeriodicCheckSagas.AddAsync(riskPeriodicCheckSaga)).Entity; 57 | } 58 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Risk/RiskPeriodicCheckSagaStep.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Risk; 2 | 3 | public enum RiskPeriodicCheckSagaStep 4 | { 5 | FindAvailable, 6 | DoNothing, 7 | SuggestReplacement, 8 | NotifyAboutPossibleRisk, 9 | NotifyAboutDemandsSatisfied 10 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Risk/RiskPeriodicCheckSagaWeeklyCheckJob.cs: -------------------------------------------------------------------------------- 1 | using Quartz; 2 | 3 | namespace DomainDrivers.SmartSchedule.Risk; 4 | 5 | public class RiskPeriodicCheckSagaWeeklyCheckJob : IJob 6 | { 7 | private readonly RiskPeriodicCheckSagaDispatcher _riskPeriodicCheckSagaDispatcher; 8 | 9 | public RiskPeriodicCheckSagaWeeklyCheckJob(RiskPeriodicCheckSagaDispatcher riskPeriodicCheckSagaDispatcher) 10 | { 11 | _riskPeriodicCheckSagaDispatcher = riskPeriodicCheckSagaDispatcher; 12 | } 13 | 14 | public async Task Execute(IJobExecutionContext context) 15 | { 16 | await _riskPeriodicCheckSagaDispatcher.HandleWeeklyCheck(); 17 | } 18 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Risk/RiskPushNotification.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Allocation; 2 | using DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 3 | using DomainDrivers.SmartSchedule.Availability; 4 | using DomainDrivers.SmartSchedule.Planning; 5 | using DomainDrivers.SmartSchedule.Shared; 6 | using Demand = DomainDrivers.SmartSchedule.Allocation.Demand; 7 | 8 | namespace DomainDrivers.SmartSchedule.Risk; 9 | 10 | public interface IRiskPushNotification 11 | { 12 | void NotifyDemandsSatisfied(ProjectAllocationsId projectId); 13 | void NotifyAboutAvailability(ProjectAllocationsId projectId, IDictionary available); 14 | void NotifyProfitableRelocationFound(ProjectAllocationsId projectId, AllocatableCapabilityId allocatableCapabilityId); 15 | void NotifyAboutPossibleRisk(ProjectAllocationsId projectId); 16 | void NotifyAboutPossibleRiskDuringPlanning(ProjectId cause, DomainDrivers.SmartSchedule.Planning.Demands demands); 17 | void NotifyAboutCriticalResourceNotAvailable(ProjectId cause, ResourceId criticalResource, TimeSlot timeSlot); 18 | void NotifyAboutResourcesNotAvailable(ProjectId projectId, ISet notAvailable); 19 | } 20 | 21 | public class RiskPushNotification : IRiskPushNotification 22 | { 23 | public void NotifyDemandsSatisfied(ProjectAllocationsId projectId) 24 | { 25 | } 26 | 27 | public void NotifyAboutAvailability(ProjectAllocationsId projectId, IDictionary available) 28 | { 29 | } 30 | 31 | public void NotifyProfitableRelocationFound(ProjectAllocationsId projectId, AllocatableCapabilityId allocatableCapabilityId) 32 | { 33 | } 34 | 35 | public void NotifyAboutPossibleRisk(ProjectAllocationsId projectId) 36 | { 37 | } 38 | 39 | public void NotifyAboutPossibleRiskDuringPlanning(ProjectId cause, DomainDrivers.SmartSchedule.Planning.Demands demands) 40 | { 41 | } 42 | 43 | public void NotifyAboutCriticalResourceNotAvailable(ProjectId cause, ResourceId criticalResource, TimeSlot timeSlot) 44 | { 45 | } 46 | 47 | public void NotifyAboutResourcesNotAvailable(ProjectId projectId, ISet notAvailable) 48 | { 49 | } 50 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Risk/VerifyCriticalResourceAvailableDuringPlanning.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Availability; 2 | using DomainDrivers.SmartSchedule.Planning; 3 | using DomainDrivers.SmartSchedule.Shared; 4 | using MediatR; 5 | 6 | namespace DomainDrivers.SmartSchedule.Risk; 7 | 8 | public class VerifyCriticalResourceAvailableDuringPlanning : INotificationHandler 9 | { 10 | private readonly IAvailabilityFacade _availabilityFacade; 11 | private readonly IRiskPushNotification _riskPushNotification; 12 | 13 | public VerifyCriticalResourceAvailableDuringPlanning(IAvailabilityFacade availabilityFacade, 14 | IRiskPushNotification riskPushNotification) 15 | { 16 | _availabilityFacade = availabilityFacade; 17 | _riskPushNotification = riskPushNotification; 18 | } 19 | 20 | public async Task Handle(CriticalStagePlanned criticalStagePlanned, CancellationToken cancellationToken) 21 | { 22 | if (criticalStagePlanned.CriticalResource == null) 23 | { 24 | return; 25 | } 26 | 27 | var calendar = 28 | await _availabilityFacade.LoadCalendar(criticalStagePlanned.CriticalResource, 29 | criticalStagePlanned.StageTimeSlot); 30 | 31 | if (!ResourceIsAvailable(criticalStagePlanned.StageTimeSlot, calendar)) 32 | { 33 | _riskPushNotification.NotifyAboutCriticalResourceNotAvailable(criticalStagePlanned.ProjectId, 34 | criticalStagePlanned.CriticalResource, criticalStagePlanned.StageTimeSlot); 35 | } 36 | } 37 | 38 | private bool ResourceIsAvailable(TimeSlot timeSlot, Calendar calendar) 39 | { 40 | return calendar.AvailableSlots().Any(slot => slot == timeSlot); 41 | } 42 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Risk/VerifyNeededResourcesAvailableInTimeSlot.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Availability; 2 | using DomainDrivers.SmartSchedule.Planning; 3 | using DomainDrivers.SmartSchedule.Shared; 4 | using MediatR; 5 | 6 | namespace DomainDrivers.SmartSchedule.Risk; 7 | 8 | public class VerifyNeededResourcesAvailableInTimeSlot : INotificationHandler 9 | { 10 | private readonly IAvailabilityFacade _availabilityFacade; 11 | private readonly IRiskPushNotification _riskPushNotification; 12 | 13 | public VerifyNeededResourcesAvailableInTimeSlot(IAvailabilityFacade availabilityFacade, 14 | IRiskPushNotification riskPushNotification) 15 | { 16 | _availabilityFacade = availabilityFacade; 17 | _riskPushNotification = riskPushNotification; 18 | } 19 | 20 | 21 | public async Task Handle(NeededResourcesChosen resourcesNeeded, CancellationToken cancellationToken) 22 | { 23 | await NotifyAboutNotAvailableResources(resourcesNeeded.NeededResources, resourcesNeeded.TimeSlot, 24 | resourcesNeeded.ProjectId); 25 | } 26 | 27 | private async Task NotifyAboutNotAvailableResources(ISet resourcedIds, TimeSlot timeSlot, 28 | ProjectId projectId) 29 | { 30 | var notAvailable = new HashSet(); 31 | var calendars = await _availabilityFacade.LoadCalendars(resourcedIds, timeSlot); 32 | 33 | foreach (var resourceId in resourcedIds) 34 | { 35 | if (calendars.Get(resourceId).AvailableSlots().Any(x => timeSlot.Within(x) == false)) 36 | { 37 | notAvailable.Add(resourceId); 38 | } 39 | } 40 | 41 | if (notAvailable.Any()) 42 | { 43 | _riskPushNotification.NotifyAboutResourcesNotAvailable(projectId, notAvailable); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Shared/Capability.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Shared; 2 | 3 | public record Capability(string Name, string Type) 4 | { 5 | public static Capability Skill(string name) 6 | { 7 | return new Capability(name, "SKILL"); 8 | } 9 | 10 | public static Capability Permission(string name) 11 | { 12 | return new Capability(name, "PERMISSION"); 13 | } 14 | 15 | public static Capability Asset(string asset) 16 | { 17 | return new Capability(asset, "ASSET"); 18 | } 19 | 20 | public static ISet Skills(params string[] skills) 21 | { 22 | return skills.Select(x => Skill(x)).ToHashSet(); 23 | } 24 | 25 | public static ISet Assets(params string[] assets) 26 | { 27 | return assets.Select(x => Asset(x)).ToHashSet(); 28 | } 29 | 30 | public static ISet Permissions(params string[] permissions) 31 | { 32 | return permissions.Select(x => Permission(x)).ToHashSet(); 33 | } 34 | 35 | public bool IsOfType(string type) 36 | { 37 | return Type == type; 38 | } 39 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Shared/CapabilitySelector.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Shared; 2 | 3 | public record CapabilitySelector(ISet Capabilities, SelectingPolicy SelectingPolicy) 4 | { 5 | public static CapabilitySelector CanPerformAllAtTheTime(ISet capabilities) 6 | { 7 | return new CapabilitySelector(capabilities, SelectingPolicy.AllSimultaneously); 8 | } 9 | 10 | public static CapabilitySelector CanPerformOneOf(ISet capabilities) 11 | { 12 | return new CapabilitySelector(capabilities, SelectingPolicy.OneOfAll); 13 | } 14 | 15 | public static CapabilitySelector CanJustPerform(Capability capability) 16 | { 17 | return new CapabilitySelector(new HashSet() { capability }, SelectingPolicy.OneOfAll); 18 | } 19 | 20 | public bool CanPerform(Capability capability) 21 | { 22 | return Capabilities.Contains(capability); 23 | } 24 | 25 | public bool CanPerform(ISet capabilities) 26 | { 27 | if (capabilities.Count == 1) 28 | { 29 | return new HashSet(Capabilities).IsSupersetOf(capabilities); 30 | } 31 | 32 | return SelectingPolicy == SelectingPolicy.AllSimultaneously && 33 | new HashSet(Capabilities).IsSupersetOf(capabilities); 34 | } 35 | 36 | public virtual bool Equals(CapabilitySelector? other) 37 | { 38 | if (ReferenceEquals(null, other)) return false; 39 | if (ReferenceEquals(this, other)) return true; 40 | return Capabilities.SetEquals(other.Capabilities) 41 | && SelectingPolicy == other.SelectingPolicy; 42 | } 43 | 44 | public override int GetHashCode() 45 | { 46 | return HashCode.Combine(Capabilities.CalculateHashCode(), SelectingPolicy); 47 | } 48 | } 49 | 50 | public enum SelectingPolicy 51 | { 52 | AllSimultaneously, 53 | OneOfAll 54 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Shared/CollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Shared; 2 | 3 | public static class CollectionExtensions 4 | { 5 | // Based on: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Set.html#hashCode() 6 | public static int CalculateHashCode(this ISet collection) where T : class 7 | { 8 | var sum = 0; 9 | foreach (var element in collection) 10 | { 11 | sum = unchecked(sum + element.GetHashCode()); 12 | } 13 | 14 | return sum; 15 | } 16 | 17 | // Based on: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/List.html#hashCode() 18 | public static int CalculateHashCode(this IList collection) where T : class 19 | { 20 | return collection 21 | .Aggregate(0, (hash, pair) => HashCode.Combine(hash, pair.GetHashCode())); 22 | } 23 | 24 | public static int CalculateHashCode(this IDictionary collection) 25 | { 26 | return CalculateHashCode(collection, value => value == null ? 0 : value.GetHashCode()); 27 | } 28 | 29 | // Based on: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Map.html#hashCode() 30 | public static int CalculateHashCode(this IDictionary collection, Func getHashCode) 31 | { 32 | var sum = 0; 33 | foreach (var element in collection) 34 | { 35 | sum = unchecked(sum + (element.Value == null ? 0 : getHashCode(element.Value))); 36 | } 37 | 38 | return sum; 39 | } 40 | 41 | public static bool DictionaryEqual(this IDictionary first, 42 | IDictionary second, IEqualityComparer? valueComparer = null) 43 | { 44 | if (ReferenceEquals(first, second)) return true; 45 | if (first.Count != second.Count) return false; 46 | 47 | valueComparer = valueComparer ?? EqualityComparer.Default; 48 | 49 | foreach (var kvp in first) 50 | { 51 | if (!second.TryGetValue(kvp.Key, out var secondValue)) return false; 52 | if (!valueComparer.Equals(kvp.Value, secondValue)) return false; 53 | } 54 | 55 | return true; 56 | } 57 | 58 | public static string ToCollectionString(this IEnumerable enumerable) 59 | { 60 | return $"[{string.Join(", ", enumerable)}]"; 61 | } 62 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Shared/IBaseRepository.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Shared; 2 | 3 | //to use if we don't want to expose DbSet from Entity Framework for entities with possible large number of records 4 | public interface IBaseRepository 5 | { 6 | T? FindById(TId id); 7 | 8 | T GetReferenceById(TId id); 9 | 10 | bool ExistsById(TId id); 11 | 12 | void DeleteById(TId id); 13 | 14 | void Delete(T entity); 15 | 16 | long Count(); 17 | 18 | void Save(T entity); 19 | 20 | IEnumerable FindAllById(IEnumerable ids); 21 | 22 | void SaveAll(IEnumerable entities); 23 | } 24 | -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Shared/IEventsPublisher.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace DomainDrivers.SmartSchedule.Shared; 4 | 5 | public interface IEventsPublisher 6 | { 7 | //remember about transactions scope 8 | Task Publish(IPublishedEvent @event); 9 | } 10 | 11 | public class EventsPublisher : IEventsPublisher 12 | { 13 | private readonly IMediator _mediator; 14 | 15 | public EventsPublisher(IMediator mediator) 16 | { 17 | _mediator = mediator; 18 | } 19 | 20 | public async Task Publish(IPublishedEvent @event) 21 | { 22 | await _mediator.Publish(@event); 23 | } 24 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Shared/IPrivateEvent.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace DomainDrivers.SmartSchedule.Shared; 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 IPrivateEvent : INotification 12 | { 13 | DateTime OccurredAt { get; } 14 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Shared/IPublishedEvent.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace DomainDrivers.SmartSchedule.Shared; 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 IPublishedEvent : INotification 12 | { 13 | DateTime OccurredAt { get; } 14 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Shared/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Shared; 2 | 3 | public interface IUnitOfWork 4 | { 5 | Task InTransaction(Func> operation); 6 | Task InTransaction(Func operation); 7 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Shared/ResourceName.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Shared; 2 | 3 | public record ResourceName(string Name); -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Shared/SharedConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Shared; 2 | 3 | public static class SharedConfiguration 4 | { 5 | public static IServiceCollection AddShared(this IServiceCollection serviceCollection) 6 | { 7 | serviceCollection.AddScoped(); 8 | serviceCollection.AddTransient(_ => TimeProvider.System); 9 | serviceCollection.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(SharedConfiguration).Assembly)); 10 | serviceCollection.AddScoped(); 11 | serviceCollection.AddScoped(); 12 | return serviceCollection; 13 | } 14 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Simulation/AdditionalPricedCapability.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Simulation; 2 | 3 | public record AdditionalPricedCapability(decimal Value, AvailableResourceCapability AvailableResourceCapability); -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Simulation/AvailableResourceCapability.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Optimization; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | 4 | namespace DomainDrivers.SmartSchedule.Simulation; 5 | 6 | public record AvailableResourceCapability(Guid ResourceId, CapabilitySelector CapabilitySelector, TimeSlot TimeSlot) 7 | : ICapacityDimension 8 | { 9 | public AvailableResourceCapability(Guid resourceId, Capability capability, TimeSlot timeSlot) : this(resourceId, 10 | CapabilitySelector.CanJustPerform(capability), timeSlot) 11 | { 12 | } 13 | 14 | public bool Performs(Capability capability) 15 | { 16 | return CapabilitySelector.CanPerform(new HashSet() { capability }); 17 | } 18 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Simulation/Demand.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Optimization; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | 4 | namespace DomainDrivers.SmartSchedule.Simulation; 5 | 6 | public record Demand(Capability Capability, TimeSlot Slot) : IWeightDimension 7 | { 8 | public static Demand DemandFor(Capability capability, TimeSlot slot) 9 | { 10 | return new Demand(capability, slot); 11 | } 12 | 13 | public bool IsSatisfiedBy(ICapacityDimension capacityDimension) 14 | { 15 | return IsSatisfiedBy((AvailableResourceCapability)capacityDimension); 16 | } 17 | 18 | public bool IsSatisfiedBy(AvailableResourceCapability availableCapability) 19 | { 20 | return availableCapability.Performs(Capability) && 21 | Slot.Within(availableCapability.TimeSlot); 22 | } 23 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Simulation/Demands.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Simulation; 4 | 5 | public record Demands(IList All) 6 | { 7 | public static Demands Of(params Demand[] demands) 8 | { 9 | return new Demands(demands.ToList()); 10 | } 11 | 12 | public virtual bool Equals(Demands? other) 13 | { 14 | if (ReferenceEquals(null, other)) return false; 15 | if (ReferenceEquals(this, other)) return true; 16 | return All.SequenceEqual(other.All); 17 | } 18 | 19 | public override int GetHashCode() 20 | { 21 | return All.CalculateHashCode(); 22 | } 23 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Simulation/ProjectId.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Simulation; 2 | 3 | public record ProjectId(Guid Id) 4 | { 5 | public static ProjectId NewOne() 6 | { 7 | return new ProjectId(Guid.NewGuid()); 8 | } 9 | 10 | public static ProjectId From(Guid key) 11 | { 12 | return new ProjectId(key); 13 | } 14 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Simulation/SimulatedCapabilities.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Simulation; 4 | 5 | public record SimulatedCapabilities(IList Capabilities) 6 | { 7 | public static SimulatedCapabilities None() 8 | { 9 | return new SimulatedCapabilities(new List()); 10 | } 11 | 12 | public SimulatedCapabilities Add(IList newCapabilities) 13 | { 14 | var newAvailabilities = new List(Capabilities); 15 | newAvailabilities.AddRange(newCapabilities); 16 | return new SimulatedCapabilities(newAvailabilities); 17 | } 18 | 19 | public SimulatedCapabilities Add(AvailableResourceCapability newCapability) 20 | { 21 | return Add(new List { newCapability }); 22 | } 23 | 24 | public virtual bool Equals(SimulatedCapabilities? other) 25 | { 26 | if (ReferenceEquals(null, other)) return false; 27 | if (ReferenceEquals(this, other)) return true; 28 | return Capabilities.SequenceEqual(other.Capabilities); 29 | } 30 | 31 | public override int GetHashCode() 32 | { 33 | return Capabilities.CalculateHashCode(); 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Simulation/SimulatedProject.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Simulation; 2 | 3 | public record SimulatedProject(ProjectId ProjectId, Func Value, Demands MissingDemands) 4 | { 5 | public decimal CalculateValue() 6 | { 7 | return Value(); 8 | } 9 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Simulation/SimulationConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Simulation; 2 | 3 | public static class SimulationConfiguration 4 | { 5 | public static IServiceCollection AddSimulation(this IServiceCollection serviceCollection) 6 | { 7 | serviceCollection.AddTransient(); 8 | return serviceCollection; 9 | } 10 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Simulation/SimulationFacade.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Optimization; 2 | 3 | namespace DomainDrivers.SmartSchedule.Simulation; 4 | 5 | public class SimulationFacade 6 | { 7 | private readonly OptimizationFacade _optimizationFacade; 8 | 9 | public SimulationFacade(OptimizationFacade optimizationFacade) 10 | { 11 | _optimizationFacade = optimizationFacade; 12 | } 13 | 14 | public double ProfitAfterBuyingNewCapability(IList projectsSimulations, 15 | SimulatedCapabilities capabilitiesWithoutNewOne, AdditionalPricedCapability newPricedCapability) 16 | { 17 | var capabilitiesWithNewResource = 18 | capabilitiesWithoutNewOne.Add(newPricedCapability.AvailableResourceCapability); 19 | var resultWithout = _optimizationFacade.Calculate(ToItems(projectsSimulations), 20 | ToCapacity(capabilitiesWithoutNewOne), Comparer.Create((x, y) => y.Value.CompareTo(x.Value))); 21 | var resultWith = _optimizationFacade.Calculate(ToItems(projectsSimulations), 22 | ToCapacity(capabilitiesWithNewResource), Comparer.Create((x, y) => y.Value.CompareTo(x.Value))); 23 | return resultWith.Profit - decimal.ToDouble(newPricedCapability.Value) - resultWithout.Profit; 24 | } 25 | 26 | public Result WhatIsTheOptimalSetup( 27 | IList projectsSimulations, SimulatedCapabilities totalCapability) 28 | { 29 | return _optimizationFacade.Calculate(ToItems(projectsSimulations), ToCapacity(totalCapability), 30 | Comparer.Create((x, y) => y.Value.CompareTo(x.Value))); 31 | } 32 | 33 | private TotalCapacity ToCapacity(SimulatedCapabilities simulatedCapabilities) 34 | { 35 | var capabilities = simulatedCapabilities.Capabilities; 36 | var capacityDimensions = new List(capabilities); 37 | return new TotalCapacity(capacityDimensions); 38 | } 39 | 40 | private IList ToItems(IList projectsSimulations) 41 | { 42 | return projectsSimulations 43 | .Select(project => ToItem(project)) 44 | .ToList(); 45 | } 46 | 47 | private Item ToItem(SimulatedProject simulatedProject) 48 | { 49 | var missingDemands = simulatedProject.MissingDemands.All; 50 | IList weights = new List(missingDemands); 51 | return new Item(simulatedProject.ProjectId.ToString(), 52 | decimal.ToDouble(simulatedProject.CalculateValue()), new TotalWeight(weights)); 53 | } 54 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/SmartScheduleDbContext.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Allocation; 2 | using DomainDrivers.SmartSchedule.Allocation.CapabilityScheduling; 3 | using DomainDrivers.SmartSchedule.Allocation.Cashflow; 4 | using DomainDrivers.SmartSchedule.Resource.Device; 5 | using DomainDrivers.SmartSchedule.Resource.Employee; 6 | using DomainDrivers.SmartSchedule.Risk; 7 | using Microsoft.EntityFrameworkCore; 8 | 9 | namespace DomainDrivers.SmartSchedule; 10 | 11 | public class SmartScheduleDbContext : DbContext, IAllocationDbContext, 12 | ICashflowDbContext, IEmployeeDbContext, IDeviceDbContext, ICapabilitySchedulingDbContext, IRiskDbContext 13 | { 14 | public SmartScheduleDbContext(DbContextOptions options) 15 | : base(options) 16 | { 17 | } 18 | 19 | public DbSet ProjectAllocations { get; set; } = null!; 20 | public DbSet Cashflows { get; set; } = null!; 21 | public DbSet Employees { get; set; } = null!; 22 | public DbSet Devices { get; set; } = null!; 23 | public DbSet AllocatableCapabilities { get; set; } = null!; 24 | public DbSet RiskPeriodicCheckSagas { get; set; } = null!; 25 | 26 | protected override void OnModelCreating(ModelBuilder modelBuilder) 27 | { 28 | modelBuilder.ApplyConfigurationsFromAssembly(typeof(SmartScheduleDbContext).Assembly); 29 | } 30 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Sorter/Edge.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Sorter; 2 | 3 | public record Edge(int Source, int Target) 4 | { 5 | public override string ToString() 6 | { 7 | return $"({Source} -> {Target})"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Sorter/FeedbackArcSetOnGraph.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Sorter; 2 | 3 | public class FeedbackArcSetOnGraph where T : class 4 | { 5 | public IList Calculate(IList> initialNodes) 6 | { 7 | var adjacencyList = CreateAdjacencyList(initialNodes); 8 | var v = adjacencyList.Count; 9 | var feedbackEdges = new List(); 10 | var visited = new int[v + 1]; 11 | 12 | foreach (var i in adjacencyList.Keys) 13 | { 14 | var neighbours = adjacencyList[i]; 15 | if (neighbours.Count != 0) 16 | { 17 | visited[i] = 1; 18 | foreach (var j in neighbours) 19 | { 20 | if (visited[j] == 1) 21 | { 22 | feedbackEdges.Add(new Edge(i, j)); 23 | } 24 | else 25 | { 26 | visited[j] = 1; 27 | } 28 | } 29 | } 30 | } 31 | 32 | return feedbackEdges; 33 | } 34 | 35 | private static Dictionary> CreateAdjacencyList(IList> initialNodes) 36 | { 37 | var adjacencyList = new Dictionary>(); 38 | 39 | for (var i = 1; i <= initialNodes.Count; i++) 40 | { 41 | adjacencyList[i] = new List(); 42 | } 43 | 44 | for (var i = 0; i < initialNodes.Count; i++) 45 | { 46 | var dependencies = initialNodes[i].Dependencies.All 47 | .Select(dependency => initialNodes.IndexOf(dependency) + 1) 48 | .ToList(); 49 | adjacencyList[i + 1] = dependencies; 50 | } 51 | 52 | return adjacencyList; 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Sorter/GraphTopologicalSort.cs: -------------------------------------------------------------------------------- 1 | namespace DomainDrivers.SmartSchedule.Sorter; 2 | 3 | public class GraphTopologicalSort 4 | { 5 | public SortedNodes Sort(Nodes nodes) 6 | { 7 | return CreateSortedNodesRecursively(nodes, SortedNodes.Empty()); 8 | } 9 | 10 | private SortedNodes CreateSortedNodesRecursively(Nodes remainingNodes, SortedNodes accumulatedSortedNodes) 11 | { 12 | var alreadyProcessedNodes = accumulatedSortedNodes.All 13 | .SelectMany(n => n.All) 14 | .ToList(); 15 | var nodesWithoutDependencies = remainingNodes.WithAllDependenciesPresentIn(alreadyProcessedNodes); 16 | 17 | if (nodesWithoutDependencies.All.Count == 0) 18 | { 19 | return accumulatedSortedNodes; 20 | } 21 | 22 | var newSortedNodes = accumulatedSortedNodes.Add(nodesWithoutDependencies); 23 | remainingNodes = remainingNodes.RemoveAll(nodesWithoutDependencies.All); 24 | return CreateSortedNodesRecursively(remainingNodes, newSortedNodes); 25 | } 26 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Sorter/Node.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace DomainDrivers.SmartSchedule.Sorter; 3 | 4 | public record Node(string Name, Nodes Dependencies, T? Content) 5 | { 6 | public Node(string name) : this(name, new Nodes(new HashSet>()), default) 7 | { 8 | } 9 | 10 | public Node(string name, T? content) : this(name, new Nodes(new HashSet>()), content) 11 | { 12 | } 13 | 14 | public Node DependsOn(Node node) 15 | { 16 | return new Node(Name, Dependencies.Add(node), Content); 17 | } 18 | 19 | public virtual bool Equals(Node? other) 20 | { 21 | if (ReferenceEquals(null, other)) return false; 22 | if (ReferenceEquals(this, other)) return true; 23 | return Name == other.Name; 24 | } 25 | 26 | public override int GetHashCode() 27 | { 28 | return Name.GetHashCode(); 29 | } 30 | 31 | public override string ToString() 32 | { 33 | return Name; 34 | } 35 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Sorter/Nodes.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Frozen; 2 | using DomainDrivers.SmartSchedule.Shared; 3 | 4 | namespace DomainDrivers.SmartSchedule.Sorter; 5 | 6 | public record Nodes(ISet> NodesCollection) 7 | { 8 | public Nodes(params Node[] nodes) : this(new HashSet>(nodes)) 9 | { 10 | } 11 | 12 | public ISet> All 13 | { 14 | get { return NodesCollection.ToFrozenSet(); } 15 | } 16 | 17 | public Nodes Add(Node node) 18 | { 19 | var newNodes = new HashSet>(NodesCollection) { node }; 20 | return new Nodes(newNodes); 21 | } 22 | 23 | public Nodes WithAllDependenciesPresentIn(IEnumerable> nodes) 24 | { 25 | return new Nodes(All 26 | .Where(n => n.Dependencies.All.All(d => nodes.Contains(d))) 27 | .ToHashSet()); 28 | } 29 | 30 | public Nodes RemoveAll(IEnumerable> nodes) 31 | { 32 | return new Nodes(All 33 | .Where(n => !nodes.Contains(n)) 34 | .ToHashSet()); 35 | } 36 | 37 | public virtual bool Equals(Nodes? other) 38 | { 39 | if (ReferenceEquals(null, other)) return false; 40 | if (ReferenceEquals(this, other)) return true; 41 | return NodesCollection.SetEquals(other.NodesCollection); 42 | } 43 | 44 | public override int GetHashCode() 45 | { 46 | return NodesCollection.CalculateHashCode(); 47 | } 48 | 49 | public override string ToString() 50 | { 51 | return $"Nodes{{node={NodesCollection.ToCollectionString()}}}"; 52 | } 53 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/Sorter/SortedNodes.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule.Sorter; 4 | 5 | public record SortedNodes(IList> All) 6 | { 7 | public static SortedNodes Empty() 8 | { 9 | return new SortedNodes(new List>()); 10 | } 11 | 12 | public SortedNodes Add(Nodes newNodes) 13 | { 14 | var result = new List>(All) { newNodes }; 15 | return new SortedNodes(result); 16 | } 17 | 18 | public virtual bool Equals(SortedNodes? other) 19 | { 20 | if (ReferenceEquals(null, other)) return false; 21 | if (ReferenceEquals(this, other)) return true; 22 | return All.SequenceEqual(other.All); 23 | } 24 | 25 | public override int GetHashCode() 26 | { 27 | return All.CalculateHashCode(); 28 | } 29 | 30 | public override string ToString() 31 | { 32 | return $"SortedNodes: {All.ToCollectionString()}"; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/UnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using DomainDrivers.SmartSchedule.Shared; 2 | 3 | namespace DomainDrivers.SmartSchedule; 4 | 5 | public class UnitOfWork : IUnitOfWork 6 | { 7 | private readonly SmartScheduleDbContext _dbContext; 8 | 9 | public UnitOfWork(SmartScheduleDbContext dbContext) 10 | { 11 | _dbContext = dbContext; 12 | } 13 | 14 | public async Task InTransaction(Func> operation) 15 | { 16 | if (_dbContext.Database.CurrentTransaction != null) 17 | { 18 | return await operation(); 19 | } 20 | 21 | await using var transaction = await _dbContext.Database.BeginTransactionAsync(); 22 | 23 | try 24 | { 25 | var result = await operation(); 26 | 27 | await _dbContext.SaveChangesAsync(); 28 | await transaction.CommitAsync(); 29 | 30 | return result; 31 | } 32 | catch 33 | { 34 | await transaction.RollbackAsync(); 35 | throw; 36 | } 37 | } 38 | 39 | public async Task InTransaction(Func operation) 40 | { 41 | if (_dbContext.Database.CurrentTransaction != null) 42 | { 43 | await operation(); 44 | return; 45 | } 46 | 47 | await using var transaction = await _dbContext.Database.BeginTransactionAsync(); 48 | 49 | try 50 | { 51 | await operation(); 52 | 53 | await _dbContext.SaveChangesAsync(); 54 | await transaction.CommitAsync(); 55 | } 56 | catch 57 | { 58 | await transaction.RollbackAsync(); 59 | throw; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /DomainDrivers.SmartSchedule/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | --------------------------------------------------------------------------------