From d31bd346de108e2fb7b46d881d96977d6c241ab7 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Mon, 8 Jun 2026 20:53:06 +0100 Subject: [PATCH] Refactor ProcessTransactionForFileLine for clarity Refactored ProcessTransactionForFileLine by extracting logic into dedicated helper methods, improving readability, modularity, and error handling. Centralized transaction context and execution, enhanced metadata handling, and improved logging for failures. --- .../Services/FileProcessorDomainService.cs | 339 ++++++++++++------ 1 file changed, 221 insertions(+), 118 deletions(-) diff --git a/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs b/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs index 6fbefb7..8a142ae 100644 --- a/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs +++ b/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs @@ -296,156 +296,259 @@ private async Task> GetOperatorIdForFileProfile(Guid estateId, Guid } public async Task ProcessTransactionForFileLine(FileCommands.ProcessTransactionForFileLineCommand command, - CancellationToken cancellationToken) { - Result result = await ApplyFileUpdates(async (FileAggregate fileAggregate) => { - FileDetails fileDetails = fileAggregate.GetFile(); + CancellationToken cancellationToken) + { + var result = await ApplyFileUpdates( + async fileAggregate => + { + var fileDetails = fileAggregate.GetFile(); - if (fileDetails.FileLines.Any() == false) { - return Result.Invalid($"File Id [{command.FileId}] has no lines added"); - } + var fileLineResult = GetFileLine(fileDetails, command); + if (fileLineResult.IsFailed) + return ResultHelpers.CreateFailure(fileLineResult); - FileLine fileLine = fileDetails.FileLines.SingleOrDefault(f => f.LineNumber == command.LineNumber); + var fileLine = fileLineResult.Data; - if (fileLine == null) { - return Result.NotFound($"File Line Number {command.LineNumber} not found in File Id {command.FileId}"); - } + if (IsAlreadyProcessed(fileLine)) + return Result.Success(); + + var profileResult = await GetFileProfile(fileDetails, cancellationToken); + if (profileResult.IsFailed) + return ResultHelpers.CreateFailure(profileResult); + + var profile = profileResult.Data; + + var parsedResult = await ProcessFileLine(fileAggregate, fileDetails, fileLine, profile, command, cancellationToken); + if (parsedResult.IsFailed) + return ResultHelpers.CreateFailure(parsedResult); - if (fileLine.ProcessingResult != ProcessingResult.NotProcessed) { - // Line already processed return Result.Success(); - } + }, + command.FileId, + cancellationToken); - Result fileProfileResult = await this.FileProcessorManager.GetFileProfile(fileDetails.FileProfileId, cancellationToken); + if (result.IsFailed) + Logger.LogWarning($"{command.LineNumber} Status {result.Status} Message {result.Message}"); - if (fileProfileResult.IsFailed) - return ResultHelpers.CreateFailure(fileProfileResult); + return result; + } - FileProfile fileProfile = fileProfileResult.Data; + private async Task> GetFileProfile(FileDetails fileDetails, + CancellationToken cancellationToken) + { + var result = await FileProcessorManager.GetFileProfile( + fileDetails.FileProfileId, + cancellationToken); - // 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); - if (stateResult.IsFailed) - return stateResult; - return Result.Success(); - } + if (result.IsFailed) + return ResultHelpers.CreateFailure(result); - // need to now parse the line (based on the file format), this builds the metadata - Dictionary transactionMetadata = this.ParseFileLine(fileLine.LineData, fileProfile.FileFormatHandler); + return Result.Success(result.Data); + } - if (transactionMetadata.Any() == false) { - // Line failed to parse so record this - Result stateResult = fileAggregate.RecordFileLineAsRejected(fileLine.LineNumber, "Invalid Format"); - if (stateResult.IsFailed) - return stateResult; - return Result.Success(); - } + private Result GetFileLine(FileDetails fileDetails, FileCommands.ProcessTransactionForFileLineCommand command) + { + if (!fileDetails.FileLines.Any()) + return Result.Invalid($"File Id [{command.FileId}] has no lines added"); - // Add the file data to the request metadata - transactionMetadata.Add("FileId", command.FileId.ToString()); - transactionMetadata.Add("FileLineNumber", fileLine.LineNumber.ToString()); + var fileLine = fileDetails.FileLines + .SingleOrDefault(x => x.LineNumber == command.LineNumber); - 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); - } + return fileLine is null + ? Result.NotFound($"File Line Number {command.LineNumber} not found in File Id {command.FileId}") + : Result.Success(fileLine); + } - Result getTokenResult = await this.GetToken(cancellationToken); - if (getTokenResult.IsFailed) { - return ResultHelpers.CreateFailure(getTokenResult); - } + private static bool IsAlreadyProcessed(FileLine fileLine) => + fileLine.ProcessingResult != ProcessingResult.NotProcessed; - this.TokenResponse = getTokenResult.Data; - Interlocked.Increment(ref TransactionNumber); + private async Task ProcessFileLine(FileAggregate fileAggregate, + FileDetails fileDetails, + FileLine fileLine, + FileProfile fileProfile, + FileCommands.ProcessTransactionForFileLineCommand command, + CancellationToken cancellationToken) + { + if (FileLineCanBeIgnored(fileLine.LineData, fileProfile.FileFormatHandler)) + { + var ignored = fileAggregate.RecordFileLineAsIgnored(fileLine.LineNumber); + return ignored.IsFailed ? ignored : Result.Success(); + } - // 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); - } + var metadata = ParseFileLine(fileLine.LineData, fileProfile.FileFormatHandler); - MerchantResponse merchant = getMerchantResult.Data; + if (!metadata.Any()) + { + var rejected = fileAggregate.RecordFileLineAsRejected(fileLine.LineNumber, "Invalid Format"); + return rejected.IsFailed ? rejected : Result.Success(); + } - Result> getContractsResult = await this.TransactionProcessorClient.GetMerchantContracts(this.TokenResponse.AccessToken, fileDetails.EstateId, fileDetails.MerchantId, cancellationToken); - if (getContractsResult.IsFailed) { - return ResultHelpers.CreateFailure(getContractsResult); - } + EnrichMetadata(metadata, command.FileId, fileLine.LineNumber); - List contracts = getContractsResult.Data; + var operatorName = ExtractOperatorName(ref metadata, fileProfile); - if (contracts.Any() == false) { - return Result.NotFound($"No contracts found for Merchant Id {fileDetails.MerchantId} on estate Id {fileDetails.EstateId}"); - } + var tokenResult = await GetToken(CancellationToken.None); + if (tokenResult.IsFailed) + return ResultHelpers.CreateFailure(tokenResult); - ContractResponse? contract = null; - if (fileProfile.OperatorName == "Voucher") { - contract = contracts.SingleOrDefault(c => c.Description.Contains(operatorName)); - } - else { - contract = contracts.SingleOrDefault(c => c.OperatorName == operatorName); - } + TokenResponse = tokenResult.Data; - if (contract == null) { - return Result.NotFound($"No merchant contract for operator Id {operatorName} found for Merchant Id {merchant.MerchantId}"); - } + var transactionContext = await BuildTransactionContext(fileDetails, operatorName, cancellationToken); + if (transactionContext.IsFailed) + return ResultHelpers.CreateFailure(transactionContext); - ContractProduct? product = contract.Products.SingleOrDefault(p => p.Value == null); + var txResult = await ExecuteTransaction(metadata, transactionContext.Data, fileDetails, cancellationToken); + if (txResult.IsFailed) + return ResultHelpers.CreateFailure(txResult); - 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 ApplyFileLineOutcome(fileAggregate, fileLine.LineNumber, txResult.Data); + } - // 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 - }; - - - // Send request to transaction processor - Result result= await this.TransactionProcessorClient.PerformTransaction(this.TokenResponse.AccessToken, saleTransactionRequest, cancellationToken); - if (result.IsFailed) - return ResultHelpers.CreateFailure(result); + private static void EnrichMetadata(Dictionary metadata, + Guid fileId, + int lineNumber) + { + metadata["FileId"] = fileId.ToString(); + metadata["FileLineNumber"] = lineNumber.ToString(); + } - // Get the sale transaction response - SaleTransactionResponse saleTransactionResponse = result.Data; + private static string ExtractOperatorName(ref Dictionary metadata, + FileProfile fileProfile) + { + var operatorName = fileProfile.OperatorName; - 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; - } + if (!metadata.TryGetValue("OperatorName", out var extracted)) + return operatorName; - return Result.Success(); + operatorName = extracted; - }, command.FileId, cancellationToken); + // remove it from metadata + metadata = metadata + .Where(x => x.Key != "OperatorName") + .ToDictionary(x => x.Key, x => x.Value); - if (result.IsFailed) - Logger.LogWarning($"{command.LineNumber} Status {result.Status} Message {result.Message}"); + return operatorName; + } - return result; + private async Task> BuildTransactionContext(FileDetails fileDetails, + string operatorName, + CancellationToken cancellationToken) + { + var tokenResult = await GetToken(cancellationToken); + if (tokenResult.IsFailed) + return ResultHelpers.CreateFailure(tokenResult); + + TokenResponse = tokenResult.Data; + + var merchantResult = await TransactionProcessorClient.GetMerchant(TokenResponse.AccessToken, + fileDetails.EstateId, + fileDetails.MerchantId, + cancellationToken); + + if (merchantResult.IsFailed) + return ResultHelpers.CreateFailure(merchantResult); + + var merchant = merchantResult.Data; + + var contractsResult = await TransactionProcessorClient.GetMerchantContracts(TokenResponse.AccessToken, + fileDetails.EstateId, + fileDetails.MerchantId, + cancellationToken); + + if (contractsResult.IsFailed) + return ResultHelpers.CreateFailure(contractsResult); + + var contracts = contractsResult.Data; + + if (!contracts.Any()) + return Result.NotFound($"No contracts found for Merchant Id {fileDetails.MerchantId} on estate Id {fileDetails.EstateId}"); + + var contract = ResolveContract(fileProfileOperator: operatorName, contracts); + + if (contract is null) + return Result.NotFound($"No merchant contract for operator Id {operatorName} found for Merchant Id {merchant.MerchantId}"); + + var product = contract.Products.SingleOrDefault(p => p.Value == null); + + if (product is null) + return Result.NotFound($"No variable value product found for operator {operatorName} and Merchant Id {merchant.MerchantId}"); + + return Result.Success(new TransactionContext + { + Merchant = merchant, + Contract = contract, + Product = product + }); + } + + private sealed class TransactionContext + { + public MerchantResponse Merchant { get; set; } = default!; + public ContractResponse Contract { get; set; } = default!; + public ContractProduct Product { get; set; } = default!; + } + + private ContractResponse? ResolveContract(string fileProfileOperator, List contracts) + { + if (fileProfileOperator == "Voucher") + { + return contracts.SingleOrDefault(c => + c.Description.Contains(fileProfileOperator)); + } + + return contracts.SingleOrDefault(c => + c.OperatorName == fileProfileOperator); + } + + private async Task> ExecuteTransaction( + Dictionary metadata, + TransactionContext context, + FileDetails fileDetails, + CancellationToken cancellationToken) + { + var request = new SaleTransactionRequest + { + EstateId = fileDetails.EstateId, + MerchantId = fileDetails.MerchantId, + TransactionDateTime = fileDetails.FileReceivedDateTime, + TransactionNumber = Interlocked.Increment(ref TransactionNumber).ToString(), + TransactionType = "Sale", + ContractId = context.Contract.ContractId, + DeviceIdentifier = context.Merchant.Devices.First().Value, + OperatorId = context.Contract.OperatorId, + ProductId = context.Product.ProductId, + AdditionalTransactionMetadata = metadata, + TransactionSource = 2 + }; + + return await TransactionProcessorClient.PerformTransaction( + TokenResponse.AccessToken, + request, + cancellationToken); + } + + private static Result ApplyFileLineOutcome( + FileAggregate fileAggregate, + int lineNumber, + SaleTransactionResponse response) + { + if (response.ResponseCode == "0000") + { + var success = fileAggregate.RecordFileLineAsSuccessful( + lineNumber, + response.TransactionId); + + return success.IsFailed ? success : Result.Success(); + } + + var failed = fileAggregate.RecordFileLineAsFailed( + lineNumber, + response.TransactionId, + response.ResponseCode, + response.ResponseMessage); + + return failed.IsFailed ? failed : Result.Success(); } private async Task ProcessFile(Guid fileId,