diff --git a/TransactionProcessor.Aggregates.Tests/MerchantStatementAggregateTests.cs b/TransactionProcessor.Aggregates.Tests/MerchantStatementAggregateTests.cs index 72e2fef8..91e8dafd 100644 --- a/TransactionProcessor.Aggregates.Tests/MerchantStatementAggregateTests.cs +++ b/TransactionProcessor.Aggregates.Tests/MerchantStatementAggregateTests.cs @@ -82,27 +82,54 @@ public void MerchantStatementForDateAggregate_AddSettledFeeToStatement_Duplicate statementLines.Count.ShouldBe(1); } - /* + [Fact] + public void MerchantStatementAggregate_AddDailySummaryRecord_RecordIsAdded() { + MerchantStatementAggregate merchantStatementAggregate = MerchantStatementAggregate.Create(TestData.MerchantStatementId); + Should.NotThrow(() => { merchantStatementAggregate.AddDailySummaryRecord(TestData.TransactionDateTime.Date, 1, 100.00m, 1, 0.10m); }); + } + + [Fact] + public void MerchantStatementAggregate_AddDailySummaryRecord_DuplicateAdd_ExceptionIsThrown() + { + MerchantStatementAggregate merchantStatementAggregate = MerchantStatementAggregate.Create(TestData.MerchantStatementId); + merchantStatementAggregate.AddDailySummaryRecord(TestData.TransactionDateTime.Date, 1, 100.00m, 1, 0.10m); + Should.Throw(() => { merchantStatementAggregate.AddDailySummaryRecord(TestData.TransactionDateTime.Date, 1, 100.00m, 1, 0.10m); }); + } + + [Fact] public void MerchantStatementAggregate_GenerateStatement_StatementIsGenerated() { MerchantStatementAggregate merchantStatementAggregate = MerchantStatementAggregate.Create(TestData.MerchantStatementId); - merchantStatementAggregate.AddTransactionToStatement(TestData.MerchantStatementId, - TestData.EventId1, - TestData.StatementCreateDate, - TestData.EstateId, - TestData.MerchantId, TestData.Transaction1); - merchantStatementAggregate.AddSettledFeeToStatement(TestData.MerchantStatementId, - TestData.EventId1, - TestData.StatementCreateDate, - TestData.EstateId, - TestData.MerchantId, TestData.SettledFee1); + merchantStatementAggregate.AddDailySummaryRecord(TestData.TransactionDateTime.Date, 1, 100.00m, 1, 0.10m); merchantStatementAggregate.GenerateStatement(TestData.StatementGeneratedDate); - var merchantStatement = merchantStatementAggregate.GetStatement(); + MerchantStatement merchantStatement = merchantStatementAggregate.GetStatement(); merchantStatement.IsGenerated.ShouldBeTrue(); } + [Fact] + public void MerchantStatementAggregate_GenerateStatement_StatementIsAlreadGenerated_ExceptionThrown() + { + MerchantStatementAggregate merchantStatementAggregate = MerchantStatementAggregate.Create(TestData.MerchantStatementId); + merchantStatementAggregate.AddDailySummaryRecord(TestData.TransactionDateTime.Date, 1, 100.00m, 1, 0.10m); + merchantStatementAggregate.GenerateStatement(TestData.StatementGeneratedDate); + Should.Throw(() => { + merchantStatementAggregate.GenerateStatement(TestData.StatementGeneratedDate); + }); + } + + [Fact] + public void MerchantStatementAggregate_GenerateStatement_NoSummaries_ExceptionThrown() + { + MerchantStatementAggregate merchantStatementAggregate = MerchantStatementAggregate.Create(TestData.MerchantStatementId); + + Should.Throw(() => { + merchantStatementAggregate.GenerateStatement(TestData.StatementGeneratedDate); + }); + } + + /* [Fact] public void MerchantStatementAggregate_GenerateStatement_StatementNotCreated_ErrorThrown() { @@ -140,13 +167,13 @@ public void MerchantStatementAggregate_GenerateStatement_StatementAlreadyGenerat public void MerchantStatementAggregate_GenerateStatement_StatementHasNoTransactionsOrSettledFees_ErrorThrown() { MerchantStatementAggregate merchantStatementAggregate = MerchantStatementAggregate.Create(TestData.MerchantStatementId); - + Should.Throw(() => { merchantStatementAggregate.GenerateStatement(TestData.StatementGeneratedDate); }); } - + [Fact] public void MerchantStatementAggregate_EmailStatement_StatementHasBeenEmailed() { diff --git a/TransactionProcessor.Aggregates/MerchantStatementAggregate.cs b/TransactionProcessor.Aggregates/MerchantStatementAggregate.cs index cfc3baae..d1e41180 100644 --- a/TransactionProcessor.Aggregates/MerchantStatementAggregate.cs +++ b/TransactionProcessor.Aggregates/MerchantStatementAggregate.cs @@ -32,6 +32,8 @@ public record MerchantStatementAggregate : Aggregate internal Guid MerchantId; internal List<(Guid merchantStatementForDateId, DateTime activityDate)> ActivityDates; + + internal List MerchantStatementSummaries; #endregion #region Constructors @@ -41,6 +43,7 @@ public MerchantStatementAggregate() { // Nothing here this.ActivityDates = new(); + this.MerchantStatementSummaries = new(); } private MerchantStatementAggregate(Guid aggregateId) @@ -49,6 +52,7 @@ private MerchantStatementAggregate(Guid aggregateId) this.AggregateId = aggregateId; this.ActivityDates = new(); + this.MerchantStatementSummaries = new(); } #endregion @@ -81,11 +85,11 @@ public static void PlayEvent(this MerchantStatementAggregate aggregate, Merchant aggregate.StatementDate = domainEvent.StatementDate; } - //public static void PlayEvent(this MerchantStatementAggregate aggregate, MerchantStatementDomainEvents.StatementGeneratedEvent domainEvent) - //{ - // aggregate.IsGenerated = true; - // aggregate.GeneratedDateTime = domainEvent.DateGenerated; - //} + public static void PlayEvent(this MerchantStatementAggregate aggregate, MerchantStatementDomainEvents.StatementGeneratedEvent domainEvent) + { + aggregate.IsGenerated = true; + aggregate.GeneratedDateTime = domainEvent.DateGenerated; + } //public static void PlayEvent(this MerchantStatementAggregate aggregate, MerchantStatementDomainEvents.StatementEmailedEvent domainEvent) //{ @@ -98,6 +102,13 @@ public static void PlayEvent(this MerchantStatementAggregate aggregate, MerchantStatementDomainEvents.ActivityDateAddedToStatementEvent domainEvent) { aggregate.ActivityDates.Add((domainEvent.MerchantStatementForDateId, domainEvent.ActivityDate)); } + + public static void PlayEvent(this MerchantStatementAggregate aggregate, + MerchantStatementDomainEvents.StatementSummaryForDateEvent domainEvent) + { + aggregate.MerchantStatementSummaries.Add(new MerchantStatementSummary(domainEvent.ActivityDate,domainEvent.NumberOfTransactions,domainEvent.ValueOfTransactions, + domainEvent.NumberOfSettledFees,domainEvent.ValueOfTransactions)); + } public static void RecordActivityDateOnStatement(this MerchantStatementAggregate aggregate, Guid statementId, @@ -137,6 +148,7 @@ public static MerchantStatement GetStatement(this MerchantStatementAggregate agg IsCreated = aggregate.IsCreated, StatementDate = aggregate.StatementDate, MerchantStatementId = aggregate.AggregateId, + IsGenerated = aggregate.IsGenerated, }; foreach ((Guid merchantStatementForDateId, DateTime activityDate) aggregateActivityDate in aggregate.ActivityDates) { @@ -158,21 +170,21 @@ public static MerchantStatement GetStatement(this MerchantStatementAggregate agg // aggregate.ApplyAndAppend(statementEmailedEvent); //} - //public static void GenerateStatement(this MerchantStatementAggregate aggregate, - // DateTime generatedDateTime) - //{ - // aggregate.EnsureStatementHasNotAlreadyBeenGenerated(); + public static void GenerateStatement(this MerchantStatementAggregate aggregate, + DateTime generatedDateTime) + { + aggregate.EnsureStatementHasNotAlreadyBeenGenerated(); - // // TODO: Validate days have been added - // //if (aggregate.Transactions.Any() == false && aggregate.SettledFees.Any() == false) - // //{ - // // throw new InvalidOperationException("Statement has no transactions or settled fees"); - // //} + // Validate days have been added + if (aggregate.MerchantStatementSummaries.Any() == false) + { + throw new InvalidOperationException("Statement has no transactions or settled fees"); + } - // MerchantStatementDomainEvents.StatementGeneratedEvent statementGeneratedEvent = new(aggregate.AggregateId, aggregate.EstateId, aggregate.MerchantId, generatedDateTime); + MerchantStatementDomainEvents.StatementGeneratedEvent statementGeneratedEvent = new(aggregate.AggregateId, aggregate.EstateId, aggregate.MerchantId, generatedDateTime); - // aggregate.ApplyAndAppend(statementGeneratedEvent); - //} + aggregate.ApplyAndAppend(statementGeneratedEvent); + } //private static void EnsureStatementHasBeenCreated(this MerchantStatementAggregate aggregate) //{ @@ -190,12 +202,23 @@ public static MerchantStatement GetStatement(this MerchantStatementAggregate agg // } //} - //private static void EnsureStatementHasNotAlreadyBeenGenerated(this MerchantStatementAggregate aggregate) - //{ - // if (aggregate.IsGenerated) - // { - // throw new InvalidOperationException("Statement header has already been generated"); - // } - //} + private static void EnsureStatementHasNotAlreadyBeenGenerated(this MerchantStatementAggregate aggregate) + { + if (aggregate.IsGenerated) + { + throw new InvalidOperationException("Statement header has already been generated"); + } + } + + public static void AddDailySummaryRecord(this MerchantStatementAggregate aggregate, DateTime activityDate, Int32 numberOfTransactions, Decimal valueOfTransactions, Int32 numberOfSettledFees, Decimal valueOfSettledFees) { + if (aggregate.MerchantStatementSummaries.Any(s => s.ActivityDate == activityDate)) { + throw new InvalidOperationException($"Summary Data for Activity Date {activityDate:yyyy-MM-dd} already exists"); + } + MerchantStatementDomainEvents.StatementSummaryForDateEvent statementSummaryForDateEvent = new(aggregate.AggregateId, aggregate.EstateId, aggregate.MerchantId, activityDate,aggregate.MerchantStatementSummaries.Count +1 + ,numberOfTransactions, valueOfTransactions, numberOfSettledFees, valueOfSettledFees); + aggregate.ApplyAndAppend(statementSummaryForDateEvent); + } } + + public record MerchantStatementSummary(DateTime ActivityDate, Int32 NumberOfTransactions, Decimal ValueOfTransactions, Int32 NumberOfSettledFees, Decimal ValueOfSettledFees); } \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic.Tests/Mediator/MediatorTests.cs b/TransactionProcessor.BusinessLogic.Tests/Mediator/MediatorTests.cs index 00997807..77dcf143 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Mediator/MediatorTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Mediator/MediatorTests.cs @@ -92,7 +92,7 @@ public MediatorTests() this.Requests.Add(TestData.Queries.GetTransactionFeesForProductQuery); // Merchant Statement Commands - //this.Requests.Add(TestData.Commands.GenerateMerchantStatementCommand); + this.Requests.Add(TestData.Commands.GenerateMerchantStatementCommand); this.Requests.Add(TestData.Commands.AddTransactionToMerchantStatementCommand); //this.Requests.Add(TestData.Commands.EmailMerchantStatementCommand); this.Requests.Add(TestData.Commands.AddSettledFeeToMerchantStatementCommand); diff --git a/TransactionProcessor.BusinessLogic.Tests/TransactionProcessor.BusinessLogic.Tests.csproj b/TransactionProcessor.BusinessLogic.Tests/TransactionProcessor.BusinessLogic.Tests.csproj index db84c90b..0b867c81 100644 --- a/TransactionProcessor.BusinessLogic.Tests/TransactionProcessor.BusinessLogic.Tests.csproj +++ b/TransactionProcessor.BusinessLogic.Tests/TransactionProcessor.BusinessLogic.Tests.csproj @@ -2,7 +2,7 @@ net8.0 - None + Full false diff --git a/TransactionProcessor.BusinessLogic/RequestHandlers/MerchantStatementRequestHandler.cs b/TransactionProcessor.BusinessLogic/RequestHandlers/MerchantStatementRequestHandler.cs index f8c1308d..0f8ea935 100644 --- a/TransactionProcessor.BusinessLogic/RequestHandlers/MerchantStatementRequestHandler.cs +++ b/TransactionProcessor.BusinessLogic/RequestHandlers/MerchantStatementRequestHandler.cs @@ -9,7 +9,7 @@ namespace TransactionProcessor.BusinessLogic.RequestHandlers { public class MerchantStatementRequestHandler : IRequestHandler, IRequestHandler, - //IRequestHandler, + IRequestHandler, //IRequestHandler, IRequestHandler { #region Fields @@ -64,10 +64,10 @@ public async Task Handle(MerchantStatementCommands.AddSettledFeeToMercha #endregion - //public async Task Handle(MerchantCommands.GenerateMerchantStatementCommand command, CancellationToken cancellationToken) - //{ - // return await this.MerchantStatementDomainService.GenerateStatement(command, cancellationToken); - //} + public async Task Handle(MerchantCommands.GenerateMerchantStatementCommand command, CancellationToken cancellationToken) + { + return await this.MerchantStatementDomainService.GenerateStatement(command, cancellationToken); + } //public async Task Handle(MerchantStatementCommands.EmailMerchantStatementCommand command, // CancellationToken cancellationToken) diff --git a/TransactionProcessor.BusinessLogic/Services/MerchantStatementDomainService.cs b/TransactionProcessor.BusinessLogic/Services/MerchantStatementDomainService.cs index aea5be07..2172564f 100644 --- a/TransactionProcessor.BusinessLogic/Services/MerchantStatementDomainService.cs +++ b/TransactionProcessor.BusinessLogic/Services/MerchantStatementDomainService.cs @@ -9,9 +9,11 @@ using Shared.Results; using SimpleResults; using System; +using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO.Abstractions; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Google.Protobuf.Reflection; @@ -164,29 +166,52 @@ internal static DateTime CalculateStatementDate(DateTime eventDateTime) return new DateTime(calculatedDateTime.Year, calculatedDateTime.Month, 1); } - /// - /// Generates the statement. - /// - /// The estate identifier. - /// The merchant identifier. - /// The statement date. - /// The cancellation token. - /// public async Task GenerateStatement(MerchantCommands.GenerateMerchantStatementCommand command, CancellationToken cancellationToken) { - //Guid statementId = GuidCalculator.Combine(command.MerchantId, command.RequestDto.MerchantStatementDate.ToGuid()); - - //Result result = await ApplyUpdates( - // async (MerchantStatementAggregate merchantStatementAggregate) => { + // Need to rebuild the date time from the command as the Kind is Utc which is different from the date time used to generate the statement id + DateTime dt = new DateTime(command.RequestDto.MerchantStatementDate.Year, command.RequestDto.MerchantStatementDate.Month, command.RequestDto.MerchantStatementDate.Day); + Guid merchantStatementId = IdGenerationService.GenerateMerchantStatementAggregateId(command.EstateId, command.MerchantId, dt); + + Result result = await ApplyUpdates( + async (MerchantStatementAggregate merchantStatementAggregate) => + { + MerchantStatement statement = merchantStatementAggregate.GetStatement(); + List<(Guid merchantStatementForDateId, DateTime activityDate)> activityDates = statement.GetActivityDates(); - // merchantStatementAggregate.GenerateStatement(DateTime.Now); + List statementForDateAggregates = new(); + foreach ((Guid merchantStatementForDateId, DateTime activityDate) activityDate in activityDates) + { + Result statementForDateResult = await this.AggregateService.GetLatest(activityDate.merchantStatementForDateId, cancellationToken); + if (statementForDateResult.IsFailed) + return ResultHelpers.CreateFailure(statementForDateResult); + MerchantStatementForDate dailyStatement = statementForDateResult.Data.GetStatement(true); + statementForDateAggregates.Add(dailyStatement); + } + + // Ok so now we have the daily statements we need to add a summary line to the statement aggregate + foreach (MerchantStatementForDate merchantStatementForDateAggregate in statementForDateAggregates) + { + // Build the summary event + var transactionsResult = merchantStatementForDateAggregate.GetStatementLines() + .Where(sl => sl.LineType == 1) + .Aggregate(new { Count = 0, TotalAmount = 0m }, + (acc, sl) => new { Count = acc.Count + 1, TotalAmount = acc.TotalAmount + sl.Amount }); + var settledFeesResult = merchantStatementForDateAggregate.GetStatementLines() + .Where(sl => sl.LineType == 2) + .Aggregate(new { Count = 0, TotalAmount = 0m }, + (acc, sl) => new { Count = acc.Count + 1, TotalAmount = acc.TotalAmount + sl.Amount }); + merchantStatementAggregate.AddDailySummaryRecord(merchantStatementForDateAggregate.ActivityDate, transactionsResult.Count, transactionsResult.TotalAmount, settledFeesResult.Count, + settledFeesResult.TotalAmount); + } + + merchantStatementAggregate.GenerateStatement(DateTime.Now); - // return Result.Success(); - // }, - // command.EstateId, statementId, cancellationToken, false); + return Result.Success(); + }, + merchantStatementId, cancellationToken, false); - //if (result.IsFailed) - // return ResultHelpers.CreateFailure(result); + if (result.IsFailed) + return ResultHelpers.CreateFailure(result); return Result.Success(); } diff --git a/TransactionProcessor.DomainEvents/MerchantStatementDomainEvents.cs b/TransactionProcessor.DomainEvents/MerchantStatementDomainEvents.cs index fa4befb2..ed77e4f2 100644 --- a/TransactionProcessor.DomainEvents/MerchantStatementDomainEvents.cs +++ b/TransactionProcessor.DomainEvents/MerchantStatementDomainEvents.cs @@ -12,6 +12,9 @@ public record StatementEmailedEvent(Guid MerchantStatementId, Guid EstateId, Gui public record StatementCreatedEvent(Guid MerchantStatementId, Guid EstateId, Guid MerchantId, DateTime StatementDate) : DomainEvent(MerchantStatementId, Guid.NewGuid()); public record ActivityDateAddedToStatementEvent(Guid MerchantStatementId, Guid EstateId, Guid MerchantId, Guid MerchantStatementForDateId, DateTime ActivityDate) : DomainEvent(MerchantStatementId, Guid.NewGuid()); + + public record StatementSummaryForDateEvent(Guid MerchantStatementId, Guid EstateId, Guid MerchantId, DateTime ActivityDate, Int32 LineNumber, + Int32 NumberOfTransactions, Decimal ValueOfTransactions, Int32 NumberOfSettledFees, Decimal ValueOfSettledFees) : DomainEvent(MerchantStatementId, Guid.NewGuid()); } [ExcludeFromCodeCoverage] diff --git a/TransactionProcessor.Testing/TestData.cs b/TransactionProcessor.Testing/TestData.cs index 73d0ec0f..7abcfdf6 100644 --- a/TransactionProcessor.Testing/TestData.cs +++ b/TransactionProcessor.Testing/TestData.cs @@ -2076,16 +2076,16 @@ public static DataTransferObjects.Requests.Contract.AddTransactionFeeForProductT public static Boolean IsAuthorisedFalse = false; public static Boolean IsAuthorisedTrue = true; - - //public static GenerateMerchantStatementRequest GenerateMerchantStatementRequest => - // new GenerateMerchantStatementRequest - // { - // MerchantStatementDate = TestData.StatementCreateDate - // }; + public static DateTime StatementCreateDate = new DateTime(2025, 5, 1); + public static GenerateMerchantStatementRequest GenerateMerchantStatementRequest => + new GenerateMerchantStatementRequest + { + MerchantStatementDate = TestData.StatementCreateDate + }; #endregion public static class Commands { - //public static MerchantCommands.GenerateMerchantStatementCommand GenerateMerchantStatementCommand => new(TestData.EstateId, TestData.MerchantId, TestData.GenerateMerchantStatementRequest); + public static MerchantCommands.GenerateMerchantStatementCommand GenerateMerchantStatementCommand => new(TestData.EstateId, TestData.MerchantId, TestData.GenerateMerchantStatementRequest); public static MerchantStatementCommands.AddTransactionToMerchantStatementCommand AddTransactionToMerchantStatementCommand => new(EstateId, MerchantId, TransactionDateTime, TransactionAmount, IsAuthorisedTrue, TransactionId); //public static MerchantStatementCommands.EmailMerchantStatementCommand EmailMerchantStatementCommand => new(EstateId, MerchantId, MerchantStatementId); diff --git a/TransactionProcessor/Bootstrapper/MediatorRegistry.cs b/TransactionProcessor/Bootstrapper/MediatorRegistry.cs index 29f5935e..c042f84f 100644 --- a/TransactionProcessor/Bootstrapper/MediatorRegistry.cs +++ b/TransactionProcessor/Bootstrapper/MediatorRegistry.cs @@ -42,7 +42,7 @@ public MediatorRegistry() private void RegisterMerchantStatementRequestHandler() { this.AddSingleton, MerchantStatementRequestHandler>(); this.AddSingleton, MerchantStatementRequestHandler>(); - //this.AddSingleton, MerchantStatementRequestHandler>(); + this.AddSingleton, MerchantStatementRequestHandler>(); //this.AddSingleton, MerchantStatementRequestHandler>(); this.AddSingleton, MerchantStatementRequestHandler>(); }