diff --git a/TransactionProcessor.BusinessLogic.Tests/Services/FloatDomainServiceTests.cs b/TransactionProcessor.BusinessLogic.Tests/Services/FloatDomainServiceTests.cs index 3b6cc5a9..bf90cc7f 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Services/FloatDomainServiceTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Services/FloatDomainServiceTests.cs @@ -208,11 +208,11 @@ 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, - 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(); } @@ -221,11 +221,11 @@ 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, - 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(); } @@ -234,11 +234,11 @@ 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, - 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;