Skip to content

Commit 00d3378

Browse files
feat: add merchant yearly operating schedule endpoint
Co-authored-by: StuartFerguson <16325469+StuartFerguson@users.noreply.github.com>
1 parent 3e5a48d commit 00d3378

23 files changed

Lines changed: 442 additions & 24 deletions

File tree

TransactionProcessor.Aggregates.Tests/MerchantAggregateTests.cs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,76 @@ public void MerchantAggregate_SetSetttlmentSchedule_SameValue_NoEventRaised(Sett
409409
Merchant merchant = aggregate.GetMerchant();
410410
merchant.SettlementSchedule.ShouldBe(originalSettlementSchedule);
411411
}
412-
412+
413+
[Fact]
414+
public void MerchantAggregate_SetOperatingSchedule_ScheduleIsSet()
415+
{
416+
MerchantAggregate aggregate = MerchantAggregate.Create(TestData.MerchantId);
417+
aggregate.Create(TestData.EstateId, TestData.MerchantName, TestData.DateMerchantCreated, TestData.AddressModel, TestData.ContactModel,
418+
TestData.SettlementScheduleModel);
419+
420+
List<MerchantOperatingSchedulePeriod> periods = new()
421+
{
422+
new MerchantOperatingSchedulePeriod(TestData.OperatingSchedulePeriod1StartDate, TestData.OperatingSchedulePeriod1EndDate, false),
423+
new MerchantOperatingSchedulePeriod(TestData.OperatingSchedulePeriod2StartDate, TestData.OperatingSchedulePeriod2EndDate, false)
424+
};
425+
426+
Result result = aggregate.SetOperatingSchedule(TestData.OperatingScheduleYear, true, periods);
427+
result.IsSuccess.ShouldBeTrue();
428+
429+
Merchant merchant = aggregate.GetMerchant();
430+
merchant.OperatingSchedules.ShouldHaveSingleItem();
431+
merchant.OperatingSchedules.Single().Year.ShouldBe(TestData.OperatingScheduleYear);
432+
merchant.OperatingSchedules.Single().DefaultIsOpen.ShouldBeTrue();
433+
merchant.OperatingSchedules.Single().Periods.Count.ShouldBe(2);
434+
merchant.OperatingSchedules.Single().Periods[0].StartDate.ShouldBe(TestData.OperatingSchedulePeriod1StartDate);
435+
merchant.OperatingSchedules.Single().Periods[0].EndDate.ShouldBe(TestData.OperatingSchedulePeriod1EndDate);
436+
merchant.OperatingSchedules.Single().Periods[0].IsOpen.ShouldBeFalse();
437+
}
438+
439+
[Fact]
440+
public void MerchantAggregate_SetOperatingSchedule_OverlappingPeriods_ErrorReturned()
441+
{
442+
MerchantAggregate aggregate = MerchantAggregate.Create(TestData.MerchantId);
443+
aggregate.Create(TestData.EstateId, TestData.MerchantName, TestData.DateMerchantCreated, TestData.AddressModel, TestData.ContactModel,
444+
TestData.SettlementScheduleModel);
445+
446+
List<MerchantOperatingSchedulePeriod> periods = new()
447+
{
448+
new MerchantOperatingSchedulePeriod(new DateTime(TestData.OperatingScheduleYear, 1, 1), new DateTime(TestData.OperatingScheduleYear, 1, 3), false),
449+
new MerchantOperatingSchedulePeriod(new DateTime(TestData.OperatingScheduleYear, 1, 3), new DateTime(TestData.OperatingScheduleYear, 1, 4), true)
450+
};
451+
452+
Result result = aggregate.SetOperatingSchedule(TestData.OperatingScheduleYear, true, periods);
453+
result.IsFailed.ShouldBeTrue();
454+
result.Status.ShouldBe(ResultStatus.Invalid);
455+
result.Message.ShouldBe("Operating schedule periods must not overlap");
456+
}
457+
458+
[Fact]
459+
public void MerchantAggregate_SetOperatingSchedule_SameValue_NoEventRaised()
460+
{
461+
MerchantAggregate aggregate = MerchantAggregate.Create(TestData.MerchantId);
462+
aggregate.Create(TestData.EstateId, TestData.MerchantName, TestData.DateMerchantCreated, TestData.AddressModel, TestData.ContactModel,
463+
TestData.SettlementScheduleModel);
464+
465+
List<MerchantOperatingSchedulePeriod> periods = new()
466+
{
467+
new MerchantOperatingSchedulePeriod(TestData.OperatingSchedulePeriod1StartDate, TestData.OperatingSchedulePeriod1EndDate, false)
468+
};
469+
470+
aggregate.SetOperatingSchedule(TestData.OperatingScheduleYear, true, periods);
471+
Result result = aggregate.SetOperatingSchedule(TestData.OperatingScheduleYear, true, periods);
472+
result.IsSuccess.ShouldBeTrue();
473+
474+
Type type = aggregate.GetType();
475+
PropertyInfo property = type.GetProperty("PendingEvents", BindingFlags.Instance | BindingFlags.NonPublic);
476+
Object value = property.GetValue(aggregate);
477+
value.ShouldNotBeNull();
478+
List<IDomainEvent> eventHistory = (List<IDomainEvent>)value;
479+
eventHistory.Count.ShouldBe(6);
480+
}
481+
413482
[Fact]
414483
public void MerchantAggregate_SwapDevice_DeviceIsSwapped(){
415484
MerchantAggregate aggregate = MerchantAggregate.Create(TestData.MerchantId);

TransactionProcessor.Aggregates/MerchantAggregate.cs

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using System.Diagnostics.CodeAnalysis;
66
using System.Runtime.CompilerServices;
77
using TransactionProcessor.DomainEvents;
8+
using OperatingScheduleModel = TransactionProcessor.Models.Merchant.MerchantOperatingSchedule;
9+
using OperatingSchedulePeriodModel = TransactionProcessor.Models.Merchant.MerchantOperatingSchedulePeriod;
810
using TransactionProcessor.Models.Contract;
911
using TransactionProcessor.Models.Merchant;
1012
using Address = TransactionProcessor.Aggregates.Models.Address;
@@ -425,6 +427,20 @@ public static Merchant GetMerchant(this MerchantAggregate aggregate)
425427
}
426428
}
427429

430+
if (aggregate.OperatingSchedules.Any())
431+
{
432+
merchantModel.OperatingSchedules = aggregate.OperatingSchedules
433+
.OrderBy(o => o.Key)
434+
.Select(o => new OperatingScheduleModel
435+
{
436+
Year = o.Value.Year,
437+
DefaultIsOpen = o.Value.DefaultIsOpen,
438+
Periods = o.Value.Periods
439+
.Select(p => new OperatingSchedulePeriodModel(p.StartDate, p.EndDate, p.IsOpen))
440+
.ToList()
441+
}).ToList();
442+
}
443+
428444
return merchantModel;
429445
}
430446

@@ -474,6 +490,61 @@ public static Result SetSettlementSchedule(this MerchantAggregate aggregate, Set
474490
return Result.Success();
475491
}
476492

493+
public static Result SetOperatingSchedule(this MerchantAggregate aggregate,
494+
Int32 year,
495+
Boolean defaultIsOpen,
496+
List<OperatingSchedulePeriodModel> periods)
497+
{
498+
Result result = aggregate.EnsureMerchantHasBeenCreated();
499+
if (result.IsFailed)
500+
return result;
501+
502+
if (year < 1)
503+
return Result.Invalid("A valid year must be provided");
504+
505+
List<OperatingSchedulePeriodModel> normalisedPeriods = (periods ?? new List<OperatingSchedulePeriodModel>())
506+
.Select(p => new OperatingSchedulePeriodModel(p.StartDate.Date, p.EndDate.Date, p.IsOpen))
507+
.OrderBy(p => p.StartDate)
508+
.ThenBy(p => p.EndDate)
509+
.ToList();
510+
511+
for (Int32 i = 0; i < normalisedPeriods.Count; i++)
512+
{
513+
OperatingSchedulePeriodModel period = normalisedPeriods[i];
514+
if (period.StartDate > period.EndDate)
515+
return Result.Invalid("Operating schedule periods must have a start date on or before the end date");
516+
517+
if (period.StartDate.Year != year || period.EndDate.Year != year)
518+
return Result.Invalid("Operating schedule periods must fall within the requested year");
519+
520+
if (i > 0)
521+
{
522+
OperatingSchedulePeriodModel previousPeriod = normalisedPeriods[i - 1];
523+
if (period.StartDate <= previousPeriod.EndDate)
524+
return Result.Invalid("Operating schedule periods must not overlap");
525+
}
526+
}
527+
528+
if (aggregate.OperatingSchedules.TryGetValue(year, out OperatingScheduleModel existingSchedule))
529+
{
530+
Boolean isUnchanged = existingSchedule.DefaultIsOpen == defaultIsOpen &&
531+
existingSchedule.Periods.SequenceEqual(normalisedPeriods);
532+
if (isUnchanged)
533+
return Result.Success();
534+
}
535+
536+
MerchantDomainEvents.MerchantOperatingScheduleSetEvent merchantOperatingScheduleSetEvent = new(
537+
aggregate.AggregateId,
538+
aggregate.EstateId,
539+
year,
540+
defaultIsOpen,
541+
normalisedPeriods.Select(p => new TransactionProcessor.DomainEvents.MerchantOperatingSchedulePeriod(p.StartDate, p.EndDate, p.IsOpen)).ToList());
542+
543+
aggregate.ApplyAndAppend(merchantOperatingScheduleSetEvent);
544+
545+
return Result.Success();
546+
}
547+
477548
public static Result Create(this MerchantAggregate aggregate,
478549
Guid estateId,
479550
String merchantName,
@@ -854,6 +925,18 @@ public static void PlayEvent(this MerchantAggregate aggregate, MerchantDomainEve
854925
aggregate.Contracts[domainEvent.ContractId] = contract.Value;
855926
}
856927

928+
public static void PlayEvent(this MerchantAggregate aggregate, MerchantDomainEvents.MerchantOperatingScheduleSetEvent domainEvent)
929+
{
930+
aggregate.OperatingSchedules[domainEvent.Year] = new OperatingScheduleModel
931+
{
932+
Year = domainEvent.Year,
933+
DefaultIsOpen = domainEvent.DefaultIsOpen,
934+
Periods = domainEvent.Periods?
935+
.Select(p => new OperatingSchedulePeriodModel(p.StartDate.Date, p.EndDate.Date, p.IsOpen))
936+
.ToList() ?? new List<OperatingSchedulePeriodModel>()
937+
};
938+
}
939+
857940
public static void PlayEvent(this MerchantAggregate aggregate, MerchantDomainEvents.DeviceSwappedForMerchantEvent domainEvent)
858941
{
859942
KeyValuePair<Guid, Device> device = aggregate.Devices.Single(d => d.Key == domainEvent.OriginalDeviceId);
@@ -887,6 +970,8 @@ public record MerchantAggregate : Aggregate
887970

888971
internal readonly Dictionary<Guid, Contract> Contracts;
889972

973+
internal readonly Dictionary<Int32, OperatingScheduleModel> OperatingSchedules;
974+
890975
#endregion
891976

892977
#region Constructors
@@ -904,6 +989,7 @@ public MerchantAggregate()
904989
this.SecurityUsers = new List<SecurityUser>();
905990
this.Devices = new Dictionary<Guid, Device>();
906991
this.Contracts = new Dictionary<Guid, Contract>();
992+
this.OperatingSchedules = new Dictionary<Int32, OperatingScheduleModel>();
907993
}
908994

909995
/// <summary>
@@ -922,6 +1008,7 @@ private MerchantAggregate(Guid aggregateId)
9221008
this.SecurityUsers = new List<SecurityUser>();
9231009
this.Devices = new Dictionary<Guid, Device>();
9241010
this.Contracts = new Dictionary<Guid, Contract>();
1011+
this.OperatingSchedules = new Dictionary<Int32, OperatingScheduleModel>();
9251012
}
9261013

9271014
#endregion
@@ -970,4 +1057,4 @@ protected override Object GetMetadata()
9701057

9711058

9721059
}
973-
}
1060+
}

TransactionProcessor.BusinessLogic.Tests/Mediator/MediatorTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public MediatorTests()
8686
this.Requests.Add(TestData.Commands.UpdateMerchantContactCommand);
8787
this.Requests.Add(TestData.Commands.RemoveOperatorFromMerchantCommand);
8888
this.Requests.Add(TestData.Commands.RemoveMerchantContractCommand);
89+
this.Requests.Add(TestData.Commands.SetMerchantOperatingScheduleCommand);
8990
this.Requests.Add(TestData.Queries.GetMerchantsQuery);
9091
this.Requests.Add(TestData.Queries.GetMerchantQuery);
9192
this.Requests.Add(TestData.Queries.GetMerchantContractsQuery);

TransactionProcessor.BusinessLogic.Tests/Services/MerchantDomainServiceTests.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1462,6 +1462,31 @@ public async Task MerchantDomainService_UpdateMerchant_SaveFailed_ResultIsFailed
14621462
result.IsFailed.ShouldBeTrue();
14631463
}
14641464

1465+
[Fact]
1466+
public async Task MerchantDomainService_SetMerchantOperatingSchedule_ScheduleSet() {
1467+
this.AggregateService.Setup(e => e.Get<EstateAggregate>(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
1468+
.ReturnsAsync(TestData.Aggregates.CreatedEstateAggregate());
1469+
this.AggregateService.Setup(m => m.GetLatest<MerchantAggregate>(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
1470+
.ReturnsAsync(Result.Success(TestData.Aggregates.CreatedMerchantAggregate()));
1471+
this.AggregateService.Setup(m => m.Save(It.IsAny<MerchantAggregate>(), It.IsAny<CancellationToken>()))
1472+
.ReturnsAsync(Result.Success());
1473+
1474+
var result = await this.DomainService.SetMerchantOperatingSchedule(TestData.Commands.SetMerchantOperatingScheduleCommand, CancellationToken.None);
1475+
result.IsSuccess.ShouldBeTrue();
1476+
}
1477+
1478+
[Fact]
1479+
public async Task MerchantDomainService_SetMerchantOperatingSchedule_ValidationFailure_ResultIsFailed()
1480+
{
1481+
this.AggregateService.Setup(e => e.Get<EstateAggregate>(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
1482+
.ReturnsAsync(TestData.Aggregates.CreatedEstateAggregate());
1483+
this.AggregateService.Setup(m => m.GetLatest<MerchantAggregate>(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
1484+
.ReturnsAsync(Result.Success(TestData.Aggregates.EmptyMerchantAggregate()));
1485+
1486+
var result = await this.DomainService.SetMerchantOperatingSchedule(TestData.Commands.SetMerchantOperatingScheduleCommand, CancellationToken.None);
1487+
result.IsFailed.ShouldBeTrue();
1488+
}
1489+
14651490
[Fact]
14661491
public async Task MerchantDomainService_AddMerchantAddress_GetEstateFailed_ResultIsFailed() {
14671492
this.AggregateService.Setup(e => e.Get<EstateAggregate>(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))

TransactionProcessor.BusinessLogic/RequestHandlers/MerchantRequestHandler.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ public class MerchantRequestHandler :
3737
IRequestHandler<MerchantCommands.AddMerchantAddressCommand, Result>,
3838
IRequestHandler<MerchantCommands.UpdateMerchantAddressCommand, Result>,
3939
IRequestHandler<MerchantCommands.AddMerchantContactCommand, Result>,
40-
IRequestHandler<MerchantCommands.UpdateMerchantContactCommand, Result>,
41-
IRequestHandler<MerchantCommands.RemoveOperatorFromMerchantCommand, Result>,
42-
IRequestHandler<MerchantCommands.RemoveMerchantContractCommand, Result>
40+
IRequestHandler<MerchantCommands.UpdateMerchantContactCommand, Result>,
41+
IRequestHandler<MerchantCommands.RemoveOperatorFromMerchantCommand, Result>,
42+
IRequestHandler<MerchantCommands.RemoveMerchantContractCommand, Result>,
43+
IRequestHandler<MerchantCommands.SetMerchantOperatingScheduleCommand, Result>
4344
{
4445
private readonly IProjectionStateRepository<MerchantBalanceState> MerchantBalanceStateRepository;
4546
private readonly IEventStoreContext EventStoreContext;
@@ -182,4 +183,9 @@ public async Task<Result> Handle(MerchantCommands.RemoveMerchantContractCommand
182183
{
183184
return await this.MerchantDomainService.RemoveContractFromMerchant(command, cancellationToken);
184185
}
185-
}
186+
187+
public async Task<Result> Handle(MerchantCommands.SetMerchantOperatingScheduleCommand command, CancellationToken cancellationToken)
188+
{
189+
return await this.MerchantDomainService.SetMerchantOperatingSchedule(command, cancellationToken);
190+
}
191+
}

TransactionProcessor.BusinessLogic/Requests/MerchantCommands.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ public record UpdateMerchantAddressCommand(Guid EstateId, Guid MerchantId, Guid
3939
public record AddMerchantContactCommand(Guid EstateId, Guid MerchantId, Contact RequestDto) : IRequest<Result>;
4040

4141
public record UpdateMerchantContactCommand(Guid EstateId, Guid MerchantId, Guid ContactId, Contact RequestDto) : IRequest<Result>;
42+
43+
public record SetMerchantOperatingScheduleCommand(Guid EstateId, Guid MerchantId, Int32 Year, SetMerchantOperatingScheduleRequest RequestDto) : IRequest<Result>;
4244
}
4345

4446

45-
}
47+
}

TransactionProcessor.BusinessLogic/Services/MerchantDomainService.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public interface IMerchantDomainService
4343
Task<Result> UpdateMerchantContact(MerchantCommands.UpdateMerchantContactCommand command, CancellationToken cancellationToken);
4444
Task<Result> RemoveOperatorFromMerchant(MerchantCommands.RemoveOperatorFromMerchantCommand command, CancellationToken cancellationToken);
4545
Task<Result> RemoveContractFromMerchant(MerchantCommands.RemoveMerchantContractCommand command, CancellationToken cancellationToken);
46+
Task<Result> SetMerchantOperatingSchedule(MerchantCommands.SetMerchantOperatingScheduleCommand command, CancellationToken cancellationToken);
4647

4748
#endregion
4849
}
@@ -490,6 +491,45 @@ public async Task<Result> UpdateMerchant(MerchantCommands.UpdateMerchantCommand
490491
}
491492
}
492493

494+
public async Task<Result> SetMerchantOperatingSchedule(MerchantCommands.SetMerchantOperatingScheduleCommand command, CancellationToken cancellationToken)
495+
{
496+
try
497+
{
498+
Result<EstateAggregate> estateResult = await DomainServiceHelper.GetAggregateOrFailure(ct => this.AggregateService.Get<EstateAggregate>(command.EstateId, ct), command.EstateId, cancellationToken);
499+
if (estateResult.IsFailed)
500+
return ResultHelpers.CreateFailure(estateResult);
501+
502+
Result<MerchantAggregate> merchantResult = await DomainServiceHelper.GetAggregateOrFailure(ct => this.AggregateService.GetLatest<MerchantAggregate>(command.MerchantId, ct), command.MerchantId, cancellationToken);
503+
if (merchantResult.IsFailed)
504+
return ResultHelpers.CreateFailure(merchantResult);
505+
506+
EstateAggregate estateAggregate = estateResult.Data;
507+
MerchantAggregate merchantAggregate = merchantResult.Data;
508+
509+
Result result = this.ValidateEstateAndMerchant(estateAggregate, merchantAggregate);
510+
if (result.IsFailed)
511+
return ResultHelpers.CreateFailure(result);
512+
513+
List<MerchantOperatingSchedulePeriod> periods = command.RequestDto.Periods?
514+
.Select(p => new MerchantOperatingSchedulePeriod(p.StartDate, p.EndDate, p.IsOpen))
515+
.ToList() ?? new List<MerchantOperatingSchedulePeriod>();
516+
517+
Result stateResult = merchantAggregate.SetOperatingSchedule(command.Year, command.RequestDto.DefaultIsOpen, periods);
518+
if (stateResult.IsFailed)
519+
return stateResult;
520+
521+
Result saveResult = await this.AggregateService.Save(merchantAggregate, cancellationToken);
522+
if (saveResult.IsFailed)
523+
return ResultHelpers.CreateFailure(saveResult);
524+
525+
return saveResult;
526+
}
527+
catch (Exception ex)
528+
{
529+
return Result.Failure(ex.GetExceptionMessages());
530+
}
531+
}
532+
493533
public async Task<Result> AddMerchantAddress(MerchantCommands.AddMerchantAddressCommand command, CancellationToken cancellationToken)
494534
{
495535
try

0 commit comments

Comments
 (0)