From dcaedd3476fe1ad4c1f53f7b7fcaf2c3929f0bf5 Mon Sep 17 00:00:00 2001 From: Stuart Ferguson Date: Wed, 8 Jan 2025 11:53:07 +0000 Subject: [PATCH 1/2] Enhance credit purchase functionality with CreditId Updated RecordCreditPurchaseCommand to include CreditId. Modified RecordCreditPurchase method to prevent duplicate entries. Enhanced PlayEvent methods for better tracking of credits and debits. Added properties in FloatActivityAggregate for credit and debit counts. Introduced new test cases to validate the updated functionality. Updated TransactionDomainEventHandler to handle CreditId. Refined ApplyFloatActivityUpdates method for clarity. Updated TestData to include FloatCreditId for testing. --- .../Services/FloatDomainServiceTests.cs | 6 +-- .../TransactionDomainEventHandler.cs | 2 +- .../Requests/FloatActivityCommands.cs | 2 +- .../Services/IFloatDomainService.cs | 6 +-- .../FloatActivityAggregateDomainEvents.cs | 4 +- .../FloatActivityAggregateTests.cs | 28 ++++++++++- .../FloatActivityAggregate.cs | 49 +++++++++++++------ TransactionProcessor.Testing/TestData.cs | 3 +- 8 files changed, 73 insertions(+), 27 deletions(-) diff --git a/TransactionProcessor.BusinessLogic.Tests/Services/FloatDomainServiceTests.cs b/TransactionProcessor.BusinessLogic.Tests/Services/FloatDomainServiceTests.cs index 3b6cc5a9..7af5e281 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Services/FloatDomainServiceTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Services/FloatDomainServiceTests.cs @@ -212,7 +212,7 @@ public async Task FloatDomainService_RecordCreditPurchase_FloatActivity_Purchase this.FloatActivityAggregateRepository.Setup(f => f.SaveChanges(It.IsAny(), It.IsAny())).ReturnsAsync(Result.Success); var command = new FloatActivityCommands.RecordCreditPurchaseCommand(TestData.EstateId, - TestData.FloatAggregateId, TestData.CreditPurchasedDateTime, TestData.FloatCreditAmount); + TestData.FloatAggregateId, TestData.CreditPurchasedDateTime, TestData.FloatCreditAmount, TestData.FloatCreditId); var result = await this.FloatDomainService.RecordCreditPurchase(command, CancellationToken.None); result.IsSuccess.ShouldBeTrue(); } @@ -225,7 +225,7 @@ public async Task FloatDomainService_RecordCreditPurchase_FloatActivity_SaveFail this.FloatActivityAggregateRepository.Setup(f => f.SaveChanges(It.IsAny(), It.IsAny())).ReturnsAsync(Result.Failure); var command = new FloatActivityCommands.RecordCreditPurchaseCommand(TestData.EstateId, - TestData.FloatAggregateId, TestData.CreditPurchasedDateTime, TestData.FloatCreditAmount); + TestData.FloatAggregateId, TestData.CreditPurchasedDateTime, TestData.FloatCreditAmount, TestData.FloatCreditId); var result = await this.FloatDomainService.RecordCreditPurchase(command, CancellationToken.None); result.IsFailed.ShouldBeTrue(); } @@ -238,7 +238,7 @@ public async Task FloatDomainService_RecordCreditPurchase_FloatActivity_Exceptio this.FloatActivityAggregateRepository.Setup(f => f.SaveChanges(It.IsAny(), It.IsAny())).ThrowsAsync(new Exception()); var command = new FloatActivityCommands.RecordCreditPurchaseCommand(TestData.EstateId, - TestData.FloatAggregateId, TestData.CreditPurchasedDateTime, TestData.FloatCreditAmount); + TestData.FloatAggregateId, TestData.CreditPurchasedDateTime, TestData.FloatCreditAmount, TestData.FloatCreditId); var result = await this.FloatDomainService.RecordCreditPurchase(command, CancellationToken.None); result.IsFailed.ShouldBeTrue(); } diff --git a/TransactionProcessor.BusinessLogic/EventHandling/TransactionDomainEventHandler.cs b/TransactionProcessor.BusinessLogic/EventHandling/TransactionDomainEventHandler.cs index 1da77fed..09d6532e 100644 --- a/TransactionProcessor.BusinessLogic/EventHandling/TransactionDomainEventHandler.cs +++ b/TransactionProcessor.BusinessLogic/EventHandling/TransactionDomainEventHandler.cs @@ -87,7 +87,7 @@ private async Task HandleSpecificDomainEvent(FloatCreditPurchasedEvent d CancellationToken cancellationToken) { FloatActivityCommands.RecordCreditPurchaseCommand command = new(domainEvent.EstateId, domainEvent.FloatId, - domainEvent.CreditPurchasedDateTime, domainEvent.Amount); + domainEvent.CreditPurchasedDateTime, domainEvent.Amount, domainEvent.EventId); return await this.Mediator.Send(command, cancellationToken); } diff --git a/TransactionProcessor.BusinessLogic/Requests/FloatActivityCommands.cs b/TransactionProcessor.BusinessLogic/Requests/FloatActivityCommands.cs index f33cc71c..590a39c2 100644 --- a/TransactionProcessor.BusinessLogic/Requests/FloatActivityCommands.cs +++ b/TransactionProcessor.BusinessLogic/Requests/FloatActivityCommands.cs @@ -7,7 +7,7 @@ namespace TransactionProcessor.BusinessLogic.Requests; [ExcludeFromCodeCoverage] public record FloatActivityCommands { - public record RecordCreditPurchaseCommand(Guid EstateId, Guid FloatId, DateTime CreditPurchasedDateTime, Decimal Amount) : IRequest; + public record RecordCreditPurchaseCommand(Guid EstateId, Guid FloatId, DateTime CreditPurchasedDateTime, Decimal Amount, Guid CreditId) : IRequest; public record RecordTransactionCommand(Guid EstateId, Guid TransactionId) : IRequest; } \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic/Services/IFloatDomainService.cs b/TransactionProcessor.BusinessLogic/Services/IFloatDomainService.cs index 2004e885..c8b36d81 100644 --- a/TransactionProcessor.BusinessLogic/Services/IFloatDomainService.cs +++ b/TransactionProcessor.BusinessLogic/Services/IFloatDomainService.cs @@ -98,7 +98,7 @@ private async Task ApplyFloatActivityUpdates(Func getFloatResult = await this.FloatActivityAggregateRepository.GetLatestVersionFromLastEvent(floatId, cancellationToken); + Result getFloatResult = await this.FloatActivityAggregateRepository.GetLatestVersion(floatId, cancellationToken); Result floatActivityAggregateResult = DomainServiceHelper.HandleGetAggregateResult(getFloatResult, floatId, isNotFoundError); if (floatActivityAggregateResult.IsFailed) @@ -192,7 +192,7 @@ public async Task RecordCreditPurchase(FloatCommands.RecordCreditPurchas public async Task RecordCreditPurchase(FloatActivityCommands.RecordCreditPurchaseCommand command, CancellationToken cancellationToken) { Result result = await ApplyFloatActivityUpdates((floatAggregate) => { - floatAggregate.RecordCreditPurchase(command.EstateId, command.CreditPurchasedDateTime, command.Amount); + floatAggregate.RecordCreditPurchase(command.EstateId, command.CreditPurchasedDateTime, command.Amount, command.CreditId); return Result.Success(); }, command.FloatId, cancellationToken,false); return result; @@ -207,7 +207,7 @@ public async Task RecordTransaction(FloatActivityCommands.RecordTransact Guid floatId = IdGenerationService.GenerateFloatAggregateId(command.EstateId, getTransactionResult.Data.ContractId, getTransactionResult.Data.ProductId); Result result = await ApplyFloatActivityUpdates((floatAggregate) => { - floatAggregate.RecordTransactionAgainstFloat(command.EstateId, getTransactionResult.Data.TransactionDateTime, getTransactionResult.Data.TransactionAmount.GetValueOrDefault()); + floatAggregate.RecordTransactionAgainstFloat(command.EstateId, getTransactionResult.Data.TransactionDateTime, getTransactionResult.Data.TransactionAmount.GetValueOrDefault(), command.TransactionId); return Result.Success(); }, floatId, cancellationToken, false); return result; diff --git a/TransactionProcessor.Float.DomainEvents/FloatActivityAggregateDomainEvents.cs b/TransactionProcessor.Float.DomainEvents/FloatActivityAggregateDomainEvents.cs index f8f12bae..fae17edc 100644 --- a/TransactionProcessor.Float.DomainEvents/FloatActivityAggregateDomainEvents.cs +++ b/TransactionProcessor.Float.DomainEvents/FloatActivityAggregateDomainEvents.cs @@ -12,11 +12,11 @@ namespace TransactionProcessor.Float.DomainEvents public record FloatAggregateCreditedEvent(Guid FloatId, Guid EstateId, DateTime ActivityDateTime, - Decimal Amount) : DomainEvent(FloatId, Guid.NewGuid()); + Decimal Amount, Guid CreditId) : DomainEvent(FloatId, Guid.NewGuid()); [ExcludeFromCodeCoverage] public record FloatAggregateDebitedEvent(Guid FloatId, Guid EstateId, DateTime ActivityDateTime, - Decimal Amount) : DomainEvent(FloatId, Guid.NewGuid()); + Decimal Amount, Guid DebitId) : DomainEvent(FloatId, Guid.NewGuid()); } diff --git a/TransactionProcessor.FloatAggregate.Tests/FloatActivityAggregateTests.cs b/TransactionProcessor.FloatAggregate.Tests/FloatActivityAggregateTests.cs index dc9f6317..ce88ee3e 100644 --- a/TransactionProcessor.FloatAggregate.Tests/FloatActivityAggregateTests.cs +++ b/TransactionProcessor.FloatAggregate.Tests/FloatActivityAggregateTests.cs @@ -16,13 +16,37 @@ public void FloatActivityAggregate_CanBeCreated_IsCreated() [Fact] public void FloatActivityAggregate_RecordCreditPurchase_PurchaseRecorded() { FloatActivityAggregate aggregate = FloatActivityAggregate.Create(TestData.FloatAggregateId); - aggregate.RecordCreditPurchase(TestData.EstateId, TestData.CreditPurchasedDateTime, TestData.FloatCreditAmount); + aggregate.RecordCreditPurchase(TestData.EstateId, TestData.CreditPurchasedDateTime, TestData.FloatCreditAmount, TestData.FloatCreditId); + aggregate.CreditCount.ShouldBe(1); + aggregate.Credits.Contains(TestData.FloatCreditId).ShouldBeTrue(); + } + + [Fact] + public void FloatActivityAggregate_RecordCreditPurchase_DuplicateCredit_PurchaseNotRecorded() + { + FloatActivityAggregate aggregate = FloatActivityAggregate.Create(TestData.FloatAggregateId); + aggregate.RecordCreditPurchase(TestData.EstateId, TestData.CreditPurchasedDateTime, TestData.FloatCreditAmount, TestData.FloatCreditId); + aggregate.CreditCount.ShouldBe(1); + aggregate.RecordCreditPurchase(TestData.EstateId, TestData.CreditPurchasedDateTime, TestData.FloatCreditAmount, TestData.FloatCreditId); + aggregate.CreditCount.ShouldBe(1); } [Fact] public void FloatActivityAggregate_RecordTransactionAgainstFloat_TransactionRecorded() { FloatActivityAggregate aggregate = FloatActivityAggregate.Create(TestData.FloatAggregateId); - aggregate.RecordTransactionAgainstFloat(TestData.EstateId, TestData.TransactionDateTime, TestData.TransactionAmount); + aggregate.RecordTransactionAgainstFloat(TestData.EstateId, TestData.TransactionDateTime, TestData.TransactionAmount, TestData.TransactionId); + aggregate.DebitCount.ShouldBe(1); + aggregate.Debits.Contains(TestData.TransactionId).ShouldBeTrue(); + } + + [Fact] + public void FloatActivityAggregate_RecordTransactionAgainstFloat_DuplicateTransaction_TransactionNotRecorded() + { + FloatActivityAggregate aggregate = FloatActivityAggregate.Create(TestData.FloatAggregateId); + aggregate.RecordTransactionAgainstFloat(TestData.EstateId, TestData.TransactionDateTime, TestData.TransactionAmount, TestData.TransactionId); + aggregate.DebitCount.ShouldBe(1); + aggregate.RecordTransactionAgainstFloat(TestData.EstateId, TestData.TransactionDateTime, TestData.TransactionAmount, TestData.TransactionId); + aggregate.DebitCount.ShouldBe(1); } } \ No newline at end of file diff --git a/TransactionProcessor.FloatAggregate/FloatActivityAggregate.cs b/TransactionProcessor.FloatAggregate/FloatActivityAggregate.cs index f6a36404..c33b9a3b 100644 --- a/TransactionProcessor.FloatAggregate/FloatActivityAggregate.cs +++ b/TransactionProcessor.FloatAggregate/FloatActivityAggregate.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using Shared.DomainDrivenDesign.EventSourcing; using Shared.EventStore.Aggregate; using Shared.General; @@ -6,29 +7,42 @@ namespace TransactionProcessor.FloatAggregate; -public static class FloatActivityAggregateExtensions -{ - public static void PlayEvent(this FloatActivityAggregate aggregate, FloatAggregateCreditedEvent domainEvent) - { +public static class FloatActivityAggregateExtensions { + + public static void PlayEvent(this FloatActivityAggregate aggregate, + FloatAggregateCreditedEvent domainEvent) { + aggregate.CreditCount++; + aggregate.Credits.Add(domainEvent.CreditId); } - public static void PlayEvent(this FloatActivityAggregate aggregate, FloatAggregateDebitedEvent domainEvent) - { + public static void PlayEvent(this FloatActivityAggregate aggregate, + FloatAggregateDebitedEvent domainEvent) { + aggregate.DebitCount++; + aggregate.Debits.Add(domainEvent.DebitId); } public static void RecordCreditPurchase(this FloatActivityAggregate aggregate, Guid estateId, DateTime activityDateTime, - Decimal creditAmount) { - FloatAggregateCreditedEvent floatAggregateCreditedEvent = - new (aggregate.AggregateId, estateId, activityDateTime, creditAmount); + Decimal creditAmount, + Guid creditId) { + + if (aggregate.Credits.Any(c => c == creditId)) + return; + + FloatAggregateCreditedEvent floatAggregateCreditedEvent = new(aggregate.AggregateId, estateId, activityDateTime, creditAmount, creditId); aggregate.ApplyAndAppend(floatAggregateCreditedEvent); } - public static void RecordTransactionAgainstFloat(this FloatActivityAggregate aggregate, Guid estateId, DateTime activityDateTime, Decimal transactionAmount) - { - FloatAggregateDebitedEvent floatAggregateCreditedEvent = - new (aggregate.AggregateId, estateId, activityDateTime, transactionAmount); + public static void RecordTransactionAgainstFloat(this FloatActivityAggregate aggregate, + Guid estateId, + DateTime activityDateTime, + Decimal transactionAmount, + Guid transactionId) { + if (aggregate.Debits.Any(c => c == transactionId)) + return; + + FloatAggregateDebitedEvent floatAggregateCreditedEvent = new(aggregate.AggregateId, estateId, activityDateTime, transactionAmount, transactionId); aggregate.ApplyAndAppend(floatAggregateCreditedEvent); } } @@ -36,6 +50,10 @@ public static void RecordTransactionAgainstFloat(this FloatActivityAggregate agg public record FloatActivityAggregate : Aggregate { public override void PlayEvent(IDomainEvent domainEvent) => FloatActivityAggregateExtensions.PlayEvent(this, (dynamic)domainEvent); + public Int32 CreditCount { get; internal set; } + public Int32 DebitCount { get; internal set; } + public List Credits { get; internal set; } + public List Debits { get; internal set; } [ExcludeFromCodeCoverage] protected override Object GetMetadata() { @@ -49,7 +67,8 @@ protected override Object GetMetadata() [ExcludeFromCodeCoverage] public FloatActivityAggregate() { - + this.Credits = new List(); + this.Debits = new List(); } private FloatActivityAggregate(Guid aggregateId) @@ -57,6 +76,8 @@ private FloatActivityAggregate(Guid aggregateId) Guard.ThrowIfInvalidGuid(aggregateId, "Aggregate Id cannot be an Empty Guid"); this.AggregateId = aggregateId; + this.Credits = new List(); + this.Debits = new List(); } public static FloatActivityAggregate Create(Guid aggregateId) diff --git a/TransactionProcessor.Testing/TestData.cs b/TransactionProcessor.Testing/TestData.cs index 1bf14380..202bf5cf 100644 --- a/TransactionProcessor.Testing/TestData.cs +++ b/TransactionProcessor.Testing/TestData.cs @@ -686,7 +686,7 @@ public static Dictionary AdditionalTransactionMetaDataForPataPaw public static SettlementCommands.AddMerchantFeePendingSettlementCommand AddMerchantFeePendingSettlementCommand => new(TransactionId, CalculatedFeeValue, TransactionFeeCalculateDateTime, CalculationType.Fixed, TransactionFeeId, CalculatedFeeValue, TransactionFeeSettlementDueDate, MerchantId, EstateId); public static SettlementCommands.AddSettledFeeToSettlementCommand AddSettledFeeToSettlementCommand => new(SettlementDate,MerchantId,EstateId,TransactionFeeId, TransactionId); - public static FloatActivityCommands.RecordCreditPurchaseCommand RecordCreditPurchaseCommand => new(EstateId, FloatAggregateId, CreditPurchasedDateTime, FloatCreditAmount); + public static FloatActivityCommands.RecordCreditPurchaseCommand RecordCreditPurchaseCommand => new(EstateId, FloatAggregateId, CreditPurchasedDateTime, FloatCreditAmount, FloatCreditId); public static FloatActivityCommands.RecordTransactionCommand RecordTransactionCommand => new(EstateId, TransactionId); public static TransactionCommands.CalculateFeesForTransactionCommand CalculateFeesForTransactionCommand => new(TransactionId, TransactionDateTime, EstateId, MerchantId); @@ -1459,6 +1459,7 @@ public static SettlementAggregate GetSettlementAggregateWithNotAllFeesSettled(In public static DateTime CreditPurchasedDateTime = DateTime.Now; + public static Guid FloatCreditId = Guid.Parse("97CAFCB1-B9BF-47FA-9438-422ABFCCD790"); public static Decimal FloatCreditAmount = 100m; public static Decimal FloatCreditCostPrice = 90m; From 912ca3823bc9838675ed8b039ddf8e05c416704b Mon Sep 17 00:00:00 2001 From: Stuart Ferguson Date: Wed, 8 Jan 2025 13:36:45 +0000 Subject: [PATCH 2/2] Update test methods to use GetLatestVersion Refactor three test methods in FloatDomainServiceTests.cs to replace the use of GetLatestVersionFromLastEvent with GetLatestVersion for retrieving the latest FloatActivityAggregate. This change ensures the tests align with the updated repository implementation. --- .../Services/FloatDomainServiceTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TransactionProcessor.BusinessLogic.Tests/Services/FloatDomainServiceTests.cs b/TransactionProcessor.BusinessLogic.Tests/Services/FloatDomainServiceTests.cs index 7af5e281..bf90cc7f 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Services/FloatDomainServiceTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Services/FloatDomainServiceTests.cs @@ -208,7 +208,7 @@ public async Task FloatDomainService_RecordCreditPurchase_ExceptionThrown() public async Task FloatDomainService_RecordCreditPurchase_FloatActivity_PurchaseRecorded() { FloatActivityAggregate floatAggregate = FloatActivityAggregate.Create(TestData.FloatAggregateId); - this.FloatActivityAggregateRepository.Setup(f => f.GetLatestVersionFromLastEvent(It.IsAny(), It.IsAny())).ReturnsAsync(floatAggregate); + this.FloatActivityAggregateRepository.Setup(f => f.GetLatestVersion(It.IsAny(), It.IsAny())).ReturnsAsync(floatAggregate); this.FloatActivityAggregateRepository.Setup(f => f.SaveChanges(It.IsAny(), It.IsAny())).ReturnsAsync(Result.Success); var command = new FloatActivityCommands.RecordCreditPurchaseCommand(TestData.EstateId, @@ -221,7 +221,7 @@ public async Task FloatDomainService_RecordCreditPurchase_FloatActivity_Purchase public async Task FloatDomainService_RecordCreditPurchase_FloatActivity_SaveFailed() { FloatActivityAggregate floatAggregate = FloatActivityAggregate.Create(TestData.FloatAggregateId); - this.FloatActivityAggregateRepository.Setup(f => f.GetLatestVersionFromLastEvent(It.IsAny(), It.IsAny())).ReturnsAsync(floatAggregate); + this.FloatActivityAggregateRepository.Setup(f => f.GetLatestVersion(It.IsAny(), It.IsAny())).ReturnsAsync(floatAggregate); this.FloatActivityAggregateRepository.Setup(f => f.SaveChanges(It.IsAny(), It.IsAny())).ReturnsAsync(Result.Failure); var command = new FloatActivityCommands.RecordCreditPurchaseCommand(TestData.EstateId, @@ -234,7 +234,7 @@ public async Task FloatDomainService_RecordCreditPurchase_FloatActivity_SaveFail public async Task FloatDomainService_RecordCreditPurchase_FloatActivity_ExceptionThrown() { FloatActivityAggregate floatAggregate = FloatActivityAggregate.Create(TestData.FloatAggregateId); - this.FloatActivityAggregateRepository.Setup(f => f.GetLatestVersionFromLastEvent(It.IsAny(), It.IsAny())).ReturnsAsync(floatAggregate); + this.FloatActivityAggregateRepository.Setup(f => f.GetLatestVersion(It.IsAny(), It.IsAny())).ReturnsAsync(floatAggregate); this.FloatActivityAggregateRepository.Setup(f => f.SaveChanges(It.IsAny(), It.IsAny())).ThrowsAsync(new Exception()); var command = new FloatActivityCommands.RecordCreditPurchaseCommand(TestData.EstateId,