diff --git a/TransactionProcessor.Aggregates.Tests/ReconciliationAggregateTests.cs b/TransactionProcessor.Aggregates.Tests/ReconciliationAggregateTests.cs index 1a6ae48..2638f04 100644 --- a/TransactionProcessor.Aggregates.Tests/ReconciliationAggregateTests.cs +++ b/TransactionProcessor.Aggregates.Tests/ReconciliationAggregateTests.cs @@ -121,7 +121,7 @@ public void ReconciliationAggregate_Authorise_ReconciliationIsAuthorised() Result result = reconciliationAggregate.Authorise(TestData.ResponseCode, TestData.ResponseMessage); result.IsSuccess.ShouldBeTrue(); - reconciliationAggregate.ResponseCode.ShouldBe(TestData.ResponseCode); + reconciliationAggregate.ResponseCode.ShouldBe(TestData.ResponseCode.ToCodeString()); reconciliationAggregate.ResponseMessage.ShouldBe(TestData.ResponseMessage); reconciliationAggregate.IsAuthorised.ShouldBeTrue(); } @@ -186,7 +186,7 @@ public void ReconciliationAggregate_Decline_ReconciliationIsDeclined() Result result = reconciliationAggregate.Decline(TestData.ResponseCode, TestData.ResponseMessage); result.IsSuccess.ShouldBeTrue(); - reconciliationAggregate.ResponseCode.ShouldBe(TestData.ResponseCode); + reconciliationAggregate.ResponseCode.ShouldBe(TestData.ResponseCode.ToCodeString()); reconciliationAggregate.ResponseMessage.ShouldBe(TestData.ResponseMessage); reconciliationAggregate.IsAuthorised.ShouldBeFalse(); } diff --git a/TransactionProcessor.Aggregates.Tests/TransactionAggregateTests.cs b/TransactionProcessor.Aggregates.Tests/TransactionAggregateTests.cs index 197b9f1..49aad81 100644 --- a/TransactionProcessor.Aggregates.Tests/TransactionAggregateTests.cs +++ b/TransactionProcessor.Aggregates.Tests/TransactionAggregateTests.cs @@ -378,7 +378,7 @@ public void TransactionAggregate_AuthoriseTransactionLocally_TransactionIsAuthor transactionAggregate.IsLocallyAuthorised.ShouldBeTrue(); transactionAggregate.IsAuthorised.ShouldBeFalse(); transactionAggregate.AuthorisationCode.ShouldBe(TestData.AuthorisationCode); - transactionAggregate.ResponseCode.ShouldBe(TestData.ResponseCode); + transactionAggregate.ResponseCode.ShouldBe(TestData.ResponseCode.ToCodeString()); transactionAggregate.ResponseMessage.ShouldBe(TestData.ResponseMessage); } @@ -543,7 +543,7 @@ public void TransactionAggregate_DeclineTransactionLocally_TransactionIsDeclined transactionAggregate.IsDeclined.ShouldBeFalse(); transactionAggregate.IsLocallyDeclined.ShouldBeTrue(); - transactionAggregate.ResponseCode.ShouldBe(TestData.DeclinedResponseCode); + transactionAggregate.ResponseCode.ShouldBe(TestData.DeclinedResponseCode.ToCodeString()); transactionAggregate.ResponseMessage.ShouldBe(TestData.DeclinedResponseMessage); } diff --git a/TransactionProcessor.Aggregates/ReconciliationAggregate.cs b/TransactionProcessor.Aggregates/ReconciliationAggregate.cs index 8cc2485..3532bdf 100644 --- a/TransactionProcessor.Aggregates/ReconciliationAggregate.cs +++ b/TransactionProcessor.Aggregates/ReconciliationAggregate.cs @@ -93,7 +93,7 @@ public static Result RecordOverallTotals(this ReconciliationAggregate aggregate, return Result.Success(); } - public static Result Authorise(this ReconciliationAggregate aggregate, String responseCode, String responseMessage) + public static Result Authorise(this ReconciliationAggregate aggregate, TransactionResponseCode responseCode, String responseMessage) { if (aggregate.IsAuthorised || aggregate.IsDeclined) { return Result.Success(); @@ -107,7 +107,7 @@ public static Result Authorise(this ReconciliationAggregate aggregate, String re return result; ReconciliationDomainEvents.ReconciliationHasBeenLocallyAuthorisedEvent reconciliationHasBeenLocallyAuthorisedEvent = new(aggregate.AggregateId, aggregate.EstateId, aggregate.MerchantId, - responseCode, responseMessage, aggregate.TransactionDateTime); + responseCode.ToCodeString(), responseMessage, aggregate.TransactionDateTime); aggregate.ApplyAndAppend(reconciliationHasBeenLocallyAuthorisedEvent); return Result.Success(); @@ -122,7 +122,7 @@ private static Result CheckReconciliationHasBeenAuthorisedOrDeclined(this Reconc return Result.Success(); } - public static Result Decline(this ReconciliationAggregate aggregate,String responseCode, String responseMessage) + public static Result Decline(this ReconciliationAggregate aggregate,TransactionResponseCode responseCode, String responseMessage) { if (aggregate.IsAuthorised || aggregate.IsDeclined) { return Result.Success(); @@ -136,7 +136,7 @@ public static Result Decline(this ReconciliationAggregate aggregate,String respo return result; ReconciliationDomainEvents.ReconciliationHasBeenLocallyDeclinedEvent reconciliationHasBeenLocallyDeclinedEvent = new(aggregate.AggregateId, aggregate.EstateId, aggregate.MerchantId, - responseCode, responseMessage, aggregate.TransactionDateTime); + responseCode.ToCodeString(), responseMessage, aggregate.TransactionDateTime); aggregate.ApplyAndAppend(reconciliationHasBeenLocallyDeclinedEvent); diff --git a/TransactionProcessor.Aggregates/TransactionAggregate.cs b/TransactionProcessor.Aggregates/TransactionAggregate.cs index f211edd..a32e19f 100644 --- a/TransactionProcessor.Aggregates/TransactionAggregate.cs +++ b/TransactionProcessor.Aggregates/TransactionAggregate.cs @@ -13,13 +13,50 @@ namespace TransactionProcessor.Aggregates using Shared.EventStore.Aggregate; using Shared.General; + public enum TransactionResponseCode + { + Success = 0, + SuccessNeedToAddDevice = 1, + + InvalidDeviceIdentifier = 1000, + InvalidEstateId = 1001, + InvalidMerchantId = 1002, + NoValidDevices = 1003, + NoEstateOperators = 1004, + OperatorNotValidForEstate = 1005, + NoMerchantOperators = 1006, + OperatorNotValidForMerchant = 1007, + TransactionDeclinedByOperator = 1008, + MerchantDoesNotHaveEnoughCredit = 1009, + OperatorCommsError = 1010, + InvalidSaleTransactionAmount = 1011, + InvalidContractIdValue = 1012, + InvalidProductIdValue = 1013, + MerchantHasNoContractsConfigured = 1014, + ContractNotValidForMerchant = 1015, + ProductNotValidForMerchant = 1016, + OperatorNotEnabledForEstate = 1017, + OperatorNotEnabledForMerchant = 1018, + + // A Catch All generic Error where reason has not been identified + UnknownFailure = 9999 + } + + public static class TransactionResponseCodeExtensions + { + public static string ToCodeString(this TransactionResponseCode code) + { + return ((int)code).ToString("D4"); + } + } + public static class TransactionAggregateExtensions{ public static Result DeclineTransaction(this TransactionAggregate aggregate, Guid operatorId, String operatorResponseCode, String operatorResponseMessage, - String responseCode, + TransactionResponseCode responseCode, String responseMessage) { Result result = aggregate.CheckTransactionHasBeenStarted(); @@ -32,6 +69,8 @@ public static Result DeclineTransaction(this TransactionAggregate aggregate, if (result.IsFailed) return result; + + TransactionDomainEvents.TransactionDeclinedByOperatorEvent transactionDeclinedByOperatorEvent = new(aggregate.AggregateId, aggregate.EstateId, @@ -39,7 +78,7 @@ public static Result DeclineTransaction(this TransactionAggregate aggregate, operatorId, operatorResponseCode, operatorResponseMessage, - responseCode, + responseCode.ToCodeString(), responseMessage, aggregate.TransactionDateTime); aggregate.ApplyAndAppend(transactionDeclinedByOperatorEvent); @@ -47,8 +86,8 @@ public static Result DeclineTransaction(this TransactionAggregate aggregate, return Result.Success(); } - public static Result DeclineTransactionLocally(this TransactionAggregate aggregate, - String responseCode, + public static Result DeclineTransactionLocally(this TransactionAggregate aggregate, + TransactionResponseCode responseCode, String responseMessage) { Result result = aggregate.CheckTransactionHasBeenStarted(); @@ -61,7 +100,7 @@ public static Result DeclineTransactionLocally(this TransactionAggregate aggrega if (result.IsFailed) return result; TransactionDomainEvents.TransactionHasBeenLocallyDeclinedEvent transactionHasBeenLocallyDeclinedEvent = - new(aggregate.AggregateId, aggregate.EstateId, aggregate.MerchantId, responseCode, responseMessage, + new(aggregate.AggregateId, aggregate.EstateId, aggregate.MerchantId, responseCode.ToCodeString(), responseMessage, aggregate.TransactionDateTime); aggregate.ApplyAndAppend(transactionHasBeenLocallyDeclinedEvent); @@ -276,7 +315,7 @@ public static Result AuthoriseTransaction(this TransactionAggregate aggregate, String operatorResponseCode, String operatorResponseMessage, String operatorTransactionId, - String responseCode, + TransactionResponseCode responseCode, String responseMessage) { Result result = aggregate.CheckTransactionHasBeenStarted(); @@ -294,7 +333,7 @@ public static Result AuthoriseTransaction(this TransactionAggregate aggregate, operatorResponseCode, operatorResponseMessage, operatorTransactionId, - responseCode, + responseCode.ToCodeString(), responseMessage, aggregate.TransactionDateTime); aggregate.ApplyAndAppend(transactionAuthorisedByOperatorEvent); @@ -304,7 +343,7 @@ public static Result AuthoriseTransaction(this TransactionAggregate aggregate, public static Result AuthoriseTransactionLocally(this TransactionAggregate aggregate, String authorisationCode, - String responseCode, + TransactionResponseCode responseCode, String responseMessage) { var result = aggregate.CheckTransactionHasBeenStarted(); @@ -317,7 +356,7 @@ public static Result AuthoriseTransactionLocally(this TransactionAggregate aggre if (result.IsFailed) return result; TransactionDomainEvents.TransactionHasBeenLocallyAuthorisedEvent transactionHasBeenLocallyAuthorisedEvent = - new(aggregate.AggregateId, aggregate.EstateId, aggregate.MerchantId, authorisationCode, responseCode, responseMessage, + new(aggregate.AggregateId, aggregate.EstateId, aggregate.MerchantId, authorisationCode, responseCode.ToCodeString(), responseMessage, aggregate.TransactionDateTime); aggregate.ApplyAndAppend(transactionHasBeenLocallyAuthorisedEvent); diff --git a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs index 9bbaa31..4101844 100644 --- a/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs +++ b/TransactionProcessor.BusinessLogic.Tests/Services/TransactionDomainServiceTests.cs @@ -254,7 +254,8 @@ public async Task TransactionDomainService_ProcessSaleTransaction_DeclinedByOper AuthorisationCode = TestData.OperatorAuthorisationCode, TransactionId = TestData.OperatorTransactionId, - ResponseCode = TestData.ResponseCode}, IsSuccess = false}); + ResponseCode = TestData.ResponseCode.ToCodeString() + }, IsSuccess = false}); TransactionCommands.ProcessSaleTransactionCommand command = new TransactionCommands.ProcessSaleTransactionCommand(TestData.TransactionId, TestData.EstateId, @@ -356,8 +357,8 @@ public async Task TransactionDomainService_ProcessSaleTransaction_TransactionIsP AuthorisationCode = TestData.OperatorAuthorisationCode, TransactionId = TestData.OperatorTransactionId, - ResponseCode = TestData.ResponseCode - }); + ResponseCode = TestData.ResponseCode.ToCodeString() + }); TransactionCommands.ProcessSaleTransactionCommand command = new TransactionCommands.ProcessSaleTransactionCommand(TestData.TransactionId, TestData.EstateId, @@ -420,7 +421,7 @@ public async Task TransactionDomainService_ProcessSaleTransaction_NoFloatFound_T AuthorisationCode = TestData.OperatorAuthorisationCode, TransactionId = TestData.OperatorTransactionId, - ResponseCode = TestData.ResponseCode + ResponseCode = TestData.ResponseCode.ToCodeString() }); TransactionCommands.ProcessSaleTransactionCommand command = @@ -503,7 +504,7 @@ public async Task TransactionDomainService_RequireFeeCalculation_IsNotAuthorised TransactionType.Sale, TestData.TransactionReference, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier, TestData.TransactionAmount); - transactionAggregate.DeclineTransaction(TestData.OperatorId, "111", "SUCCESS", "0000", "SUCCESS"); + transactionAggregate.DeclineTransaction(TestData.OperatorId, "111", "SUCCESS", TransactionResponseCode.Success, "SUCCESS"); // TODO: maybe move this to an extension on aggregate var result = TransactionHelpers.RequireFeeCalculation(transactionAggregate); @@ -518,7 +519,7 @@ public async Task TransactionDomainService_RequireFeeCalculation_IsNotCompelted_ TransactionType.Sale, TestData.TransactionReference, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier, TestData.TransactionAmount); - transactionAggregate.AuthoriseTransaction(TestData.OperatorId, "111", "111", "SUCCESS", "1234", "0000", "SUCCESS"); + transactionAggregate.AuthoriseTransaction(TestData.OperatorId, "111", "111", "SUCCESS", "1234", TransactionResponseCode.Success, "SUCCESS"); var result = TransactionHelpers.RequireFeeCalculation(transactionAggregate); result.ShouldBeFalse(); @@ -532,7 +533,7 @@ public async Task TransactionDomainService_RequireFeeCalculation_IsALogon_Return TransactionType.Logon, TestData.TransactionReference, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier, TestData.TransactionAmount); - transactionAggregate.AuthoriseTransactionLocally("111", "0001", "SUCCESS"); + transactionAggregate.AuthoriseTransactionLocally("111", TransactionResponseCode.InvalidDeviceIdentifier, "SUCCESS"); transactionAggregate.CompleteTransaction(); @@ -548,7 +549,7 @@ public async Task TransactionDomainService_RequireFeeCalculation_NoContractId_Re TransactionType.Sale, TestData.TransactionReference, TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier, TestData.TransactionAmount); - transactionAggregate.AuthoriseTransaction(TestData.OperatorId, "111", "111", "SUCCESS", "1234", "0000", "SUCCESS"); + transactionAggregate.AuthoriseTransaction(TestData.OperatorId, "111", "111", "SUCCESS", "1234", TransactionResponseCode.Success, "SUCCESS"); transactionAggregate.CompleteTransaction(); @@ -565,7 +566,7 @@ public async Task TransactionDomainService_RequireFeeCalculation_NullAmount_Retu TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier, null); transactionAggregate.AddProductDetails(TestData.ContractId, TestData.ProductId); - transactionAggregate.AuthoriseTransaction(TestData.OperatorId, "111", "111", "SUCCESS", "1234", "0000", "SUCCESS"); + transactionAggregate.AuthoriseTransaction(TestData.OperatorId, "111", "111", "SUCCESS", "1234", TransactionResponseCode.Success, "SUCCESS"); transactionAggregate.CompleteTransaction(); @@ -582,7 +583,7 @@ public async Task TransactionDomainService_RequireFeeCalculation_ReturnsTrue() TestData.EstateId, TestData.MerchantId, TestData.DeviceIdentifier, TestData.TransactionAmount); transactionAggregate.AddProductDetails(TestData.ContractId, TestData.ProductId); - transactionAggregate.AuthoriseTransaction(TestData.OperatorId, "111", "111", "SUCCESS", "1234", "0000", "SUCCESS"); + transactionAggregate.AuthoriseTransaction(TestData.OperatorId, "111", "111", "SUCCESS", "1234", TransactionResponseCode.Success, "SUCCESS"); transactionAggregate.CompleteTransaction(); diff --git a/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs b/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs index 3595245..ed67766 100644 --- a/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs +++ b/TransactionProcessor.BusinessLogic/Services/TransactionDomainService.cs @@ -114,13 +114,13 @@ public async Task> ProcessLogonTransacti } // Record the successful validation - stateResult = transactionAggregate.AuthoriseTransactionLocally(TransactionHelpers.GenerateAuthCode(), ((Int32)validationResult.Data.ResponseCode).ToString().PadLeft(4, '0'), validationResult.Data.ResponseMessage); + stateResult = transactionAggregate.AuthoriseTransactionLocally(TransactionHelpers.GenerateAuthCode(), validationResult.Data.ResponseCode, validationResult.Data.ResponseMessage); if (stateResult.IsFailed) return ResultHelpers.CreateFailure(stateResult); } else { // Record the failure - stateResult = transactionAggregate.DeclineTransactionLocally(((Int32)validationResult.Data.ResponseCode).ToString().PadLeft(4, '0'), validationResult.Data.ResponseMessage); + stateResult = transactionAggregate.DeclineTransactionLocally(validationResult.Data.ResponseCode, validationResult.Data.ResponseMessage); if (stateResult.IsFailed) return ResultHelpers.CreateFailure(stateResult); } @@ -170,13 +170,13 @@ public async Task> ProcessRecon if (validationResult.IsSuccess && validationResult.Data.ResponseCode == TransactionResponseCode.Success) { // Record the successful validation - stateResult = reconciliationAggregate.Authorise(((Int32)validationResult.Data.ResponseCode).ToString().PadLeft(4, '0'), validationResult.Data.ResponseMessage); + stateResult = reconciliationAggregate.Authorise(validationResult.Data.ResponseCode, validationResult.Data.ResponseMessage); if (stateResult.IsFailed) return ResultHelpers.CreateFailure(stateResult); } else { // Record the failure - stateResult = reconciliationAggregate.Decline(((Int32)validationResult.Data.ResponseCode).ToString().PadLeft(4, '0'), validationResult.Data.ResponseMessage); + stateResult = reconciliationAggregate.Decline(validationResult.Data.ResponseCode, validationResult.Data.ResponseMessage); if (stateResult.IsFailed) return ResultHelpers.CreateFailure(stateResult); } @@ -203,142 +203,7 @@ public async Task> ProcessRecon return Result.Failure(ex.GetExceptionMessages()); } } - - public async Task> ProcessSaleTransaction(TransactionCommands.ProcessSaleTransactionCommand command, - CancellationToken cancellationToken) { - try - { - Result transactionResult = await DomainServiceHelper.GetAggregateOrFailure(ct => this.AggregateService.GetLatest(command.TransactionId, ct), command.TransactionId, cancellationToken, false); - if (transactionResult.IsFailed) - return ResultHelpers.CreateFailure(transactionResult); - - TransactionAggregate transactionAggregate = transactionResult.Data; - - TransactionType transactionType = TransactionType.Sale; - TransactionSource transactionSourceValue = (TransactionSource)command.TransactionSource; - - // Generate a transaction reference - String transactionReference = TransactionHelpers.GenerateTransactionReference(); - - // Extract the transaction amount from the metadata - Decimal? transactionAmount = command.AdditionalTransactionMetadata.ExtractFieldFromMetadata("Amount"); - - Result validationResult = await this.TransactionValidationService.ValidateSaleTransaction(command.EstateId, command.MerchantId, command.ContractId, command.ProductId, command.DeviceIdentifier, command.OperatorId, transactionAmount, cancellationToken); - - Logger.LogInformation($"Validation response is [{JsonConvert.SerializeObject(validationResult)}]"); - - Guid floatAggregateId = IdGenerationService.GenerateFloatAggregateId(command.EstateId, command.ContractId, command.ProductId); - Result floatAggregateResult = await DomainServiceHelper.GetAggregateOrFailure(ct => this.AggregateService.GetLatest(floatAggregateId, ct), floatAggregateId, cancellationToken, false); - Decimal unitCost = 0; - Decimal totalCost = 0; - if (floatAggregateResult.IsSuccess) { - FloatAggregate floatAggregate = floatAggregateResult.Data; - unitCost = floatAggregate.GetUnitCostPrice(); - totalCost = floatAggregate.GetTotalCostPrice(transactionAmount.GetValueOrDefault()); - } - - Result stateResult = transactionAggregate.StartTransaction(command.TransactionDateTime, command.TransactionNumber, transactionType, transactionReference, command.EstateId, command.MerchantId, command.DeviceIdentifier, transactionAmount); - if (stateResult.IsFailed) - return ResultHelpers.CreateFailure(stateResult); - - // Add the product details (unless invalid estate) - if (validationResult.Data.ResponseCode != TransactionResponseCode.InvalidEstateId && validationResult.Data.ResponseCode != TransactionResponseCode.InvalidContractIdValue && validationResult.Data.ResponseCode != TransactionResponseCode.InvalidProductIdValue && validationResult.Data.ResponseCode != TransactionResponseCode.ContractNotValidForMerchant && validationResult.Data.ResponseCode != TransactionResponseCode.ProductNotValidForMerchant) { - stateResult = transactionAggregate.AddProductDetails(command.ContractId, command.ProductId); - if (stateResult.IsFailed) - return ResultHelpers.CreateFailure(stateResult); - } - - stateResult = transactionAggregate.RecordCostPrice(unitCost, totalCost); - if (stateResult.IsFailed) - return ResultHelpers.CreateFailure(stateResult); - - // Add the transaction source - stateResult = transactionAggregate.AddTransactionSource(transactionSourceValue); - if (stateResult.IsFailed) - return ResultHelpers.CreateFailure(stateResult); - DateTime? operatorStartDateTime = null; - DateTime? operatorEndDateTime = null; - if (validationResult.Data.ResponseCode == TransactionResponseCode.Success) { - // Record any additional request metadata - stateResult = transactionAggregate.RecordAdditionalRequestData(command.OperatorId, command.AdditionalTransactionMetadata); - if (stateResult.IsFailed) - return ResultHelpers.CreateFailure(stateResult); - - // Do the online processing with the operator here - Result merchantResult = await this.GetMerchant(command.MerchantId, cancellationToken); - if (merchantResult.IsFailed) - return ResultHelpers.CreateFailure(merchantResult); - operatorStartDateTime =DateTime.Now; - Result operatorResult = await this.ProcessMessageWithOperator(merchantResult.Data, command.TransactionId, command.TransactionDateTime, command.OperatorId, command.AdditionalTransactionMetadata, transactionReference, cancellationToken); - operatorEndDateTime = DateTime.Now; - - if (operatorResult.IsSuccess) { - TransactionResponseCode transactionResponseCode = TransactionResponseCode.Success; - String responseMessage = "SUCCESS"; - - stateResult= transactionAggregate.AuthoriseTransaction(command.OperatorId, operatorResult.Data.AuthorisationCode, operatorResult.Data.ResponseCode, operatorResult.Data.ResponseMessage, operatorResult.Data.TransactionId, ((Int32)transactionResponseCode).ToString().PadLeft(4, '0'), responseMessage); - if (stateResult.IsFailed) - return ResultHelpers.CreateFailure(stateResult); - } - else { - TransactionResponseCode transactionResponseCode = TransactionResponseCode.TransactionDeclinedByOperator; - String responseMessage = "DECLINED BY OPERATOR"; - - stateResult = transactionAggregate.DeclineTransaction(command.OperatorId, operatorResult.Data.ResponseCode, operatorResult.Data.ResponseMessage, ((Int32)transactionResponseCode).ToString().PadLeft(4, '0'), responseMessage); - if (stateResult.IsFailed) - return ResultHelpers.CreateFailure(stateResult); - } - - // Record any additional operator response metadata - stateResult = transactionAggregate.RecordAdditionalResponseData(command.OperatorId, operatorResult.Data.AdditionalTransactionResponseMetadata); - if (stateResult.IsFailed) - return ResultHelpers.CreateFailure(stateResult); - } - else { - // Record the failure - stateResult = transactionAggregate.DeclineTransactionLocally(((Int32)validationResult.Data.ResponseCode).ToString().PadLeft(4, '0'), validationResult.Data.ResponseMessage); - if (stateResult.IsFailed) - return ResultHelpers.CreateFailure(stateResult); - } - - stateResult = transactionAggregate.RecordTransactionTimings(command.TransactionReceivedDateTime, operatorStartDateTime, operatorEndDateTime, DateTime.Now);; - if (stateResult.IsFailed) - return ResultHelpers.CreateFailure(stateResult); - - stateResult= transactionAggregate.CompleteTransaction(); - if (stateResult.IsFailed) - return ResultHelpers.CreateFailure(stateResult); - - // Determine if the email receipt is required - if (String.IsNullOrEmpty(command.CustomerEmailAddress) == false) { - stateResult = transactionAggregate.RequestEmailReceipt(command.CustomerEmailAddress); - if (stateResult.IsFailed) - return ResultHelpers.CreateFailure(stateResult); - } - - // Get the model from the aggregate - Models.Transaction transaction = transactionAggregate.GetTransaction(); - - Result saveResult = await this.AggregateService.Save(transactionAggregate, cancellationToken); - if (saveResult.IsFailed) - return ResultHelpers.CreateFailure(saveResult); - - return Result.Success(new ProcessSaleTransactionResponse - { - ResponseMessage = transaction.ResponseMessage, - ResponseCode = transaction.ResponseCode, - EstateId = command.EstateId, - MerchantId = command.MerchantId, - AdditionalTransactionMetadata = transaction.AdditionalResponseMetadata, - TransactionId = command.TransactionId - }); - } - catch (Exception ex) - { - return Result.Failure(ex.GetExceptionMessages()); - } - } - + public async Task ResendTransactionReceipt(TransactionCommands.ResendTransactionReceiptCommand command, CancellationToken cancellationToken) { @@ -666,5 +531,161 @@ private async Task ResendEmailMessage(String accessToken, List transactionFees = product.TransactionFees.ToList(); return Result.Success(transactionFees); } + + public async Task> ProcessSaleTransaction(TransactionCommands.ProcessSaleTransactionCommand command, + CancellationToken cancellationToken) { + Logger.LogInformation($"Starting ProcessSaleTransaction for TransactionId {command.TransactionId}"); + + try { + Result transactionResult = await GetTransactionAggregate(command, cancellationToken); + if (transactionResult.IsFailed) + return ResultHelpers.CreateFailure(transactionResult); + + TransactionAggregate transactionAggregate = transactionResult.Data; + + Result validationResult = await ValidateTransaction(command, cancellationToken); + (Decimal unitCost, Decimal totalCost) = await GetFloatCost(command, cancellationToken); + + Result startResult = StartTransaction(transactionAggregate, command, unitCost, totalCost, validationResult); + if (startResult.IsFailed) + return ResultHelpers.CreateFailure(startResult); + + (DateTime? Start, DateTime? End) operatorTiming = await HandleOperatorProcessing(transactionAggregate, command, validationResult, cancellationToken); + + transactionAggregate.RecordTransactionTimings(command.TransactionReceivedDateTime, operatorTiming.Start, operatorTiming.End, DateTime.Now); + + transactionAggregate.CompleteTransaction(); + + if (!string.IsNullOrEmpty(command.CustomerEmailAddress)) + transactionAggregate.RequestEmailReceipt(command.CustomerEmailAddress); + + Result saveResult = await AggregateService.Save(transactionAggregate, cancellationToken); + if (saveResult.IsFailed) + return ResultHelpers.CreateFailure(saveResult); + + Transaction transaction = transactionAggregate.GetTransaction(); + Logger.LogInformation($"Transaction {command.TransactionId} completed successfully"); + + return Result.Success(new ProcessSaleTransactionResponse { + ResponseMessage = transaction.ResponseMessage, + ResponseCode = transaction.ResponseCode, + EstateId = command.EstateId, + MerchantId = command.MerchantId, + AdditionalTransactionMetadata = transaction.AdditionalResponseMetadata, + TransactionId = command.TransactionId + }); + } + catch (Exception ex) { + Logger.LogError($"Unhandled exception during ProcessSaleTransaction for {command.TransactionId}", ex); + return Result.Failure(ex.GetExceptionMessages()); + } + } + + private async Task> GetTransactionAggregate(TransactionCommands.ProcessSaleTransactionCommand command, + CancellationToken cancellationToken) { + Logger.LogDebug($"Fetching TransactionAggregate for TransactionId {command.TransactionId}"); + + Result result = await DomainServiceHelper.GetAggregateOrFailure(ct => AggregateService.GetLatest(command.TransactionId, ct), command.TransactionId, cancellationToken, false); + + if (result.IsFailed) + Logger.LogWarning($"TransactionAggregate not found for TransactionId {command.TransactionId}"); + + return result; + } + + + private async Task> ValidateTransaction(TransactionCommands.ProcessSaleTransactionCommand command, + CancellationToken cancellationToken) { + Decimal? amount = command.AdditionalTransactionMetadata.ExtractFieldFromMetadata("Amount"); + Logger.LogInformation($"Validating Sale Transaction for Merchant {command.MerchantId}, Product {command.ProductId}"); + + Result result = await TransactionValidationService.ValidateSaleTransaction(command.EstateId, command.MerchantId, command.ContractId, command.ProductId, command.DeviceIdentifier, command.OperatorId, amount, cancellationToken); + + Logger.LogInformation($"Validation completed with ResponseCode {result.Data?.ResponseCode}, Message {result.Data?.ResponseMessage}"); + + return result; + } + + + private async Task<(decimal UnitCost, decimal TotalCost)> GetFloatCost(TransactionCommands.ProcessSaleTransactionCommand command, + CancellationToken cancellationToken) { + Guid floatAggregateId = IdGenerationService.GenerateFloatAggregateId(command.EstateId, command.ContractId, command.ProductId); + + Logger.LogDebug($"Fetching FloatAggregate {floatAggregateId}"); + + Result floatAggregateResult = await DomainServiceHelper.GetAggregateOrFailure(ct => AggregateService.GetLatest(floatAggregateId, ct), floatAggregateId, cancellationToken, false); + + if (floatAggregateResult.IsFailed) + return (0, 0); + + FloatAggregate floatAggregate = floatAggregateResult.Data; + Decimal amount = command.AdditionalTransactionMetadata.ExtractFieldFromMetadata("Amount") ?? 0; + Decimal unitCost = floatAggregate.GetUnitCostPrice(); + Decimal totalCost = floatAggregate.GetTotalCostPrice(amount); + + Logger.LogInformation($"Float cost calculated: UnitCost={unitCost}, TotalCost={totalCost}"); + return (unitCost, totalCost); + } + + + private Result StartTransaction(TransactionAggregate transactionAggregate, + TransactionCommands.ProcessSaleTransactionCommand command, + decimal unitCost, + decimal totalCost, + Result validationResult) { + TransactionType transactionType = TransactionType.Sale; + TransactionSource transactionSourceValue = (TransactionSource)command.TransactionSource; + String transactionReference = TransactionHelpers.GenerateTransactionReference(); + Decimal? amount = command.AdditionalTransactionMetadata.ExtractFieldFromMetadata("Amount"); + + Result result = transactionAggregate.StartTransaction(command.TransactionDateTime, command.TransactionNumber, transactionType, transactionReference, command.EstateId, command.MerchantId, command.DeviceIdentifier, amount); + + if (result.IsFailed) + return result; + + if (validationResult.Data.ResponseCode is not (TransactionResponseCode.InvalidEstateId or TransactionResponseCode.InvalidContractIdValue or TransactionResponseCode.InvalidProductIdValue or TransactionResponseCode.ContractNotValidForMerchant or TransactionResponseCode.ProductNotValidForMerchant)) { + transactionAggregate.AddProductDetails(command.ContractId, command.ProductId); + } + + transactionAggregate.RecordCostPrice(unitCost, totalCost); + transactionAggregate.AddTransactionSource(transactionSourceValue); + return Result.Success(); + } + + private async Task<(DateTime? Start, DateTime? End)> HandleOperatorProcessing(TransactionAggregate transactionAggregate, + TransactionCommands.ProcessSaleTransactionCommand command, + Result validationResult, + CancellationToken cancellationToken) { + DateTime? start = null, end = null; + + if (validationResult.Data.ResponseCode != TransactionResponseCode.Success) { + Logger.LogWarning($"Validation failed with ResponseCode {validationResult.Data.ResponseCode}: {validationResult.Data.ResponseMessage}"); + transactionAggregate.DeclineTransactionLocally(validationResult.Data.ResponseCode, validationResult.Data.ResponseMessage); + return (start, end); + } + + Logger.LogInformation($"Validation success, processing with Operator {command.OperatorId}"); + + transactionAggregate.RecordAdditionalRequestData(command.OperatorId, command.AdditionalTransactionMetadata); + Result merchantResult = await GetMerchant(command.MerchantId, cancellationToken); + if (merchantResult.IsFailed) + return (start, end); + + start = DateTime.Now; + Result operatorResult = await ProcessMessageWithOperator(merchantResult.Data, command.TransactionId, command.TransactionDateTime, command.OperatorId, command.AdditionalTransactionMetadata, TransactionHelpers.GenerateTransactionReference(), cancellationToken); + end = DateTime.Now; + + if (operatorResult.IsSuccess) { + Logger.LogInformation("Operator authorised transaction successfully"); + transactionAggregate.AuthoriseTransaction(command.OperatorId, operatorResult.Data.AuthorisationCode, operatorResult.Data.ResponseCode, operatorResult.Data.ResponseMessage, operatorResult.Data.TransactionId, TransactionResponseCode.Success, "SUCCESS"); + } + else { + Logger.LogWarning($"Operator declined transaction: {operatorResult.Data.ResponseMessage}"); + transactionAggregate.DeclineTransaction(command.OperatorId, operatorResult.Data.ResponseCode, operatorResult.Data.ResponseMessage, TransactionResponseCode.TransactionDeclinedByOperator, "DECLINED BY OPERATOR"); + } + + transactionAggregate.RecordAdditionalResponseData(command.OperatorId, operatorResult.Data.AdditionalTransactionResponseMetadata); + return (start, end); + } } } \ No newline at end of file diff --git a/TransactionProcessor.BusinessLogic/Services/TransactionResponseCode.cs b/TransactionProcessor.BusinessLogic/Services/TransactionResponseCode.cs index 9feb7bc..d35c668 100644 --- a/TransactionProcessor.BusinessLogic/Services/TransactionResponseCode.cs +++ b/TransactionProcessor.BusinessLogic/Services/TransactionResponseCode.cs @@ -1,6 +1,6 @@ namespace TransactionProcessor.BusinessLogic.Services { - public enum TransactionResponseCode + public enum TransactionResponseCodeX { Success = 0, SuccessNeedToAddDevice = 1, diff --git a/TransactionProcessor.BusinessLogic/Services/TransactionValidationException.cs b/TransactionProcessor.BusinessLogic/Services/TransactionValidationException.cs index 93a4f1f..0080995 100644 --- a/TransactionProcessor.BusinessLogic/Services/TransactionValidationException.cs +++ b/TransactionProcessor.BusinessLogic/Services/TransactionValidationException.cs @@ -1,4 +1,6 @@ -namespace TransactionProcessor.BusinessLogic.Services +using TransactionProcessor.Aggregates; + +namespace TransactionProcessor.BusinessLogic.Services { using System; using System.Diagnostics.CodeAnalysis; diff --git a/TransactionProcessor.Testing/TestData.cs b/TransactionProcessor.Testing/TestData.cs index c67fbbe..a7fcfb4 100644 --- a/TransactionProcessor.Testing/TestData.cs +++ b/TransactionProcessor.Testing/TestData.cs @@ -348,7 +348,7 @@ public class TestData public static String AuthorisationCode = "ABCD1234"; - public static String DeclinedResponseCode = "0001"; + public static TransactionResponseCode DeclinedResponseCode = TransactionResponseCode.InvalidEstateId; public static String DeclinedResponseMessage = "DeclinedResponseMessage"; @@ -396,7 +396,7 @@ public class TestData public static Boolean RequireCustomTerminalNumberTrue = true; - public static String ResponseCode = "0000"; + public static TransactionResponseCode ResponseCode = TransactionResponseCode.Success; public static String ResponseMessage = "SUCCESS"; @@ -1038,14 +1038,14 @@ public static Dictionary AdditionalTransactionMetaDataForPataPaw new ProcessLogonTransactionResponse { ResponseMessage = TestData.ResponseMessage, - ResponseCode = TestData.ResponseCode + ResponseCode = TestData.ResponseCode.ToCodeString() }; public static ProcessSaleTransactionResponse ProcessSaleTransactionResponseModel => new ProcessSaleTransactionResponse { ResponseMessage = TestData.ResponseMessage, - ResponseCode = TestData.ResponseCode, + ResponseCode = TestData.ResponseCode.ToCodeString(), AdditionalTransactionMetadata = new Dictionary { {"OperatorResponseCode", "1000"} @@ -1069,7 +1069,7 @@ public static Dictionary AdditionalTransactionMetaDataForPataPaw new ProcessReconciliationTransactionResponse { ResponseMessage = TestData.ResponseMessage, - ResponseCode = TestData.ResponseCode + ResponseCode = TestData.ResponseCode.ToCodeString() }; #endregion @@ -1335,7 +1335,7 @@ public static TransactionAggregate GetLocallyDeclinedTransactionAggregate(Transa TestData.DeviceIdentifier, TestData.TransactionAmount); - transactionAggregate.DeclineTransactionLocally(TestData.GetResponseCodeAsString(transactionResponseCode), + transactionAggregate.DeclineTransactionLocally(transactionResponseCode, TestData.GetResponseCodeMessage(transactionResponseCode)); return transactionAggregate; @@ -1357,7 +1357,7 @@ public static TransactionAggregate GetDeclinedTransactionAggregate(TransactionRe transactionAggregate.DeclineTransaction(TestData.OperatorId, TestData.DeclinedOperatorResponseCode, TestData.DeclinedOperatorResponseMessage, - TestData.GetResponseCodeAsString(transactionResponseCode), + transactionResponseCode, TestData.GetResponseCodeMessage(transactionResponseCode)); return transactionAggregate; @@ -2603,7 +2603,7 @@ public static class DomainEvents { public static TransactionDomainEvents.TransactionHasBeenCompletedEvent TransactionHasBeenCompletedEvent => new TransactionDomainEvents.TransactionHasBeenCompletedEvent(TestData.TransactionId, TestData.EstateId, TestData.MerchantId, - TestData.ResponseCode, + TestData.ResponseCode.ToCodeString(), TestData.ResponseMessage, TestData.IsAuthorised, TestData.TransactionDateTime,