diff --git a/ScheduledTasks/ReplayParkedQueue/ReplayParkedQueue.ps1 b/ScheduledTasks/ReplayParkedQueue/ReplayParkedQueue.ps1 index dda6cf4..e46066d 100644 --- a/ScheduledTasks/ReplayParkedQueue/ReplayParkedQueue.ps1 +++ b/ScheduledTasks/ReplayParkedQueue/ReplayParkedQueue.ps1 @@ -51,7 +51,7 @@ Get-ChildItem -Path $LogDirectory -Filter "ReplayParkedSubscriptions-*.log" -Fil Remove-Item $_.FullName -Force } catch { - # Avoid failing the run due to cleanup issues + Write-Trace "WARN" "Failed to remove stale log file $($_.FullName): $($_.Exception.Message)" } } diff --git a/TransactionProcessing.MerchantFileProcessor/Reporting/FileStatusReportService.cs b/TransactionProcessing.MerchantFileProcessor/Reporting/FileStatusReportService.cs index 5524034..bb9bad8 100644 --- a/TransactionProcessing.MerchantFileProcessor/Reporting/FileStatusReportService.cs +++ b/TransactionProcessing.MerchantFileProcessor/Reporting/FileStatusReportService.cs @@ -70,6 +70,11 @@ public sealed class FileStatusReportService( IDbContextFactory dbContextFactory, IMerchantProcessingConfigurationState configurationState) : IFileStatusReportService { + private const string TableStart = " "; + private const string TableEnd = "
"; + private const string TableBodyStart = " "; + private const string TableBodyEnd = " "; + public async Task GetReportAsync(CancellationToken cancellationToken) { await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); @@ -87,9 +92,9 @@ public async Task RenderHtmlAsync(CancellationToken cancellationToken) html.AppendLine("

Merchant File Status

"); html.AppendLine($"

Generated at {Encode(report.GeneratedUtc.ToString("u"))}. Auto-refreshes every 30 seconds.

"); html.AppendLine("

Merchants

"); - html.AppendLine(" "); + html.AppendLine(TableStart); html.AppendLine(" "); - html.AppendLine(" "); + html.AppendLine(TableBodyStart); foreach (var merchant in report.MerchantSummaries) { @@ -97,8 +102,8 @@ public async Task RenderHtmlAsync(CancellationToken cancellationToken) $" "); } - html.AppendLine(" "); - html.AppendLine("
MerchantEnabledSuccessful FilesFailed FilesLast Processed (UTC)Next Scheduled Send (UTC)Details
{Encode(merchant.MerchantName)}
{Encode(merchant.MerchantId)}
{(merchant.Enabled ? "Yes" : "No")}{merchant.SuccessfulFilesSent}{merchant.FailedFiles}{Encode(merchant.LastProcessedUtc?.ToString("u") ?? "Never")}{Encode(merchant.NextScheduledUtc?.ToString("u") ?? "Disabled")}View details
"); + html.AppendLine(TableBodyEnd); + html.AppendLine(TableEnd); AppendDocumentEnd(html); return html.ToString(); @@ -119,18 +124,18 @@ public async Task RenderHtmlAsync(CancellationToken cancellationToken) html.AppendLine($"

{Encode(report.Merchant.MerchantId)}

"); html.AppendLine($"

Generated at {Encode(report.GeneratedUtc.ToString("u"))}. Auto-refreshes every 30 seconds.

"); html.AppendLine("

Summary

"); - html.AppendLine(" "); + html.AppendLine(TableStart); html.AppendLine(" "); - html.AppendLine(" "); + html.AppendLine(TableBodyStart); html.AppendLine( $" "); - html.AppendLine(" "); - html.AppendLine("
EnabledSuccessful FilesFailed FilesLast Processed (UTC)Next Scheduled Send (UTC)
{(report.Merchant.Enabled ? "Yes" : "No")}{report.Merchant.SuccessfulFilesSent}{report.Merchant.FailedFiles}{Encode(report.Merchant.LastProcessedUtc?.ToString("u") ?? "Never")}{Encode(report.Merchant.NextScheduledUtc?.ToString("u") ?? "Disabled")}
"); + html.AppendLine(TableBodyEnd); + html.AppendLine(TableEnd); html.AppendLine("

Recent File Activity

"); - html.AppendLine(" "); + html.AppendLine(TableStart); html.AppendLine(" "); - html.AppendLine(" "); + html.AppendLine(TableBodyStart); foreach (var file in report.RecentFiles) { @@ -143,8 +148,8 @@ public async Task RenderHtmlAsync(CancellationToken cancellationToken) html.AppendLine(" "); } - html.AppendLine(" "); - html.AppendLine("
Processed (UTC)ContractStatusProfileDetails
No file activity recorded yet.
"); + html.AppendLine(TableBodyEnd); + html.AppendLine(TableEnd); AppendDocumentEnd(html); return html.ToString(); @@ -163,22 +168,22 @@ public async Task RenderHtmlAsync(CancellationToken cancellationToken) html.AppendLine($"

← Back to merchant

"); html.AppendLine("

File Details

"); html.AppendLine($"

Merchant {Encode(report.Merchant.MerchantName)}
{Encode(report.Merchant.MerchantId)}

"); - html.AppendLine(" "); + html.AppendLine(TableStart); html.AppendLine(" "); - html.AppendLine(" "); + html.AppendLine(TableBodyStart); html.AppendLine( $" "); - html.AppendLine(" "); - html.AppendLine("
Processed (UTC)ContractUpload StatusProcessingProfileFormat
{Encode(report.File.ProcessedUtc.ToString("u"))}{Encode(report.File.ContractName)}
{Encode(report.File.ContractId)}
{Encode(report.File.Status)}{Encode(report.File.ProcessingCompleted ? "Complete" : "Pending")}{(report.File.LastStatusCheckUtc.HasValue ? $"
Last checked {Encode(report.File.LastStatusCheckUtc.Value.ToString("u"))}" : string.Empty)}
{Encode(report.File.FileProfileId)}{Encode(report.File.Format)}
"); + html.AppendLine(TableBodyEnd); + html.AppendLine(TableEnd); html.AppendLine("

File Data

"); - html.AppendLine(" "); + html.AppendLine(TableStart); html.AppendLine(" "); - html.AppendLine(" "); + html.AppendLine(TableBodyStart); html.AppendLine( $" "); - html.AppendLine(" "); - html.AppendLine("
File NameRecordsTotal Amount
{Encode(string.IsNullOrWhiteSpace(report.File.FileName) ? "(not generated)" : report.File.FileName)}{report.File.RecordCount}{report.File.TotalAmount:0.00}
"); + html.AppendLine(TableBodyEnd); + html.AppendLine(TableEnd); html.AppendLine("

File Lines

"); if (report.FileLines.Count == 0) @@ -189,15 +194,15 @@ public async Task RenderHtmlAsync(CancellationToken cancellationToken) { html.AppendLine(" "); html.AppendLine(" "); - html.AppendLine(" "); + html.AppendLine(TableBodyStart); foreach (var line in report.FileLines) { html.AppendLine($" "); } - html.AppendLine(" "); - html.AppendLine("
LineContentStatusTransactionRejection
{line.LineNumber}{Encode(line.Content)}{Encode(line.ProcessingStatus)}{Encode(line.TransactionId ?? string.Empty)}{Encode(line.RejectionReason ?? string.Empty)}
"); + html.AppendLine(TableBodyEnd); + html.AppendLine(TableEnd); } html.AppendLine("

Error Details

"); diff --git a/TransactionProcessing.MerchantFileProcessor/Reporting/OperationsDashboardService.cs b/TransactionProcessing.MerchantFileProcessor/Reporting/OperationsDashboardService.cs index 40618b6..798f5d2 100644 --- a/TransactionProcessing.MerchantFileProcessor/Reporting/OperationsDashboardService.cs +++ b/TransactionProcessing.MerchantFileProcessor/Reporting/OperationsDashboardService.cs @@ -79,12 +79,15 @@ public sealed class OperationsDashboardService( IConfiguration configuration) : IOperationsDashboardService { private const string SectionEnd = " "; + private const string SectionPanelStart = "
"; + private const string DivEnd = " "; private const string TableStart = " "; private const string TableEnd = "
"; private const string TableBodyStart = " "; private const string TableBodyEnd = " "; private const string TableRowStart = " "; private const string TableRowEnd = " "; + private const string NeutralBadgeTone = "neutral"; public Task GetDashboardModelAsync(CancellationToken cancellationToken) => this.BuildDashboardModelAsync(cancellationToken); @@ -102,17 +105,17 @@ public async Task RenderDashboardHtmlAsync(CancellationToken cancellatio html.AppendLine("
Operations console
"); html.AppendLine("

Merchant File Processor

"); html.AppendLine("

Live configuration, scheduling, and processing visibility for the worker, using the same runtime settings that power the service.

"); - html.AppendLine(" "); + html.AppendLine(DivEnd); html.AppendLine("
"); html.AppendLine($"
Environment
{Encode(model.EnvironmentName)}
"); html.AppendLine($"
Generated
{Encode(model.GeneratedUtc.ToString("u"))}
"); html.AppendLine($"
Connection
{Encode(model.ConnectionStringSummary)}
"); - html.AppendLine("
"); + html.AppendLine(DivEnd); html.AppendLine(SectionEnd); AppendMetrics(html, model.Metrics); - html.AppendLine("
"); + html.AppendLine(SectionPanelStart); html.AppendLine("
Merchant schedule
"); html.AppendLine(TableStart); html.AppendLine(" MerchantEnabledEstateRun times (UTC)Last merchant runNext runSuccessFailedPending checksStatus"); @@ -122,7 +125,7 @@ public async Task RenderDashboardHtmlAsync(CancellationToken cancellatio { html.AppendLine(TableRowStart); html.AppendLine($" {Encode(merchant.MerchantName)}
{Encode(merchant.MerchantId)}"); - html.AppendLine($" {RenderBadge(merchant.Enabled ? "Enabled" : "Disabled", merchant.Enabled ? "good" : "neutral")}"); + html.AppendLine($" {RenderBadge(merchant.Enabled ? "Enabled" : "Disabled", merchant.Enabled ? "good" : NeutralBadgeTone)}"); html.AppendLine($" {Encode(merchant.EstateId)}"); html.AppendLine($" {Encode(merchant.RunTimesUtc)}"); html.AppendLine($" {Encode(merchant.LastMerchantRunUtc?.ToString("u") ?? "Never")}"); @@ -148,9 +151,9 @@ public async Task RenderDashboardHtmlAsync(CancellationToken cancellatio AppendContractsSection(html, model.Contracts); html.AppendLine(SectionEnd); - html.AppendLine("
"); + html.AppendLine(SectionPanelStart); html.AppendLine("
Recent merchant runs
"); - html.AppendLine(" "); + html.AppendLine(TableStart); html.AppendLine(" "); html.AppendLine(TableBodyStart); @@ -191,12 +194,12 @@ public async Task RenderConfigurationHtmlAsync(CancellationToken cancell html.AppendLine("
Configuration inventory
"); html.AppendLine("

Appsettings review

"); html.AppendLine("

This page mirrors the effective settings loaded at startup, so operators can validate what the worker is using without opening the deployment package.

"); - html.AppendLine(" "); + html.AppendLine(DivEnd); html.AppendLine("
"); html.AppendLine($"
Authentication
{Encode(model.AuthenticationSummary)}
"); html.AppendLine($"
File processing
{Encode(model.FileProcessingSummary)}
"); html.AppendLine($"
Polling
{Encode(model.PollingSummary)}
"); - html.AppendLine("
"); + html.AppendLine(DivEnd); html.AppendLine(SectionEnd); html.AppendLine("
"); @@ -229,7 +232,7 @@ public async Task RenderConfigurationHtmlAsync(CancellationToken cancell { html.AppendLine(TableRowStart); html.AppendLine($"
"); - html.AppendLine($" "); + html.AppendLine($" "); html.AppendLine($" "); html.AppendLine($" "); html.AppendLine($" "); @@ -262,7 +265,7 @@ public async Task RenderRunsHtmlAsync(CancellationToken cancellationToke html.AppendLine("
Run history
"); 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(DivEnd); html.AppendLine(SectionEnd); html.AppendLine("
"); @@ -595,7 +598,7 @@ private static void AppendMetrics(StringBuilder html, IReadOnlyList{Encode(metric.Label)}"); html.AppendLine($"
{Encode(metric.Value)}
"); html.AppendLine($"
{Encode(metric.Detail)}
"); - html.AppendLine(" "); + html.AppendLine(DivEnd); } html.AppendLine(SectionEnd); } @@ -631,17 +634,17 @@ private static void AppendMerchantConfigPanel(StringBuilder html) private static void AppendFileProfilesSection(StringBuilder html, IReadOnlyList fileProfiles) { - html.AppendLine("
"); + html.AppendLine(SectionPanelStart); html.AppendLine("
File profiles
"); - html.AppendLine("
MerchantScheduledCompletedStatusError
{Encode(merchant.MerchantName)}{RenderBadge(merchant.Enabled ? "Enabled" : "Disabled", merchant.Enabled ? "good" : "neutral")}{RenderBadge(merchant.Enabled ? "Enabled" : "Disabled", merchant.Enabled ? "good" : NeutralBadgeTone)}{Encode(merchant.EstateId)}{Encode(merchant.MerchantId)}{Encode(merchant.RunTimesUtc)}
"); + html.AppendLine(TableStart); html.AppendLine(" "); - html.AppendLine(" "); + html.AppendLine(TableBodyStart); foreach (var profile in fileProfiles) { html.AppendLine(" "); html.AppendLine($" "); - html.AppendLine($" "); + html.AppendLine($" "); html.AppendLine($" "); html.AppendLine($" "); html.AppendLine($" "); @@ -660,11 +663,11 @@ private static void AppendFileProfilesSection(StringBuilder html, IReadOnlyList< private static void AppendContractsSection(StringBuilder html, IReadOnlyList contracts) { - html.AppendLine("
"); + html.AppendLine(SectionPanelStart); html.AppendLine("
Contract mappings
"); - html.AppendLine("
ProfileFormatLayoutFieldsMapping
{Encode(profile.FileProfileId)}
{Encode(profile.FileProcessorFileProfileId)}
{RenderBadge(profile.Format, "neutral")}{RenderBadge(profile.Format, NeutralBadgeTone)}{Encode(profile.LayoutSummary)}
.{Encode(profile.FileExtension)}{(string.IsNullOrWhiteSpace(profile.ContentType) ? string.Empty : $" / {Encode(profile.ContentType)}")}
{profile.FieldCount} total
{profile.HeaderFieldCount} header, {profile.TrailerFieldCount} trailer
{Encode(profile.FieldMap)}
"); + html.AppendLine(TableStart); html.AppendLine(" "); - html.AppendLine(" "); + html.AppendLine(TableBodyStart); foreach (var contract in contracts) { @@ -767,7 +770,7 @@ private static string RunTone(string status) return "bad"; } - return "neutral"; + return NeutralBadgeTone; } private static string Encode(string value) => WebUtility.HtmlEncode(value); diff --git a/TransactionProcessing.MerchantFileProcessor/Services/FileStatusStore.cs b/TransactionProcessing.MerchantFileProcessor/Services/FileStatusStore.cs index 360030e..7612591 100644 --- a/TransactionProcessing.MerchantFileProcessor/Services/FileStatusStore.cs +++ b/TransactionProcessing.MerchantFileProcessor/Services/FileStatusStore.cs @@ -143,7 +143,7 @@ public Task RecordSuccessAsync( Guid fileProcessorFileId, GeneratedFile file, CancellationToken cancellationToken) => - this.RecordAsync(runId, merchant, contract, scheduledRunUtc, file, FileSendStatuses.Succeeded, null, fileProcessorFileId, cancellationToken); + this.RecordAsync(new FileStatusRecordRequest(runId, merchant, contract, scheduledRunUtc, file, FileSendStatuses.Succeeded, null, fileProcessorFileId), cancellationToken); public Task RecordFailureAsync( Guid runId, @@ -153,7 +153,7 @@ public Task RecordFailureAsync( GeneratedFile? file, string errorMessage, CancellationToken cancellationToken) => - this.RecordAsync(runId, merchant, contract, scheduledRunUtc, file, FileSendStatuses.Failed, errorMessage, null, cancellationToken); + this.RecordAsync(new FileStatusRecordRequest(runId, merchant, contract, scheduledRunUtc, file, FileSendStatuses.Failed, errorMessage, null), cancellationToken); public async Task RecordMerchantRunResultAsync( Guid runId, @@ -250,38 +250,29 @@ public async Task UpdateFileStatusAsync( await dbContext.SaveChangesAsync(cancellationToken); } - private async Task RecordAsync( - Guid runId, - MerchantOptions merchant, - ContractOptions contract, - DateTimeOffset scheduledRunUtc, - GeneratedFile? file, - string status, - string? errorMessage, - Guid? fileProcessorFileId, - CancellationToken cancellationToken) + private async Task RecordAsync(FileStatusRecordRequest request, CancellationToken cancellationToken) { await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); var fileSendRecord = new FileSendRecord { - RunId = runId, - MerchantId = merchant.MerchantId, - EstateId = merchant.EstateId, - MerchantName = ResolveMerchantName(merchant), - ContractId = contract.ContractId, - ContractName = ResolveContractName(contract), - FileName = file?.FileName, - FileProfileId = file?.FileProfileId, - Format = file?.Format, - FileProcessorFileId = fileProcessorFileId?.ToString(), - ScheduledRunUtc = scheduledRunUtc, - FileContent = ResolveFileContent(file), - RecordCount = file?.RecordCount, - TotalAmount = file?.TotalAmount, - Status = status, - ErrorMessage = string.IsNullOrWhiteSpace(errorMessage) ? null : Truncate(errorMessage, 2048), - ProcessingCompleted = status == FileSendStatuses.Failed, + RunId = request.RunId, + MerchantId = request.Merchant.MerchantId, + EstateId = request.Merchant.EstateId, + MerchantName = ResolveMerchantName(request.Merchant), + ContractId = request.Contract.ContractId, + ContractName = ResolveContractName(request.Contract), + FileName = request.File?.FileName, + FileProfileId = request.File?.FileProfileId, + Format = request.File?.Format, + FileProcessorFileId = request.FileProcessorFileId?.ToString(), + ScheduledRunUtc = request.ScheduledRunUtc, + FileContent = ResolveFileContent(request.File), + RecordCount = request.File?.RecordCount, + TotalAmount = request.File?.TotalAmount, + Status = request.Status, + ErrorMessage = string.IsNullOrWhiteSpace(request.ErrorMessage) ? null : Truncate(request.ErrorMessage, 2048), + ProcessingCompleted = request.Status == FileSendStatuses.Failed, ProcessedUtc = DateTimeOffset.UtcNow }; @@ -289,18 +280,18 @@ private async Task RecordAsync( await dbContext.SaveChangesAsync(cancellationToken); - if (file is null) + if (request.File is null) { return; } - var fileLines = GetFileLines(file); + var fileLines = GetFileLines(request.File); if (fileLines.Count == 0) { return; } - if (status == FileSendStatuses.Succeeded) + if (request.Status == FileSendStatuses.Succeeded) { dbContext.FileSendRecordLineStatuses.AddRange( fileLines.Select((line, index) => new FileSendRecordLineStatus @@ -316,6 +307,16 @@ private async Task RecordAsync( } } + private sealed record FileStatusRecordRequest( + Guid RunId, + MerchantOptions Merchant, + ContractOptions Contract, + DateTimeOffset ScheduledRunUtc, + GeneratedFile? File, + string Status, + string? ErrorMessage, + Guid? FileProcessorFileId); + private static string Truncate(string value, int maxLength) => value.Length <= maxLength ? value : value[..maxLength]; diff --git a/TransactionProcessing.MerchantPos/Pages/Merchants.cshtml.cs b/TransactionProcessing.MerchantPos/Pages/Merchants.cshtml.cs index 4f52bbc..80d3a78 100644 --- a/TransactionProcessing.MerchantPos/Pages/Merchants.cshtml.cs +++ b/TransactionProcessing.MerchantPos/Pages/Merchants.cshtml.cs @@ -7,6 +7,7 @@ namespace TransactionProcessing.MerchantPos.Pages; public sealed class MerchantsModel : PageModel { + private const string MerchantsPagePath = "/Merchants"; private static readonly string SecretMask = new('*', 5); private readonly MerchantPosSettingsStore _settingsStore; @@ -38,7 +39,7 @@ public void OnGet() public async Task OnPostAddAsync() { - return RedirectToPage("/Merchants", new { selected = -1, message = "New merchant ready" }); + return RedirectToPage(MerchantsPagePath, new { selected = -1, message = "New merchant ready" }); } public async Task OnPostSaveAsync() @@ -59,7 +60,7 @@ public async Task OnPostSaveAsync() } await _settingsStore.SaveAsync(settings); - return RedirectToPage("/Merchants", new { selected = Selected, message = "Merchant saved" }); + return RedirectToPage(MerchantsPagePath, new { selected = Selected, message = "Merchant saved" }); } public async Task OnPostRemoveAsync() @@ -67,7 +68,7 @@ public async Task OnPostRemoveAsync() var settings = _settingsStore.Current; if (settings.WorkerSettings.Merchants.Count == 0) { - return RedirectToPage("/Merchants", new { message = "No merchant to remove" }); + return RedirectToPage(MerchantsPagePath, new { message = "No merchant to remove" }); } Selected = ClampSelected(Selected); @@ -75,7 +76,7 @@ public async Task OnPostRemoveAsync() await _settingsStore.SaveAsync(settings); var selected = Math.Clamp(Selected, 0, Math.Max(0, settings.WorkerSettings.Merchants.Count - 1)); - return RedirectToPage("/Merchants", new { selected, message = "Merchant removed" }); + return RedirectToPage(MerchantsPagePath, new { selected, message = "Merchant removed" }); } private void LoadSelectedMerchant() diff --git a/TransactionProcessing.MerchantPos/Runtime/ApiClient.cs b/TransactionProcessing.MerchantPos/Runtime/ApiClient.cs index 5d63cae..ffc12b5 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 AmountKey = "Amount"; private const string DefaultCustomerAccountNumber = "07777777705"; private readonly ISecurityServiceClient SecurityClient; private readonly ITransactionProcessorClient TransactionProcessorClient; @@ -314,7 +315,7 @@ private async Task>> BuildBillPayment { "CustomerAccountNumber", extraDetails.Data.accountNumber }, { "CustomerName", extraDetails.Data.accountName }, { "MobileNumber", extraDetails.Data.mobileNumber }, - { "Amount", value.ToString() }, + { AmountKey, value.ToString() }, { "PataPawaPostPaidMessageType", "ProcessBill" } }; } @@ -323,7 +324,7 @@ private async Task>> BuildBillPayment { "MeterNumber", extraDetails.Data.accountNumber }, { "CustomerName", extraDetails.Data.accountName }, { "PataPawaPrePayMessageType", "vend" }, - { "Amount", value.ToString() }, + { AmountKey, value.ToString() }, }; } requestMessages.Add(paymentRequestMessage); @@ -346,7 +347,7 @@ private Result> BuildMobileSaleTransactionRe TransactionNumber = transactionNumber.ToString("D4") }; - saleTransactionRequest.AdditionalRequestMetadata = new Dictionary { { "Amount", value.ToString() }, { "CustomerAccountNumber", DefaultCustomerAccountNumber } }; + saleTransactionRequest.AdditionalRequestMetadata = new Dictionary { { AmountKey, 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() }, + { AmountKey, value.ToString() }, { "RecipientMobile", DefaultCustomerAccountNumber } }; requestMessages.Add(saleTransactionRequest); diff --git a/TransactionProcessing.MerchantPos/Runtime/MerchantMetrics.cs b/TransactionProcessing.MerchantPos/Runtime/MerchantMetrics.cs index 3344048..dc8f14a 100644 --- a/TransactionProcessing.MerchantPos/Runtime/MerchantMetrics.cs +++ b/TransactionProcessing.MerchantPos/Runtime/MerchantMetrics.cs @@ -12,34 +12,64 @@ public MerchantMetricSnapshot Get(Guid merchantId) public void IncrementSales(Guid merchantId) { var metric = Get(merchantId); - Interlocked.Increment(ref metric.SalesCount); - metric.LastSaleUtc = DateTime.UtcNow; + metric.IncrementSales(); } public void IncrementFailedSales(Guid merchantId) { var metric = Get(merchantId); - Interlocked.Increment(ref metric.FailedSales); + metric.IncrementFailedSales(); } public void SetBalance(Guid merchantId, decimal balance) { var metric = Get(merchantId); - metric.Balance = balance; + metric.SetBalance(balance); } public void SetLastEndOfDay(Guid merchantId) { var metric = Get(merchantId); - metric.LastEndOfDay = DateTime.UtcNow; + metric.SetLastEndOfDay(DateTime.UtcNow); } } public sealed class MerchantMetricSnapshot { - public int SalesCount; - public int FailedSales; - public decimal Balance; - public DateTime? LastSaleUtc; - public DateTime? LastEndOfDay; + private int salesCount; + private int failedSales; + private decimal balance; + private DateTime? lastSaleUtc; + private DateTime? lastEndOfDay; + + public int SalesCount => salesCount; + + public int FailedSales => failedSales; + + public decimal Balance => balance; + + public DateTime? LastSaleUtc => lastSaleUtc; + + public DateTime? LastEndOfDay => lastEndOfDay; + + public void IncrementSales() + { + Interlocked.Increment(ref salesCount); + lastSaleUtc = DateTime.UtcNow; + } + + public void IncrementFailedSales() + { + Interlocked.Increment(ref failedSales); + } + + public void SetBalance(decimal value) + { + balance = value; + } + + public void SetLastEndOfDay(DateTime value) + { + lastEndOfDay = value; + } } diff --git a/TransactionProcessing.MerchantPos/Runtime/MerchantRuntime.cs b/TransactionProcessing.MerchantPos/Runtime/MerchantRuntime.cs index 9973c4a..deab62d 100644 --- a/TransactionProcessing.MerchantPos/Runtime/MerchantRuntime.cs +++ b/TransactionProcessing.MerchantPos/Runtime/MerchantRuntime.cs @@ -38,10 +38,6 @@ public async Task RunAsync((String clientId, String clientSecret) serviceClient, { try { - // Get the service client token here (can manage the expiry/caching at this level) - //Result serviceToken = await this.GetToken(this.CurrentServiceToken, serviceClient, cancellationToken); - //this.CurrentServiceToken = serviceToken.Data; - await StartupSequence(config, cancellationToken); await RunMainLoop( config, cancellationToken); } @@ -195,13 +191,11 @@ private async Task RunMainLoop(MerchantConfig cfg, TokenResponse userToken = this.CurrentUserToken ?? throw new InvalidOperationException($"User token was not initialized for merchant {cfg.MerchantName}."); await ApiClient.SendLogon(cfg, userToken, merchant.TransactionNumber, token); - //_lastDailyLogonDate = now.Date; await this.Repository.UpdateLastLogon(cfg.MerchantId, cfg.MerchantName, now); Logger.LogInformation($"Performed daily logon for merchant {cfg.MerchantName} on {now:yyyy-MM-dd}"); } } else { - // Sell product await DoSaleCycle(cfg, merchant.TransactionNumber, token); } diff --git a/TransactionProcessing.MerchantPos/WorkerHost.cs b/TransactionProcessing.MerchantPos/WorkerHost.cs index 61c9cbb..044c86e 100644 --- a/TransactionProcessing.MerchantPos/WorkerHost.cs +++ b/TransactionProcessing.MerchantPos/WorkerHost.cs @@ -114,6 +114,7 @@ private async Task MonitorMerchantWorkerAsync(Guid merchantId, MerchantWorkerSta } catch (OperationCanceledException) when (workerState.CancellationTokenSource.IsCancellationRequested) { + Logger.LogDebug($"Merchant worker cancellation observed for {workerState.MerchantName} ({merchantId})."); } catch (Exception ex) { diff --git a/TransactionProcessor.HealthChecksUI/TransactionProcessor.HealthChecksUI/Program.cs b/TransactionProcessor.HealthChecksUI/TransactionProcessor.HealthChecksUI/Program.cs index 0ce64b0..59438a0 100644 --- a/TransactionProcessor.HealthChecksUI/TransactionProcessor.HealthChecksUI/Program.cs +++ b/TransactionProcessor.HealthChecksUI/TransactionProcessor.HealthChecksUI/Program.cs @@ -11,7 +11,7 @@ namespace TransactionProcessor.HealthChecksUI { using System.IO; - public class Program + public static class Program { public static void Main(string[] args) { diff --git a/TransactionProcessor.HealthChecksUI/TransactionProcessor.HealthChecksUI/Startup.cs b/TransactionProcessor.HealthChecksUI/TransactionProcessor.HealthChecksUI/Startup.cs index 7281a9b..d79e0b6 100644 --- a/TransactionProcessor.HealthChecksUI/TransactionProcessor.HealthChecksUI/Startup.cs +++ b/TransactionProcessor.HealthChecksUI/TransactionProcessor.HealthChecksUI/Startup.cs @@ -36,7 +36,7 @@ public void ConfigureServices(IServiceCollection services){ }).AddInMemoryStorage(); } - private HttpClientHandler ApiEndpointHttpHandler(IServiceProvider arg) + private HttpClientHandler ApiEndpointHttpHandler(IServiceProvider _) { return new HttpClientHandler(); } diff --git a/TransactionProcessor.SystemSetupTool/EstateSetupFunctions.cs b/TransactionProcessor.SystemSetupTool/EstateSetupFunctions.cs index ab5e5ad..a9cb600 100644 --- a/TransactionProcessor.SystemSetupTool/EstateSetupFunctions.cs +++ b/TransactionProcessor.SystemSetupTool/EstateSetupFunctions.cs @@ -569,7 +569,6 @@ private async Task CreateFloats(CancellationToken cancellationToken) { // Create the required floats CreateFloatForContractProductRequest request = new CreateFloatForContractProductRequest { ContractId = contractResponse.ContractId, ProductId = contractProduct.ProductId, CreateDateTime = DateTime.Now }; - // TODO: Need a way to verify the float exists Result createFloatResult = await this.TransactionProcessorClient.CreateFloatForContractProduct(this.TokenResponse.AccessToken, this.EstateId, request, cancellationToken); if (createFloatResult.IsFailed) return ResultHelpers.CreateFailure(createFloatResult); diff --git a/TransactionProcessor.SystemSetupTool/EventStoreFunctions.cs b/TransactionProcessor.SystemSetupTool/EventStoreFunctions.cs index feeaa8f..7520edd 100644 --- a/TransactionProcessor.SystemSetupTool/EventStoreFunctions.cs +++ b/TransactionProcessor.SystemSetupTool/EventStoreFunctions.cs @@ -11,6 +11,12 @@ namespace TransactionProcessor.SystemSetupTool; public class EventStoreFunctions{ + private const string TransactionProcessorGroup = "Transaction Processor"; + private const string TransactionProcessorDomainGroup = "Transaction Processor - Domain"; + private const string TransactionProcessorOrderedGroup = "Transaction Processor - Ordered"; + private const string FileProcessorGroup = "File Processor"; + private const string MessagingServiceGroup = "Messaging Service"; + private readonly KurrentDBProjectionManagementClient ProjectionClient; private readonly KurrentDBPersistentSubscriptionsClient PersistentSubscriptionsClient; @@ -32,44 +38,44 @@ public async Task SetupEventStore(CancellationToken cancellationToken) private async Task SetupSubscriptions(CancellationToken cancellationToken){ List<(String streamName, String groupName, Int32 retryCount)> subscriptions = [ - ("$ce-TransactionAggregate", "Transaction Processor", 0), - ("$ce-SettlementAggregate", "Transaction Processor", 0), - ("$ce-VoucherAggregate", "Transaction Processor", 0), - ("$ce-FloatAggregate", "Transaction Processor", 0), - ("$ce-MerchantStatementAggregate", "Transaction Processor", 0), - ("$ce-ContractAggregate", "Transaction Processor", 0), - ("$ce-EstateAggregate", "Transaction Processor", 0), - ("$ce-MerchantAggregate", "Transaction Processor", 0), - ("$ce-CallbackMessageAggregate", "Transaction Processor", 0), - ("$ce-ReconciliationAggregate", "Transaction Processor", 0), - ("$ce-FileAggregate", "Transaction Processor", 0), - ("$ce-FileImportLogAggregate", "Transaction Processor", 0), - ("$ce-OperatorAggregate", "Transaction Processor", 0), - ("$ce-MerchantBalanceArchive", "Transaction Processor", 0), - - ("$ce-TransactionAggregate", "Transaction Processor - Domain", 0), - ("$ce-SettlementAggregate", "Transaction Processor - Domain", 0), - ("$ce-FloatAggregate", "Transaction Processor - Domain", 0), - ("$ce-MerchantStatementForDateAggregate", "Transaction Processor - Domain", 0), - - ("$ce-EstateAggregate", "Transaction Processor - Ordered", 1), - ("$ce-SettlementAggregate", "Transaction Processor - Ordered", 1), - ("$ce-VoucherAggregate", "Transaction Processor - Ordered", 1), - ("$ce-TransactionAggregate", "Transaction Processor - Ordered", 0), - ("$ce-MerchantStatementAggregate", "Transaction Processor - Ordered", 0), - ("$ce-EstateAggregate", "Transaction Processor - Ordered", 0), - - ("$ce-FileAggregate", "File Processor", 0), - ("$ce-FileImportLogAggregate", "File Processor", 0), - - ("$ce-EmailAggregate", "Messaging Service", 0), - ("$ce-SMSAggregate", "Messaging Service", 0) + ("$ce-TransactionAggregate", TransactionProcessorGroup, 0), + ("$ce-SettlementAggregate", TransactionProcessorGroup, 0), + ("$ce-VoucherAggregate", TransactionProcessorGroup, 0), + ("$ce-FloatAggregate", TransactionProcessorGroup, 0), + ("$ce-MerchantStatementAggregate", TransactionProcessorGroup, 0), + ("$ce-ContractAggregate", TransactionProcessorGroup, 0), + ("$ce-EstateAggregate", TransactionProcessorGroup, 0), + ("$ce-MerchantAggregate", TransactionProcessorGroup, 0), + ("$ce-CallbackMessageAggregate", TransactionProcessorGroup, 0), + ("$ce-ReconciliationAggregate", TransactionProcessorGroup, 0), + ("$ce-FileAggregate", TransactionProcessorGroup, 0), + ("$ce-FileImportLogAggregate", TransactionProcessorGroup, 0), + ("$ce-OperatorAggregate", TransactionProcessorGroup, 0), + ("$ce-MerchantBalanceArchive", TransactionProcessorGroup, 0), + + ("$ce-TransactionAggregate", TransactionProcessorDomainGroup, 0), + ("$ce-SettlementAggregate", TransactionProcessorDomainGroup, 0), + ("$ce-FloatAggregate", TransactionProcessorDomainGroup, 0), + ("$ce-MerchantStatementForDateAggregate", TransactionProcessorDomainGroup, 0), + + ("$ce-EstateAggregate", TransactionProcessorOrderedGroup, 1), + ("$ce-SettlementAggregate", TransactionProcessorOrderedGroup, 1), + ("$ce-VoucherAggregate", TransactionProcessorOrderedGroup, 1), + ("$ce-TransactionAggregate", TransactionProcessorOrderedGroup, 0), + ("$ce-MerchantStatementAggregate", TransactionProcessorOrderedGroup, 0), + ("$ce-EstateAggregate", TransactionProcessorOrderedGroup, 0), + + ("$ce-FileAggregate", FileProcessorGroup, 0), + ("$ce-FileImportLogAggregate", FileProcessorGroup, 0), + + ("$ce-EmailAggregate", MessagingServiceGroup, 0), + ("$ce-SMSAggregate", MessagingServiceGroup, 0) ]; foreach ((String streamName, String groupName, Int32 retryCount) subscription in subscriptions){ Boolean exists = false; try{ - PersistentSubscriptionInfo subscriptionInfo = await this.PersistentSubscriptionsClient.GetInfoToStreamAsync(subscription.streamName, subscription.groupName, cancellationToken: cancellationToken, deadline:TimeSpan.FromSeconds(30)); + await this.PersistentSubscriptionsClient.GetInfoToStreamAsync(subscription.streamName, subscription.groupName, cancellationToken: cancellationToken, deadline: TimeSpan.FromSeconds(30)); exists = true; } catch(PersistentSubscriptionNotFoundException){ @@ -115,18 +121,15 @@ private async Task DeployProjections(CancellationToken cancellationToken body = body.Substring(x); - // Is this already deployed (in the master list) - if (currentProjections.Any(p => string.Equals(p.Name, name, StringComparison.Ordinal)) == false) - { - // Projection does not exist so create - await this.ProjectionClient.CreateContinuousAsync(name, body, true, cancellationToken: cancellationToken); - } - else - { - // Already exists so we need to update but do not reset - await this.ProjectionClient.DisableAsync(name, cancellationToken: cancellationToken); - await this.ProjectionClient.UpdateAsync(name, body, true, cancellationToken: cancellationToken); - await this.ProjectionClient.EnableAsync(name, cancellationToken: cancellationToken); + if (currentProjections.Any(p => string.Equals(p.Name, name, StringComparison.Ordinal)) == false) + { + await this.ProjectionClient.CreateContinuousAsync(name, body, true, cancellationToken: cancellationToken); + } + else + { + await this.ProjectionClient.DisableAsync(name, cancellationToken: cancellationToken); + await this.ProjectionClient.UpdateAsync(name, body, true, cancellationToken: cancellationToken); + await this.ProjectionClient.EnableAsync(name, cancellationToken: cancellationToken); } }
ContractFile profile