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)
{