diff --git a/Directory.Packages.props b/Directory.Packages.props index b542dbe..5964492 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ - + @@ -39,14 +39,14 @@ - - - - - - - - + + + + + + + + @@ -55,9 +55,9 @@ - - - + + + diff --git a/FileProcessor.BusinessLogic.Tests/AssemblyInfo.cs b/FileProcessor.BusinessLogic.Tests/AssemblyInfo.cs new file mode 100644 index 0000000..291e621 --- /dev/null +++ b/FileProcessor.BusinessLogic.Tests/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using Xunit; +using Xunit.v3; +using Xunit.Sdk; + +[assembly: Parallelization(Mode = ParallelMode.None)] diff --git a/FileProcessor.BusinessLogic.Tests/DomainEventHandlerTests.cs b/FileProcessor.BusinessLogic.Tests/DomainEventHandlerTests.cs index d4ade47..2e15aaa 100644 --- a/FileProcessor.BusinessLogic.Tests/DomainEventHandlerTests.cs +++ b/FileProcessor.BusinessLogic.Tests/DomainEventHandlerTests.cs @@ -21,6 +21,11 @@ namespace FileProcessor.BusinessLogic.Tests public class DomainEventHandlerTests { + public DomainEventHandlerTests() + { + Shared.Logger.Logger.Initialise(Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); + } + [Fact] public async Task FileDomainEventHandler_FileLineAddedEvent_MediatorFailureIsReturned() { diff --git a/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs b/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs index 4e7a496..1f730c7 100644 --- a/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs +++ b/FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs @@ -605,8 +605,9 @@ private async Task> SendSaleTransaction(FileDeta } MerchantResponse merchant = getMerchantResult.Data; - if (merchant == null) - return Result.NotFound($"Merchant Id {fileDetails.MerchantId} not found on estate Id {fileDetails.EstateId}"); + Result merchantResult = this.ValidateMerchant(merchant, fileDetails); + if (merchantResult.IsFailed) + return ResultHelpers.CreateFailure(merchantResult); processingContext.Stage = FileLineProcessingStage.ContractLookup; Result> getContractsResult = await this.TransactionProcessorClient.GetMerchantContracts(this.TokenResponse.AccessToken, fileDetails.EstateId, fileDetails.MerchantId, cancellationToken); @@ -615,33 +616,82 @@ private async Task> SendSaleTransaction(FileDeta } List contracts = getContractsResult.Data; + Result contractsResult = this.ValidateContracts(contracts, fileDetails); + if (contractsResult.IsFailed) + return ResultHelpers.CreateFailure(contractsResult); + + processingContext.Stage = FileLineProcessingStage.ContractOperatorMatch; + Result contractResult = this.FindContract(contracts, fileProfile.OperatorName, operatorName, merchant.MerchantId); + if (contractResult.IsFailed) + return ResultHelpers.CreateFailure(contractResult); + + processingContext.Stage = FileLineProcessingStage.VariableValueProductLookup; + Result productResult = this.FindVariableValueProduct(contractResult.Data, fileProfile.OperatorName, merchant.MerchantId); + if (productResult.IsFailed) + return ResultHelpers.CreateFailure(productResult); + + processingContext.Stage = FileLineProcessingStage.MerchantDeviceResolution; + Result merchantDeviceResult = this.ResolveMerchantDevice(merchant, fileDetails); + if (merchantDeviceResult.IsFailed) + return ResultHelpers.CreateFailure(merchantDeviceResult); + + return Result.Success((contractResult.Data.ContractId, contractResult.Data.OperatorId, productResult.Data.ProductId, merchantDeviceResult.Data)); + } + + private Result ValidateMerchant(MerchantResponse merchant, + FileDetails fileDetails) + { + if (merchant == null) + return Result.NotFound($"Merchant Id {fileDetails.MerchantId} not found on estate Id {fileDetails.EstateId}"); - if (contracts == null || contracts.Any() == false) { + return Result.Success(); + } + + private Result ValidateContracts(List contracts, + FileDetails fileDetails) + { + 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 { + return Result.Success(); + } + + private Result FindContract(List contracts, + String profileOperatorName, + String operatorName, + Guid merchantId) + { + ContractResponse contract = profileOperatorName 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}"); - } + if (contract == null) + return Result.NotFound($"No merchant contract for operator Id {operatorName} found for Merchant Id {merchantId}"); - processingContext.Stage = FileLineProcessingStage.VariableValueProductLookup; + return Result.Success(contract); + } + + private Result FindVariableValueProduct(ContractResponse contract, + String profileOperatorName, + Guid 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}"); - } + if (product == null) + return Result.NotFound($"No variable value product found on the merchant contract for operator Id {profileOperatorName} and Merchant Id {merchantId}"); - processingContext.Stage = FileLineProcessingStage.MerchantDeviceResolution; - if (merchant == null || merchant.Devices == null || merchant.Devices.Any() == false || String.IsNullOrWhiteSpace(merchant.Devices.First().Value)) + return Result.Success(product); + } + + private Result ResolveMerchantDevice(MerchantResponse merchant, + FileDetails fileDetails) + { + if (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)); + return Result.Success(merchant.Devices.First().Value); } private async Task ProcessFile(Guid fileId, diff --git a/FileProcessor.IntegrationTesting.Helpers/FileProcessorSteps.cs b/FileProcessor.IntegrationTesting.Helpers/FileProcessorSteps.cs index 4a21409..50a09de 100644 --- a/FileProcessor.IntegrationTesting.Helpers/FileProcessorSteps.cs +++ b/FileProcessor.IntegrationTesting.Helpers/FileProcessorSteps.cs @@ -50,6 +50,12 @@ await Retry.For(async () => getFileResult.IsSuccess.ShouldBeTrue(); FileDetails? fileDetails = getFileResult.Data; fileDetails.ShouldNotBeNull(); + + if (!fileDetails.ProcessingCompleted) + { + Console.WriteLine(this.FormatFileDetails(fileDetails)); + } + fileDetails.ProcessingCompleted.ShouldBeTrue(); }, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(30)); @@ -57,6 +63,32 @@ await Retry.For(async () => return fileId; } + private String FormatFileDetails(FileDetails fileDetails) + { + String summary = fileDetails.ProcessingSummary == null + ? "null" + : $"total={fileDetails.ProcessingSummary.TotalLines}, " + + $"successful={fileDetails.ProcessingSummary.SuccessfullyProcessedLines}, " + + $"failed={fileDetails.ProcessingSummary.FailedLines}, " + + $"ignored={fileDetails.ProcessingSummary.IgnoredLines}, " + + $"rejected={fileDetails.ProcessingSummary.RejectedLines}, " + + $"notProcessed={fileDetails.ProcessingSummary.NotProcessedLines}"; + + String lines = fileDetails.FileLines == null + ? "null" + : String.Join(Environment.NewLine, + fileDetails.FileLines.Select(line => + $" line={line.LineNumber}, result={line.ProcessingResult}, " + + $"transactionId={line.TransactionId}, rejectionReason={line.RejectionReason ?? ""}, " + + $"data={line.LineData}")); + + return $"[File processing state] fileId={fileDetails.FileId}, " + + $"processingCompleted={fileDetails.ProcessingCompleted}, " + + $"estateId={fileDetails.EstateId}, merchantId={fileDetails.MerchantId}, " + + $"fileProfileId={fileDetails.FileProfileId}, fileImportLogId={fileDetails.FileImportLogId}, " + + $"summary=[{summary}], lines={Environment.NewLine}{lines}"; + } + public async Task>> GetFileProfiles(String accessToken, CancellationToken cancellationToken) { return await this.FileProcessorClient.GetFileProfiles(accessToken, cancellationToken); diff --git a/FileProcessor.IntegrationTests/Common/DockerHelper.cs b/FileProcessor.IntegrationTests/Common/DockerHelper.cs index 262c883..0d114cf 100644 --- a/FileProcessor.IntegrationTests/Common/DockerHelper.cs +++ b/FileProcessor.IntegrationTests/Common/DockerHelper.cs @@ -56,7 +56,7 @@ public DockerHelper(){ #region Methods - public override async Task CreateSubscriptions(){ + public override async Task CreateSubscriptions(CancellationToken cancellationToken){ List<(String streamName, String groupName, Int32 maxRetries)> subscriptions = new(); subscriptions.AddRange(MessagingService.IntegrationTesting.Helpers.SubscriptionsHelper.GetSubscriptions()); subscriptions.AddRange(TransactionProcessor.IntegrationTesting.Helpers.SubscriptionsHelper.GetSubscriptions()); @@ -83,9 +83,17 @@ public override ContainerBuilder SetupFileProcessorContainer() { public String AccessToken; - public override async Task StartContainersForScenarioRun(String scenarioName, DockerServices dockerServices){ + protected override String GenerateEventStoreConnectionString(String userName = "admin", String password = "changeit") + { + String eventStoreAddress = $"esdb://{userName}:{password}@{this.EventStoreContainerName}:{DockerPorts.EventStoreHttpDockerPort}?tls=false&defaultDeadline=240000"; + + return eventStoreAddress; + } + + public override async Task StartContainersForScenarioRun(String scenarioName, DockerServices dockerServices, CancellationToken cancellationToken) + { - await base.StartContainersForScenarioRun(scenarioName, dockerServices); + await base.StartContainersForScenarioRun(scenarioName, dockerServices, cancellationToken); // Setup the base address resolvers String SecurityServiceBaseAddressResolver(String api) => $"https://127.0.0.1:{this.SecurityServicePort}"; diff --git a/FileProcessor.IntegrationTests/Common/GenericSteps.cs b/FileProcessor.IntegrationTests/Common/GenericSteps.cs index 08c6427..8f99710 100644 --- a/FileProcessor.IntegrationTests/Common/GenericSteps.cs +++ b/FileProcessor.IntegrationTests/Common/GenericSteps.cs @@ -54,7 +54,7 @@ public async Task StartSystem() this.TestingContext.Logger = logger; this.TestingContext.Logger.LogInformation("About to Start Containers for Scenario Run"); - await this.TestingContext.DockerHelper.StartContainersForScenarioRun(scenarioName, dockerServices).ConfigureAwait(false); + await this.TestingContext.DockerHelper.StartContainersForScenarioRun(scenarioName, dockerServices, CancellationToken.None).ConfigureAwait(false); this.TestingContext.Logger.LogInformation("Containers for Scenario Run Started"); } diff --git a/FileProcessor.IntegrationTests/Steps/SharedSteps.cs b/FileProcessor.IntegrationTests/Steps/SharedSteps.cs index 1af950b..093a725 100644 --- a/FileProcessor.IntegrationTests/Steps/SharedSteps.cs +++ b/FileProcessor.IntegrationTests/Steps/SharedSteps.cs @@ -129,7 +129,7 @@ public async Task WhenICreateTheFollowingEstates(DataTable table) { List requests = table.Rows.ToCreateEstateRequests(); - List verifiedEstates = await this.GetTransactionProcessorSteps().WhenICreateTheFollowingEstatesX(this.TestingContext.AccessToken, requests); + List verifiedEstates = await this.CreateAndVerifyEstates(requests); foreach (EstateResponse verifiedEstate in verifiedEstates) { @@ -140,6 +140,46 @@ public async Task WhenICreateTheFollowingEstates(DataTable table) } } + private async Task> CreateAndVerifyEstates(List requests) + { + List results = new List(); + + foreach (CreateEstateRequest request in requests) + { + var createResult = await this.TestingContext.DockerHelper.TransactionProcessorClient + .CreateEstate(this.TestingContext.AccessToken, request, CancellationToken.None) + .ConfigureAwait(false); + + createResult.IsSuccess.ShouldBeTrue($"CreateEstate failed for EstateId {request.EstateId}: {createResult.Message}"); + + EstateResponse estate = null; + await Retry.For(async () => + { + var getEstatesResult = await this.TestingContext.DockerHelper.TransactionProcessorClient + .GetEstates(this.TestingContext.AccessToken, request.EstateId, CancellationToken.None) + .ConfigureAwait(false); + + this.TestingContext.Logger.LogInformation($"GetEstates verification for EstateId {request.EstateId}: " + + $"isSuccess={getEstatesResult.IsSuccess}, message={getEstatesResult.Message}, " + + $"rows={(getEstatesResult.Data == null ? "null" : getEstatesResult.Data.Count.ToString())}"); + + getEstatesResult.IsSuccess.ShouldBeTrue(getEstatesResult.Message); + List estates = getEstatesResult.Data; + estates.ShouldNotBeNull(); + estates.ShouldHaveSingleItem(); + + estate = estates.Single(); + estate.EstateName.ShouldBe(request.EstateName); + estate.EstateReference.ShouldNotBeNullOrWhiteSpace(); + }, + retryFor: TimeSpan.FromSeconds(180)).ConfigureAwait(false); + + results.Add(estate); + } + + return results; + } + [Given(@"I create the following api scopes")] public async Task GivenICreateTheFollowingApiScopes(DataTable table) {