From 0ca5cb9a7992362bcc4ea283ecbb1715d1e87835 Mon Sep 17 00:00:00 2001 From: Stuart Ferguson Date: Wed, 22 Apr 2020 11:51:44 +0100 Subject: [PATCH] Record Customer Email with Transaction Details --- .../Requests/RequestTests.cs | 2 + .../TransactionAggregateManagerTests.cs | 15 + .../Services/TransactionDomainServiceTests.cs | 13 + .../TransactionRequestHandler.cs | 3 +- .../Requests/ProcessSaleTransactionRequest.cs | 14 + .../Services/ITransactionAggregateManager.cs | 13 + .../Services/ITransactionDomainService.cs | 2 + .../Services/TransactionAggregateManager.cs | 18 ++ .../Services/TransactionDomainService.cs | 296 ++++++++++-------- .../SaleTransactionRequest.cs | 8 + .../SaleTransactionFeature.feature | 10 +- .../SaleTransactionFeature.feature.cs | 49 ++- .../Shared/SharedSteps.cs | 9 +- ...ansactionProcessor.IntegrationTests.csproj | 4 + TransactionProcessor.Testing/TestData.cs | 3 + .../AdditionalRequestDataRecordedEvent.cs | 22 +- .../AdditionalResponseDataRecordedEvent.cs | 9 +- .../CustomerEmailReceiptRequestedEvent.cs | 57 ++++ .../TransactionAuthorisedByOperatorEvent.cs | 2 +- .../TransactionDeclinedByOperatorEvent.cs | 2 +- .../TransactionHasBeenCompletedEvent.cs | 2 +- ...ransactionHasBeenLocallyAuthorisedEvent.cs | 2 +- .../TransactionHasBeenLocallyDeclinedEvent.cs | 2 +- .../TransactionHasStartedEvent.cs | 3 +- .../DomainEventTests.cs | 18 ++ .../TransactionAggregateTests.cs | 42 +++ .../TransactionAggregate.cs | 95 ++++-- .../Controllers/TransactionController.cs | 1 + 28 files changed, 524 insertions(+), 192 deletions(-) create mode 100644 TransactionProcessor.Transaction.DomainEvents/CustomerEmailReceiptRequestedEvent.cs diff --git a/TransactionProcessor.BusinessLogic.Tests/Requests/RequestTests.cs b/TransactionProcessor.BusinessLogic.Tests/Requests/RequestTests.cs index d91b6e49..d003dc0c 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Requests/RequestTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Requests/RequestTests.cs @@ -33,6 +33,7 @@ public void ProcessSaleTransactionRequest_CanBeCreated_IsCreated() ProcessSaleTransactionRequest processSaleTransactionRequest = ProcessSaleTransactionRequest.Create(TestData.TransactionId, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier, TestData.TransactionTypeLogon.ToString(), TestData.TransactionDateTime, TestData.TransactionNumber, TestData.OperatorIdentifier1, + TestData.CustomerEmailAddress, TestData.AdditionalTransactionMetaData); processSaleTransactionRequest.ShouldNotBeNull(); @@ -44,6 +45,7 @@ public void ProcessSaleTransactionRequest_CanBeCreated_IsCreated() processSaleTransactionRequest.TransactionNumber.ShouldBe(TestData.TransactionNumber); processSaleTransactionRequest.TransactionId.ShouldBe(TestData.TransactionId); processSaleTransactionRequest.OperatorIdentifier.ShouldBe(TestData.OperatorIdentifier1); + processSaleTransactionRequest.CustomerEmailAddress.ShouldBe(TestData.CustomerEmailAddress); processSaleTransactionRequest.AdditionalTransactionMetadata.ShouldNotBeNull(); processSaleTransactionRequest.AdditionalTransactionMetadata.Count.ShouldBe(TestData.AdditionalTransactionMetaData.Count); } diff --git a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionAggregateManagerTests.cs b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionAggregateManagerTests.cs index 0f6b308c..b4ee1762 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionAggregateManagerTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionAggregateManagerTests.cs @@ -232,5 +232,20 @@ await transactionAggregateManager.CompleteTransaction(TestData.EstateId, TestData.TransactionId, CancellationToken.None); } + + [Fact] + public async Task TransactionAggregateManager_RequestEmailReceipt_EmailRecieptRequested() + { + Mock> aggregateRepository = new Mock>(); + aggregateRepository.Setup(a => a.GetLatestVersion(It.IsAny(), It.IsAny())).ReturnsAsync(TestData.GetCompletedTransactionAggregate); + Mock aggregateRepositoryManager = new Mock(); + aggregateRepositoryManager.Setup(a => a.GetAggregateRepository(It.IsAny())).Returns(aggregateRepository.Object); + TransactionAggregateManager transactionAggregateManager = new TransactionAggregateManager(aggregateRepositoryManager.Object); + + await transactionAggregateManager.RequestEmailReceipt(TestData.EstateId, + TestData.TransactionId, + TestData.CustomerEmailAddress, + CancellationToken.None); + } } } diff --git a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs index 15488e12..f2d7dda8 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs @@ -291,6 +291,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_SuccesfulOpera TestData.TransactionNumber, TestData.DeviceIdentifier, TestData.OperatorIdentifier1, + TestData.CustomerEmailAddress, TestData.AdditionalTransactionMetaData, CancellationToken.None); @@ -341,6 +342,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_FailedOperator TestData.TransactionNumber, TestData.DeviceIdentifier, TestData.OperatorIdentifier1, + TestData.CustomerEmailAddress, TestData.AdditionalTransactionMetaData, CancellationToken.None); @@ -379,6 +381,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_MerchantWithNu TestData.TransactionNumber, TestData.DeviceIdentifier, TestData.OperatorIdentifier1, + TestData.CustomerEmailAddress, TestData.AdditionalTransactionMetaData, CancellationToken.None); @@ -418,6 +421,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_MerchantWithNo TestData.TransactionNumber, TestData.DeviceIdentifier, TestData.OperatorIdentifier1, + TestData.CustomerEmailAddress, TestData.AdditionalTransactionMetaData, CancellationToken.None); @@ -456,6 +460,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_IncorrectDevic TestData.TransactionNumber, TestData.DeviceIdentifier1, TestData.OperatorIdentifier1, + TestData.CustomerEmailAddress, TestData.AdditionalTransactionMetaData, CancellationToken.None); @@ -494,6 +499,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_InvalidEstate_ TestData.TransactionNumber, TestData.DeviceIdentifier1, TestData.OperatorIdentifier1, + TestData.CustomerEmailAddress, TestData.AdditionalTransactionMetaData, CancellationToken.None); @@ -532,6 +538,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_InvalidMerchan TestData.TransactionNumber, TestData.DeviceIdentifier1, TestData.OperatorIdentifier1, + TestData.CustomerEmailAddress, TestData.AdditionalTransactionMetaData, CancellationToken.None); @@ -570,6 +577,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_EstateWithEmpt TestData.TransactionNumber, TestData.DeviceIdentifier, TestData.OperatorIdentifier1, + TestData.CustomerEmailAddress, TestData.AdditionalTransactionMetaData, CancellationToken.None); @@ -608,6 +616,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_EstateWithNull TestData.TransactionNumber, TestData.DeviceIdentifier, TestData.OperatorIdentifier1, + TestData.CustomerEmailAddress, TestData.AdditionalTransactionMetaData, CancellationToken.None); @@ -646,6 +655,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_OperatorNotSup TestData.TransactionNumber, TestData.DeviceIdentifier, TestData.OperatorIdentifier2, + TestData.CustomerEmailAddress, TestData.AdditionalTransactionMetaData, CancellationToken.None); @@ -684,6 +694,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_MerchantWithEm TestData.TransactionNumber, TestData.DeviceIdentifier, TestData.OperatorIdentifier1, + TestData.CustomerEmailAddress, TestData.AdditionalTransactionMetaData, CancellationToken.None); @@ -722,6 +733,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_MerchantWithNu TestData.TransactionNumber, TestData.DeviceIdentifier, TestData.OperatorIdentifier1, + TestData.CustomerEmailAddress, TestData.AdditionalTransactionMetaData, CancellationToken.None); @@ -760,6 +772,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_OperatorNotSup TestData.TransactionNumber, TestData.DeviceIdentifier, TestData.OperatorIdentifier1, + TestData.CustomerEmailAddress, TestData.AdditionalTransactionMetaData, CancellationToken.None); diff --git a/TransactionProcessor.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs b/TransactionProcessor.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs index 0466071f..33d66bd1 100644 --- a/TransactionProcessor.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs +++ b/TransactionProcessor.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs @@ -10,7 +10,7 @@ /// /// /// - /// + /// /// public class TransactionRequestHandler : IRequestHandler, IRequestHandler @@ -74,6 +74,7 @@ public async Task Handle(ProcessSaleTransactionR request.TransactionNumber, request.DeviceIdentifier, request.OperatorIdentifier, + request.CustomerEmailAddress, request.AdditionalTransactionMetadata, cancellationToken); diff --git a/TransactionProcessor.BusinessLogic/Requests/ProcessSaleTransactionRequest.cs b/TransactionProcessor.BusinessLogic/Requests/ProcessSaleTransactionRequest.cs index c661da07..0ed59628 100644 --- a/TransactionProcessor.BusinessLogic/Requests/ProcessSaleTransactionRequest.cs +++ b/TransactionProcessor.BusinessLogic/Requests/ProcessSaleTransactionRequest.cs @@ -23,6 +23,7 @@ public class ProcessSaleTransactionRequest : IRequestThe transaction date time. /// The transaction number. /// The operator identifier. + /// The customer email address. /// The additional transaction metadata. private ProcessSaleTransactionRequest(Guid transactionId, Guid estateId, @@ -32,6 +33,7 @@ private ProcessSaleTransactionRequest(Guid transactionId, DateTime transactionDateTime, String transactionNumber, String operatorIdentifier, + String customerEmailAddress, Dictionary additionalTransactionMetadata) { this.TransactionId = transactionId; @@ -41,6 +43,7 @@ private ProcessSaleTransactionRequest(Guid transactionId, this.TransactionDateTime = transactionDateTime; this.TransactionNumber = transactionNumber; this.OperatorIdentifier = operatorIdentifier; + this.CustomerEmailAddress = customerEmailAddress; this.AdditionalTransactionMetadata = additionalTransactionMetadata; this.TransactionType = transactionType; } @@ -113,6 +116,14 @@ private ProcessSaleTransactionRequest(Guid transactionId, /// public String OperatorIdentifier { get; } + /// + /// Gets the customer email address. + /// + /// + /// The customer email address. + /// + public String CustomerEmailAddress { get; private set; } + /// /// Gets or sets the additional transaction metadata. /// @@ -136,6 +147,7 @@ private ProcessSaleTransactionRequest(Guid transactionId, /// The transaction date time. /// The transaction number. /// The operator identifier. + /// The customer email address. /// The additional transaction metadata. /// public static ProcessSaleTransactionRequest Create(Guid transactionId, @@ -146,6 +158,7 @@ public static ProcessSaleTransactionRequest Create(Guid transactionId, DateTime transactionDateTime, String transactionNumber, String operatorIdentifier, + String customerEmailAddress, Dictionary additionalTransactionMetadata) { return new ProcessSaleTransactionRequest(transactionId, @@ -156,6 +169,7 @@ public static ProcessSaleTransactionRequest Create(Guid transactionId, transactionDateTime, transactionNumber, operatorIdentifier, + customerEmailAddress, additionalTransactionMetadata); } diff --git a/TransactionProcessor.BusinessLogic/Services/ITransactionAggregateManager.cs b/TransactionProcessor.BusinessLogic/Services/ITransactionAggregateManager.cs index efc7b946..d0396823 100644 --- a/TransactionProcessor.BusinessLogic/Services/ITransactionAggregateManager.cs +++ b/TransactionProcessor.BusinessLogic/Services/ITransactionAggregateManager.cs @@ -60,6 +60,19 @@ Task CompleteTransaction(Guid estateId, Guid transactionId, CancellationToken cancellationToken); + /// + /// Requests the email receipt. + /// + /// The estate identifier. + /// The transaction identifier. + /// The customer email address. + /// The cancellation token. + /// + Task RequestEmailReceipt(Guid estateId, + Guid transactionId, + String customerEmailAddress, + CancellationToken cancellationToken); + /// /// Declines the transaction. /// diff --git a/TransactionProcessor.BusinessLogic/Services/ITransactionDomainService.cs b/TransactionProcessor.BusinessLogic/Services/ITransactionDomainService.cs index 649193ca..0f56c253 100644 --- a/TransactionProcessor.BusinessLogic/Services/ITransactionDomainService.cs +++ b/TransactionProcessor.BusinessLogic/Services/ITransactionDomainService.cs @@ -42,6 +42,7 @@ Task ProcessLogonTransaction(Guid transactionId /// The transaction number. /// The device identifier. /// The operator identifier. + /// The customer email address. /// The additional transaction metadata. /// The cancellation token. /// @@ -52,6 +53,7 @@ Task ProcessSaleTransaction(Guid transactionId, String transactionNumber, String deviceIdentifier, String operatorId, + String customerEmailAddress, Dictionary additionalTransactionMetadata, CancellationToken cancellationToken); diff --git a/TransactionProcessor.BusinessLogic/Services/TransactionAggregateManager.cs b/TransactionProcessor.BusinessLogic/Services/TransactionAggregateManager.cs index 1056961a..59944c80 100644 --- a/TransactionProcessor.BusinessLogic/Services/TransactionAggregateManager.cs +++ b/TransactionProcessor.BusinessLogic/Services/TransactionAggregateManager.cs @@ -246,6 +246,24 @@ public async Task RecordAdditionalResponseData(Guid estateId, } } + /// + /// Requests the email receipt. + /// + /// The estate identifier. + /// The transaction identifier. + /// The cancellation token. + public async Task RequestEmailReceipt(Guid estateId, Guid transactionId, String customerEmailAddress, CancellationToken cancellationToken) + { + IAggregateRepository transactionAggregateRepository = + this.AggregateRepositoryManager.GetAggregateRepository(estateId); + + TransactionAggregate transactionAggregate = await transactionAggregateRepository.GetLatestVersion(transactionId, cancellationToken); + + transactionAggregate.RequestEmailReceipt(customerEmailAddress); + + await transactionAggregateRepository.SaveChanges(transactionAggregate, cancellationToken); + } + /// /// Starts the transaction. /// diff --git a/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs b/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs index 1ae5b478..4765d650 100644 --- a/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs +++ b/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs @@ -12,9 +12,6 @@ using OperatorInterfaces; using SecurityService.Client; using SecurityService.DataTransferObjects.Responses; - using Shared.DomainDrivenDesign.EventStore; - using Shared.EventStore.EventStore; - using Shared.Exceptions; using Shared.General; using Shared.Logger; using TransactionAggregate; @@ -27,22 +24,24 @@ public class TransactionDomainService : ITransactionDomainService { #region Fields - /// - /// The transaction aggregate manager - /// - private readonly ITransactionAggregateManager TransactionAggregateManager; - /// /// The estate client /// private readonly IEstateClient EstateClient; + private readonly Func OperatorProxyResolver; + /// /// The security service client /// private readonly ISecurityServiceClient SecurityServiceClient; - private readonly Func OperatorProxyResolver; + private TokenResponse TokenResponse; + + /// + /// The transaction aggregate manager + /// + private readonly ITransactionAggregateManager TransactionAggregateManager; #endregion @@ -104,7 +103,8 @@ await this.TransactionAggregateManager.StartTransaction(transactionId, deviceIdentifier, cancellationToken); - (String responseMessage, TransactionResponseCode responseCode) validationResult = await this.ValidateLogonTransaction(estateId, merchantId, deviceIdentifier, cancellationToken); + (String responseMessage, TransactionResponseCode responseCode) validationResult = + await this.ValidateLogonTransaction(estateId, merchantId, deviceIdentifier, cancellationToken); if (validationResult.responseCode == TransactionResponseCode.Success) { @@ -131,20 +131,6 @@ await this.TransactionAggregateManager.StartTransaction(transactionId, }; } - /// - /// Generates the transaction reference. - /// - /// - private String GenerateTransactionReference() - { - Int64 i = 1; - foreach (Byte b in Guid.NewGuid().ToByteArray()) - { - i *= ((Int32)b + 1); - } - return $"{i - DateTime.Now.Ticks:x}"; - } - /// /// Processes the sale transaction. /// @@ -155,6 +141,7 @@ private String GenerateTransactionReference() /// The transaction number. /// The device identifier. /// The operator identifier. + /// The customer email address. /// The additional transaction metadata. /// The cancellation token. /// @@ -165,6 +152,7 @@ public async Task ProcessSaleTransaction(Guid tr String transactionNumber, String deviceIdentifier, String operatorIdentifier, + String customerEmailAddress, Dictionary additionalTransactionMetadata, CancellationToken cancellationToken) { @@ -183,17 +171,28 @@ await this.TransactionAggregateManager.StartTransaction(transactionId, deviceIdentifier, cancellationToken); - (String responseMessage, TransactionResponseCode responseCode) validationResult = await this.ValidateSaleTransaction(estateId, merchantId, deviceIdentifier, operatorIdentifier, cancellationToken); + (String responseMessage, TransactionResponseCode responseCode) validationResult = + await this.ValidateSaleTransaction(estateId, merchantId, deviceIdentifier, operatorIdentifier, cancellationToken); if (validationResult.responseCode == TransactionResponseCode.Success) { // Record any additional request metadata - await this.TransactionAggregateManager.RecordAdditionalRequestData(estateId, transactionId, operatorIdentifier, additionalTransactionMetadata, cancellationToken); - + await this.TransactionAggregateManager.RecordAdditionalRequestData(estateId, + transactionId, + operatorIdentifier, + additionalTransactionMetadata, + cancellationToken); + // Do the online processing with the operator here MerchantResponse merchant = await this.GetMerchant(estateId, merchantId, cancellationToken); - IOperatorProxy operatorProxy = OperatorProxyResolver(operatorIdentifier); - OperatorResponse operatorResponse = await operatorProxy.ProcessSaleMessage(transactionId, merchant, transactionDateTime, transactionReference, additionalTransactionMetadata, cancellationToken); + IOperatorProxy operatorProxy = this.OperatorProxyResolver(operatorIdentifier); + OperatorResponse operatorResponse = + await operatorProxy.ProcessSaleMessage(transactionId, + merchant, + transactionDateTime, + transactionReference, + additionalTransactionMetadata, + cancellationToken); if (operatorResponse.IsSuccessful) { @@ -223,8 +222,11 @@ await this.TransactionAggregateManager.DeclineTransaction(estateId, } // Record any additional operator response metadata - await this.TransactionAggregateManager.RecordAdditionalResponseData(estateId, transactionId, operatorIdentifier, operatorResponse.AdditionalTransactionResponseMetadata, cancellationToken); - + await this.TransactionAggregateManager.RecordAdditionalResponseData(estateId, + transactionId, + operatorIdentifier, + operatorResponse.AdditionalTransactionResponseMetadata, + cancellationToken); } else { @@ -234,6 +236,12 @@ await this.TransactionAggregateManager.DeclineTransaction(estateId, await this.TransactionAggregateManager.CompleteTransaction(estateId, transactionId, cancellationToken); + // Determine if the email receipt is required + if (String.IsNullOrEmpty(customerEmailAddress) == false) + { + await this.TransactionAggregateManager.RequestEmailReceipt(estateId, transactionId, customerEmailAddress, cancellationToken); + } + TransactionAggregate transactionAggregate = await this.TransactionAggregateManager.GetAggregate(estateId, transactionId, cancellationToken); return new ProcessSaleTransactionResponse @@ -246,44 +254,75 @@ await this.TransactionAggregateManager.DeclineTransaction(estateId, }; } + private async Task AddDeviceToMerchant(Guid estateId, + Guid merchantId, + String deviceIdentifier, + CancellationToken cancellationToken) + { + await this.GetToken(cancellationToken); + + // Add the device to the merchant + await this.EstateClient.AddDeviceToMerchant(this.TokenResponse.AccessToken, + estateId, + merchantId, + new AddMerchantDeviceRequest + { + DeviceIdentifier = deviceIdentifier + }, + cancellationToken); + } + /// - /// Validates the transaction. + /// Generates the transaction reference. /// - /// The estate identifier. - /// The merchant identifier. - /// The cancellation token. /// - /// - /// Estate Id [{estateId}] is not a valid estate - /// or - /// Merchant Id [{merchantId}] is not a valid merchant for estate [{estate.EstateName}] - /// - private async Task<(EstateResponse estate, MerchantResponse merchant)> ValidateTransaction(Guid estateId, - Guid merchantId, CancellationToken cancellationToken) + private String GenerateTransactionReference() { - EstateResponse estate = null; - // Validate the Estate Record is a valid estate - try - { - estate = await this.GetEstate(estateId, cancellationToken); - } - catch (Exception ex) when (ex.InnerException != null && ex.InnerException.GetType() == typeof(KeyNotFoundException)) + Int64 i = 1; + foreach (Byte b in Guid.NewGuid().ToByteArray()) { - throw new TransactionValidationException($"Estate Id [{estateId}] is not a valid estate", TransactionResponseCode.InvalidEstateId); + i *= (b + 1); } - // get the merchant record and validate the device - // TODO: Token - MerchantResponse merchant = await this.GetMerchant(estateId, merchantId, cancellationToken); + return $"{i - DateTime.Now.Ticks:x}"; + } - // TODO: Remove this once GetMerchant returns correct response when merchant not found - if (merchant.MerchantName == null) + private async Task GetEstate(Guid estateId, + CancellationToken cancellationToken) + { + await this.GetToken(cancellationToken); + + EstateResponse estate = await this.EstateClient.GetEstate(this.TokenResponse.AccessToken, estateId, cancellationToken); + + return estate; + } + + private async Task GetMerchant(Guid estateId, + Guid merchantId, + CancellationToken cancellationToken) + { + await this.GetToken(cancellationToken); + + MerchantResponse merchant = await this.EstateClient.GetMerchant(this.TokenResponse.AccessToken, estateId, merchantId, cancellationToken); + + return merchant; + } + + private async Task GetToken(CancellationToken cancellationToken) + { + if (this.TokenResponse == null) { - throw new TransactionValidationException($"Merchant Id [{merchantId}] is not a valid merchant for estate [{estate.EstateName}]", - TransactionResponseCode.InvalidMerchantId); - } + // Get a token to talk to the estate service + String clientId = ConfigurationReader.GetValue("AppSettings", "ClientId"); + String clientSecret = ConfigurationReader.GetValue("AppSettings", "ClientSecret"); - return (estate, merchant); + Logger.LogInformation($"Client Id is {clientId}"); + Logger.LogInformation($"Client Secret is {clientSecret}"); + + TokenResponse token = await this.SecurityServiceClient.GetToken(clientId, clientSecret, cancellationToken); + Logger.LogInformation($"Token is {token.AccessToken}"); + this.TokenResponse = token; + } } /// @@ -296,9 +335,9 @@ await this.TransactionAggregateManager.DeclineTransaction(estateId, /// /// Device Identifier {deviceIdentifier} not valid for Merchant {merchant.MerchantName} private async Task<(String responseMessage, TransactionResponseCode responseCode)> ValidateLogonTransaction(Guid estateId, - Guid merchantId, - String deviceIdentifier, - CancellationToken cancellationToken) + Guid merchantId, + String deviceIdentifier, + CancellationToken cancellationToken) { try { @@ -322,11 +361,11 @@ await this.TransactionAggregateManager.DeclineTransaction(estateId, TransactionResponseCode.InvalidDeviceIdentifier); } } - + // If we get here everything is good return ("SUCCESS", TransactionResponseCode.Success); } - catch (TransactionValidationException tvex) + catch(TransactionValidationException tvex) { return (tvex.Message, tvex.ResponseCode); } @@ -355,133 +394,114 @@ await this.TransactionAggregateManager.DeclineTransaction(estateId, /// Operator {operatorIdentifier} not configured for Merchant [{merchant.MerchantName}] /// private async Task<(String responseMessage, TransactionResponseCode responseCode)> ValidateSaleTransaction(Guid estateId, - Guid merchantId, - String deviceIdentifier, - String operatorIdentifier, - CancellationToken cancellationToken) + Guid merchantId, + String deviceIdentifier, + String operatorIdentifier, + CancellationToken cancellationToken) { try { (EstateResponse estate, MerchantResponse merchant) validateTransactionResponse = await this.ValidateTransaction(estateId, merchantId, cancellationToken); EstateResponse estate = validateTransactionResponse.estate; MerchantResponse merchant = validateTransactionResponse.merchant; - + // Device Validation if (merchant.Devices == null || merchant.Devices.Any() == false) { throw new TransactionValidationException($"Merchant {merchant.MerchantName} has no valid Devices for this transaction.", TransactionResponseCode.NoValidDevices); } - else - { - // Validate the device - KeyValuePair device = merchant.Devices.SingleOrDefault(d => d.Value == deviceIdentifier); - if (device.Key == Guid.Empty) - { - // Device not found,throw error - throw new TransactionValidationException($"Device Identifier {deviceIdentifier} not valid for Merchant {merchant.MerchantName}", - TransactionResponseCode.InvalidDeviceIdentifier); - } + // Validate the device + KeyValuePair device = merchant.Devices.SingleOrDefault(d => d.Value == deviceIdentifier); + + if (device.Key == Guid.Empty) + { + // Device not found,throw error + throw new TransactionValidationException($"Device Identifier {deviceIdentifier} not valid for Merchant {merchant.MerchantName}", + TransactionResponseCode.InvalidDeviceIdentifier); } // Operator Validation (Estate) if (estate.Operators == null || estate.Operators.Any() == false) { - throw new TransactionValidationException($"Estate {estate.EstateName} has no operators defined", - TransactionResponseCode.NoEstateOperators); + throw new TransactionValidationException($"Estate {estate.EstateName} has no operators defined", TransactionResponseCode.NoEstateOperators); } - else + { // Operators have been configured for the estate EstateOperatorResponse operatorRecord = estate.Operators.SingleOrDefault(o => o.Name == operatorIdentifier); if (operatorRecord == null) { - throw new TransactionValidationException($"Operator {operatorIdentifier} not configured for Estate [{estate.EstateName}]", TransactionResponseCode.OperatorNotValidForEstate); + throw new TransactionValidationException($"Operator {operatorIdentifier} not configured for Estate [{estate.EstateName}]", + TransactionResponseCode.OperatorNotValidForEstate); } } // Operator Validation (Merchant) if (merchant.Operators == null || merchant.Operators.Any() == false) { - throw new TransactionValidationException($"Merchant {merchant.MerchantName} has no operators defined", - TransactionResponseCode.NoEstateOperators); + throw new TransactionValidationException($"Merchant {merchant.MerchantName} has no operators defined", TransactionResponseCode.NoEstateOperators); } - else + { // Operators have been configured for the estate MerchantOperatorResponse operatorRecord = merchant.Operators.SingleOrDefault(o => o.Name == operatorIdentifier); if (operatorRecord == null) { - throw new TransactionValidationException($"Operator {operatorIdentifier} not configured for Merchant [{merchant.MerchantName}]", TransactionResponseCode.OperatorNotValidForMerchant); + throw new TransactionValidationException($"Operator {operatorIdentifier} not configured for Merchant [{merchant.MerchantName}]", + TransactionResponseCode.OperatorNotValidForMerchant); } } - // If we get here everything is good return ("SUCCESS", TransactionResponseCode.Success); } - catch (TransactionValidationException tvex) + catch(TransactionValidationException tvex) { return (tvex.Message, tvex.ResponseCode); } } - private TokenResponse TokenResponse; - - private async Task GetEstate(Guid estateId, CancellationToken cancellationToken) - { - await this.GetToken(cancellationToken); - - EstateResponse estate = await this.EstateClient.GetEstate(this.TokenResponse.AccessToken, estateId, cancellationToken); - - return estate; - } - - private async Task GetMerchant(Guid estateId, - Guid merchantId, - CancellationToken cancellationToken) - { - await this.GetToken(cancellationToken); - - MerchantResponse merchant = await this.EstateClient.GetMerchant(this.TokenResponse.AccessToken, estateId, merchantId, cancellationToken); - - return merchant; - } - - private async Task GetToken(CancellationToken cancellationToken) + /// + /// Validates the transaction. + /// + /// The estate identifier. + /// The merchant identifier. + /// The cancellation token. + /// + /// + /// Estate Id [{estateId}] is not a valid estate + /// or + /// Merchant Id [{merchantId}] is not a valid merchant for estate [{estate.EstateName}] + /// + private async Task<(EstateResponse estate, MerchantResponse merchant)> ValidateTransaction(Guid estateId, + Guid merchantId, + CancellationToken cancellationToken) { - if (this.TokenResponse == null) + EstateResponse estate = null; + // Validate the Estate Record is a valid estate + try { - // Get a token to talk to the estate service - String clientId = ConfigurationReader.GetValue("AppSettings", "ClientId"); - String clientSecret = ConfigurationReader.GetValue("AppSettings", "ClientSecret"); + estate = await this.GetEstate(estateId, cancellationToken); + } + catch(Exception ex) when(ex.InnerException != null && ex.InnerException.GetType() == typeof(KeyNotFoundException)) + { + throw new TransactionValidationException($"Estate Id [{estateId}] is not a valid estate", TransactionResponseCode.InvalidEstateId); + } - Logger.LogInformation($"Client Id is {clientId}"); - Logger.LogInformation($"Client Secret is {clientSecret}"); + // get the merchant record and validate the device + // TODO: Token + MerchantResponse merchant = await this.GetMerchant(estateId, merchantId, cancellationToken); - TokenResponse token = await this.SecurityServiceClient.GetToken(clientId, clientSecret, cancellationToken); - Logger.LogInformation($"Token is {token.AccessToken}"); - this.TokenResponse = token; + // TODO: Remove this once GetMerchant returns correct response when merchant not found + if (merchant.MerchantName == null) + { + throw new TransactionValidationException($"Merchant Id [{merchantId}] is not a valid merchant for estate [{estate.EstateName}]", + TransactionResponseCode.InvalidMerchantId); } - } - - private async Task AddDeviceToMerchant(Guid estateId, - Guid merchantId, - String deviceIdentifier, - CancellationToken cancellationToken) - { - await this.GetToken(cancellationToken); - // Add the device to the merchant - await this.EstateClient.AddDeviceToMerchant(this.TokenResponse.AccessToken, - estateId, - merchantId, - new AddMerchantDeviceRequest - { - DeviceIdentifier = deviceIdentifier - }, - cancellationToken); + return (estate, merchant); } #endregion diff --git a/TransactionProcessor.DataTransferObjects/SaleTransactionRequest.cs b/TransactionProcessor.DataTransferObjects/SaleTransactionRequest.cs index 6497b4ba..e2062053 100644 --- a/TransactionProcessor.DataTransferObjects/SaleTransactionRequest.cs +++ b/TransactionProcessor.DataTransferObjects/SaleTransactionRequest.cs @@ -49,6 +49,14 @@ public class SaleTransactionRequest : DataTransferObject /// public String OperatorIdentifier { get; set; } + /// + /// Gets or sets the customer email address. + /// + /// + /// The customer email address. + /// + public String CustomerEmailAddress { get; set; } + /// /// Gets or sets the additional transaction metadata. /// diff --git a/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature b/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature index acb94deb..637641c1 100644 --- a/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature +++ b/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature @@ -48,16 +48,18 @@ Background: Scenario: Sale Transactions When I perform the following transactions - | DateTime | TransactionNumber | TransactionType | MerchantName | DeviceIdentifier | EstateName | OperatorName | TransactionAmount | CustomerAccountNumber | - | Today | 1 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Safaricom | 1000.00 | 123456789 | - | Today | 2 | Sale | Test Merchant 2 | 123456781 | Test Estate 1 | Safaricom | 1000.00 |123456789 | - | Today | 3 | Sale | Test Merchant 3 | 123456782 | Test Estate 2 | Safaricom | 1000.00 |123456789 | + | DateTime | TransactionNumber | TransactionType | MerchantName | DeviceIdentifier | EstateName | OperatorName | TransactionAmount | CustomerAccountNumber | CustomerEmailAddress | + | Today | 1 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Safaricom | 1000.00 | 123456789 | | + | Today | 2 | Sale | Test Merchant 2 | 123456781 | Test Estate 1 | Safaricom | 1000.00 | 123456789 | | + | Today | 3 | Sale | Test Merchant 3 | 123456782 | Test Estate 2 | Safaricom | 1000.00 | 123456789 | | + | Today | 4 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Safaricom | 1000.00 | 123456789 | testcustomer@vustomer.co.uk | Then transaction response should contain the following information | EstateName | MerchantName | TransactionNumber | ResponseCode | ResponseMessage | | Test Estate 1 | Test Merchant 1 | 1 | 0000 | SUCCESS | | Test Estate 1 | Test Merchant 2 | 2 | 0000 | SUCCESS | | Test Estate 2 | Test Merchant 3 | 3 | 0000 | SUCCESS | + | Test Estate 1 | Test Merchant 1 | 4 | 0000 | SUCCESS | @PRTest Scenario: Sale Transaction with Invalid Device diff --git a/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature.cs b/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature.cs index ca401de0..8273d42d 100644 --- a/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature.cs +++ b/TransactionProcessor.IntegrationTests/SaleTransaction/SaleTransactionFeature.feature.cs @@ -290,7 +290,8 @@ public virtual void SaleTransactions() "EstateName", "OperatorName", "TransactionAmount", - "CustomerAccountNumber"}); + "CustomerAccountNumber", + "CustomerEmailAddress"}); table30.AddRow(new string[] { "Today", "1", @@ -300,7 +301,8 @@ public virtual void SaleTransactions() "Test Estate 1", "Safaricom", "1000.00", - "123456789"}); + "123456789", + ""}); table30.AddRow(new string[] { "Today", "2", @@ -310,7 +312,8 @@ public virtual void SaleTransactions() "Test Estate 1", "Safaricom", "1000.00", - "123456789"}); + "123456789", + ""}); table30.AddRow(new string[] { "Today", "3", @@ -320,7 +323,19 @@ public virtual void SaleTransactions() "Test Estate 2", "Safaricom", "1000.00", - "123456789"}); + "123456789", + ""}); + table30.AddRow(new string[] { + "Today", + "4", + "Sale", + "Test Merchant 1", + "123456780", + "Test Estate 1", + "Safaricom", + "1000.00", + "123456789", + "testcustomer@vustomer.co.uk"}); #line 50 testRunner.When("I perform the following transactions", ((string)(null)), table30, "When "); #line hidden @@ -348,7 +363,13 @@ public virtual void SaleTransactions() "3", "0000", "SUCCESS"}); -#line 56 + table31.AddRow(new string[] { + "Test Estate 1", + "Test Merchant 1", + "4", + "0000", + "SUCCESS"}); +#line 57 testRunner.Then("transaction response should contain the following information", ((string)(null)), table31, "Then "); #line hidden } @@ -365,7 +386,7 @@ public virtual void SaleTransactionWithInvalidDevice() "PRTest"}; TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Sale Transaction with Invalid Device", null, new string[] { "PRTest"}); -#line 63 +#line 65 this.ScenarioInitialize(scenarioInfo); #line hidden bool isScenarioIgnored = default(bool); @@ -406,7 +427,7 @@ public virtual void SaleTransactionWithInvalidDevice() "Test Estate 1", "Safaricom", "1000.00"}); -#line 65 +#line 67 testRunner.When("I perform the following transactions", ((string)(null)), table32, "When "); #line hidden TechTalk.SpecFlow.Table table33 = new TechTalk.SpecFlow.Table(new string[] { @@ -421,7 +442,7 @@ public virtual void SaleTransactionWithInvalidDevice() "1", "1000", "Device Identifier 123456781 not valid for Merchant Test Merchant 1"}); -#line 69 +#line 71 testRunner.Then("transaction response should contain the following information", ((string)(null)), table33, "Then "); #line hidden } @@ -435,7 +456,7 @@ public virtual void SaleTransactionWithInvalidEstate() { string[] tagsOfScenario = ((string[])(null)); TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Sale Transaction with Invalid Estate", null, ((string[])(null))); -#line 73 +#line 75 this.ScenarioInitialize(scenarioInfo); #line hidden bool isScenarioIgnored = default(bool); @@ -476,7 +497,7 @@ public virtual void SaleTransactionWithInvalidEstate() "InvalidEstate", "Safaricom", "1000.00"}); -#line 75 +#line 77 testRunner.When("I perform the following transactions", ((string)(null)), table34, "When "); #line hidden TechTalk.SpecFlow.Table table35 = new TechTalk.SpecFlow.Table(new string[] { @@ -491,7 +512,7 @@ public virtual void SaleTransactionWithInvalidEstate() "1", "1001", "Estate Id [79902550-64df-4491-b0c1-4e78943928a3] is not a valid estate"}); -#line 79 +#line 81 testRunner.Then("transaction response should contain the following information", ((string)(null)), table35, "Then "); #line hidden } @@ -505,7 +526,7 @@ public virtual void SaleTransactionWithInvalidMerchant() { string[] tagsOfScenario = ((string[])(null)); TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Sale Transaction with Invalid Merchant", null, ((string[])(null))); -#line 83 +#line 85 this.ScenarioInitialize(scenarioInfo); #line hidden bool isScenarioIgnored = default(bool); @@ -546,7 +567,7 @@ public virtual void SaleTransactionWithInvalidMerchant() "Test Estate 1", "Safaricom", "1000.00"}); -#line 85 +#line 87 testRunner.When("I perform the following transactions", ((string)(null)), table36, "When "); #line hidden TechTalk.SpecFlow.Table table37 = new TechTalk.SpecFlow.Table(new string[] { @@ -562,7 +583,7 @@ public virtual void SaleTransactionWithInvalidMerchant() "1002", "Merchant Id [d59320fa-4c3e-4900-a999-483f6a10c69a] is not a valid merchant for es" + "tate [Test Estate 1]"}); -#line 89 +#line 91 testRunner.Then("transaction response should contain the following information", ((string)(null)), table37, "Then "); #line hidden } diff --git a/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs b/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs index a3d568da..09262a80 100644 --- a/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs +++ b/TransactionProcessor.IntegrationTests/Shared/SharedSteps.cs @@ -251,6 +251,7 @@ public async Task WhenIPerformTheFollowingTransactions(Table table) String operatorName = SpecflowTableHelper.GetStringRowValue(tableRow, "OperatorName"); Decimal transactionAmount = SpecflowTableHelper.GetDecimalValue(tableRow, "TransactionAmount"); String customerAccountNumber = SpecflowTableHelper.GetStringRowValue(tableRow, "CustomerAccountNumber"); + String customerEmailAddress = SpecflowTableHelper.GetStringRowValue(tableRow, "CustomerEmailAddress"); transactionResponse = await this.PerformSaleTransaction(estateDetails.EstateId, merchantId, @@ -261,6 +262,7 @@ public async Task WhenIPerformTheFollowingTransactions(Table table) operatorName, transactionAmount, customerAccountNumber, + customerEmailAddress, CancellationToken.None); break; @@ -309,7 +311,7 @@ await this.TestingContext.DockerHelper.TransactionProcessorClient.PerformTransac return responseSerialisedMessage; } - private async Task PerformSaleTransaction(Guid estateId, Guid merchantId, DateTime transactionDateTime, String transactionType, String transactionNumber, String deviceIdentifier, String operatorIdentifier, Decimal transactionAmount, String customerAccountNumber, CancellationToken cancellationToken) + private async Task PerformSaleTransaction(Guid estateId, Guid merchantId, DateTime transactionDateTime, String transactionType, String transactionNumber, String deviceIdentifier, String operatorIdentifier, Decimal transactionAmount, String customerAccountNumber, String customerEmailAddres, CancellationToken cancellationToken) { SaleTransactionRequest saleTransactionRequest = new SaleTransactionRequest { @@ -324,8 +326,9 @@ private async Task PerformSaleTransaction(Guid estateId, Guid { {"Amount", transactionAmount.ToString()}, {"CustomerAccountNumber", customerAccountNumber} - } - }; + }, + CustomerEmailAddress = customerEmailAddres + }; SerialisedMessage serialisedMessage = new SerialisedMessage(); serialisedMessage.Metadata.Add(MetadataContants.KeyNameEstateId, estateId.ToString()); diff --git a/TransactionProcessor.IntegrationTests/TransactionProcessor.IntegrationTests.csproj b/TransactionProcessor.IntegrationTests/TransactionProcessor.IntegrationTests.csproj index 878c2414..0ed715b8 100644 --- a/TransactionProcessor.IntegrationTests/TransactionProcessor.IntegrationTests.csproj +++ b/TransactionProcessor.IntegrationTests/TransactionProcessor.IntegrationTests.csproj @@ -42,6 +42,9 @@ True LogonTransaction.feature + + True + @@ -57,6 +60,7 @@ SpecFlowSingleFileGenerator + SaleTransactionFeature.feature.cs diff --git a/TransactionProcessor.Testing/TestData.cs b/TransactionProcessor.Testing/TestData.cs index 3959afe2..5e45b128 100644 --- a/TransactionProcessor.Testing/TestData.cs +++ b/TransactionProcessor.Testing/TestData.cs @@ -40,6 +40,8 @@ public class TestData public static Guid OperatorId = Guid.Parse("804E9D8D-C6FE-4A46-9E55-6A04EA3E1AE5"); + public static String CustomerEmailAddress = "testcustomer1@customer.co.uk"; + public static String OperatorIdentifier1 = "Safaricom"; public static String OperatorIdentifier2 = "NotSupported"; @@ -345,6 +347,7 @@ public class TestData TestData.TransactionDateTime, TestData.TransactionNumber, TestData.OperatorIdentifier1, + TestData.CustomerEmailAddress, TestData.AdditionalTransactionMetaData); public static ProcessSaleTransactionResponse ProcessSaleTransactionResponseModel => diff --git a/TransactionProcessor.Transaction.DomainEvents/AdditionalRequestDataRecordedEvent.cs b/TransactionProcessor.Transaction.DomainEvents/AdditionalRequestDataRecordedEvent.cs index 7cb990cc..bd73d13e 100644 --- a/TransactionProcessor.Transaction.DomainEvents/AdditionalRequestDataRecordedEvent.cs +++ b/TransactionProcessor.Transaction.DomainEvents/AdditionalRequestDataRecordedEvent.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; using Shared.DomainDrivenDesign.EventSourcing; @@ -13,6 +14,15 @@ public class AdditionalRequestDataRecordedEvent : DomainEvent { #region Constructors + /// + /// Initializes a new instance of the class. + /// + [ExcludeFromCodeCoverage] + public AdditionalRequestDataRecordedEvent() + { + + } + /// /// Initializes a new instance of the class. /// @@ -22,12 +32,12 @@ public class AdditionalRequestDataRecordedEvent : DomainEvent /// The merchant identifier. /// The operator identifier. /// The additional transaction request metadata. - public AdditionalRequestDataRecordedEvent(Guid aggregateId, - Guid eventId, - Guid estateId, - Guid merchantId, - String operatorIdentifier, - Dictionary additionalTransactionRequestMetadata) : base(aggregateId, eventId) + private AdditionalRequestDataRecordedEvent(Guid aggregateId, + Guid eventId, + Guid estateId, + Guid merchantId, + String operatorIdentifier, + Dictionary additionalTransactionRequestMetadata) : base(aggregateId, eventId) { this.TransactionId = aggregateId; this.EstateId = estateId; diff --git a/TransactionProcessor.Transaction.DomainEvents/AdditionalResponseDataRecordedEvent.cs b/TransactionProcessor.Transaction.DomainEvents/AdditionalResponseDataRecordedEvent.cs index f516782a..4816aee8 100644 --- a/TransactionProcessor.Transaction.DomainEvents/AdditionalResponseDataRecordedEvent.cs +++ b/TransactionProcessor.Transaction.DomainEvents/AdditionalResponseDataRecordedEvent.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; using Shared.DomainDrivenDesign.EventSourcing; @@ -13,6 +14,12 @@ public class AdditionalResponseDataRecordedEvent : DomainEvent { #region Constructors + [ExcludeFromCodeCoverage] + public AdditionalResponseDataRecordedEvent() + { + + } + /// /// Initializes a new instance of the class. /// @@ -22,7 +29,7 @@ public class AdditionalResponseDataRecordedEvent : DomainEvent /// The merchant identifier. /// The operator identifier. /// The additional transaction response metadata. - public AdditionalResponseDataRecordedEvent(Guid aggregateId, + private AdditionalResponseDataRecordedEvent(Guid aggregateId, Guid eventId, Guid estateId, Guid merchantId, diff --git a/TransactionProcessor.Transaction.DomainEvents/CustomerEmailReceiptRequestedEvent.cs b/TransactionProcessor.Transaction.DomainEvents/CustomerEmailReceiptRequestedEvent.cs new file mode 100644 index 00000000..0e21d3d7 --- /dev/null +++ b/TransactionProcessor.Transaction.DomainEvents/CustomerEmailReceiptRequestedEvent.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace TransactionProcessor.Transaction.DomainEvents +{ + using System.Diagnostics.CodeAnalysis; + using Newtonsoft.Json; + using Shared.DomainDrivenDesign.EventSourcing; + + public class CustomerEmailReceiptRequestedEvent : DomainEvent + { + [JsonProperty] + public Guid EstateId { get; private set; } + + [JsonProperty] + public Guid MerchantId { get; private set; } + + [JsonProperty] + public String CustomerEmailAddress { get; private set; } + + /// + /// Gets the transaction identifier. + /// + /// + /// The transaction identifier. + /// + [JsonProperty] + public Guid TransactionId { get; private set; } + + [ExcludeFromCodeCoverage] + public CustomerEmailReceiptRequestedEvent() + { + + } + + private CustomerEmailReceiptRequestedEvent(Guid aggregateId, + Guid eventId, + Guid estateId, + Guid merchantId, + String customerEmailAddress) : base(aggregateId, eventId) + { + this.TransactionId = aggregateId; + this.EstateId = estateId; + this.MerchantId = merchantId; + this.CustomerEmailAddress = customerEmailAddress; + } + + public static CustomerEmailReceiptRequestedEvent Create(Guid aggregateId, + Guid estateId, + Guid merchantId, + String customerEmailAddress) + { + return new CustomerEmailReceiptRequestedEvent(aggregateId,Guid.NewGuid(), estateId,merchantId, customerEmailAddress); + } + } +} diff --git a/TransactionProcessor.Transaction.DomainEvents/TransactionAuthorisedByOperatorEvent.cs b/TransactionProcessor.Transaction.DomainEvents/TransactionAuthorisedByOperatorEvent.cs index 2cf9988e..97a539a7 100644 --- a/TransactionProcessor.Transaction.DomainEvents/TransactionAuthorisedByOperatorEvent.cs +++ b/TransactionProcessor.Transaction.DomainEvents/TransactionAuthorisedByOperatorEvent.cs @@ -29,7 +29,7 @@ public TransactionAuthorisedByOperatorEvent() /// The operator transaction identifier. /// The response code. /// The response message. - public TransactionAuthorisedByOperatorEvent(Guid aggregateId, + private TransactionAuthorisedByOperatorEvent(Guid aggregateId, Guid eventId, Guid estateId, Guid merchantId, diff --git a/TransactionProcessor.Transaction.DomainEvents/TransactionDeclinedByOperatorEvent.cs b/TransactionProcessor.Transaction.DomainEvents/TransactionDeclinedByOperatorEvent.cs index 690f9d88..b70ebd96 100644 --- a/TransactionProcessor.Transaction.DomainEvents/TransactionDeclinedByOperatorEvent.cs +++ b/TransactionProcessor.Transaction.DomainEvents/TransactionDeclinedByOperatorEvent.cs @@ -27,7 +27,7 @@ public TransactionDeclinedByOperatorEvent() /// The operator response message. /// The response code. /// The response message. - public TransactionDeclinedByOperatorEvent(Guid aggregateId, + private TransactionDeclinedByOperatorEvent(Guid aggregateId, Guid eventId, Guid estateId, Guid merchantId, diff --git a/TransactionProcessor.Transaction.DomainEvents/TransactionHasBeenCompletedEvent.cs b/TransactionProcessor.Transaction.DomainEvents/TransactionHasBeenCompletedEvent.cs index 6ee19718..ecd2915a 100644 --- a/TransactionProcessor.Transaction.DomainEvents/TransactionHasBeenCompletedEvent.cs +++ b/TransactionProcessor.Transaction.DomainEvents/TransactionHasBeenCompletedEvent.cs @@ -32,7 +32,7 @@ public TransactionHasBeenCompletedEvent() /// The response code. /// The response message. /// if set to true [is authorised]. - public TransactionHasBeenCompletedEvent(Guid aggregateId, + private TransactionHasBeenCompletedEvent(Guid aggregateId, Guid eventId, Guid estateId, Guid merchantId, diff --git a/TransactionProcessor.Transaction.DomainEvents/TransactionHasBeenLocallyAuthorisedEvent.cs b/TransactionProcessor.Transaction.DomainEvents/TransactionHasBeenLocallyAuthorisedEvent.cs index 730e53f8..583210d0 100644 --- a/TransactionProcessor.Transaction.DomainEvents/TransactionHasBeenLocallyAuthorisedEvent.cs +++ b/TransactionProcessor.Transaction.DomainEvents/TransactionHasBeenLocallyAuthorisedEvent.cs @@ -24,7 +24,7 @@ public class TransactionHasBeenLocallyAuthorisedEvent : DomainEvent /// The authorisation code. /// The response code. /// The response message. - public TransactionHasBeenLocallyAuthorisedEvent(Guid aggregateId, + private TransactionHasBeenLocallyAuthorisedEvent(Guid aggregateId, Guid eventId, Guid estateId, Guid merchantId, diff --git a/TransactionProcessor.Transaction.DomainEvents/TransactionHasBeenLocallyDeclinedEvent.cs b/TransactionProcessor.Transaction.DomainEvents/TransactionHasBeenLocallyDeclinedEvent.cs index 2ea9b924..496f119c 100644 --- a/TransactionProcessor.Transaction.DomainEvents/TransactionHasBeenLocallyDeclinedEvent.cs +++ b/TransactionProcessor.Transaction.DomainEvents/TransactionHasBeenLocallyDeclinedEvent.cs @@ -19,7 +19,7 @@ public class TransactionHasBeenLocallyDeclinedEvent : DomainEvent /// The merchant identifier. /// The response code. /// The response message. - public TransactionHasBeenLocallyDeclinedEvent(Guid aggregateId, + private TransactionHasBeenLocallyDeclinedEvent(Guid aggregateId, Guid eventId, Guid estateId, Guid merchantId, diff --git a/TransactionProcessor.Transaction.DomainEvents/TransactionHasStartedEvent.cs b/TransactionProcessor.Transaction.DomainEvents/TransactionHasStartedEvent.cs index 4bbd77fd..d2411ea7 100644 --- a/TransactionProcessor.Transaction.DomainEvents/TransactionHasStartedEvent.cs +++ b/TransactionProcessor.Transaction.DomainEvents/TransactionHasStartedEvent.cs @@ -32,8 +32,9 @@ public TransactionHasStartedEvent() /// The transaction date time. /// The transaction number. /// Type of the transaction. + /// The transaction reference. /// The device identifier. - public TransactionHasStartedEvent(Guid aggregateId, + private TransactionHasStartedEvent(Guid aggregateId, Guid eventId, Guid estateId, Guid merchantId, diff --git a/TransactionProcessor.TransactionAggregate.Tests/DomainEventTests.cs b/TransactionProcessor.TransactionAggregate.Tests/DomainEventTests.cs index f5753ffc..789160c4 100644 --- a/TransactionProcessor.TransactionAggregate.Tests/DomainEventTests.cs +++ b/TransactionProcessor.TransactionAggregate.Tests/DomainEventTests.cs @@ -210,5 +210,23 @@ public void AdditionalRequestDataRecordedEvent_CanBeCreated_IsCreated() additionalRequestDataRecordedEvent.AdditionalTransactionRequestMetadata.ShouldContainKeyAndValue(keyValuePair.Key, keyValuePair.Value); } } + + [Fact] + public void CustomerEmailReceiptRequestedEvent_CanBeCreated_IsCreated() + { + CustomerEmailReceiptRequestedEvent customerEmailReceiptRequestedEvent = CustomerEmailReceiptRequestedEvent.Create(TestData.TransactionId, + TestData.EstateId, + TestData.MerchantId, + TestData.CustomerEmailAddress); + + customerEmailReceiptRequestedEvent.ShouldNotBeNull(); + customerEmailReceiptRequestedEvent.AggregateId.ShouldBe(TestData.TransactionId); + customerEmailReceiptRequestedEvent.EventCreatedDateTime.ShouldNotBe(DateTime.MinValue); + customerEmailReceiptRequestedEvent.EventId.ShouldNotBe(Guid.Empty); + customerEmailReceiptRequestedEvent.TransactionId.ShouldBe(TestData.TransactionId); + customerEmailReceiptRequestedEvent.EstateId.ShouldBe(TestData.EstateId); + customerEmailReceiptRequestedEvent.MerchantId.ShouldBe(TestData.MerchantId); + customerEmailReceiptRequestedEvent.CustomerEmailAddress.ShouldBe(TestData.CustomerEmailAddress); + } } } diff --git a/TransactionProcessor.TransactionAggregate.Tests/TransactionAggregateTests.cs b/TransactionProcessor.TransactionAggregate.Tests/TransactionAggregateTests.cs index c8cd14a2..38f7cb52 100644 --- a/TransactionProcessor.TransactionAggregate.Tests/TransactionAggregateTests.cs +++ b/TransactionProcessor.TransactionAggregate.Tests/TransactionAggregateTests.cs @@ -713,5 +713,47 @@ public void TransactionAggregate_RecordAdditionalResponseData_AlreadyCompleted_E }); } + [Fact] + public void TransactionAggregate_RequestEmailReceipt_CustomerEmailReceiptHasBeenRequested() + { + TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TransactionType.Sale, TestData.TransactionReference, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); + transactionAggregate.RecordAdditionalRequestData(TestData.OperatorIdentifier1, TestData.AdditionalTransactionMetaData); + transactionAggregate.AuthoriseTransaction(TestData.OperatorIdentifier1, TestData.OperatorAuthorisationCode, TestData.OperatorResponseCode, TestData.OperatorResponseMessage, TestData.OperatorTransactionId, TestData.ResponseCode, TestData.ResponseMessage); + transactionAggregate.RecordAdditionalResponseData(TestData.OperatorIdentifier1, TestData.AdditionalTransactionMetaData); + transactionAggregate.CompleteTransaction(); + + transactionAggregate.RequestEmailReceipt(TestData.CustomerEmailAddress); + + transactionAggregate.CustomerEmailReceiptHasBeenRequested.ShouldBeTrue(); + } + + [Fact] + public void TransactionAggregate_RequestEmailReceipt_TransactionNotCompleted_ErrorThrown() + { + TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TransactionType.Sale, TestData.TransactionReference, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); + transactionAggregate.RecordAdditionalRequestData(TestData.OperatorIdentifier1, TestData.AdditionalTransactionMetaData); + transactionAggregate.AuthoriseTransaction(TestData.OperatorIdentifier1, TestData.OperatorAuthorisationCode, TestData.OperatorResponseCode, TestData.OperatorResponseMessage, TestData.OperatorTransactionId, TestData.ResponseCode, TestData.ResponseMessage); + transactionAggregate.RecordAdditionalResponseData(TestData.OperatorIdentifier1, TestData.AdditionalTransactionMetaData); + + Should.Throw(() => { transactionAggregate.RequestEmailReceipt(TestData.CustomerEmailAddress); }); + } + + [Fact] + public void TransactionAggregate_RequestEmailReceipt_EmailReceiptAlreadyRequested_ErrorThrown() + { + TransactionAggregate transactionAggregate = TransactionAggregate.Create(TestData.TransactionId); + transactionAggregate.StartTransaction(TestData.TransactionDateTime, TestData.TransactionNumber, TransactionType.Sale, TestData.TransactionReference, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier); + transactionAggregate.RecordAdditionalRequestData(TestData.OperatorIdentifier1, TestData.AdditionalTransactionMetaData); + transactionAggregate.AuthoriseTransaction(TestData.OperatorIdentifier1, TestData.OperatorAuthorisationCode, TestData.OperatorResponseCode, TestData.OperatorResponseMessage, TestData.OperatorTransactionId, TestData.ResponseCode, TestData.ResponseMessage); + transactionAggregate.RecordAdditionalResponseData(TestData.OperatorIdentifier1, TestData.AdditionalTransactionMetaData); + transactionAggregate.CompleteTransaction(); + + transactionAggregate.RequestEmailReceipt(TestData.CustomerEmailAddress); + + Should.Throw(() => { transactionAggregate.RequestEmailReceipt(TestData.CustomerEmailAddress); }); + } + } } diff --git a/TransactionProcessor.TransactionAgrgegate/TransactionAggregate.cs b/TransactionProcessor.TransactionAgrgegate/TransactionAggregate.cs index 2820d207..c264988b 100644 --- a/TransactionProcessor.TransactionAgrgegate/TransactionAggregate.cs +++ b/TransactionProcessor.TransactionAgrgegate/TransactionAggregate.cs @@ -64,6 +64,14 @@ private TransactionAggregate(Guid aggregateId) /// public String AuthorisationCode { get; private set; } + /// + /// Gets a value indicating whether [customer email receipt has been requested]. + /// + /// + /// true if [customer email receipt has been requested]; otherwise, false. + /// + public Boolean CustomerEmailReceiptHasBeenRequested { get; private set; } + /// /// Gets the device identifier. /// @@ -193,20 +201,20 @@ private TransactionAggregate(Guid aggregateId) public String TransactionNumber { get; private set; } /// - /// Gets the type of the transaction. + /// Gets the transaction reference. /// /// - /// The type of the transaction. + /// The transaction reference. /// - public TransactionType TransactionType { get; private set; } + public String TransactionReference { get; private set; } /// - /// Gets the transaction reference. + /// Gets the type of the transaction. /// /// - /// The transaction reference. + /// The type of the transaction. /// - public String TransactionReference { get; private set; } + public TransactionType TransactionType { get; private set; } #endregion @@ -222,7 +230,7 @@ private TransactionAggregate(Guid aggregateId) /// The operator transaction identifier. /// The response code. /// The response message. - public void AuthoriseTransaction(String operatorIdentifier, + public void AuthoriseTransaction(String operatorIdentifier, String authorisationCode, String operatorResponseCode, String operatorResponseMessage, @@ -303,7 +311,7 @@ public static TransactionAggregate Create(Guid aggregateId) /// The operator response message. /// The response code. /// The response message. - public void DeclineTransaction(String operatorIdentifier, + public void DeclineTransaction(String operatorIdentifier, String operatorResponseCode, String operatorResponseMessage, String responseCode, @@ -347,7 +355,8 @@ public void DeclineTransactionLocally(String responseCode, /// /// The operator identifier. /// The additional transaction request metadata. - public void RecordAdditionalRequestData(String operatorIdentifier, Dictionary additionalTransactionRequestMetadata) + public void RecordAdditionalRequestData(String operatorIdentifier, + Dictionary additionalTransactionRequestMetadata) { this.CheckTransactionNotAlreadyCompleted(); this.CheckTransactionHasBeenStarted(); @@ -366,7 +375,8 @@ public void RecordAdditionalRequestData(String operatorIdentifier, Dictionary /// The operator identifier. /// The additional transaction response metadata. - public void RecordAdditionalResponseData(String operatorIdentifier, Dictionary additionalTransactionResponseMetadata) + public void RecordAdditionalResponseData(String operatorIdentifier, + Dictionary additionalTransactionResponseMetadata) { this.CheckTransactionHasBeenStarted(); this.CheckAdditionalResponseDataNotAlreadyRecorded(); @@ -377,6 +387,33 @@ public void RecordAdditionalResponseData(String operatorIdentifier, Dictionary + /// Requests the email receipt. + /// + /// The customer email address. + public void RequestEmailReceipt(String customerEmailAddress) + { + this.CheckTransactionHasBeenCompleted(); + this.CheckCustomerHasNotAlreadyRequestedEmailReceipt(); + + CustomerEmailReceiptRequestedEvent customerEmailReceiptRequestedEvent = + CustomerEmailReceiptRequestedEvent.Create(this.AggregateId, this.EstateId, this.MerchantId, customerEmailAddress); + + this.ApplyAndPend(customerEmailReceiptRequestedEvent); + } + + /// + /// Checks the transaction has been completed. + /// + /// Transaction [{this.AggregateId}] has not been completed + private void CheckTransactionHasBeenCompleted() + { + if (this.IsCompleted == false) + { + throw new InvalidOperationException($"Transaction [{this.AggregateId}] has not been completed"); + } + } + /// /// Starts the transaction. /// @@ -423,15 +460,14 @@ public void StartTransaction(DateTime transactionDateTime, this.CheckTransactionNotAlreadyStarted(); this.CheckTransactionNotAlreadyCompleted(); - TransactionHasStartedEvent transactionHasStartedEvent = - TransactionHasStartedEvent.Create(this.AggregateId, - estateId, - merchantId, - transactionDateTime, - transactionNumber, - transactionType.ToString(), - transactionReference, - deviceIdentifier); + TransactionHasStartedEvent transactionHasStartedEvent = TransactionHasStartedEvent.Create(this.AggregateId, + estateId, + merchantId, + transactionDateTime, + transactionNumber, + transactionType.ToString(), + transactionReference, + deviceIdentifier); this.ApplyAndPend(transactionHasStartedEvent); } @@ -482,6 +518,18 @@ private void CheckAdditionalResponseDataNotAlreadyRecorded() } } + /// + /// Checks the customer has not already requested email receipt. + /// + /// Customer Email Receipt already requested for Transaction [{this.AggregateId}] + private void CheckCustomerHasNotAlreadyRequestedEmailReceipt() + { + if (this.CustomerEmailReceiptHasBeenRequested) + { + throw new InvalidOperationException($"Customer Email Receipt already requested for Transaction [{this.AggregateId}]"); + } + } + /// /// Checks the transaction can be locally authorised. /// @@ -569,6 +617,15 @@ private void CheckTransactionNotAlreadyStarted() } } + /// + /// Plays the event. + /// + /// The domain event. + private void PlayEvent(CustomerEmailReceiptRequestedEvent domainEvent) + { + this.CustomerEmailReceiptHasBeenRequested = true; + } + /// /// Plays the event. /// diff --git a/TransactionProcessor/Controllers/TransactionController.cs b/TransactionProcessor/Controllers/TransactionController.cs index 7ac06a05..f6316b52 100644 --- a/TransactionProcessor/Controllers/TransactionController.cs +++ b/TransactionProcessor/Controllers/TransactionController.cs @@ -135,6 +135,7 @@ private async Task ProcessSpecificMessage(SaleTransactionRequ saleTransactionRequest.TransactionDateTime, saleTransactionRequest.TransactionNumber, saleTransactionRequest.OperatorIdentifier, + saleTransactionRequest.CustomerEmailAddress, saleTransactionRequest.AdditionalTransactionMetadata); ProcessSaleTransactionResponse response = await this.Mediator.Send(request, cancellationToken);