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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user