Make the GL account-code check report instead of refusing to start

The check shipped in f753cae threw on a missing code, which core-dumped
erpcore.service on deploy: 11 of the 12 configured Gl*Code values turned
out never to have existed in GL's chart of accounts, so a long-standing
mismatch became an immediate outage.

Detection was the point; refusing to serve the rest of the app was not.
GL already rejects a posting to an unknown account, so the per-flow error
stays the real enforcement — this now logs the whole set at Error once at
boot instead of surfacing one flow at a time.

Hard-fail is still available behind
GeneralLedgerService:FailOnMissingAccountCodes, for once the chart of
accounts is known-good and drift should stop a deploy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 11:40:46 +05:30
parent b236b2b126
commit 9eab6372b3
2 changed files with 46 additions and 8 deletions
@@ -26,8 +26,12 @@ public class GlAccountCodeValidatorTests
await validator.StartAsync(CancellationToken.None);
}
/// <summary>
/// The default must not block startup. Throwing here cost an outage on 2026-10-07 — a config
/// mismatch that had existed all along became a core-dump the moment the check shipped.
/// </summary>
[Fact]
public async Task StartAsync_Throws_AndNamesEveryMissingCode()
public async Task StartAsync_DoesNotThrow_WhenCodesAreMissing_ByDefault()
{
var validator = CreateValidator(
new()
@@ -36,6 +40,22 @@ public class GlAccountCodeValidatorTests
["SalesDayEnd:GlCashAccountCode"] = "1000-01",
["Adjustment:GlLossAccountCode"] = "5100"
},
new StubGl(["1000-01"]));
await validator.StartAsync(CancellationToken.None);
}
[Fact]
public async Task StartAsync_Throws_AndNamesEveryMissingCode_WhenConfiguredToFail()
{
var validator = CreateValidator(
new()
{
["GeneralLedgerService:FailOnMissingAccountCodes"] = "true",
["Grn:GlInventoryAccountCode"] = "1100",
["SalesDayEnd:GlCashAccountCode"] = "1000-01",
["Adjustment:GlLossAccountCode"] = "5100"
},
new StubGl(["1100"]));
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
@@ -43,6 +63,7 @@ public class GlAccountCodeValidatorTests
Assert.Contains("1000-01", ex.Message);
Assert.Contains("5100", ex.Message);
Assert.Contains("2 of 3", ex.Message);
Assert.DoesNotContain("1100,", ex.Message);
}
@@ -9,10 +9,17 @@ namespace ERPCore.Infra.Gl;
/// 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.
/// <b>Reports, does not block.</b> An earlier revision threw on a missing code, on the reasoning
/// that the config is wrong and waiting won't fix it. In practice that took erpcore.service down
/// with a core-dump on 2026-10-07: 11 of the 12 configured codes turned out never to have existed
/// in GL, so a long-standing data mismatch became an immediate outage. Detection was the point;
/// refusing to serve the other 90% of the app was not. GL already rejects a posting to an unknown
/// account, so the per-flow error remains the real enforcement — this just surfaces the whole set
/// at boot instead of one flow at a time.
/// </para>
/// <para>
/// Set <c>GeneralLedgerService:FailOnMissingAccountCodes</c> to <c>true</c> to restore hard-fail
/// once the chart of accounts is known-good and you want drift to stop a deploy.
/// </para>
/// </summary>
public sealed class GlAccountCodeValidator(
@@ -78,9 +85,19 @@ public sealed class GlAccountCodeValidator(
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.");
{
var message =
$"General Ledger is missing {missing.Length} of {expected.Length} configured account code(s): " +
$"{string.Join(", ", missing.Order(StringComparer.Ordinal))}. Every flow that posts to one of these " +
"will fail against GL until they are seeded in GL's chart of accounts, or the Gl*Code settings are " +
"corrected to match the codes GL actually uses.";
if (configuration.GetValue("GeneralLedgerService:FailOnMissingAccountCodes", false))
throw new InvalidOperationException(message);
logger.LogError("{Message}", message);
return;
}
logger.LogInformation("GL account codes verified: {Count} present.", expected.Length);
}