diff --git a/TransactionProcessor.BusinessLogic/EventHandling/TransactionDomainEventHandler.cs b/TransactionProcessor.BusinessLogic/EventHandling/TransactionDomainEventHandler.cs index 1983ed49..c296dcc4 100644 --- a/TransactionProcessor.BusinessLogic/EventHandling/TransactionDomainEventHandler.cs +++ b/TransactionProcessor.BusinessLogic/EventHandling/TransactionDomainEventHandler.cs @@ -10,25 +10,21 @@ using EstateManagement.Client; using EstateManagement.DataTransferObjects; using EstateManagement.DataTransferObjects.Responses; - using EstateManagement.Estate.DomainEvents; using Manager; using MessagingService.Client; using MessagingService.DataTransferObjects; using Models; - using Newtonsoft.Json; using SecurityService.Client; using SecurityService.DataTransferObjects.Responses; using Services; using SettlementAggregates; using Shared.DomainDrivenDesign.EventSourcing; - using Shared.EntityFramework; using Shared.EventStore.Aggregate; using Shared.EventStore.EventHandling; using Shared.General; using Shared.Logger; using Transaction.DomainEvents; using TransactionAggregate; - using TransactionProcessor.ProjectionEngine.Database; using CalculationType = Models.CalculationType; using FeeType = Models.FeeType; @@ -52,19 +48,14 @@ public class TransactionDomainEventHandler : IDomainEventHandler private readonly IFeeCalculationManager FeeCalculationManager; /// - /// The security service client - /// - private readonly ISecurityServiceClient SecurityServiceClient; - - /// - /// The transaction receipt builder + /// The messaging service client /// - private readonly ITransactionReceiptBuilder TransactionReceiptBuilder; + private readonly IMessagingServiceClient MessagingServiceClient; /// - /// The messaging service client + /// The security service client /// - private readonly IMessagingServiceClient MessagingServiceClient; + private readonly ISecurityServiceClient SecurityServiceClient; private readonly IAggregateRepository SettlementAggregateRepository; @@ -78,6 +69,11 @@ public class TransactionDomainEventHandler : IDomainEventHandler /// private readonly ITransactionAggregateManager TransactionAggregateManager; + /// + /// The transaction receipt builder + /// + private readonly ITransactionReceiptBuilder TransactionReceiptBuilder; + #endregion #region Constructors @@ -88,8 +84,7 @@ public TransactionDomainEventHandler(ITransactionAggregateManager transactionAgg ISecurityServiceClient securityServiceClient, ITransactionReceiptBuilder transactionReceiptBuilder, IMessagingServiceClient messagingServiceClient, - IAggregateRepository settlementAggregateRepository) - { + IAggregateRepository settlementAggregateRepository) { this.TransactionAggregateManager = transactionAggregateManager; this.FeeCalculationManager = feeCalculationManager; this.EstateClient = estateClient; @@ -104,34 +99,43 @@ public TransactionDomainEventHandler(ITransactionAggregateManager transactionAgg #region Methods public async Task Handle(IDomainEvent domainEvent, - CancellationToken cancellationToken) - { + CancellationToken cancellationToken) { await this.HandleSpecificDomainEvent((dynamic)domainEvent, cancellationToken); } + private DateTime CalculateSettlementDate(SettlementSchedule merchantSettlementSchedule, + DateTime completeDateTime) { + if (merchantSettlementSchedule == SettlementSchedule.Weekly) { + return completeDateTime.Date.AddDays(7).Date; + } + + if (merchantSettlementSchedule == SettlementSchedule.Monthly) { + return completeDateTime.Date.AddMonths(1).Date; + } + + return completeDateTime.Date; + } + /// /// Gets the token. /// /// The cancellation token. /// [ExcludeFromCodeCoverage] - private async Task GetToken(CancellationToken cancellationToken) - { + private async Task GetToken(CancellationToken cancellationToken) { // Get a token to talk to the estate service String clientId = ConfigurationReader.GetValue("AppSettings", "ClientId"); String clientSecret = ConfigurationReader.GetValue("AppSettings", "ClientSecret"); Logger.LogInformation($"Client Id is {clientId}"); Logger.LogInformation($"Client Secret is {clientSecret}"); - if (this.TokenResponse == null) - { + if (this.TokenResponse == null) { TokenResponse token = await this.SecurityServiceClient.GetToken(clientId, clientSecret, cancellationToken); Logger.LogInformation($"Token is {token.AccessToken}"); return token; } - if (this.TokenResponse.Expires.UtcDateTime.Subtract(DateTime.UtcNow) < TimeSpan.FromMinutes(2)) - { + if (this.TokenResponse.Expires.UtcDateTime.Subtract(DateTime.UtcNow) < TimeSpan.FromMinutes(2)) { Logger.LogInformation($"Token is about to expire at {this.TokenResponse.Expires.DateTime:O}"); TokenResponse token = await this.SecurityServiceClient.GetToken(clientId, clientSecret, cancellationToken); Logger.LogInformation($"Token is {token.AccessToken}"); @@ -140,59 +144,55 @@ private async Task GetToken(CancellationToken cancellationToken) return this.TokenResponse; } - + private async Task HandleSpecificDomainEvent(TransactionHasBeenCompletedEvent domainEvent, - CancellationToken cancellationToken) - { + CancellationToken cancellationToken) { TransactionAggregate transactionAggregate = await this.TransactionAggregateManager.GetAggregate(domainEvent.EstateId, domainEvent.TransactionId, cancellationToken); - if (transactionAggregate.IsAuthorised == false) - { + if (transactionAggregate.IsAuthorised == false) { // Ignore not successful transactions return; } if (transactionAggregate.IsCompleted == false || transactionAggregate.TransactionType == TransactionType.Logon || - (transactionAggregate.ContractId == Guid.Empty || transactionAggregate.ProductId == Guid.Empty)) - { + (transactionAggregate.ContractId == Guid.Empty || transactionAggregate.ProductId == Guid.Empty)) { // These transactions cannot have fee values calculated so skip return; } this.TokenResponse = await this.GetToken(cancellationToken); - + // Ok we should have filtered out the not applicable transactions // Get the fees to be calculated List feesForProduct = await this.EstateClient.GetTransactionFeesForProduct(this.TokenResponse.AccessToken, - transactionAggregate.EstateId, - transactionAggregate.MerchantId, - transactionAggregate.ContractId, - transactionAggregate.ProductId, - cancellationToken); + transactionAggregate.EstateId, + transactionAggregate.MerchantId, + transactionAggregate.ContractId, + transactionAggregate.ProductId, + cancellationToken); List feesForCalculation = new List(); - foreach (ContractProductTransactionFee contractProductTransactionFee in feesForProduct) - { - TransactionFeeToCalculate transactionFeeToCalculate = new TransactionFeeToCalculate - { - FeeId = contractProductTransactionFee.TransactionFeeId, - Value = contractProductTransactionFee.Value, - FeeType = (FeeType)contractProductTransactionFee.FeeType, - CalculationType = (CalculationType)contractProductTransactionFee.CalculationType - }; + foreach (ContractProductTransactionFee contractProductTransactionFee in feesForProduct) { + TransactionFeeToCalculate transactionFeeToCalculate = new TransactionFeeToCalculate { + FeeId = contractProductTransactionFee.TransactionFeeId, + Value = contractProductTransactionFee.Value, + FeeType = (FeeType)contractProductTransactionFee.FeeType, + CalculationType = + (CalculationType)contractProductTransactionFee.CalculationType + }; feesForCalculation.Add(transactionFeeToCalculate); } // Do the fee calculation - List resultFees = this.FeeCalculationManager.CalculateFees(feesForCalculation, transactionAggregate.TransactionAmount.Value, domainEvent.CompletedDateTime); + List resultFees = + this.FeeCalculationManager.CalculateFees(feesForCalculation, transactionAggregate.TransactionAmount.Value, domainEvent.CompletedDateTime); // Process the non merchant fees IEnumerable nonMerchantFees = resultFees.Where(f => f.FeeType == FeeType.ServiceProvider); - foreach (CalculatedFee calculatedFee in nonMerchantFees) - { + foreach (CalculatedFee calculatedFee in nonMerchantFees) { // Add Fee to the Transaction await this.TransactionAggregateManager.AddFee(transactionAggregate.EstateId, transactionAggregate.AggregateId, calculatedFee, cancellationToken); } @@ -202,20 +202,16 @@ private async Task HandleSpecificDomainEvent(TransactionHasBeenCompletedEvent do // get the merchant now to see the settlement schedule this.TokenResponse = await this.GetToken(cancellationToken); - MerchantResponse merchant = await this.EstateClient.GetMerchant(this.TokenResponse.AccessToken, domainEvent.EstateId, domainEvent.MerchantId, cancellationToken); + MerchantResponse merchant = + await this.EstateClient.GetMerchant(this.TokenResponse.AccessToken, domainEvent.EstateId, domainEvent.MerchantId, cancellationToken); - if (merchant.SettlementSchedule == SettlementSchedule.NotSet) - { + if (merchant.SettlementSchedule == SettlementSchedule.NotSet) { throw new NotSupportedException($"Merchant {merchant.MerchantId} does not have a settlement schedule configured"); } foreach (CalculatedFee calculatedFee in merchantFees) { // Determine when the fee should be applied - DateTime settlementDate = CalculateSettlementDate(merchant.SettlementSchedule, domainEvent.CompletedDateTime); - - Logger.LogDebug($"Completed Date {domainEvent.CompletedDateTime}"); - Logger.LogDebug($"SettlementSchedule {merchant.SettlementSchedule}"); - Logger.LogDebug($"Settlement Date {settlementDate}"); + DateTime settlementDate = this.CalculateSettlementDate(merchant.SettlementSchedule, domainEvent.CompletedDateTime); Guid aggregateId = Helpers.CalculateSettlementAggregateId(settlementDate, domainEvent.EstateId); @@ -226,68 +222,63 @@ private async Task HandleSpecificDomainEvent(TransactionHasBeenCompletedEvent do aggregate.Create(transactionAggregate.EstateId, settlementDate); } + //Guid eventId = IdGenerationService.GenerateEventId(new { + // transactionAggregate.MerchantId, + // transactionAggregate.AggregateId, + // calculatedFee.FeeId + // }); + aggregate.AddFee(transactionAggregate.MerchantId, transactionAggregate.AggregateId, calculatedFee); - if (merchant.SettlementSchedule == SettlementSchedule.Immediate) - { + if (merchant.SettlementSchedule == SettlementSchedule.Immediate) { // Add fees to transaction now if settlement is immediate - await this.TransactionAggregateManager.AddSettledFee(transactionAggregate.EstateId, transactionAggregate.AggregateId, calculatedFee, DateTime.Now.Date, DateTime.Now, cancellationToken); - aggregate.MarkFeeAsSettled(transactionAggregate.MerchantId, transactionAggregate.AggregateId, calculatedFee.FeeId); + await this.TransactionAggregateManager.AddSettledFee(transactionAggregate.EstateId, + transactionAggregate.AggregateId, + calculatedFee, + DateTime.Now.Date, + DateTime.Now, + cancellationToken); + aggregate.ImmediatelyMarkFeeAsSettled(transactionAggregate.MerchantId, transactionAggregate.AggregateId, calculatedFee.FeeId); } await this.SettlementAggregateRepository.SaveChanges(aggregate, cancellationToken); } } - - - private DateTime CalculateSettlementDate(SettlementSchedule merchantSettlementSchedule, DateTime completeDateTime) - { - if (merchantSettlementSchedule == SettlementSchedule.Weekly) - { - return completeDateTime.Date.AddDays(7).Date; - } - - if (merchantSettlementSchedule == SettlementSchedule.Monthly) - { - return completeDateTime.Date.AddMonths(1).Date; - } - - return completeDateTime.Date; - } - private async Task HandleSpecificDomainEvent(CustomerEmailReceiptRequestedEvent domainEvent, - CancellationToken cancellationToken) - { + CancellationToken cancellationToken) { this.TokenResponse = await this.GetToken(cancellationToken); - TransactionAggregate transactionAggregate = await this.TransactionAggregateManager.GetAggregate(domainEvent.EstateId, domainEvent.TransactionId, cancellationToken); + TransactionAggregate transactionAggregate = + await this.TransactionAggregateManager.GetAggregate(domainEvent.EstateId, domainEvent.TransactionId, cancellationToken); - MerchantResponse merchant = await this.EstateClient.GetMerchant(this.TokenResponse.AccessToken, domainEvent.EstateId, domainEvent.MerchantId, cancellationToken); + MerchantResponse merchant = + await this.EstateClient.GetMerchant(this.TokenResponse.AccessToken, domainEvent.EstateId, domainEvent.MerchantId, cancellationToken); // Determine the body of the email String receiptMessage = await this.TransactionReceiptBuilder.GetEmailReceiptMessage(transactionAggregate.GetTransaction(), merchant, cancellationToken); // Send the message - await this.SendEmailMessage(this.TokenResponse.AccessToken, domainEvent.EventId, domainEvent.EstateId, "Transaction Successful", receiptMessage, domainEvent.CustomerEmailAddress, cancellationToken); - + await this.SendEmailMessage(this.TokenResponse.AccessToken, + domainEvent.EventId, + domainEvent.EstateId, + "Transaction Successful", + receiptMessage, + domainEvent.CustomerEmailAddress, + cancellationToken); } private async Task HandleSpecificDomainEvent(CustomerEmailReceiptResendRequestedEvent domainEvent, - CancellationToken cancellationToken) - { + CancellationToken cancellationToken) { this.TokenResponse = await this.GetToken(cancellationToken); // Send the message await this.ResendEmailMessage(this.TokenResponse.AccessToken, domainEvent.EventId, domainEvent.EstateId, cancellationToken); - } private async Task HandleSpecificDomainEvent(MerchantFeeAddedToTransactionEvent domainEvent, - CancellationToken cancellationToken) - { - if (domainEvent.SettlementDueDate == DateTime.MinValue) - { + CancellationToken cancellationToken) { + if (domainEvent.SettlementDueDate == DateTime.MinValue) { // Old event format before settlement return; } @@ -301,62 +292,54 @@ private async Task HandleSpecificDomainEvent(MerchantFeeAddedToTransactionEvent await this.SettlementAggregateRepository.SaveChanges(pendingSettlementAggregate, cancellationToken); } - private async Task SendEmailMessage(String accessToken, - Guid messageId, - Guid estateId, - String subject, - String body, - String emailAddress, - CancellationToken cancellationToken) - { - SendEmailRequest sendEmailRequest = new SendEmailRequest - { - MessageId = messageId, - Body = body, - ConnectionIdentifier = estateId, - FromAddress = "golfhandicapping@btinternet.com", // TODO: lookup from config - IsHtml = true, - Subject = subject, - ToAddresses = new List - { - emailAddress - } - }; - - // TODO: may decide to record the message Id againsts the Transaction Aggregate in future, but for now - // we wont do this... - try - { - await this.MessagingServiceClient.SendEmail(accessToken, sendEmailRequest, cancellationToken); + private async Task ResendEmailMessage(String accessToken, + Guid messageId, + Guid estateId, + CancellationToken cancellationToken) { + ResendEmailRequest resendEmailRequest = new ResendEmailRequest { + ConnectionIdentifier = estateId, + MessageId = messageId + }; + try { + await this.MessagingServiceClient.ResendEmail(accessToken, resendEmailRequest, cancellationToken); } - catch(Exception ex) when (ex.InnerException != null && ex.InnerException.GetType() == typeof(InvalidOperationException)) - { + catch(Exception ex) when(ex.InnerException != null && ex.InnerException.GetType() == typeof(InvalidOperationException)) { // Only bubble up if not a duplicate message - if (ex.InnerException.Message.Contains("Cannot send a message to provider that has already been sent", StringComparison.InvariantCultureIgnoreCase) == false) - { + if (ex.InnerException.Message.Contains("Cannot send a message to provider that has already been sent", StringComparison.InvariantCultureIgnoreCase) == + false) { throw; } } - } - private async Task ResendEmailMessage(String accessToken, + private async Task SendEmailMessage(String accessToken, Guid messageId, Guid estateId, + String subject, + String body, + String emailAddress, CancellationToken cancellationToken) { - ResendEmailRequest resendEmailRequest = new ResendEmailRequest { - ConnectionIdentifier = estateId, - MessageId = messageId - }; - try - { - await this.MessagingServiceClient.ResendEmail(accessToken, resendEmailRequest, cancellationToken); + SendEmailRequest sendEmailRequest = new SendEmailRequest { + MessageId = messageId, + Body = body, + ConnectionIdentifier = estateId, + FromAddress = "golfhandicapping@btinternet.com", // TODO: lookup from config + IsHtml = true, + Subject = subject, + ToAddresses = new List { + emailAddress + } + }; + + // TODO: may decide to record the message Id againsts the Transaction Aggregate in future, but for now + // we wont do this... + try { + await this.MessagingServiceClient.SendEmail(accessToken, sendEmailRequest, cancellationToken); } - catch (Exception ex) when (ex.InnerException != null && ex.InnerException.GetType() == typeof(InvalidOperationException)) - { + catch(Exception ex) when(ex.InnerException != null && ex.InnerException.GetType() == typeof(InvalidOperationException)) { // Only bubble up if not a duplicate message - if (ex.InnerException.Message.Contains("Cannot send a message to provider that has already been sent", StringComparison.InvariantCultureIgnoreCase) == false) - { + if (ex.InnerException.Message.Contains("Cannot send a message to provider that has already been sent", StringComparison.InvariantCultureIgnoreCase) == + false) { throw; } } diff --git a/TransactionProcessor.BusinessLogic/Services/IdGenerationService.cs b/TransactionProcessor.BusinessLogic/Services/IdGenerationService.cs new file mode 100644 index 00000000..c78a2933 --- /dev/null +++ b/TransactionProcessor.BusinessLogic/Services/IdGenerationService.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TransactionProcessor.BusinessLogic.Services +{ + using System.Security.Cryptography; + using Newtonsoft.Json; + using Shared.Serialisation; + + public class IdGenerationService + { + internal delegate Guid GenerateUniqueIdFromObject(Object payload); + + internal delegate Guid GenerateUniqueIdFromString(String payload); + + + private static readonly JsonSerialiser JsonSerialiser = new(() => new JsonSerializerSettings { + Formatting = Formatting.None + }); + + private static readonly GenerateUniqueIdFromObject GenerateUniqueId = + data => IdGenerationService.GenerateGuidFromString(IdGenerationService.JsonSerialiser.Serialise(data)); + + private static readonly GenerateUniqueIdFromString GenerateGuidFromString = uniqueKey => { + using SHA256 sha256Hash = SHA256.Create(); + //Generate hash from the key + Byte[] bytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(uniqueKey)); + + Byte[] j = bytes.Skip(Math.Max(0, bytes.Count() - 16)).ToArray(); //Take last 16 + + //Create our Guid. + return new Guid(j); + }; + + public static Guid GenerateEventId(Object o) => IdGenerationService.GenerateUniqueId(o); + } +} diff --git a/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs b/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs index 1810d456..dbd8715e 100644 --- a/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs +++ b/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs @@ -1058,7 +1058,7 @@ public async Task WhenIGetTheCompletedSettlementsTheFollowingInformationShouldBe Guid aggregateId = Helpers.CalculateSettlementAggregateId(settlementDate, estateDetails.EstateId); await Retry.For(async () => { - SettlementResponse settlements = + TransactionProcessor.DataTransferObjects.SettlementResponse settlements = await this.TestingContext.DockerHelper.TransactionProcessorClient.GetSettlementByDate(this.TestingContext.AccessToken, settlementDate, estateDetails.EstateId, @@ -1083,7 +1083,7 @@ public async Task WhenIGetThePendingSettlementsTheFollowingInformationShouldBeRe Guid aggregateid = Helpers.CalculateSettlementAggregateId(settlementDate, estateDetails.EstateId); await Retry.For(async () => { - SettlementResponse settlements = + TransactionProcessor.DataTransferObjects.SettlementResponse settlements = await this.TestingContext.DockerHelper.TransactionProcessorClient.GetSettlementByDate(this.TestingContext.AccessToken, settlementDate, estateDetails.EstateId, @@ -1107,7 +1107,7 @@ await this.TestingContext.DockerHelper.TransactionProcessorClient.ProcessSettlem await Retry.For(async () => { - SettlementResponse settlement = + TransactionProcessor.DataTransferObjects.SettlementResponse settlement = await this.TestingContext.DockerHelper.TransactionProcessorClient.GetSettlementByDate(this.TestingContext.AccessToken, settlementDate, estateDetails.EstateId, diff --git a/TransactionProcessor.SettlementAggregates/SettlementAggregate.cs b/TransactionProcessor.SettlementAggregates/SettlementAggregate.cs index 0d90f523..f5765551 100644 --- a/TransactionProcessor.SettlementAggregates/SettlementAggregate.cs +++ b/TransactionProcessor.SettlementAggregates/SettlementAggregate.cs @@ -220,7 +220,8 @@ protected override Object GetMetadata() private Boolean HasFeeAlreadyBeenAdded(Guid transactionId, CalculatedFee calculatedFee) { - return this.CalculatedFeesPendingSettlement.Any(c => c.calculatedFee.FeeId == calculatedFee.FeeId && c.transactionId == transactionId); + return this.CalculatedFeesPendingSettlement.Any(c => c.calculatedFee.FeeId == calculatedFee.FeeId && c.transactionId == transactionId) || + this.SettledCalculatedFees.Any(c => c.calculatedFee.FeeId == calculatedFee.FeeId && c.transactionId == transactionId); } private void CheckHasBeenCreated() diff --git a/TransactionProcessor/Controllers/DomainEventController.cs b/TransactionProcessor/Controllers/DomainEventController.cs index 42c6fc28..6a93e00a 100644 --- a/TransactionProcessor/Controllers/DomainEventController.cs +++ b/TransactionProcessor/Controllers/DomainEventController.cs @@ -7,6 +7,7 @@ namespace TransactionProcessor.Controllers { using System.Diagnostics.CodeAnalysis; using System.Threading; + using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -57,7 +58,9 @@ public DomainEventController(IDomainEventHandlerResolver domainEventHandlerResol public async Task PostEventAsync([FromBody] Object request, CancellationToken cancellationToken) { - var domainEvent = await this.GetDomainEvent(request); + IDomainEvent domainEvent = await this.GetDomainEvent(request); + + List eventHandlers = this.GetDomainEventHandlers(domainEvent); cancellationToken.Register(() => this.Callback(cancellationToken, domainEvent.EventId)); @@ -65,8 +68,6 @@ public async Task PostEventAsync([FromBody] Object request, { Logger.LogInformation($"Processing event - ID [{domainEvent.EventId}], Type[{domainEvent.GetType().Name}]"); - List eventHandlers = this.DomainEventHandlerResolver.GetDomainEventHandlers(domainEvent); - if (eventHandlers == null || eventHandlers.Any() == false) { // Log a warning out @@ -105,6 +106,24 @@ private void Callback(CancellationToken cancellationToken, } } + private List GetDomainEventHandlers(IDomainEvent domainEvent) { + + if (this.Request.Headers.ContainsKey("EventHandler")) { + var eventHandler = this.Request.Headers["EventHandler"]; + var eventHandlerType = this.Request.Headers["EventHandlerType"]; + var resolver = Startup.Container.GetInstance(eventHandlerType); + // We are being told by the caller to use a specific handler + var allhandlers = resolver.GetDomainEventHandlers(domainEvent); + var handlers = allhandlers.Where(h => h.GetType().Name.Contains(eventHandler)); + + return handlers.ToList(); + + } + + List eventHandlers = this.DomainEventHandlerResolver.GetDomainEventHandlers(domainEvent); + return eventHandlers; + } + private async Task GetDomainEvent(Object domainEvent) { String eventType = this.Request.Headers["eventType"].ToString();