From 2f2f4a882507208b1c0bbb567e683e3086f531af Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Tue, 9 Jun 2026 12:50:47 +0100 Subject: [PATCH] Refactor transaction processing for clarity and modularity Extracted metadata, transaction, and contract logic into private methods for improved readability and separation of concerns. Enhanced error handling, streamlined main processing flow, and used switch expressions for result handling. OperatorName extraction and metadata manipulation are now encapsulated. --- .../Services/FileProcessorDomainService.cs | 214 ++++++++++-------- 1 file changed, 122 insertions(+), 92 deletions(-) diff --git a/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs b/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs index 6fbefb7..a5172e1 100644 --- a/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs +++ b/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs @@ -321,131 +321,161 @@ public async Task ProcessTransactionForFileLine(FileCommands.ProcessTran return ResultHelpers.CreateFailure(fileProfileResult); FileProfile fileProfile = fileProfileResult.Data; + + Result stateResult; // Determine if we need to actually process this file line if (this.FileLineCanBeIgnored(fileLine.LineData, fileProfile.FileFormatHandler)) { // Write something to aggregate to say line was explicity ignored - Result stateResult = fileAggregate.RecordFileLineAsIgnored(fileLine.LineNumber); + stateResult = fileAggregate.RecordFileLineAsIgnored(fileLine.LineNumber); if (stateResult.IsFailed) return stateResult; return Result.Success(); } - // need to now parse the line (based on the file format), this builds the metadata - Dictionary transactionMetadata = this.ParseFileLine(fileLine.LineData, fileProfile.FileFormatHandler); - - if (transactionMetadata.Any() == false) { - // Line failed to parse so record this - Result stateResult = fileAggregate.RecordFileLineAsRejected(fileLine.LineNumber, "Invalid Format"); + Result> metadataResult = this.BuildTransactionMetadata(command, fileLine, fileProfile, out String operatorName); + if (metadataResult.IsFailed) { + stateResult = fileAggregate.RecordFileLineAsRejected(fileLine.LineNumber, "Invalid Format"); if (stateResult.IsFailed) - return stateResult; - return Result.Success(); - } + return ResultHelpers.CreateFailure(stateResult); - // Add the file data to the request metadata - transactionMetadata.Add("FileId", command.FileId.ToString()); - transactionMetadata.Add("FileLineNumber", fileLine.LineNumber.ToString()); - - String operatorName = fileProfile.OperatorName; - if (transactionMetadata.ContainsKey("OperatorName")) { - // extract the value - operatorName = transactionMetadata["OperatorName"]; - transactionMetadata = transactionMetadata.Where(x => x.Key != "OperatorName").ToDictionary(x => x.Key, x => x.Value); - } - - Result getTokenResult = await this.GetToken(cancellationToken); - if (getTokenResult.IsFailed) { - return ResultHelpers.CreateFailure(getTokenResult); + return stateResult; } - this.TokenResponse = getTokenResult.Data; - Interlocked.Increment(ref TransactionNumber); - // Get the merchant details - Result getMerchantResult = await this.TransactionProcessorClient.GetMerchant(this.TokenResponse.AccessToken, fileDetails.EstateId, fileDetails.MerchantId, cancellationToken); - if (getMerchantResult.IsFailed) { - return ResultHelpers.CreateFailure(getMerchantResult); - } + Result<(Guid ContractId, Guid OperatorId, Guid ProductId, String MerchantDevice)> detailsForTransaction = await this.GetDetailsForTransaction(cancellationToken, fileDetails, fileProfile, operatorName); + if (detailsForTransaction.IsFailed) + return ResultHelpers.CreateFailure(detailsForTransaction); + + Result saleResult = await this.SendSaleTransaction(fileDetails, detailsForTransaction, metadataResult.Data, cancellationToken); + + stateResult = saleResult switch { + _ when saleResult.IsSuccess && saleResult.Data.ResponseCode == "0000" => fileAggregate.RecordFileLineAsSuccessful(command.LineNumber, saleResult.Data.TransactionId), + _ when saleResult.IsSuccess => fileAggregate.RecordFileLineAsFailed(command.LineNumber, saleResult.Data.TransactionId, saleResult.Data.ResponseCode, saleResult.Data.ResponseMessage), + _ when saleResult.IsFailed => fileAggregate.RecordFileLineAsFailed(command.LineNumber, Guid.Empty, "9999", "Failed to Send to Transaction Processor"), + }; - MerchantResponse merchant = getMerchantResult.Data; + return stateResult; + }, command.FileId, cancellationToken); - Result> getContractsResult = await this.TransactionProcessorClient.GetMerchantContracts(this.TokenResponse.AccessToken, fileDetails.EstateId, fileDetails.MerchantId, cancellationToken); - if (getContractsResult.IsFailed) { - return ResultHelpers.CreateFailure(getContractsResult); - } + if (result.IsFailed) + Logger.LogWarning($"{command.LineNumber} Status {result.Status} Message {result.Message}"); - List contracts = getContractsResult.Data; + return result; + } - if (contracts.Any() == false) { - return Result.NotFound($"No contracts found for Merchant Id {fileDetails.MerchantId} on estate Id {fileDetails.EstateId}"); - } + private Result> BuildTransactionMetadata(FileCommands.ProcessTransactionForFileLineCommand command, + FileLine fileLine, + FileProfile fileProfile, + out String operatorName) { + // need to now parse the line (based on the file format), this builds the metadata + Dictionary transactionMetadata = this.ParseFileLine(fileLine.LineData, fileProfile.FileFormatHandler); + + if (transactionMetadata.Any() == false) { + // Line failed to parse so record this + operatorName = null; + return Result.Failure("Invalid Format"); + } - ContractResponse? contract = null; - if (fileProfile.OperatorName == "Voucher") { - contract = contracts.SingleOrDefault(c => c.Description.Contains(operatorName)); - } - else { - contract = contracts.SingleOrDefault(c => c.OperatorName == operatorName); - } + // Add the file data to the request metadata + transactionMetadata.Add("FileId", command.FileId.ToString()); + transactionMetadata.Add("FileLineNumber", fileLine.LineNumber.ToString()); - if (contract == null) { - return Result.NotFound($"No merchant contract for operator Id {operatorName} found for Merchant Id {merchant.MerchantId}"); - } + operatorName = fileProfile.OperatorName; + if (transactionMetadata.ContainsKey("OperatorName")) { + // extract the value + operatorName = transactionMetadata["OperatorName"]; + transactionMetadata = transactionMetadata.Where(x => x.Key != "OperatorName").ToDictionary(x => x.Key, x => x.Value); + } - ContractProduct? product = contract.Products.SingleOrDefault(p => p.Value == null); + return Result.Success(transactionMetadata); + } - if (product == null) { - return Result.NotFound($"No variable value product found on the merchant contract for operator Id {fileProfile.OperatorName} and Merchant Id {merchant.MerchantId}"); - } + private async Task> SendSaleTransaction(FileDetails fileDetails, + Result<(Guid ContractId, Guid OperatorId, Guid ProductId, String MerchantDevice)> detailsForTransaction, + Dictionary transactionMetadata, + CancellationToken cancellationToken) { + Result getTokenResult = await this.GetToken(cancellationToken); + if (getTokenResult.IsFailed) { + return ResultHelpers.CreateFailure(getTokenResult); + } - // Build a transaction request message - SaleTransactionRequest saleTransactionRequest = new SaleTransactionRequest - { - EstateId = fileDetails.EstateId, - MerchantId = fileDetails.MerchantId, - TransactionDateTime = fileDetails.FileReceivedDateTime, - TransactionNumber = TransactionNumber.ToString(), - TransactionType = "Sale", - ContractId = contract.ContractId, - DeviceIdentifier = merchant.Devices.First().Value, - OperatorId = contract.OperatorId, - ProductId = product.ProductId, - AdditionalTransactionMetadata = transactionMetadata, - TransactionSource = 2, // File based transaction - }; + this.TokenResponse = getTokenResult.Data; + + // Build a transaction request message + SaleTransactionRequest saleTransactionRequest = new() { + EstateId = fileDetails.EstateId, + MerchantId = fileDetails.MerchantId, + TransactionDateTime = fileDetails.FileReceivedDateTime, + TransactionNumber = TransactionNumber.ToString(), + TransactionType = "Sale", + ContractId = detailsForTransaction.Data.ContractId, + DeviceIdentifier = detailsForTransaction.Data.MerchantDevice, + OperatorId = detailsForTransaction.Data.OperatorId, + ProductId = detailsForTransaction.Data.ProductId, + AdditionalTransactionMetadata = transactionMetadata, + TransactionSource = 2, // File based transaction + }; - // Send request to transaction processor - Result result= await this.TransactionProcessorClient.PerformTransaction(this.TokenResponse.AccessToken, saleTransactionRequest, cancellationToken); - if (result.IsFailed) - return ResultHelpers.CreateFailure(result); + // Send request to transaction processor + Result result= await this.TransactionProcessorClient.PerformTransaction(this.TokenResponse.AccessToken, saleTransactionRequest, cancellationToken); + if (result.IsFailed) + return ResultHelpers.CreateFailure(result); - // Get the sale transaction response - SaleTransactionResponse saleTransactionResponse = result.Data; + return result; + } - if (saleTransactionResponse.ResponseCode == "0000") { - // record response against file line in file aggregate - Result stateResult = fileAggregate.RecordFileLineAsSuccessful(command.LineNumber, saleTransactionResponse.TransactionId); - if (stateResult.IsFailed) - return stateResult; - } - else - { - Result stateResult = fileAggregate.RecordFileLineAsFailed(command.LineNumber, saleTransactionResponse.TransactionId, saleTransactionResponse.ResponseCode, saleTransactionResponse.ResponseMessage); - if (stateResult.IsFailed) - return stateResult; - } + private async Task> GetDetailsForTransaction(CancellationToken cancellationToken, + FileDetails fileDetails, + FileProfile fileProfile, + String operatorName) { - return Result.Success(); + Result getTokenResult = await this.GetToken(cancellationToken); + if (getTokenResult.IsFailed) + { + return ResultHelpers.CreateFailure(getTokenResult); + } - }, command.FileId, cancellationToken); + this.TokenResponse = getTokenResult.Data; - if (result.IsFailed) - Logger.LogWarning($"{command.LineNumber} Status {result.Status} Message {result.Message}"); + // Get the merchant details + Result getMerchantResult = await this.TransactionProcessorClient.GetMerchant(this.TokenResponse.AccessToken, fileDetails.EstateId, fileDetails.MerchantId, cancellationToken); + if (getMerchantResult.IsFailed) { + return ResultHelpers.CreateFailure(getMerchantResult); + } - return result; + MerchantResponse merchant = getMerchantResult.Data; + + Result> getContractsResult = await this.TransactionProcessorClient.GetMerchantContracts(this.TokenResponse.AccessToken, fileDetails.EstateId, fileDetails.MerchantId, cancellationToken); + if (getContractsResult.IsFailed) { + return ResultHelpers.CreateFailure(getContractsResult); + } + + List contracts = getContractsResult.Data; + + if (contracts.Any() == false) { + return Result.NotFound($"No contracts found for Merchant Id {fileDetails.MerchantId} on estate Id {fileDetails.EstateId}"); + } + + ContractResponse contract = fileProfile.OperatorName switch { + "Voucher" => contracts.SingleOrDefault(c => c.Description.Contains(operatorName)), + _ => contracts.SingleOrDefault(c => c.OperatorName == operatorName) + }; + + if (contract == null) { + return Result.NotFound($"No merchant contract for operator Id {operatorName} found for Merchant Id {merchant.MerchantId}"); + } + + ContractProduct product = contract.Products.SingleOrDefault(p => p.Value == null); + + if (product == null) { + return Result.NotFound($"No variable value product found on the merchant contract for operator Id {fileProfile.OperatorName} and Merchant Id {merchant.MerchantId}"); + } + + return Result.Success((contract.ContractId, contract.OperatorId, product.ProductId, merchant.Devices.First().Value)); } private async Task ProcessFile(Guid fileId,