Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<PackageVersion Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
<PackageVersion Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="9.0.0" />
<PackageVersion Include="AspNetCore.HealthChecks.Uris" Version="9.0.0" />
<PackageVersion Include="ClientProxyBase" Version="2026.10.1" />
<PackageVersion Include="ClientProxyBase" Version="2026.10.2" />
<PackageVersion Include="HealthMonitoring.Client" Version="2026.9.4.48" />
<PackageVersion Include="OpenIddict.Validation.AspNetCore" Version="7.7.1" />
<PackageVersion Include="OpenIddict.Validation.SystemNetHttp" Version="7.7.1" />
Expand Down Expand Up @@ -39,14 +39,14 @@
<PackageVersion Include="Sentry.AspNetCore" Version="6.11.1" />
<PackageVersion Include="SharpCompress" Version="0.50.4" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="Shared" Version="2026.10.1" />
<PackageVersion Include="Shared.DomainDrivenDesign" Version="2026.10.1" />
<PackageVersion Include="Shared.EventStore" Version="2026.10.1" />
<PackageVersion Include="Shared.IntegrationTesting" Version="2026.10.1" />
<PackageVersion Include="Shared.Results" Version="2026.10.1" />
<PackageVersion Include="Shared.Results.Web" Version="2026.10.1" />
<PackageVersion Include="SecurityService.Client" Version="2026.9.3.196" />
<PackageVersion Include="SecurityService.IntegrationTesting.Helpers" Version="2026.9.3.196" />
<PackageVersion Include="Shared" Version="2026.10.2" />
<PackageVersion Include="Shared.DomainDrivenDesign" Version="2026.10.2" />
<PackageVersion Include="Shared.EventStore" Version="2026.10.2" />
<PackageVersion Include="Shared.IntegrationTesting" Version="2026.10.2" />
<PackageVersion Include="Shared.Results" Version="2026.10.2" />
<PackageVersion Include="Shared.Results.Web" Version="2026.10.2" />
<PackageVersion Include="SecurityService.Client" Version="2026.9.3.198" />
<PackageVersion Include="SecurityService.IntegrationTesting.Helpers" Version="2026.9.3.198" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.2.3" />
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="10.2.3" />
<PackageVersion Include="Swashbuckle.AspNetCore.Filters" Version="10.0.1" />
Expand All @@ -55,9 +55,9 @@
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.2.3" />
<PackageVersion Include="System.IO.Abstractions" Version="22.2.0" />
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.2.0" />
<PackageVersion Include="TransactionProcessor.Client" Version="2026.9.3.403" />
<PackageVersion Include="TransactionProcessor.Database" Version="2026.9.3.403" />
<PackageVersion Include="TransactionProcessor.IntegrationTesting.Helpers" Version="2026.9.3.403" />
<PackageVersion Include="TransactionProcessor.Client" Version="2026.9.3.407" />
<PackageVersion Include="TransactionProcessor.Database" Version="2026.9.3.407" />
<PackageVersion Include="TransactionProcessor.IntegrationTesting.Helpers" Version="2026.9.3.407" />
<PackageVersion Include="xunit.v3" Version="4.0.1" />
<PackageVersion Include="xunit.runner.visualstudio" Version="4.0.0" />
</ItemGroup>
Expand Down
5 changes: 5 additions & 0 deletions FileProcessor.BusinessLogic.Tests/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Xunit;
using Xunit.v3;
using Xunit.Sdk;

[assembly: Parallelization(Mode = ParallelMode.None)]
5 changes: 5 additions & 0 deletions FileProcessor.BusinessLogic.Tests/DomainEventHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
82 changes: 66 additions & 16 deletions FileProcessor.BusinessLogic/Services/FileProcessorDomainService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -605,8 +605,9 @@ private async Task<Result<SaleTransactionResponse>> 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<List<ContractResponse>> getContractsResult = await this.TransactionProcessorClient.GetMerchantContracts(this.TokenResponse.AccessToken, fileDetails.EstateId, fileDetails.MerchantId, cancellationToken);
Expand All @@ -615,33 +616,82 @@ private async Task<Result<SaleTransactionResponse>> SendSaleTransaction(FileDeta
}

List<ContractResponse> contracts = getContractsResult.Data;
Result contractsResult = this.ValidateContracts(contracts, fileDetails);
if (contractsResult.IsFailed)
return ResultHelpers.CreateFailure(contractsResult);

processingContext.Stage = FileLineProcessingStage.ContractOperatorMatch;
Result<ContractResponse> contractResult = this.FindContract(contracts, fileProfile.OperatorName, operatorName, merchant.MerchantId);
if (contractResult.IsFailed)
return ResultHelpers.CreateFailure(contractResult);

processingContext.Stage = FileLineProcessingStage.VariableValueProductLookup;
Result<ContractProduct> productResult = this.FindVariableValueProduct(contractResult.Data, fileProfile.OperatorName, merchant.MerchantId);
if (productResult.IsFailed)
return ResultHelpers.CreateFailure(productResult);

processingContext.Stage = FileLineProcessingStage.MerchantDeviceResolution;
Result<String> 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<ContractResponse> 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<ContractResponse> FindContract(List<ContractResponse> 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<ContractProduct> 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<String> 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<Result> ProcessFile(Guid fileId,
Expand Down
32 changes: 32 additions & 0 deletions FileProcessor.IntegrationTesting.Helpers/FileProcessorSteps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,45 @@
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));

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 ?? "<none>"}, "
+ $"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<Result<List<FileProfileModel>>> GetFileProfiles(String accessToken, CancellationToken cancellationToken)
{
return await this.FileProcessorClient.GetFileProfiles(accessToken, cancellationToken);
Expand Down Expand Up @@ -94,7 +126,7 @@
{
DateTime queryStartDate = ReqnrollTableHelper.GetDateForDateString(startDateString, DateTime.Now);
DateTime queryEndDate = ReqnrollTableHelper.GetDateForDateString(endDateString, DateTime.Now);
EstateDetails1 estate = estateDetailsList.SingleOrDefault(e => e.EstateDetails.EstateName == estateName);

Check warning on line 129 in FileProcessor.IntegrationTesting.Helpers/FileProcessorSteps.cs

View workflow job for this annotation

GitHub Actions / Build and Test Pull Requests

Converting null literal or possible null value to non-nullable type.
estate.ShouldNotBeNull();

await Retry.For(async () => {
Expand All @@ -121,7 +153,7 @@

public async Task WhenIGetTheImportLogForTheFollowingFileInformationIsReturned(String accessToken, String estateName, String startDate, List<EstateDetails1> estateDetailsList, List<(String, Guid)> expectedFiles)
{
EstateDetails1 estate = estateDetailsList.SingleOrDefault(e => e.EstateDetails.EstateName == estateName);

Check warning on line 156 in FileProcessor.IntegrationTesting.Helpers/FileProcessorSteps.cs

View workflow job for this annotation

GitHub Actions / Build and Test Pull Requests

Converting null literal or possible null value to non-nullable type.
estate.ShouldNotBeNull();

DateTime queryStartDate = ReqnrollTableHelper.GetDateForDateString(startDate, DateTime.Now);
Expand Down Expand Up @@ -150,7 +182,7 @@

foreach ((String, Guid) expectedFile in expectedFiles)
{
FileImportLogFile file = fileImportLog.Files.SingleOrDefault(f => f.OriginalFileName == expectedFile.Item1 && f.MerchantId == expectedFile.Item2);

Check warning on line 185 in FileProcessor.IntegrationTesting.Helpers/FileProcessorSteps.cs

View workflow job for this annotation

GitHub Actions / Build and Test Pull Requests

Converting null literal or possible null value to non-nullable type.

file.ShouldNotBeNull();
estate.AddFileImportLogFile(file);
Expand All @@ -160,7 +192,7 @@
public async Task WhenIGetTheImportLogForEstateTheDateOnTheImportLogIs(String accessToken, String estateName, String expectedDateString, List<EstateDetails1> estateDetailsList)
{

EstateDetails1 estate = estateDetailsList.SingleOrDefault(e => e.EstateDetails.EstateName == estateName);

Check warning on line 195 in FileProcessor.IntegrationTesting.Helpers/FileProcessorSteps.cs

View workflow job for this annotation

GitHub Actions / Build and Test Pull Requests

Converting null literal or possible null value to non-nullable type.
estate.ShouldNotBeNull();

DateTime expectedDate = ReqnrollTableHelper.GetDateForDateString(expectedDateString, DateTime.Now);
Expand All @@ -175,12 +207,12 @@
getFileImportLogsResult.IsSuccess.ShouldBeTrue();
FileImportLogList? importLogs = getFileImportLogsResult.Data;

FileImportLog i = importLogs.FileImportLogs.SingleOrDefault(x => x.ImportLogDate == expectedDate.Date);

Check warning on line 210 in FileProcessor.IntegrationTesting.Helpers/FileProcessorSteps.cs

View workflow job for this annotation

GitHub Actions / Build and Test Pull Requests

Converting null literal or possible null value to non-nullable type.
i.ShouldNotBeNull();
}

public async Task WhenIGetTheFileForEstateTheFollowingFileInformationIsReturned(String accessToken, String fileName, String estateName, List<EstateDetails1> estateDetailsList, ReqnrollExtensions.FileProcessingSummary fileSummary){
EstateDetails1 estate = estateDetailsList.SingleOrDefault(e => e.EstateDetails.EstateName == estateName);

Check warning on line 215 in FileProcessor.IntegrationTesting.Helpers/FileProcessorSteps.cs

View workflow job for this annotation

GitHub Actions / Build and Test Pull Requests

Converting null literal or possible null value to non-nullable type.
estate.ShouldNotBeNull();

Guid fileId = estate.GetFileId(fileName);
Expand Down
14 changes: 11 additions & 3 deletions FileProcessor.IntegrationTests/Common/DockerHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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}";
Expand Down
2 changes: 1 addition & 1 deletion FileProcessor.IntegrationTests/Common/GenericSteps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand Down
42 changes: 41 additions & 1 deletion FileProcessor.IntegrationTests/Steps/SharedSteps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public async Task WhenICreateTheFollowingEstates(DataTable table)
{
List<CreateEstateRequest> requests = table.Rows.ToCreateEstateRequests();

List<EstateResponse> verifiedEstates = await this.GetTransactionProcessorSteps().WhenICreateTheFollowingEstatesX(this.TestingContext.AccessToken, requests);
List<EstateResponse> verifiedEstates = await this.CreateAndVerifyEstates(requests);

foreach (EstateResponse verifiedEstate in verifiedEstates)
{
Expand All @@ -140,6 +140,46 @@ public async Task WhenICreateTheFollowingEstates(DataTable table)
}
}

private async Task<List<EstateResponse>> CreateAndVerifyEstates(List<CreateEstateRequest> requests)
{
List<EstateResponse> results = new List<EstateResponse>();

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<EstateResponse> 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)
{
Expand Down
Loading