Lower values make newly added or enabled merchants appear in the schedule faster. Higher values reduce how often the worker re-checks the configuration.
Back to configuration hub
diff --git a/TransactionProcessing.MerchantFileProcessor/Configuration/FileStatusPollingOptions.cs b/TransactionProcessing.MerchantFileProcessor/Configuration/FileStatusPollingOptions.cs
index b66f377..d30c814 100644
--- a/TransactionProcessing.MerchantFileProcessor/Configuration/FileStatusPollingOptions.cs
+++ b/TransactionProcessing.MerchantFileProcessor/Configuration/FileStatusPollingOptions.cs
@@ -4,4 +4,4 @@ public sealed class FileStatusPollingOptions {
public int PollIntervalSeconds { get; init; } = 30;
public TimeSpan GetPollInterval() => TimeSpan.FromSeconds(this.PollIntervalSeconds);
-}
\ No newline at end of file
+}
diff --git a/TransactionProcessing.MerchantFileProcessor/Configuration/MerchantProcessingConfigurationStore.cs b/TransactionProcessing.MerchantFileProcessor/Configuration/MerchantProcessingConfigurationStore.cs
index 86ed253..3924fb0 100644
--- a/TransactionProcessing.MerchantFileProcessor/Configuration/MerchantProcessingConfigurationStore.cs
+++ b/TransactionProcessing.MerchantFileProcessor/Configuration/MerchantProcessingConfigurationStore.cs
@@ -122,6 +122,7 @@ private MerchantProcessingConfigurationSnapshot BuildBootstrapSnapshot()
var fileProcessing = await dbContext.MerchantProcessingFileProcessingRecords.AsNoTracking().FirstOrDefaultAsync(record => record.Id == 1, cancellationToken);
var transactionGeneration = await dbContext.MerchantProcessingTransactionGenerationRecords.AsNoTracking().FirstOrDefaultAsync(record => record.Id == 1, cancellationToken);
var fileStatusPolling = await dbContext.MerchantProcessingFileStatusPollingRecords.AsNoTracking().FirstOrDefaultAsync(record => record.Id == 1, cancellationToken);
+ var merchantScan = await dbContext.MerchantProcessingMerchantScanRecords.AsNoTracking().FirstOrDefaultAsync(record => record.Id == 1, cancellationToken);
var fileProfiles = await dbContext.MerchantProcessingFileProfileRecords.AsNoTracking()
.OrderBy(record => record.SortOrder)
@@ -135,13 +136,13 @@ private MerchantProcessingConfigurationSnapshot BuildBootstrapSnapshot()
.OrderBy(record => record.SortOrder)
.ToListAsync(cancellationToken);
- if (authentication is null && fileProcessing is null && transactionGeneration is null && fileStatusPolling is null &&
+ if (authentication is null && fileProcessing is null && transactionGeneration is null && fileStatusPolling is null && merchantScan is null &&
fileProfiles.Count == 0 && contractDefinitions.Count == 0 && merchants.Count == 0)
{
return null;
}
- if (authentication is null || fileProcessing is null || transactionGeneration is null || fileStatusPolling is null)
+ if (authentication is null || fileProcessing is null || transactionGeneration is null || fileStatusPolling is null || merchantScan is null)
{
return null;
}
@@ -173,7 +174,8 @@ private MerchantProcessingConfigurationSnapshot BuildBootstrapSnapshot()
FileStatusPolling = new FileStatusPollingOptions
{
PollIntervalSeconds = fileStatusPolling.PollIntervalSeconds
- }
+ },
+ MerchantScanIntervalSeconds = merchantScan.MerchantScanIntervalSeconds
};
options.FileProfiles.AddRange(fileProfiles.Select(record =>
@@ -271,6 +273,7 @@ private async Task HasNormalizedConfigurationAsync(MerchantFileProcessorDb
await dbContext.MerchantProcessingFileProcessingRecords.AnyAsync(record => record.Id == 1, cancellationToken) ||
await dbContext.MerchantProcessingTransactionGenerationRecords.AnyAsync(record => record.Id == 1, cancellationToken) ||
await dbContext.MerchantProcessingFileStatusPollingRecords.AnyAsync(record => record.Id == 1, cancellationToken) ||
+ await dbContext.MerchantProcessingMerchantScanRecords.AnyAsync(record => record.Id == 1, cancellationToken) ||
await dbContext.MerchantProcessingFileProfileRecords.AnyAsync(cancellationToken) ||
await dbContext.MerchantProcessingContractDefinitionRecords.AnyAsync(cancellationToken) ||
await dbContext.MerchantProcessingMerchantRecords.AnyAsync(cancellationToken);
@@ -358,6 +361,13 @@ private static async Task PersistSingletonsAsync(
UpdatedUtc = updatedUtc
});
+ dbContext.MerchantProcessingMerchantScanRecords.Add(new MerchantProcessingMerchantScanRecord
+ {
+ Id = 1,
+ MerchantScanIntervalSeconds = options.MerchantScanIntervalSeconds,
+ UpdatedUtc = updatedUtc
+ });
+
await dbContext.SaveChangesAsync(cancellationToken);
}
@@ -514,6 +524,7 @@ private static async Task ClearConfigurationTablesAsync(MerchantFileProcessorDbC
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM MerchantProcessingFileProcessingRecords;", cancellationToken);
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM MerchantProcessingTransactionGenerationRecords;", cancellationToken);
await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM MerchantProcessingFileStatusPollingRecords;", cancellationToken);
+ await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM MerchantProcessingMerchantScanRecords;", cancellationToken);
}
private static DateTimeOffset MaxUtc(params IEnumerable?[] sources)
diff --git a/TransactionProcessing.MerchantFileProcessor/Configuration/MerchantProcessingOptions.cs b/TransactionProcessing.MerchantFileProcessor/Configuration/MerchantProcessingOptions.cs
index ec5b917..55bbee1 100644
--- a/TransactionProcessing.MerchantFileProcessor/Configuration/MerchantProcessingOptions.cs
+++ b/TransactionProcessing.MerchantFileProcessor/Configuration/MerchantProcessingOptions.cs
@@ -11,9 +11,11 @@ public sealed class MerchantProcessingOptions {
public FileStatusPollingOptions FileStatusPolling { get; init; } = new();
+ public int MerchantScanIntervalSeconds { get; init; } = 5;
+
public List ContractDefinitions { get; init; } = [];
public List FileProfiles { get; init; } = [];
public List Merchants { get; init; } = [];
-}
\ No newline at end of file
+}
diff --git a/TransactionProcessing.MerchantFileProcessor/Configuration/MerchantProcessingOptionsValidator.cs b/TransactionProcessing.MerchantFileProcessor/Configuration/MerchantProcessingOptionsValidator.cs
index 22feb77..1ef0d1b 100644
--- a/TransactionProcessing.MerchantFileProcessor/Configuration/MerchantProcessingOptionsValidator.cs
+++ b/TransactionProcessing.MerchantFileProcessor/Configuration/MerchantProcessingOptionsValidator.cs
@@ -24,6 +24,10 @@ public static bool Validate(MerchantProcessingOptions options) {
return false;
}
+ if (options.MerchantScanIntervalSeconds <= 0) {
+ return false;
+ }
+
if (options.ContractDefinitions.Count == 0) {
return false;
}
@@ -78,4 +82,4 @@ private static bool AreFieldsValid(IEnumerable fields) {
return true;
}
-}
\ No newline at end of file
+}
diff --git a/TransactionProcessing.MerchantFileProcessor/Persistence/MerchantFileProcessorDbContext.cs b/TransactionProcessing.MerchantFileProcessor/Persistence/MerchantFileProcessorDbContext.cs
index 1e3118d..3290a46 100644
--- a/TransactionProcessing.MerchantFileProcessor/Persistence/MerchantFileProcessorDbContext.cs
+++ b/TransactionProcessing.MerchantFileProcessor/Persistence/MerchantFileProcessorDbContext.cs
@@ -16,6 +16,8 @@ public sealed class MerchantFileProcessorDbContext(DbContextOptions MerchantProcessingFileStatusPollingRecords => this.Set();
+ public DbSet MerchantProcessingMerchantScanRecords => this.Set();
+
public DbSet MerchantProcessingFileProfileRecords => this.Set();
public DbSet MerchantProcessingFileProfileFieldRecords => this.Set();
@@ -104,6 +106,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.Property(record => record.UpdatedUtc);
});
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("MerchantProcessingMerchantScanRecords");
+ entity.HasKey(record => record.Id);
+ entity.Property(record => record.UpdatedUtc);
+ });
+
modelBuilder.Entity(entity =>
{
entity.ToTable("MerchantProcessingFileProfiles");
diff --git a/TransactionProcessing.MerchantFileProcessor/Persistence/MerchantProcessingConfigurationEntities.cs b/TransactionProcessing.MerchantFileProcessor/Persistence/MerchantProcessingConfigurationEntities.cs
index 468b6f9..0413f0f 100644
--- a/TransactionProcessing.MerchantFileProcessor/Persistence/MerchantProcessingConfigurationEntities.cs
+++ b/TransactionProcessing.MerchantFileProcessor/Persistence/MerchantProcessingConfigurationEntities.cs
@@ -44,6 +44,15 @@ public sealed class MerchantProcessingFileStatusPollingRecord
public DateTimeOffset UpdatedUtc { get; set; }
}
+public sealed class MerchantProcessingMerchantScanRecord
+{
+ public int Id { get; set; }
+
+ public int MerchantScanIntervalSeconds { get; set; }
+
+ public DateTimeOffset UpdatedUtc { get; set; }
+}
+
public sealed class MerchantProcessingFileProfileRecord
{
public int Id { get; set; }
diff --git a/TransactionProcessing.MerchantFileProcessor/Program.cs b/TransactionProcessing.MerchantFileProcessor/Program.cs
index f240934..cf35629 100644
--- a/TransactionProcessing.MerchantFileProcessor/Program.cs
+++ b/TransactionProcessing.MerchantFileProcessor/Program.cs
@@ -365,6 +365,16 @@ UpdatedUtc TEXT NOT NULL
""",
cancellationToken);
+ await dbContext.Database.ExecuteSqlRawAsync(
+ """
+ CREATE TABLE IF NOT EXISTS MerchantProcessingMerchantScanRecords (
+ Id INTEGER NOT NULL CONSTRAINT PK_MerchantProcessingMerchantScanRecords PRIMARY KEY,
+ MerchantScanIntervalSeconds INTEGER NOT NULL,
+ UpdatedUtc TEXT NOT NULL
+ );
+ """,
+ cancellationToken);
+
await dbContext.Database.ExecuteSqlRawAsync(
"""
CREATE TABLE IF NOT EXISTS MerchantProcessingFileProfiles (
diff --git a/TransactionProcessing.MerchantFileProcessor/Reporting/ConfigurationManagementEndpointRouteBuilderExtensions.cs b/TransactionProcessing.MerchantFileProcessor/Reporting/ConfigurationManagementEndpointRouteBuilderExtensions.cs
index 23e3ef8..a0dc5a3 100644
--- a/TransactionProcessing.MerchantFileProcessor/Reporting/ConfigurationManagementEndpointRouteBuilderExtensions.cs
+++ b/TransactionProcessing.MerchantFileProcessor/Reporting/ConfigurationManagementEndpointRouteBuilderExtensions.cs
@@ -40,6 +40,7 @@ public static IEndpointRouteBuilder MapConfigurationManagementEndpoints(this IEn
{
PollIntervalSeconds = ParseInt(form["PollIntervalSeconds"], 30)
},
+ MerchantScanIntervalSeconds = ParseInt(form["MerchantScanIntervalSeconds"], 5),
ContractDefinitions = options.ContractDefinitions,
FileProfiles = options.FileProfiles,
Merchants = options.Merchants
@@ -70,6 +71,26 @@ public static IEndpointRouteBuilder MapConfigurationManagementEndpoints(this IEn
var merchantIndex = options.Merchants.FindIndex(entry =>
string.Equals(entry.MerchantId, originalMerchantId ?? merchantId, StringComparison.OrdinalIgnoreCase));
+ if (ParseBool(form["Delete"]))
+ {
+ if (merchantIndex < 0)
+ {
+ return Results.Redirect($"{returnUrl}?error={Uri.EscapeDataString("Merchant not found.")}");
+ }
+
+ options.Merchants.RemoveAt(merchantIndex);
+
+ try
+ {
+ await configurationStore.SaveAsync(options, cancellationToken);
+ return Results.Redirect("/ops/config/merchants?removed=1");
+ }
+ catch (Exception ex)
+ {
+ return Results.Redirect($"{returnUrl}?error={Uri.EscapeDataString(ex.Message)}");
+ }
+ }
+
var merchant = new MerchantOptions
{
Name = GetRequired(form, "Name"),
@@ -308,6 +329,7 @@ private static MerchantProcessingOptions Clone(MerchantProcessingOptions options
{
PollIntervalSeconds = options.FileStatusPolling.PollIntervalSeconds
},
+ MerchantScanIntervalSeconds = options.MerchantScanIntervalSeconds,
ContractDefinitions = options.ContractDefinitions
.Select(contract => new ContractDefinitionOptions
{
diff --git a/TransactionProcessing.MerchantFileProcessor/Worker.cs b/TransactionProcessing.MerchantFileProcessor/Worker.cs
index 375861a..e07e6d1 100644
--- a/TransactionProcessing.MerchantFileProcessor/Worker.cs
+++ b/TransactionProcessing.MerchantFileProcessor/Worker.cs
@@ -14,10 +14,16 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
while (!stoppingToken.IsCancellationRequested) {
MerchantProcessingOptions options = configurationState.Current;
MerchantOptions[] enabledMerchants = options.Merchants.Where(merchant => merchant.Enabled).ToArray();
+ TimeSpan scanInterval = TimeSpan.FromSeconds(Math.Max(1, options.MerchantScanIntervalSeconds));
if (enabledMerchants.Length == 0) {
logger.LogWarning("No enabled merchants are configured for processing. Waiting before checking again.");
- await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
+ try {
+ await Task.Delay(scanInterval, stoppingToken);
+ }
+ catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) {
+ break;
+ }
continue;
}
@@ -29,8 +35,23 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
LocalLogger.Context merchantLogger = LocalLogger.For(scheduledMerchant.Merchant.MerchantId);
merchantLogger.LogInformation($"Next merchant processing run scheduled at {nextRun:O} for slot {scheduledMerchant.ScheduledRunUtc:O}");
+ if (delay > scanInterval) {
+ try {
+ await Task.Delay(scanInterval, stoppingToken);
+ }
+ catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) {
+ break;
+ }
+ continue;
+ }
+
if (delay > TimeSpan.Zero) {
- await Task.Delay(delay, stoppingToken);
+ try {
+ await Task.Delay(delay, stoppingToken);
+ }
+ catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) {
+ break;
+ }
}
if (stoppingToken.IsCancellationRequested) {
diff --git a/TransactionProcessing.MerchantFileProcessor/merchant-processing.bootstrap.json b/TransactionProcessing.MerchantFileProcessor/merchant-processing.bootstrap.json
index 333f2f7..5a32966 100644
--- a/TransactionProcessing.MerchantFileProcessor/merchant-processing.bootstrap.json
+++ b/TransactionProcessing.MerchantFileProcessor/merchant-processing.bootstrap.json
@@ -14,6 +14,7 @@
"FileStatusPolling": {
"PollIntervalSeconds": 30
},
+ "MerchantScanIntervalSeconds": 5,
"ContractDefinitions": [
{
"ContractId": "052A6CA5-47FC-4844-ADCA-7D44EFD8F21C",
diff --git a/TransactionProcessing.MerchantPos/Pages/Config.cshtml b/TransactionProcessing.MerchantPos/Pages/Config.cshtml
index 9a8b186..2a503c8 100644
--- a/TransactionProcessing.MerchantPos/Pages/Config.cshtml
+++ b/TransactionProcessing.MerchantPos/Pages/Config.cshtml
@@ -30,7 +30,9 @@
+
+
Smaller values check for added, enabled, disabled, or removed merchants more quickly. Larger values reduce how often the service re-scans the database.