diff --git a/FileProcessor.BusinessLogic.Tests/CapturingLogger.cs b/FileProcessor.BusinessLogic.Tests/CapturingLogger.cs new file mode 100644 index 0000000..4d6ac68 --- /dev/null +++ b/FileProcessor.BusinessLogic.Tests/CapturingLogger.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; + +namespace FileProcessor.BusinessLogic.Tests; + +internal sealed class CapturingLogger : ILogger +{ + public List Messages { get; } = new(); + + public IDisposable BeginScope(TState state) where TState : notnull => NullScope.Instance; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, + EventId eventId, + TState state, + Exception exception, + Func formatter) + { + this.Messages.Add(formatter(state, exception)); + } + + private sealed class NullScope : IDisposable + { + public static NullScope Instance { get; } = new(); + + public void Dispose() + { + } + } +} diff --git a/FileProcessor.BusinessLogic.Tests/DomainEventHandlerTests.cs b/FileProcessor.BusinessLogic.Tests/DomainEventHandlerTests.cs index 55f3e47..d4ade47 100644 --- a/FileProcessor.BusinessLogic.Tests/DomainEventHandlerTests.cs +++ b/FileProcessor.BusinessLogic.Tests/DomainEventHandlerTests.cs @@ -12,6 +12,8 @@ namespace FileProcessor.BusinessLogic.Tests using FileImportLog.DomainEvents; using MediatR; using Imposter.Abstractions; + using Requests; + using SimpleResults; using Shouldly; using Shouldly.Configuration; using Testing; @@ -19,10 +21,38 @@ namespace FileProcessor.BusinessLogic.Tests public class DomainEventHandlerTests { + [Fact] + public async Task FileDomainEventHandler_FileLineAddedEvent_MediatorFailureIsReturned() + { + IMediatorImposter mediator = new IMediatorImposter(); + mediator.Send(Arg>.Any(), Arg.Any()) + .ReturnsAsync(Result.Failure("Processing failed")); + FileDomainEventHandler eventHandler = new(mediator.Instance()); + + Result result = await eventHandler.Handle(TestData.FileLineAddedEvent, CancellationToken.None); + + result.IsFailed.ShouldBeTrue(); + } + + [Fact] + public async Task FileDomainEventHandler_FileLineAddedEvent_MediatorExceptionIsPropagated() + { + IMediatorImposter mediator = new IMediatorImposter(); + mediator.Send(Arg>.Any(), Arg.Any()) + .ThrowsAsync(new InvalidOperationException("Mediator failed")); + FileDomainEventHandler eventHandler = new(mediator.Instance()); + + InvalidOperationException exception = await Should.ThrowAsync( + () => eventHandler.Handle(TestData.FileLineAddedEvent, CancellationToken.None)); + + exception.Message.ShouldBe("Mediator failed"); + } + [Fact] public void FileDomainEventHandler_FileLineAddedEvent_EventIsHandled() { IMediatorImposter mediator = new IMediatorImposter(); + mediator.Send(Arg>.Any(), Arg.Any()).ReturnsAsync(Result.Success()); FileDomainEventHandler eventHandler = new FileDomainEventHandler(mediator.Instance()); FileLineAddedEvent fileLineAddedEvent = TestData.FileLineAddedEvent; Should.NotThrow(async () => @@ -35,6 +65,7 @@ public void FileDomainEventHandler_FileLineAddedEvent_EventIsHandled() public void FileDomainEventHandler_FileAddedToImportLogEvent_EventIsHandled() { IMediatorImposter mediator = new IMediatorImposter(); + mediator.Send(Arg>.Any(), Arg.Any()).ReturnsAsync(Result.Success()); FileDomainEventHandler eventHandler = new FileDomainEventHandler(mediator.Instance()); FileAddedToImportLogEvent fileAddedToImportLogEvent = TestData.FileAddedToImportLogEvent; Should.NotThrow(async () => diff --git a/FileProcessor.BusinessLogic.Tests/FileProcessorDomainServiceTests.cs b/FileProcessor.BusinessLogic.Tests/FileProcessorDomainServiceTests.cs index 4631c4f..1105517 100644 --- a/FileProcessor.BusinessLogic.Tests/FileProcessorDomainServiceTests.cs +++ b/FileProcessor.BusinessLogic.Tests/FileProcessorDomainServiceTests.cs @@ -49,6 +49,7 @@ public class FileProcessorDomainServiceTests private FileProcessorDomainService FileProcessorDomainService; private MockFileSystem FileSystem; + private CapturingLogger DiagnosticLogger; public FileProcessorDomainServiceTests() { IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection(TestData.DefaultAppSettings).Build(); @@ -65,6 +66,7 @@ public FileProcessorDomainServiceTests() this.SecurityServiceClient = new ISecurityServiceClientImposter(); this.FileFormatHandler = new IFileFormatHandlerImposter(); this.FileSystem = new MockFileSystem(); + this.DiagnosticLogger = new CapturingLogger(); Func fileFormatHandlerResolver = (format) => { @@ -78,7 +80,7 @@ public FileProcessorDomainServiceTests() this.SecurityServiceClient.Instance(), fileFormatHandlerResolver, this.FileSystem); - Logger.Initialise(NullLogger.Instance); + Logger.Initialise(this.DiagnosticLogger); } [Fact] @@ -552,6 +554,9 @@ public async Task FileRequestHandler_ProcessTransactionForFileLineRequest_SaveFa Result result = await this.FileProcessorDomainService.ProcessTransactionForFileLine(TestData.ProcessTransactionForFileLineCommand, CancellationToken.None); result.IsFailed.ShouldBeTrue(); + this.DiagnosticLogger.Messages.ShouldContain(message => message.Contains("file-line-state-persisted/failed") && + message.Contains(TestData.FileId.ToString()) && + message.Contains(TestData.ProcessTransactionForFileLineCommand.LineNumber.ToString())); } [Theory] @@ -810,6 +815,11 @@ public async Task FileRequestHandler_ProcessTransactionForFileLineRequest_NoMerc var result = await this.FileProcessorDomainService.ProcessTransactionForFileLine(TestData.ProcessTransactionForFileLineCommand, CancellationToken.None); result.IsFailed.ShouldBeTrue(); result.Status.ShouldBe(ResultStatus.NotFound); + this.DiagnosticLogger.Messages.ShouldContain(message => message.Contains("contract-lookup") && + message.Contains(TestData.FileId.ToString()) && + message.Contains(TestData.ProcessTransactionForFileLineCommand.LineNumber.ToString())); + this.TransactionProcessorClient.PerformTransaction(Arg.Any(), Arg.Any(), Arg.Any()) + .Called(Count.Never()); } [Fact] @@ -944,6 +954,9 @@ public async Task FileRequestHandler_ProcessTransactionForFileLineRequest_Transa var result = await this.FileProcessorDomainService.ProcessTransactionForFileLine(TestData.ProcessTransactionForFileLineCommand, CancellationToken.None); result.IsFailed.ShouldBeTrue(); result.Status.ShouldBe(ResultStatus.Failure); + this.DiagnosticLogger.Messages.ShouldContain(message => message.Contains("transaction-dispatch-failed") && + message.Contains("PerformTransaction") && + message.Contains(TestData.FileId.ToString())); } [Fact] @@ -970,6 +983,9 @@ public async Task FileRequestHandler_ProcessTransactionForFileLineRequest_Transa var result = await this.FileProcessorDomainService.ProcessTransactionForFileLine(TestData.ProcessTransactionForFileLineCommand, CancellationToken.None); result.IsFailed.ShouldBeTrue(); result.Status.ShouldBe(ResultStatus.Failure); + this.DiagnosticLogger.Messages.ShouldContain(message => message.Contains("transaction-dispatch-completed") && + message.Contains(TestData.ResponseCodeFailed) && + message.Contains(TestData.FileId.ToString())); } diff --git a/FileProcessor.BusinessLogic/Common/FileLineProcessingDiagnostics.cs b/FileProcessor.BusinessLogic/Common/FileLineProcessingDiagnostics.cs new file mode 100644 index 0000000..3f1b10b --- /dev/null +++ b/FileProcessor.BusinessLogic/Common/FileLineProcessingDiagnostics.cs @@ -0,0 +1,148 @@ +using System; +using FileProcessor.BusinessLogic.Requests; +using FileProcessor.File.DomainEvents; +using FileProcessor.FileAggregate; +using FileProcessor.Models; +using Shared.Logger; +using SimpleResults; +using TransactionProcessor.DataTransferObjects; + +namespace FileProcessor.BusinessLogic.Common; + +internal enum FileLineProcessingStage +{ + FileAggregateLoad, + FileLineLookup, + FileProfileLookup, + LineIgnoreEvaluation, + TransactionDetailsParsing, + TokenAcquisition, + MerchantLookup, + ContractLookup, + ContractOperatorMatch, + VariableValueProductLookup, + MerchantDeviceResolution, + TransactionDetailsResolved, + TransactionDispatchStarted, + TransactionDispatchCompleted, + FileLineStateUpdate, + FileLineStatePersisted +} + +internal sealed class FileLineProcessingContext +{ + public FileLineProcessingContext(FileCommands.ProcessTransactionForFileLineCommand command) + { + this.Command = command; + } + + public FileCommands.ProcessTransactionForFileLineCommand Command { get; } + + public FileLineProcessingStage Stage { get; set; } = FileLineProcessingStage.FileAggregateLoad; + + public Boolean TransactionDispatchAttempted { get; set; } + + public Boolean TransactionDispatchSucceeded { get; set; } + + public Guid EstateId { get; set; } + + public Guid MerchantId { get; set; } + + public Guid FileProfileId { get; set; } + + public String OperatorName { get; set; } + + public String ResponseCode { get; set; } +} + +internal static class FileLineProcessingDiagnostics +{ + public static void LineProcessingStarted(FileLineProcessingContext context) + { + Logger.LogInformation($"line-processing-started FileId={context.Command.FileId} LineNumber={context.Command.LineNumber} EstateId={context.EstateId} MerchantId={context.MerchantId} FileProfileId={context.FileProfileId}"); + } + + public static void LineProcessingSkipped(FileLineProcessingContext context, ProcessingResult processingResult) + { + Logger.LogInformation($"file-line-processing-skipped FileId={context.Command.FileId} LineNumber={context.Command.LineNumber} ProcessingResult={processingResult}"); + } + + public static void LineIgnored(FileLineProcessingContext context) + { + Logger.LogInformation($"file-line-ignored FileId={context.Command.FileId} LineNumber={context.Command.LineNumber} EstateId={context.EstateId} MerchantId={context.MerchantId} Operator={context.OperatorName} Reason=Ignored by file format handler"); + } + + public static void LineRejected(FileLineProcessingContext context, String reason) + { + Logger.LogWarning($"file-line-rejected FileId={context.Command.FileId} LineNumber={context.Command.LineNumber} EstateId={context.EstateId} MerchantId={context.MerchantId} Operator={context.OperatorName} Reason={reason}"); + } + + public static void LineStateDetermined(FileLineProcessingContext context, ProcessingResult processingResult) + { + Logger.LogInformation($"file-line-state-determined FileId={context.Command.FileId} LineNumber={context.Command.LineNumber} ProcessingResult={processingResult} TransactionDispatchAttempted={context.TransactionDispatchAttempted} TransactionDispatchSucceeded={context.TransactionDispatchSucceeded} ResponseCode={context.ResponseCode}"); + } + + public static void ProcessingFailed(FileLineProcessingContext context, Result result) + { + String stage = context.Stage == FileLineProcessingStage.FileLineStatePersisted + ? "file-line-state-persisted/failed" + : GetStageName(context.Stage); + Logger.LogError($"{stage} failed FileId={context.Command.FileId} LineNumber={context.Command.LineNumber} EstateId={context.EstateId} MerchantId={context.MerchantId} FileProfileId={context.FileProfileId} Operator={context.OperatorName} TransactionDispatchAttempted={context.TransactionDispatchAttempted} TransactionDispatchSucceeded={context.TransactionDispatchSucceeded} ResponseCode={context.ResponseCode} ResultStatus={result.Status} Error={result.Message}"); + } + + public static void LineStatePersisted(FileLineProcessingContext context) + { + Logger.LogInformation($"file-line-state-persisted FileId={context.Command.FileId} LineNumber={context.Command.LineNumber} EstateId={context.EstateId} MerchantId={context.MerchantId} FileProfileId={context.FileProfileId} Operator={context.OperatorName} TransactionDispatchAttempted={context.TransactionDispatchAttempted} TransactionDispatchSucceeded={context.TransactionDispatchSucceeded} ResponseCode={context.ResponseCode}"); + } + + public static void TransactionDispatchStarted(FileLineProcessingContext context) + { + Logger.LogInformation($"transaction-dispatch-started FileId={context.Command.FileId} LineNumber={context.Command.LineNumber} EstateId={context.EstateId} MerchantId={context.MerchantId} Operator={context.OperatorName} ClientOperation=PerformTransaction"); + } + + public static void TransactionDispatchFailed(FileLineProcessingContext context, Result result) + { + Logger.LogError($"transaction-dispatch-failed FileId={context.Command.FileId} LineNumber={context.Command.LineNumber} EstateId={context.EstateId} MerchantId={context.MerchantId} Operator={context.OperatorName} ClientOperation=PerformTransaction ResultStatus={result.Status} Error={result.Message}"); + } + + public static void TransactionDispatchCompleted(FileLineProcessingContext context, SaleTransactionResponse response) + { + Logger.LogInformation($"transaction-dispatch-completed FileId={context.Command.FileId} LineNumber={context.Command.LineNumber} EstateId={context.EstateId} MerchantId={context.MerchantId} Operator={context.OperatorName} ClientOperation=PerformTransaction TransactionId={response.TransactionId} ResponseCode={response.ResponseCode} ResponseMessage={response.ResponseMessage}"); + } + + public static void EventHandlerDispatchFailed(FileLineAddedEvent domainEvent, Result result) + { + Logger.LogError($"event-handler-dispatch-failed FileId={domainEvent.FileId} LineNumber={domainEvent.LineNumber} EstateId={domainEvent.EstateId} MerchantId={domainEvent.MerchantId} EventType={nameof(FileLineAddedEvent)} ResultStatus={result.Status} Error={result.Message}"); + } + + public static void EventHandlerDispatchCompleted(FileLineAddedEvent domainEvent) + { + Logger.LogDebug($"event-handler-dispatch-completed FileId={domainEvent.FileId} LineNumber={domainEvent.LineNumber} EstateId={domainEvent.EstateId} MerchantId={domainEvent.MerchantId} EventType={nameof(FileLineAddedEvent)}"); + } + + public static void EventHandlerDispatchThrew(FileLineAddedEvent domainEvent, Exception exception) + { + Logger.LogError($"event-handler-dispatch-threw FileId={domainEvent.FileId} LineNumber={domainEvent.LineNumber} EstateId={domainEvent.EstateId} MerchantId={domainEvent.MerchantId} EventType={nameof(FileLineAddedEvent)} Exception={exception}"); + } + + private static String GetStageName(FileLineProcessingStage stage) => stage switch + { + FileLineProcessingStage.FileAggregateLoad => "file-aggregate-load", + FileLineProcessingStage.FileLineLookup => "file-line-lookup", + FileLineProcessingStage.FileProfileLookup => "file-profile-lookup", + FileLineProcessingStage.LineIgnoreEvaluation => "line-ignore-evaluation", + FileLineProcessingStage.TransactionDetailsParsing => "transaction-details-parsing", + FileLineProcessingStage.TokenAcquisition => "token-acquisition", + FileLineProcessingStage.MerchantLookup => "merchant-lookup", + FileLineProcessingStage.ContractLookup => "contract-lookup", + FileLineProcessingStage.ContractOperatorMatch => "contract-operator-match", + FileLineProcessingStage.VariableValueProductLookup => "variable-value-product-lookup", + FileLineProcessingStage.MerchantDeviceResolution => "merchant-device-resolution", + FileLineProcessingStage.TransactionDetailsResolved => "transaction-details-resolved", + FileLineProcessingStage.TransactionDispatchStarted => "transaction-dispatch-started", + FileLineProcessingStage.TransactionDispatchCompleted => "transaction-dispatch-completed", + FileLineProcessingStage.FileLineStateUpdate => "file-line-state-update", + FileLineProcessingStage.FileLineStatePersisted => "file-line-state-persisted", + _ => throw new ArgumentOutOfRangeException(nameof(stage), stage, "Unknown file-line processing stage") + }; +} diff --git a/FileProcessor.BusinessLogic/EventHandling/FileDomainEventHandler.cs b/FileProcessor.BusinessLogic/EventHandling/FileDomainEventHandler.cs index 6e68c92..b78b223 100644 --- a/FileProcessor.BusinessLogic/EventHandling/FileDomainEventHandler.cs +++ b/FileProcessor.BusinessLogic/EventHandling/FileDomainEventHandler.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; -using Shared.Logger; +using System; using SimpleResults; +using FileProcessor.BusinessLogic.Common; namespace FileProcessor.BusinessLogic.EventHandling { @@ -62,8 +63,25 @@ private async Task HandleSpecificDomainEvent(FileLineAddedEvent domainEv CancellationToken cancellationToken) { FileCommands.ProcessTransactionForFileLineCommand command = new (domainEvent.FileId, domainEvent.LineNumber, domainEvent.FileLine); + try + { + Result result = await this.Mediator.Send(command, cancellationToken); + if (result.IsFailed) + { + FileLineProcessingDiagnostics.EventHandlerDispatchFailed(domainEvent, result); + } + else + { + FileLineProcessingDiagnostics.EventHandlerDispatchCompleted(domainEvent); + } - return await this.Mediator.Send(command, cancellationToken); + return result; + } + catch (Exception ex) + { + FileLineProcessingDiagnostics.EventHandlerDispatchThrew(domainEvent, ex); + throw; + } } /// diff --git a/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs b/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs index a0d9b86..255a4fc 100644 --- a/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs +++ b/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs @@ -299,9 +299,15 @@ private async Task> GetOperatorIdForFileProfile(Guid estateId, Guid public async Task ProcessTransactionForFileLine(FileCommands.ProcessTransactionForFileLineCommand command, CancellationToken cancellationToken) { + FileLineProcessingContext processingContext = new(command); Result result = await ApplyFileUpdates(async (FileAggregate fileAggregate) => { FileDetails fileDetails = fileAggregate.GetFile(); + processingContext.EstateId = fileDetails.EstateId; + processingContext.MerchantId = fileDetails.MerchantId; + processingContext.FileProfileId = fileDetails.FileProfileId; + FileLineProcessingDiagnostics.LineProcessingStarted(processingContext); + processingContext.Stage = FileLineProcessingStage.FileLineLookup; if (fileDetails.FileLines.Any() == false) { return Result.Invalid($"File Id [{command.FileId}] has no lines added"); } @@ -314,56 +320,98 @@ public async Task ProcessTransactionForFileLine(FileCommands.ProcessTran if (fileLine.ProcessingResult != ProcessingResult.NotProcessed) { // Line already processed + FileLineProcessingDiagnostics.LineProcessingSkipped(processingContext, fileLine.ProcessingResult); return Result.Success(); } + processingContext.Stage = FileLineProcessingStage.FileProfileLookup; Result fileProfileResult = await this.FileProcessorManager.GetFileProfile(fileDetails.FileProfileId, cancellationToken); if (fileProfileResult.IsFailed) return ResultHelpers.CreateFailure(fileProfileResult); FileProfileModel fileProfile = fileProfileResult.Data; + processingContext.OperatorName = fileProfile.OperatorName; Result stateResult; // Determine if we need to actually process this file line + processingContext.Stage = FileLineProcessingStage.LineIgnoreEvaluation; if (this.FileLineCanBeIgnored(fileLine.LineData, fileProfile.FileFormatHandler)) { + processingContext.Stage = FileLineProcessingStage.FileLineStateUpdate; // Write something to aggregate to say line was explicity ignored stateResult = fileAggregate.RecordFileLineAsIgnored(fileLine.LineNumber); if (stateResult.IsFailed) return stateResult; + FileLineProcessingDiagnostics.LineIgnored(processingContext); + processingContext.Stage = FileLineProcessingStage.FileLineStatePersisted; return Result.Success(); } - Result> metadataResult = this.BuildTransactionMetadata(command, fileLine, fileProfile, out String operatorName); + processingContext.Stage = FileLineProcessingStage.TransactionDetailsParsing; + Result> metadataResult = this.BuildTransactionMetadata(command, fileLine, fileProfile, out String parsedOperatorName); + processingContext.OperatorName = parsedOperatorName; if (metadataResult.IsFailed) { + processingContext.Stage = FileLineProcessingStage.FileLineStateUpdate; stateResult = fileAggregate.RecordFileLineAsRejected(fileLine.LineNumber, "Invalid Format"); if (stateResult.IsFailed) return ResultHelpers.CreateFailure(stateResult); + if (stateResult.IsSuccess) + { + FileLineProcessingDiagnostics.LineRejected(processingContext, "Invalid Format"); + processingContext.Stage = FileLineProcessingStage.FileLineStatePersisted; + } return stateResult; } Interlocked.Increment(ref TransactionNumber); - Result<(Guid ContractId, Guid OperatorId, Guid ProductId, String MerchantDevice)> detailsForTransaction = await this.GetDetailsForTransaction(cancellationToken, fileDetails, fileProfile, operatorName); + Result<(Guid ContractId, Guid OperatorId, Guid ProductId, String MerchantDevice)> detailsForTransaction = await this.GetDetailsForTransaction(cancellationToken, + fileDetails, + fileProfile, + processingContext.OperatorName, + processingContext); if (detailsForTransaction.IsFailed) return ResultHelpers.CreateFailure(detailsForTransaction); - Result saleResult = await this.SendSaleTransaction(fileDetails, detailsForTransaction, metadataResult.Data, cancellationToken); + processingContext.Stage = FileLineProcessingStage.TransactionDetailsResolved; + Result saleResult = await this.SendSaleTransaction(fileDetails, + detailsForTransaction, + metadataResult.Data, + processingContext, + cancellationToken); + processingContext.TransactionDispatchSucceeded = saleResult.IsSuccess; + if (saleResult.IsSuccess) + processingContext.ResponseCode = saleResult.Data.ResponseCode; + processingContext.Stage = FileLineProcessingStage.FileLineStateUpdate; 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"), }; + ProcessingResult processingResult = saleResult.IsSuccess && saleResult.Data.ResponseCode == "0000" + ? ProcessingResult.Successful + : ProcessingResult.Failed; + FileLineProcessingDiagnostics.LineStateDetermined(processingContext, processingResult); + if (stateResult.IsSuccess) + processingContext.Stage = FileLineProcessingStage.FileLineStatePersisted; + return stateResult; }, command.FileId, cancellationToken); if (result.IsFailed) + { + FileLineProcessingDiagnostics.ProcessingFailed(processingContext, result); Logger.LogWarning($"{command.LineNumber} Status {result.Status} Message {result.Message}"); + } + else if (processingContext.Stage == FileLineProcessingStage.FileLineStatePersisted) + { + FileLineProcessingDiagnostics.LineStatePersisted(processingContext); + } return result; } @@ -398,7 +446,9 @@ private Result> BuildTransactionMetadata(FileCommands private async Task> SendSaleTransaction(FileDetails fileDetails, Result<(Guid ContractId, Guid OperatorId, Guid ProductId, String MerchantDevice)> detailsForTransaction, Dictionary transactionMetadata, + FileLineProcessingContext processingContext, CancellationToken cancellationToken) { + processingContext.Stage = FileLineProcessingStage.TokenAcquisition; Result getTokenResult = await this.GetToken(cancellationToken); if (getTokenResult.IsFailed) { return ResultHelpers.CreateFailure(getTokenResult); @@ -423,9 +473,18 @@ private async Task> SendSaleTransaction(FileDeta // Send request to transaction processor - Result result= await this.TransactionProcessorClient.PerformTransaction(this.TokenResponse.AccessToken, saleTransactionRequest, cancellationToken); + processingContext.Stage = FileLineProcessingStage.TransactionDispatchStarted; + processingContext.TransactionDispatchAttempted = true; + FileLineProcessingDiagnostics.TransactionDispatchStarted(processingContext); + Result result = await this.TransactionProcessorClient.PerformTransaction(this.TokenResponse.AccessToken, saleTransactionRequest, cancellationToken); if (result.IsFailed) + { + FileLineProcessingDiagnostics.TransactionDispatchFailed(processingContext, result); return ResultHelpers.CreateFailure(result); + } + + FileLineProcessingDiagnostics.TransactionDispatchCompleted(processingContext, result.Data); + processingContext.Stage = FileLineProcessingStage.TransactionDispatchCompleted; return result; } @@ -433,8 +492,10 @@ private async Task> SendSaleTransaction(FileDeta private async Task> GetDetailsForTransaction(CancellationToken cancellationToken, FileDetails fileDetails, FileProfileModel fileProfile, - String operatorName) { + String operatorName, + FileLineProcessingContext processingContext) { + processingContext.Stage = FileLineProcessingStage.TokenAcquisition; Result getTokenResult = await this.GetToken(cancellationToken); if (getTokenResult.IsFailed) { @@ -444,13 +505,17 @@ private async Task> SendSaleTransaction(FileDeta this.TokenResponse = getTokenResult.Data; // Get the merchant details + processingContext.Stage = FileLineProcessingStage.MerchantLookup; Result getMerchantResult = await this.TransactionProcessorClient.GetMerchant(this.TokenResponse.AccessToken, fileDetails.EstateId, fileDetails.MerchantId, cancellationToken); if (getMerchantResult.IsFailed) { return ResultHelpers.CreateFailure(getMerchantResult); } MerchantResponse merchant = getMerchantResult.Data; + if (merchant == null) + return Result.NotFound($"Merchant Id {fileDetails.MerchantId} not found on estate Id {fileDetails.EstateId}"); + processingContext.Stage = FileLineProcessingStage.ContractLookup; Result> getContractsResult = await this.TransactionProcessorClient.GetMerchantContracts(this.TokenResponse.AccessToken, fileDetails.EstateId, fileDetails.MerchantId, cancellationToken); if (getContractsResult.IsFailed) { return ResultHelpers.CreateFailure(getContractsResult); @@ -458,10 +523,11 @@ private async Task> SendSaleTransaction(FileDeta List contracts = getContractsResult.Data; - if (contracts.Any() == false) { + if (contracts == null || contracts.Any() == false) { return Result.NotFound($"No contracts found for Merchant Id {fileDetails.MerchantId} on estate Id {fileDetails.EstateId}"); } + processingContext.Stage = FileLineProcessingStage.ContractOperatorMatch; ContractResponse contract = fileProfile.OperatorName switch { "Voucher" => contracts.SingleOrDefault(c => c.Description.Contains(operatorName)), _ => contracts.SingleOrDefault(c => c.OperatorName == operatorName) @@ -471,12 +537,17 @@ private async Task> SendSaleTransaction(FileDeta 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); + processingContext.Stage = FileLineProcessingStage.VariableValueProductLookup; + 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}"); } + processingContext.Stage = FileLineProcessingStage.MerchantDeviceResolution; + if (merchant == null || merchant.Devices == null || merchant.Devices.Any() == false || String.IsNullOrWhiteSpace(merchant.Devices.First().Value)) + return Result.NotFound($"No valid merchant device found for Merchant Id {merchant.MerchantId} on estate Id {fileDetails.EstateId}"); + return Result.Success((contract.ContractId, contract.OperatorId, product.ProductId, merchant.Devices.First().Value)); }