From 14902a4403ea348b8c3a9f7ea5c159fe8d984faa Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 26 Aug 2026 14:59:29 +0100 Subject: [PATCH 1/3] Improve deployment error context and HTML rendering consistency --- SqlServerAgentJobDeploymentTool/MainForm.cs | 7 +- .../SqlAgentDeploymentService.cs | 9 +-- .../FileBuilding/TransactionFileModels.cs | 29 +++++---- ...anagementEndpointRouteBuilderExtensions.cs | 2 - .../Reporting/OperationsDashboardService.cs | 64 +++++++++++-------- .../Worker.cs | 4 +- .../Runtime/ApiClient.cs | 9 +-- .../EstateSetupFunctions.cs | 4 +- 8 files changed, 71 insertions(+), 57 deletions(-) diff --git a/SqlServerAgentJobDeploymentTool/MainForm.cs b/SqlServerAgentJobDeploymentTool/MainForm.cs index a07328d..72f90b8 100644 --- a/SqlServerAgentJobDeploymentTool/MainForm.cs +++ b/SqlServerAgentJobDeploymentTool/MainForm.cs @@ -415,7 +415,7 @@ private async Task LoadManifestFromPathAsync(string manifestPath) private async Task RunDeploymentAsync(bool dryRun) { SetBusyState(true); - string currentOperation = "preparing deployment"; + string currentOperation = string.Empty; string? currentContext = null; try @@ -465,10 +465,11 @@ private async Task RunDeploymentAsync(bool dryRun) } catch (Exception ex) { - DeploymentErrorReporter.ReportUi(ex, _logger, AppendOutput, currentOperation, currentContext); + string operation = string.IsNullOrWhiteSpace(currentOperation) ? "preparing deployment" : currentOperation; + DeploymentErrorReporter.ReportUi(ex, _logger, AppendOutput, operation, currentContext); UpdateValidationSummary(); SetStatus("Deployment failed"); - MessageBox.Show(this, DeploymentErrorReporter.GetUserMessage(ex, currentOperation, currentContext), "Deployment failed", MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show(this, DeploymentErrorReporter.GetUserMessage(ex, operation, currentContext), "Deployment failed", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { diff --git a/SqlServerAgentJobDeploymentTool/SqlAgentDeploymentService.cs b/SqlServerAgentJobDeploymentTool/SqlAgentDeploymentService.cs index 17777bf..8593abc 100644 --- a/SqlServerAgentJobDeploymentTool/SqlAgentDeploymentService.cs +++ b/SqlServerAgentJobDeploymentTool/SqlAgentDeploymentService.cs @@ -6,6 +6,7 @@ namespace SqlServerAgentJobDeploymentTool; internal sealed class SqlAgentDeploymentService { + private const string JobIdParameterName = "@job_id"; private readonly SqlConnection _connection; private readonly TextWriter _writer; private readonly ILogger _logger; @@ -55,7 +56,7 @@ private async Task DeployJobAsync(JobDefinition job, string? databaseNameOverrid job.Schedules.Count); int deleteLevel = job.DeleteLevel ?? 0; - SqlParameter jobIdParameter = new("@job_id", System.Data.SqlDbType.UniqueIdentifier) + SqlParameter jobIdParameter = new(JobIdParameterName, System.Data.SqlDbType.UniqueIdentifier) { Direction = System.Data.ParameterDirection.Output }; @@ -110,7 +111,7 @@ private async Task AddJobStepAsync(Guid jobId, JobStepDefinition step, int stepI List parameters = [ - new SqlParameter("@job_id", jobId), + new SqlParameter(JobIdParameterName, jobId), new SqlParameter("@step_id", stepId), new SqlParameter("@step_name", step.Name), new SqlParameter("@subsystem", ToSubsystemName(step.Subsystem)), @@ -143,7 +144,7 @@ private async Task AddJobScheduleAsync(Guid jobId, JobScheduleDefinition schedul List parameters = [ - new SqlParameter("@job_id", jobId), + new SqlParameter(JobIdParameterName, jobId), new SqlParameter("@name", schedule.Name), new SqlParameter("@enabled", schedule.Enabled), new SqlParameter("@freq_type", (int)schedule.FrequencyType), @@ -233,7 +234,7 @@ private async Task AddJobServerAsync(Guid jobId, string? targetServerName, Cance { List parameters = [ - new SqlParameter("@job_id", jobId) + new SqlParameter(JobIdParameterName, jobId) ]; if (!string.IsNullOrWhiteSpace(targetServerName)) diff --git a/TransactionProcessing.MerchantFileProcessor/FileBuilding/TransactionFileModels.cs b/TransactionProcessing.MerchantFileProcessor/FileBuilding/TransactionFileModels.cs index 70f7d0f..6fa2af9 100644 --- a/TransactionProcessing.MerchantFileProcessor/FileBuilding/TransactionFileModels.cs +++ b/TransactionProcessing.MerchantFileProcessor/FileBuilding/TransactionFileModels.cs @@ -108,9 +108,15 @@ public static object GetValue(FileFieldOptions field, TransactionFileContext con throw new InvalidOperationException($"Field '{field.Name}' must define either a source or a literal value."); } - var transaction = context.Transaction; + return ResolveFieldSourceValue(field.Source.Trim(), field.Format, context); + } + + private static GeneratedTransaction GetTransaction(GeneratedTransaction? transaction) => + transaction ?? throw new InvalidOperationException("The requested field requires a transaction record, but no transaction context was supplied."); - var source = field.Source.Trim(); + private static object ResolveFieldSourceValue(string source, string? format, TransactionFileContext context) + { + var transaction = context.Transaction; if (source.Equals(TransactionFileFieldSources.MerchantId, StringComparison.OrdinalIgnoreCase)) { @@ -144,17 +150,17 @@ public static object GetValue(FileFieldOptions field, TransactionFileContext con if (source.Equals(TransactionFileFieldSources.Quantity, StringComparison.OrdinalIgnoreCase)) { - return ApplyFormat(GetTransaction(transaction).Quantity, field.Format); + return ApplyFormat(GetTransaction(transaction).Quantity, format); } if (source.Equals(TransactionFileFieldSources.UnitAmount, StringComparison.OrdinalIgnoreCase)) { - return ApplyFormat(GetTransaction(transaction).UnitAmount, field.Format); + return ApplyFormat(GetTransaction(transaction).UnitAmount, format); } if (source.Equals(TransactionFileFieldSources.TotalAmount, StringComparison.OrdinalIgnoreCase)) { - return ApplyFormat(GetTransaction(transaction).TotalAmount, field.Format); + return ApplyFormat(GetTransaction(transaction).TotalAmount, format); } if (source.Equals(TransactionFileFieldSources.Currency, StringComparison.OrdinalIgnoreCase)) @@ -164,30 +170,27 @@ public static object GetValue(FileFieldOptions field, TransactionFileContext con if (source.Equals(TransactionFileFieldSources.TransactionDateUtc, StringComparison.OrdinalIgnoreCase)) { - return ApplyDateFormat(GetTransaction(transaction).TransactionDateUtc, field.Format); + return ApplyDateFormat(GetTransaction(transaction).TransactionDateUtc, format); } if (source.Equals(TransactionFileFieldSources.ProcessingDateUtc, StringComparison.OrdinalIgnoreCase)) { - return ApplyDateFormat(context.ProcessingTimestampUtc, field.Format); + return ApplyDateFormat(context.ProcessingTimestampUtc, format); } if (source.Equals(TransactionFileFieldSources.RecordCount, StringComparison.OrdinalIgnoreCase)) { - return ApplyFormat(context.RecordCount, field.Format); + return ApplyFormat(context.RecordCount, format); } if (source.Equals(TransactionFileFieldSources.FileTotalAmount, StringComparison.OrdinalIgnoreCase)) { - return ApplyFormat(context.FileTotalAmount, field.Format ?? "0.00"); + return ApplyFormat(context.FileTotalAmount, format ?? "0.00"); } - throw new InvalidOperationException($"Unsupported field source '{field.Source}'."); + throw new InvalidOperationException($"Unsupported field source '{source}'."); } - private static GeneratedTransaction GetTransaction(GeneratedTransaction? transaction) => - transaction ?? throw new InvalidOperationException("The requested field requires a transaction record, but no transaction context was supplied."); - private static object ApplyFormat(T value, string? format) where T : IFormattable { diff --git a/TransactionProcessing.MerchantFileProcessor/Reporting/ConfigurationManagementEndpointRouteBuilderExtensions.cs b/TransactionProcessing.MerchantFileProcessor/Reporting/ConfigurationManagementEndpointRouteBuilderExtensions.cs index ab28692..cb8f55d 100644 --- a/TransactionProcessing.MerchantFileProcessor/Reporting/ConfigurationManagementEndpointRouteBuilderExtensions.cs +++ b/TransactionProcessing.MerchantFileProcessor/Reporting/ConfigurationManagementEndpointRouteBuilderExtensions.cs @@ -1,4 +1,3 @@ -using System.Text.Json; using Microsoft.AspNetCore.Http; using TransactionProcessing.MerchantFileProcessor.Configuration; using Microsoft.Extensions.Primitives; @@ -186,7 +185,6 @@ private static async Task HandleFileProfileFieldConfigurationAsync( string section, HttpRequest request, IMerchantProcessingConfigurationStore configurationStore, - JsonSerializerOptions jsonSerializerOptions, CancellationToken cancellationToken) { var form = await request.ReadFormAsync(cancellationToken); diff --git a/TransactionProcessing.MerchantFileProcessor/Reporting/OperationsDashboardService.cs b/TransactionProcessing.MerchantFileProcessor/Reporting/OperationsDashboardService.cs index 54f877e..a34a9fc 100644 --- a/TransactionProcessing.MerchantFileProcessor/Reporting/OperationsDashboardService.cs +++ b/TransactionProcessing.MerchantFileProcessor/Reporting/OperationsDashboardService.cs @@ -78,6 +78,14 @@ public sealed class OperationsDashboardService( IHostEnvironment hostEnvironment, IConfiguration configuration) : IOperationsDashboardService { + private const string SectionEnd = " "; + private const string TableStart = " "; + private const string TableEnd = "
"; + private const string TableBodyStart = " "; + private const string TableBodyEnd = " "; + private const string TableRowStart = " "; + private const string TableRowEnd = " "; + public Task GetDashboardModelAsync(CancellationToken cancellationToken) => this.BuildDashboardModelAsync(cancellationToken); @@ -100,19 +108,19 @@ public async Task RenderDashboardHtmlAsync(CancellationToken cancellatio html.AppendLine($"
Generated
{Encode(model.GeneratedUtc.ToString("u"))}
"); html.AppendLine($"
Connection
{Encode(model.ConnectionStringSummary)}
"); html.AppendLine(" "); - html.AppendLine(" "); + html.AppendLine(SectionEnd); AppendMetrics(html, model.Metrics); html.AppendLine("
"); html.AppendLine("
Merchant schedule
"); - html.AppendLine(" "); + html.AppendLine(TableStart); html.AppendLine(" "); - html.AppendLine(" "); + html.AppendLine(TableBodyStart); foreach (var merchant in model.Merchants) { - html.AppendLine(" "); + html.AppendLine(TableRowStart); html.AppendLine($" "); html.AppendLine($" "); html.AppendLine($" "); @@ -123,7 +131,7 @@ public async Task RenderDashboardHtmlAsync(CancellationToken cancellatio html.AppendLine($" "); html.AppendLine($" "); html.AppendLine($" "); - html.AppendLine(" "); + html.AppendLine(TableRowEnd); } if (model.Merchants.Count == 0) @@ -131,30 +139,30 @@ public async Task RenderDashboardHtmlAsync(CancellationToken cancellatio html.AppendLine(" "); } - html.AppendLine(" "); - html.AppendLine("
MerchantEnabledEstateRun times (UTC)Last merchant runNext runSuccessFailedPending checksStatus
{Encode(merchant.MerchantName)}
{Encode(merchant.MerchantId)}
{RenderBadge(merchant.Enabled ? "Enabled" : "Disabled", merchant.Enabled ? "good" : "neutral")}{Encode(merchant.EstateId)}{merchant.FailedFiles}{merchant.PendingStatusChecks}View status
No merchants are configured.
"); - html.AppendLine("
"); + html.AppendLine(TableBodyEnd); + html.AppendLine(TableEnd); + html.AppendLine(SectionEnd); html.AppendLine("
"); AppendFileProfilesSection(html, model.FileProfiles); AppendContractsSection(html, model.Contracts); - html.AppendLine("
"); + html.AppendLine(SectionEnd); html.AppendLine("
"); html.AppendLine("
Recent merchant runs
"); html.AppendLine(" "); html.AppendLine(" "); - html.AppendLine(" "); + html.AppendLine(TableBodyStart); foreach (var run in model.RecentRuns) { - html.AppendLine(" "); + html.AppendLine(TableRowStart); html.AppendLine($" "); html.AppendLine($" "); html.AppendLine($" "); html.AppendLine($" "); html.AppendLine($" "); - html.AppendLine(" "); + html.AppendLine(TableRowEnd); } if (model.RecentRuns.Count == 0) @@ -164,7 +172,7 @@ public async Task RenderDashboardHtmlAsync(CancellationToken cancellatio html.AppendLine(" "); html.AppendLine("
MerchantScheduledCompletedStatusError
{Encode(run.MerchantName)}
{Encode(run.MerchantId)}
{Encode(run.ScheduledRunUtc.ToString("u"))}{Encode(run.CompletedUtc.ToString("u"))}{RenderBadge(run.Status, RunTone(run.Status))}{Encode(run.ErrorMessage ?? string.Empty)}
"); - html.AppendLine("
"); + html.AppendLine(SectionEnd); AppendDocumentEnd(html); return html.ToString(); @@ -189,7 +197,7 @@ public async Task RenderConfigurationHtmlAsync(CancellationToken cancell html.AppendLine($"
File processing
{Encode(model.FileProcessingSummary)}
"); html.AppendLine($"
Polling
{Encode(model.PollingSummary)}
"); html.AppendLine(" "); - html.AppendLine(" "); + html.AppendLine(SectionEnd); html.AppendLine("
"); AppendSummaryPanel(html, "Runtime summary", new[] @@ -204,28 +212,28 @@ public async Task RenderConfigurationHtmlAsync(CancellationToken cancell }); AppendMerchantConfigPanel(html); - html.AppendLine("
"); + html.AppendLine(SectionEnd); html.AppendLine("
"); AppendFileProfilesSection(html, model.FileProfiles); AppendContractsSection(html, model.Contracts); - html.AppendLine("
"); + html.AppendLine(SectionEnd); html.AppendLine("
"); html.AppendLine("
Merchant definitions
"); html.AppendLine(" "); html.AppendLine(" "); - html.AppendLine(" "); + html.AppendLine(TableBodyStart); foreach (var merchant in model.Merchants) { - html.AppendLine(" "); + html.AppendLine(TableRowStart); html.AppendLine($" "); html.AppendLine($" "); html.AppendLine($" "); html.AppendLine($" "); html.AppendLine($" "); - html.AppendLine(" "); + html.AppendLine(TableRowEnd); } if (model.Merchants.Count == 0) @@ -235,7 +243,7 @@ public async Task RenderConfigurationHtmlAsync(CancellationToken cancell html.AppendLine(" "); html.AppendLine("
MerchantEnabledEstateMerchant IDRun schedule
{Encode(merchant.MerchantName)}{RenderBadge(merchant.Enabled ? "Enabled" : "Disabled", merchant.Enabled ? "good" : "neutral")}{Encode(merchant.EstateId)}{Encode(merchant.MerchantId)}{Encode(merchant.RunTimesUtc)}
"); - html.AppendLine("
"); + html.AppendLine(SectionEnd); AppendDocumentEnd(html); return html.ToString(); @@ -255,22 +263,22 @@ public async Task RenderRunsHtmlAsync(CancellationToken cancellationToke html.AppendLine("

Merchant execution audit trail

"); html.AppendLine("

Each record shows the scheduled slot, completion time, and final outcome for a merchant processing cycle.

"); html.AppendLine(" "); - html.AppendLine(" "); + html.AppendLine(SectionEnd); html.AppendLine("
"); html.AppendLine(" "); html.AppendLine(" "); - html.AppendLine(" "); + html.AppendLine(TableBodyStart); foreach (var run in model.RecentRuns) { - html.AppendLine(" "); + html.AppendLine(TableRowStart); html.AppendLine($" "); html.AppendLine($" "); html.AppendLine($" "); html.AppendLine($" "); html.AppendLine($" "); - html.AppendLine(" "); + html.AppendLine(TableRowEnd); } if (model.RecentRuns.Count == 0) @@ -280,7 +288,7 @@ public async Task RenderRunsHtmlAsync(CancellationToken cancellationToke html.AppendLine(" "); html.AppendLine("
MerchantScheduledCompletedStatusError
{Encode(run.MerchantName)}
{Encode(run.MerchantId)}
{Encode(run.ScheduledRunUtc.ToString("u"))}{Encode(run.CompletedUtc.ToString("u"))}{RenderBadge(run.Status, RunTone(run.Status))}{Encode(run.ErrorMessage ?? string.Empty)}
"); - html.AppendLine("
"); + html.AppendLine(SectionEnd); AppendDocumentEnd(html); return html.ToString(); @@ -589,7 +597,7 @@ private static void AppendMetrics(StringBuilder html, IReadOnlyList{Encode(metric.Detail)}"); html.AppendLine(" "); } - html.AppendLine(" "); + html.AppendLine(SectionEnd); } private static void AppendSummaryPanel(StringBuilder html, string title, IEnumerable<(string Label, string Value)> items) @@ -646,7 +654,7 @@ private static void AppendFileProfilesSection(StringBuilder html, IReadOnlyList< } html.AppendLine(" "); - html.AppendLine(" "); + html.AppendLine(TableEnd); html.AppendLine(" "); } @@ -672,7 +680,7 @@ private static void AppendContractsSection(StringBuilder html, IReadOnlyList"); - html.AppendLine(" "); + html.AppendLine(TableEnd); html.AppendLine(" "); } diff --git a/TransactionProcessing.MerchantFileProcessor/Worker.cs b/TransactionProcessing.MerchantFileProcessor/Worker.cs index 6e76998..b19b1e5 100644 --- a/TransactionProcessing.MerchantFileProcessor/Worker.cs +++ b/TransactionProcessing.MerchantFileProcessor/Worker.cs @@ -11,7 +11,9 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { LocalLogger.Context logger = LocalLogger.For(); logger.LogInformation("Merchant processor started"); - while (await RunIterationAsync(stoppingToken)) { + while (await RunIterationAsync(stoppingToken)) + { + continue; } } diff --git a/TransactionProcessing.MerchantPos/Runtime/ApiClient.cs b/TransactionProcessing.MerchantPos/Runtime/ApiClient.cs index 472adbb..5d63cae 100644 --- a/TransactionProcessing.MerchantPos/Runtime/ApiClient.cs +++ b/TransactionProcessing.MerchantPos/Runtime/ApiClient.cs @@ -36,6 +36,7 @@ Task> SendSale(MerchantConfig cfg, TokenResponse token, Pro } public class ApiClient : ClientProxyBase.ClientProxyBase, IApiClient { + private const string DefaultCustomerAccountNumber = "07777777705"; private readonly ISecurityServiceClient SecurityClient; private readonly ITransactionProcessorClient TransactionProcessorClient; private readonly MerchantPosSettingsStore SettingsStore; @@ -345,7 +346,7 @@ private Result> BuildMobileSaleTransactionRe TransactionNumber = transactionNumber.ToString("D4") }; - saleTransactionRequest.AdditionalRequestMetadata = new Dictionary { { "Amount", value.ToString() }, { "CustomerAccountNumber", "07777777705" } }; + saleTransactionRequest.AdditionalRequestMetadata = new Dictionary { { "Amount", value.ToString() }, { "CustomerAccountNumber", DefaultCustomerAccountNumber } }; requestMessages.Add(saleTransactionRequest); return Result.Success(requestMessages); } @@ -369,7 +370,7 @@ private Result> BuildVoucherTransactionReque saleTransactionRequest.AdditionalRequestMetadata = new Dictionary { { "Amount", value.ToString() }, - { "RecipientMobile", "07777777705" } + { "RecipientMobile", DefaultCustomerAccountNumber } }; requestMessages.Add(saleTransactionRequest); return Result.Success(requestMessages); @@ -419,7 +420,7 @@ private String BuildRequestUrl(String route) { return ResultHelpers.CreateFailure(result); } - return Result.Success((body.account_number.ToString(), body.account_name, "07777777705")); + return Result.Success((body.account_number.ToString(), body.account_name, DefaultCustomerAccountNumber)); } @@ -441,7 +442,7 @@ private String BuildRequestUrl(String route) { return ResultHelpers.CreateFailure(result); } - return Result.Success((body.meter_number.ToString(), body.customer_name, "07777777705")); + return Result.Success((body.meter_number.ToString(), body.customer_name, DefaultCustomerAccountNumber)); } } diff --git a/TransactionProcessor.SystemSetupTool/EstateSetupFunctions.cs b/TransactionProcessor.SystemSetupTool/EstateSetupFunctions.cs index 6ec477f..ab5e5ad 100644 --- a/TransactionProcessor.SystemSetupTool/EstateSetupFunctions.cs +++ b/TransactionProcessor.SystemSetupTool/EstateSetupFunctions.cs @@ -419,7 +419,7 @@ private async Task UpdateMerchant(Merchant merchant, return ResultHelpers.CreateFailure(ensureUserResult); } - var ensureOperatorsResult = await EnsureMerchantOperatorsAsync(merchant, existingMerchant, cancellationToken); + var ensureOperatorsResult = await EnsureMerchantOperatorsAsync(existingMerchant, cancellationToken); if (ensureOperatorsResult.IsFailed) { return ResultHelpers.CreateFailure(ensureOperatorsResult); @@ -470,7 +470,7 @@ private async Task EnsureMerchantUserAsync(Merchant merchant, Cancellati return createMerchantUserResult.IsFailed ? ResultHelpers.CreateFailure(createMerchantUserResult) : Result.Success(); } - private async Task EnsureMerchantOperatorsAsync(Merchant merchant, MerchantResponse existingMerchant, CancellationToken cancellationToken) + private async Task EnsureMerchantOperatorsAsync(MerchantResponse existingMerchant, CancellationToken cancellationToken) { var getEstateResult = await this.TransactionProcessorClient.GetEstate(this.TokenResponse.AccessToken, this.EstateId, cancellationToken); if (getEstateResult.IsFailed) From 90c33f9efb4d7d09da9ef4b466a72fb3da10a8fe Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 26 Aug 2026 15:06:55 +0100 Subject: [PATCH 2/3] Reduce transaction field resolver complexity --- .../FileBuilding/TransactionFileModels.cs | 91 +++++-------------- 1 file changed, 21 insertions(+), 70 deletions(-) diff --git a/TransactionProcessing.MerchantFileProcessor/FileBuilding/TransactionFileModels.cs b/TransactionProcessing.MerchantFileProcessor/FileBuilding/TransactionFileModels.cs index 6fa2af9..1e916fe 100644 --- a/TransactionProcessing.MerchantFileProcessor/FileBuilding/TransactionFileModels.cs +++ b/TransactionProcessing.MerchantFileProcessor/FileBuilding/TransactionFileModels.cs @@ -91,6 +91,24 @@ private static string BuildMobileNumber() => public static class TransactionFileFieldResolver { + private static readonly Dictionary> FieldSourceHandlers = new(StringComparer.OrdinalIgnoreCase) + { + [TransactionFileFieldSources.MerchantId] = static (context, _) => context.Merchant.MerchantId, + [TransactionFileFieldSources.ContractId] = static (context, _) => context.Contract.ContractId, + [TransactionFileFieldSources.ContractIssuer] = static (context, _) => context.Contract.Issuer, + [TransactionFileFieldSources.ProductCode] = static (context, _) => GetTransaction(context.Transaction).ProductCode, + [TransactionFileFieldSources.Description] = static (context, _) => GetTransaction(context.Transaction).Description, + [TransactionFileFieldSources.RecipientMobileNumber] = static (context, _) => GetTransaction(context.Transaction).RecipientMobileNumber, + [TransactionFileFieldSources.Quantity] = static (context, format) => ApplyFormat(GetTransaction(context.Transaction).Quantity, format), + [TransactionFileFieldSources.UnitAmount] = static (context, format) => ApplyFormat(GetTransaction(context.Transaction).UnitAmount, format), + [TransactionFileFieldSources.TotalAmount] = static (context, format) => ApplyFormat(GetTransaction(context.Transaction).TotalAmount, format), + [TransactionFileFieldSources.Currency] = static (context, _) => GetTransaction(context.Transaction).Currency, + [TransactionFileFieldSources.TransactionDateUtc] = static (context, format) => ApplyDateFormat(GetTransaction(context.Transaction).TransactionDateUtc, format), + [TransactionFileFieldSources.ProcessingDateUtc] = static (context, format) => ApplyDateFormat(context.ProcessingTimestampUtc, format), + [TransactionFileFieldSources.RecordCount] = static (context, format) => ApplyFormat(context.RecordCount, format), + [TransactionFileFieldSources.FileTotalAmount] = static (context, format) => ApplyFormat(context.FileTotalAmount, format ?? "0.00") + }; + public static string GetTextValue(FileFieldOptions field, TransactionFileContext context) { return GetValue(field, context)?.ToString() ?? string.Empty; @@ -116,79 +134,12 @@ private static GeneratedTransaction GetTransaction(GeneratedTransaction? transac private static object ResolveFieldSourceValue(string source, string? format, TransactionFileContext context) { - var transaction = context.Transaction; - - if (source.Equals(TransactionFileFieldSources.MerchantId, StringComparison.OrdinalIgnoreCase)) - { - return context.Merchant.MerchantId; - } - - if (source.Equals(TransactionFileFieldSources.ContractId, StringComparison.OrdinalIgnoreCase)) - { - return context.Contract.ContractId; - } - - if (source.Equals(TransactionFileFieldSources.ContractIssuer, StringComparison.OrdinalIgnoreCase)) - { - return context.Contract.Issuer; - } - - if (source.Equals(TransactionFileFieldSources.ProductCode, StringComparison.OrdinalIgnoreCase)) - { - return GetTransaction(transaction).ProductCode; - } - - if (source.Equals(TransactionFileFieldSources.Description, StringComparison.OrdinalIgnoreCase)) - { - return GetTransaction(transaction).Description; - } - - if (source.Equals(TransactionFileFieldSources.RecipientMobileNumber, StringComparison.OrdinalIgnoreCase)) - { - return GetTransaction(transaction).RecipientMobileNumber; - } - - if (source.Equals(TransactionFileFieldSources.Quantity, StringComparison.OrdinalIgnoreCase)) - { - return ApplyFormat(GetTransaction(transaction).Quantity, format); - } - - if (source.Equals(TransactionFileFieldSources.UnitAmount, StringComparison.OrdinalIgnoreCase)) - { - return ApplyFormat(GetTransaction(transaction).UnitAmount, format); - } - - if (source.Equals(TransactionFileFieldSources.TotalAmount, StringComparison.OrdinalIgnoreCase)) - { - return ApplyFormat(GetTransaction(transaction).TotalAmount, format); - } - - if (source.Equals(TransactionFileFieldSources.Currency, StringComparison.OrdinalIgnoreCase)) - { - return GetTransaction(transaction).Currency; - } - - if (source.Equals(TransactionFileFieldSources.TransactionDateUtc, StringComparison.OrdinalIgnoreCase)) - { - return ApplyDateFormat(GetTransaction(transaction).TransactionDateUtc, format); - } - - if (source.Equals(TransactionFileFieldSources.ProcessingDateUtc, StringComparison.OrdinalIgnoreCase)) - { - return ApplyDateFormat(context.ProcessingTimestampUtc, format); - } - - if (source.Equals(TransactionFileFieldSources.RecordCount, StringComparison.OrdinalIgnoreCase)) - { - return ApplyFormat(context.RecordCount, format); - } - - if (source.Equals(TransactionFileFieldSources.FileTotalAmount, StringComparison.OrdinalIgnoreCase)) + if (!FieldSourceHandlers.TryGetValue(source, out var handler)) { - return ApplyFormat(context.FileTotalAmount, format ?? "0.00"); + throw new InvalidOperationException($"Unsupported field source '{source}'."); } - throw new InvalidOperationException($"Unsupported field source '{source}'."); + return handler(context, format); } private static object ApplyFormat(T value, string? format) From ba2a2e846e1249545c039189cd373a25bae3bcab Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 26 Aug 2026 15:12:56 +0100 Subject: [PATCH 3/3] Reduce dashboard HTML literal repetition --- .../Reporting/OperationsDashboardService.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/TransactionProcessing.MerchantFileProcessor/Reporting/OperationsDashboardService.cs b/TransactionProcessing.MerchantFileProcessor/Reporting/OperationsDashboardService.cs index a34a9fc..40618b6 100644 --- a/TransactionProcessing.MerchantFileProcessor/Reporting/OperationsDashboardService.cs +++ b/TransactionProcessing.MerchantFileProcessor/Reporting/OperationsDashboardService.cs @@ -170,7 +170,7 @@ public async Task RenderDashboardHtmlAsync(CancellationToken cancellatio html.AppendLine(" No merchant runs have been recorded yet."); } - html.AppendLine(" "); + html.AppendLine(TableBodyEnd); html.AppendLine(" "); html.AppendLine(SectionEnd); @@ -241,7 +241,7 @@ public async Task RenderConfigurationHtmlAsync(CancellationToken cancell html.AppendLine(" No merchants are configured."); } - html.AppendLine(" "); + html.AppendLine(TableBodyEnd); html.AppendLine(" "); html.AppendLine(SectionEnd); @@ -286,7 +286,7 @@ public async Task RenderRunsHtmlAsync(CancellationToken cancellationToke html.AppendLine(" No merchant runs are recorded yet."); } - html.AppendLine(" "); + html.AppendLine(TableBodyEnd); html.AppendLine(" "); html.AppendLine(SectionEnd); @@ -653,7 +653,7 @@ private static void AppendFileProfilesSection(StringBuilder html, IReadOnlyList< html.AppendLine(" No file profiles are configured."); } - html.AppendLine(" "); + html.AppendLine(TableBodyEnd); html.AppendLine(TableEnd); html.AppendLine(" "); } @@ -679,7 +679,7 @@ private static void AppendContractsSection(StringBuilder html, IReadOnlyListNo contract definitions are configured."); } - html.AppendLine(" "); + html.AppendLine(TableBodyEnd); html.AppendLine(TableEnd); html.AppendLine(" "); }