Refactor GL period and entry date handling to use BusinessClock for accurate date resolution; add GlAccountCodeValidator for startup validation of configured account codes.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
using ERPCore.Services.Interfaces;
|
||||
using ERPCore.System.Errors;
|
||||
|
||||
namespace ERPCore.Infra.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// Startup guard for the GL chart of accounts. Every <c>Gl*Code</c> in configuration names an
|
||||
/// account that must already exist in GL; until now a typo or an un-seeded account only surfaced
|
||||
/// as a 404 when some user happened to trigger that one flow, mid-transaction — one flow, one
|
||||
/// discovery, repeat. This asserts the whole set once, at boot.
|
||||
/// <para>
|
||||
/// Deliberately asymmetric about failure: a <em>confirmed missing</em> code is fatal (the config is
|
||||
/// wrong and no amount of waiting fixes it), but an <em>unreachable</em> GL only logs a warning.
|
||||
/// GL runs on the same host as ERPCore, so on a host reboot it may simply not be listening yet —
|
||||
/// hard-failing there would turn a transient ordering issue into a deploy crash-loop.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class GlAccountCodeValidator(
|
||||
IServiceProvider services,
|
||||
IConfiguration configuration,
|
||||
ILogger<GlAccountCodeValidator> logger) : IHostedService
|
||||
{
|
||||
/// <summary>Configuration sections that carry GL account codes — see appsettings.json.</summary>
|
||||
private static readonly string[] Sections =
|
||||
["Grn", "SalesDayEnd", "Adjustment", "SalesInvoice", "PurchaseInvoice", "Fifo"];
|
||||
|
||||
/// <summary>Startup must not stall on a hung GL — HttpClient's own default is 100s.</summary>
|
||||
private static readonly TimeSpan FetchTimeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
public async Task StartAsync(CancellationToken ct)
|
||||
{
|
||||
var expected = Sections
|
||||
.SelectMany(s => configuration.GetSection(s).GetChildren())
|
||||
.Where(c => c.Key.StartsWith("Gl", StringComparison.Ordinal) && c.Key.EndsWith("Code", StringComparison.Ordinal))
|
||||
.Select(c => c.Value)
|
||||
.Where(v => !string.IsNullOrWhiteSpace(v))
|
||||
.Select(v => v!)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
if (expected.Length == 0)
|
||||
{
|
||||
logger.LogWarning("No GL account codes are configured — skipping chart-of-accounts verification.");
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<string> present;
|
||||
try
|
||||
{
|
||||
using var scope = services.CreateScope();
|
||||
var gl = scope.ServiceProvider.GetRequiredService<IGeneralLedgerService>();
|
||||
|
||||
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
timeout.CancelAfter(FetchTimeout);
|
||||
present = await gl.ListAccountCodesAsync(timeout.Token);
|
||||
}
|
||||
catch (Exception e) when (e is DomainException or OperationCanceledException && !ct.IsCancellationRequested)
|
||||
{
|
||||
// Unreachable, timed out, or rejected the read — can't verify, so don't block startup.
|
||||
// The per-flow 404 remains the backstop it has always been.
|
||||
logger.LogWarning(e,
|
||||
"Could not read GL's chart of accounts at startup; {Count} configured account code(s) left unverified.",
|
||||
expected.Length);
|
||||
return;
|
||||
}
|
||||
|
||||
// A GL that answers 2xx but yields no codes at all is far more likely a response shape this
|
||||
// reader doesn't understand than a genuinely empty chart of accounts — and treating it as
|
||||
// "everything is missing" would take the whole app down on a contract change. Warn, don't fail.
|
||||
if (present.Count == 0)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"GL returned no account codes; {Count} configured account code(s) left unverified. " +
|
||||
"If GL's chart of accounts is not actually empty, the /accounts response shape has changed.",
|
||||
expected.Length);
|
||||
return;
|
||||
}
|
||||
|
||||
var missing = expected.Where(c => !present.Contains(c)).ToArray();
|
||||
if (missing.Length > 0)
|
||||
throw new InvalidOperationException(
|
||||
$"General Ledger is missing {missing.Length} configured account code(s): {string.Join(", ", missing)}. " +
|
||||
"Seed them in GL's chart of accounts, or correct the Gl*Code settings in configuration.");
|
||||
|
||||
logger.LogInformation("GL account codes verified: {Count} present.", expected.Length);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
|
||||
}
|
||||
Reference in New Issue
Block a user