Skip to content

Commit 87b6dcb

Browse files
Merge pull request #373 from TransactionProcessing/task/#372_bulk_file_generator
initial bulk file generator version
2 parents 238ebe7 + 2198b2e commit 87b6dcb

47 files changed

Lines changed: 3254 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/createrelease.yml

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,19 @@ jobs:
2929
dotnet restore TransactionProcessor.HealthChecksUI/TransactionProcessor.HealthChecksUI.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }}
3030
dotnet restore TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }}
3131
dotnet restore TransactionProcessing.MerchantPos/TransactionProcessing.MerchantPos.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }}
32+
dotnet restore TransactionProcessing.MerchantFileProcessor/TransactionProcessing.MerchantFileProcessor.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }}
3233
3334
- name: Build Code
3435
run: |
3536
dotnet build TransactionProcessor.HealthChecksUI/TransactionProcessor.HealthChecksUI.sln --configuration Release
3637
dotnet build TransactionProcessing.MerchantPos/TransactionProcessing.MerchantPos.sln --configuration Release
38+
dotnet build TransactionProcessing.MerchantFileProcessor/TransactionProcessing.MerchantFileProcessor.sln --configuration Release
3739
3840
- name: Publish API
3941
run: |
4042
dotnet publish "TransactionProcessor.HealthChecksUI/TransactionProcessor.HealthChecksUI/TransactionProcessor.HealthChecksUI.csproj" --configuration Release --output TransactionProcessor.HealthChecksUI/publishOutput -r win-x64 --self-contained
4143
dotnet publish "TransactionProcessing.MerchantPos/TransactionProcessing.MerchantPos.csproj" --configuration Release --output TransactionProcessing.MerchantPos/publishOutput -r win-x64 --self-contained
44+
dotnet publish "TransactionProcessing.MerchantFileProcessor/TransactionProcessing.MerchantFileProcessor.csproj" --configuration Release --output TransactionProcessing.MerchantFileProcessor/publishOutput -r win-x64 --self-contained
4245
4346
- name: Build Release Package (Health Check UI)
4447
run: |
@@ -63,6 +66,18 @@ jobs:
6366
with:
6467
name: merchantpos
6568
path: /home/runner/work/SupportTools/SupportTools/TransactionProcessing.MerchantPos/merchantpos.zip
69+
70+
- name: Build Release Package (Merchant File Processor)
71+
run: |
72+
cd /home/runner/work/SupportTools/SupportTools/TransactionProcessing.MerchantFileProcessor/publishOutput
73+
zip -r ../merchantfileprocessor.zip ./*
74+
echo "Zip file created at: $(realpath ../merchantfileprocessor.zip)"
75+
76+
- name: Upload the artifact (Merchant File Processor)
77+
uses: actions/upload-artifact@v4.4.0
78+
with:
79+
name: merchantfileprocessor
80+
path: /home/runner/work/SupportTools/SupportTools/TransactionProcessing.MerchantFileProcessor/merchantfileprocessor.zip
6681

6782
deploystaging:
6883
runs-on: [stagingserver, windows]
@@ -123,6 +138,27 @@ jobs:
123138
New-Service -Name $serviceName -BinaryPathName $servicePath -Description $serviceName -DisplayName $serviceName -StartupType Automatic
124139
Start-Service -Name $serviceName
125140
141+
- name: Remove existing Windows service (Merchant File Processor)
142+
run: |
143+
$serviceName = "Transaction Processing - Merchant File Processor"
144+
# Check if the service exists
145+
if (Get-Service -Name $serviceName -ErrorAction SilentlyContinue) {
146+
Stop-Service -Name $serviceName
147+
sc.exe delete $serviceName
148+
}
149+
150+
- name: Unzip the files (Merchant File Processor)
151+
run: |
152+
Expand-Archive -Path merchantfileprocessor.zip -DestinationPath "C:\txnproc\transactionprocessing\merchantfileprocessor" -Force
153+
154+
- name: Install as a Windows service (Merchant File Processor)
155+
run: |
156+
$serviceName = "Transaction Processing - Merchant File Processor"
157+
$servicePath = "C:\txnproc\transactionprocessing\merchantfileprocessor\TransactionProcessing.MerchantFileProcessor.exe"
158+
159+
New-Service -Name $serviceName -BinaryPathName $servicePath -Description $serviceName -DisplayName $serviceName -StartupType Automatic
160+
Start-Service -Name $serviceName
161+
126162
deployproduction:
127163
runs-on: [productionserver, windows]
128164
needs: [build, deploystaging]
@@ -175,4 +211,25 @@ jobs:
175211
$servicePath = "C:\txnproc\transactionprocessing\merchantpos\TransactionProcessor.MerchantPos.exe"
176212
177213
New-Service -Name $serviceName -BinaryPathName $servicePath -Description $serviceName -DisplayName $serviceName -StartupType Automatic
178-
Start-Service -Name $serviceName
214+
Start-Service -Name $serviceName
215+
216+
- name: Remove existing Windows service (Merchant File Processor)
217+
run: |
218+
$serviceName = "Transaction Processing - Merchant File Processor"
219+
# Check if the service exists
220+
if (Get-Service -Name $serviceName -ErrorAction SilentlyContinue) {
221+
Stop-Service -Name $serviceName
222+
sc.exe delete $serviceName
223+
}
224+
225+
- name: Unzip the files (Merchant File Processor)
226+
run: |
227+
Expand-Archive -Path merchantfileprocessor.zip -DestinationPath "C:\txnproc\transactionprocessing\merchantfileprocessor" -Force
228+
229+
- name: Install as a Windows service (Merchant File Processor)
230+
run: |
231+
$serviceName = "Transaction Processing - Merchant File Processor"
232+
$servicePath = "C:\txnproc\transactionprocessing\merchantfileprocessor\TransactionProcessing.MerchantFileProcessor.exe"
233+
234+
New-Service -Name $serviceName -BinaryPathName $servicePath -Description $serviceName -DisplayName $serviceName -StartupType Automatic
235+
Start-Service -Name $serviceName

.github/workflows/pullrequest.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,15 @@ jobs:
2626
dotnet restore TransactionProcessor.HealthChecksUI/TransactionProcessor.HealthChecksUI.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }}
2727
dotnet restore TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }}
2828
dotnet restore TransactionProcessing.MerchantPos/TransactionProcessing.MerchantPos.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }}
29+
dotnet restore TransactionProcessing.MerchantFileProcessor/TransactionProcessing.MerchantFileProcessor.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }}
2930
3031
3132
- name: Build Code
3233
run: |
3334
dotnet build TransactionProcessor.HealthChecksUI/TransactionProcessor.HealthChecksUI.sln --configuration Release
3435
dotnet build TransactionProcessing.SchedulerService/TransactionProcessing.SchedulerService.sln --configuration Release
3536
dotnet build TransactionProcessing.MerchantPos/TransactionProcessing.MerchantPos.sln --configuration Release
37+
dotnet build TransactionProcessing.MerchantFileProcessor/TransactionProcessing.MerchantFileProcessor.sln --configuration Release
3638
3739
pester-tests:
3840
name: "Run PowerShell Tests"
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using FileProcessor.Client;
2+
using FileProcessor.DataTransferObjects;
3+
using FileProcessor.DataTransferObjects.Responses;
4+
using SimpleResults;
5+
using TransactionProcessing.MerchantFileProcessor.Configuration;
6+
using TransactionProcessing.MerchantFileProcessor.FileBuilding;
7+
using TransactionProcessing.MerchantFileProcessor.Services;
8+
9+
namespace TransactionProcessing.MerchantFileProcessor.Clients;
10+
11+
public interface IFileProcessingClient
12+
{
13+
Task<Result<Guid>> Upload(MerchantOptions merchant,
14+
ContractOptions contract,
15+
string accessToken,
16+
GeneratedFile file,
17+
CancellationToken cancellationToken);
18+
19+
Task<Result<FileProcessingStatusSnapshot>> GetFileStatus(string accessToken,
20+
Guid estateId,
21+
Guid fileId,
22+
CancellationToken cancellationToken);
23+
}
24+
25+
public sealed class FileProcessingClient(IFileProcessorClient fileProcessorClient, MerchantProcessingOptions options) : IFileProcessingClient {
26+
public async Task<Result<Guid>> Upload(MerchantOptions merchant,
27+
ContractOptions contract,
28+
string accessToken,
29+
GeneratedFile file,
30+
CancellationToken cancellationToken) {
31+
FileProfileOptions? fileProfile = options.FileProfiles.FirstOrDefault(profile => profile.FileProfileId.Equals(file.FileProfileId, StringComparison.OrdinalIgnoreCase));
32+
33+
if (fileProfile is null) {
34+
return new Result<Guid> { IsSuccess = false, Status = ResultStatus.Failure, Message = $"Generated file references unknown file profile '{file.FileProfileId}'." };
35+
}
36+
37+
UploadFileRequest request = new UploadFileRequest {
38+
EstateId = merchant.GetEstateGuid(),
39+
MerchantId = merchant.GetMerchantGuid(),
40+
UserId = options.FileProcessing.GetUserGuid(),
41+
FileProfileId = fileProfile.GetFileProcessorFileProfileGuid(),
42+
UploadDateTime = DateTime.UtcNow
43+
};
44+
45+
Result<Guid>? result = await fileProcessorClient.UploadFile(accessToken, file.FileName, file.Content, request, cancellationToken);
46+
47+
if (result.IsFailed) {
48+
return new Result<Guid> { IsSuccess = false, Status = ResultStatus.Failure, Message = $"File processor client failed to upload file '{file.FileName}'." };
49+
}
50+
51+
return Result.Success(result.Data);
52+
}
53+
54+
public async Task<Result<FileProcessingStatusSnapshot>> GetFileStatus(string accessToken,
55+
Guid estateId,
56+
Guid fileId,
57+
CancellationToken cancellationToken) {
58+
Result<FileDetails>? result = await fileProcessorClient.GetFile(accessToken, estateId, fileId, cancellationToken);
59+
60+
if (result.IsFailed || result.Data is null) {
61+
return new Result<FileProcessingStatusSnapshot> { IsSuccess = false, Status = ResultStatus.Failure, Message = $"File processor client failed to retrieve status for file '{fileId}'." };
62+
}
63+
64+
FileDetails? fileDetails = result.Data;
65+
FileProcessingLineStatusSnapshot[] lineStatuses = fileDetails.FileLines?.OrderBy(line => line.LineNumber).Select(MapLineStatus).ToArray() ?? [];
66+
67+
return Result.Success(new FileProcessingStatusSnapshot(fileDetails.ProcessingCompleted || AreAllLinesResolved(lineStatuses), lineStatuses));
68+
}
69+
70+
private static FileProcessingLineStatusSnapshot MapLineStatus(FileLine line) => new(line.LineNumber, line.LineData, line.ProcessingResult.ToString(), string.IsNullOrWhiteSpace(line.RejectionReason) ? null : line.RejectionReason, line.TransactionId == Guid.Empty ? null : line.TransactionId);
71+
72+
private static bool AreAllLinesResolved(IEnumerable<FileProcessingLineStatusSnapshot> lines) {
73+
bool hasLines = false;
74+
75+
foreach (FileProcessingLineStatusSnapshot line in lines) {
76+
hasLines = true;
77+
78+
if (line.ProcessingStatus.Equals(FileLineStatuses.Unknown, StringComparison.OrdinalIgnoreCase) || line.ProcessingStatus.Equals(FileLineStatuses.NotProcessed, StringComparison.OrdinalIgnoreCase)) {
79+
return false;
80+
}
81+
}
82+
83+
return hasLines;
84+
}
85+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
using Shared.Logger;
2+
using SimpleResults;
3+
using TransactionProcessing.MerchantFileProcessor.Configuration;
4+
using TransactionProcessor.Client;
5+
using TransactionProcessor.DataTransferObjects.Responses.Contract;
6+
7+
namespace TransactionProcessing.MerchantFileProcessor.Clients;
8+
9+
public interface IMerchantContractDataClient
10+
{
11+
Task<Result<IReadOnlyList<ContractOptions>>> GetContracts(
12+
MerchantOptions merchant,
13+
string accessToken,
14+
CancellationToken cancellationToken);
15+
}
16+
17+
public sealed class MerchantContractDataClient(
18+
ITransactionProcessorClient transactionProcessorClient) : IMerchantContractDataClient
19+
{
20+
public async Task<Result<IReadOnlyList<ContractOptions>>> GetContracts(
21+
MerchantOptions merchant,
22+
string accessToken,
23+
CancellationToken cancellationToken)
24+
{
25+
Logger.LogInformation($"Requesting merchant contracts from TransactionProcessor for merchant {merchant.MerchantId} in estate {merchant.EstateId}");
26+
27+
var result = await transactionProcessorClient.GetMerchantContracts(
28+
accessToken,
29+
merchant.GetEstateGuid(),
30+
merchant.GetMerchantGuid(),
31+
cancellationToken);
32+
33+
if (result.IsFailed || result.Data is null || result.Data.Count == 0)
34+
{
35+
return new Result<IReadOnlyList<ContractOptions>>
36+
{
37+
IsSuccess = false,
38+
Status = ResultStatus.Failure,
39+
Message = $"Transaction processor client did not return any contracts for merchant '{merchant.MerchantId}'."
40+
};
41+
}
42+
43+
var contracts = MapContracts(result.Data);
44+
var validationResult = ValidateContracts(merchant.MerchantId, contracts);
45+
46+
if (validationResult.IsFailed)
47+
{
48+
return new Result<IReadOnlyList<ContractOptions>>
49+
{
50+
IsSuccess = false,
51+
Status = validationResult.Status,
52+
Message = validationResult.Message,
53+
Errors = validationResult.Errors.ToList()
54+
};
55+
}
56+
57+
Logger.LogInformation($"Retrieved {contracts.Count} contracts from TransactionProcessor for merchant {merchant.MerchantId}");
58+
59+
return Result.Success<IReadOnlyList<ContractOptions>>(contracts);
60+
}
61+
62+
private static List<ContractOptions> MapContracts(IReadOnlyList<ContractResponse> sourceContracts)
63+
{
64+
return sourceContracts
65+
.Select(contract => new ContractOptions
66+
{
67+
ContractId = contract.ContractId.ToString(),
68+
ContractName = ResolveContractName(contract),
69+
Issuer = ResolveContractIssuer(contract),
70+
Products = contract.Products?
71+
.Select(product => new ProductOptions
72+
{
73+
ProductCode = product.ProductId.ToString(),
74+
Description = string.IsNullOrWhiteSpace(product.DisplayText) ? product.Name : product.DisplayText,
75+
IsFixedValue = product.Value.HasValue,
76+
Quantity = 1,
77+
UnitAmount = product.Value ?? 0m,
78+
Currency = "GBP"
79+
})
80+
.ToList() ?? []
81+
})
82+
.ToList();
83+
}
84+
85+
private static string ResolveContractName(ContractResponse contract)
86+
{
87+
if (!string.IsNullOrWhiteSpace(contract.Description))
88+
{
89+
return contract.Description.Trim();
90+
}
91+
92+
return contract.ContractId.ToString();
93+
}
94+
95+
private static string ResolveContractIssuer(ContractResponse contract)
96+
{
97+
if (string.IsNullOrWhiteSpace(contract.Description))
98+
{
99+
return string.Empty;
100+
}
101+
102+
const string contractSuffix = " Contract";
103+
104+
return contract.Description.EndsWith(contractSuffix, StringComparison.OrdinalIgnoreCase)
105+
? contract.Description[..^contractSuffix.Length].TrimEnd()
106+
: contract.Description.Trim();
107+
}
108+
109+
private static Result ValidateContracts(string merchantId, IReadOnlyList<ContractOptions> contracts)
110+
{
111+
foreach (var contract in contracts)
112+
{
113+
if (string.IsNullOrWhiteSpace(contract.ContractId) || contract.Products.Count == 0)
114+
{
115+
return Result.Failure($"Contract data for merchant '{merchantId}' is missing a contract identifier or products.");
116+
}
117+
118+
foreach (var product in contract.Products)
119+
{
120+
if (string.IsNullOrWhiteSpace(product.ProductCode) ||
121+
product.Quantity <= 0 ||
122+
product.UnitAmount < 0 ||
123+
string.IsNullOrWhiteSpace(product.Currency))
124+
{
125+
return Result.Failure($"Contract data for merchant '{merchantId}' contains an invalid product definition.");
126+
}
127+
}
128+
}
129+
130+
return Result.Success();
131+
}
132+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using Shared.Logger;
2+
using SimpleResults;
3+
using TransactionProcessing.MerchantFileProcessor.Configuration;
4+
using TransactionProcessor.Client;
5+
using TransactionProcessor.DataTransferObjects.Requests.Merchant;
6+
7+
namespace TransactionProcessing.MerchantFileProcessor.Clients;
8+
9+
public interface IMerchantDepositClient
10+
{
11+
Task<Result> MakeDeposit(
12+
MerchantOptions merchant,
13+
string accessToken,
14+
decimal amount,
15+
string reference,
16+
DateTimeOffset depositTimestampUtc,
17+
CancellationToken cancellationToken);
18+
}
19+
20+
public sealed class MerchantDepositClient(
21+
ITransactionProcessorClient transactionProcessorClient) : IMerchantDepositClient
22+
{
23+
public async Task<Result> MakeDeposit(
24+
MerchantOptions merchant,
25+
string accessToken,
26+
decimal amount,
27+
string reference,
28+
DateTimeOffset depositTimestampUtc,
29+
CancellationToken cancellationToken)
30+
{
31+
if (amount <= 0)
32+
{
33+
return Result.Failure($"Deposit amount for merchant '{merchant.MerchantId}' must be greater than zero.");
34+
}
35+
36+
var request = new MakeMerchantDepositRequest
37+
{
38+
Amount = amount,
39+
DepositDateTime = depositTimestampUtc.UtcDateTime,
40+
Reference = reference
41+
};
42+
43+
Logger.LogInformation($"Making merchant deposit of {amount:0.00} for merchant {merchant.MerchantId} using reference {reference}");
44+
45+
var result = await transactionProcessorClient.MakeMerchantDeposit(
46+
accessToken,
47+
merchant.GetEstateGuid(),
48+
merchant.GetMerchantGuid(),
49+
request,
50+
cancellationToken);
51+
52+
return result.IsFailed
53+
? Result.Failure($"Transaction processor client failed to make a deposit for merchant '{merchant.MerchantId}'.")
54+
: Result.Success();
55+
}
56+
}

0 commit comments

Comments
 (0)