diff --git a/Backend/ERPCore.Tests/GlAccountCodeValidatorTests.cs b/Backend/ERPCore.Tests/GlAccountCodeValidatorTests.cs index 739d843..73196d9 100644 --- a/Backend/ERPCore.Tests/GlAccountCodeValidatorTests.cs +++ b/Backend/ERPCore.Tests/GlAccountCodeValidatorTests.cs @@ -26,8 +26,12 @@ public class GlAccountCodeValidatorTests await validator.StartAsync(CancellationToken.None); } + /// + /// 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. + /// [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( @@ -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); } diff --git a/Backend/ERPCore/Infra/Gl/GlAccountCodeValidator.cs b/Backend/ERPCore/Infra/Gl/GlAccountCodeValidator.cs index 74340bf..ece1c0a 100644 --- a/Backend/ERPCore/Infra/Gl/GlAccountCodeValidator.cs +++ b/Backend/ERPCore/Infra/Gl/GlAccountCodeValidator.cs @@ -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. /// -/// Deliberately asymmetric about failure: a confirmed missing code is fatal (the config is -/// wrong and no amount of waiting fixes it), but an unreachable 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. +/// Reports, does not block. 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. +/// +/// +/// Set GeneralLedgerService:FailOnMissingAccountCodes to true to restore hard-fail +/// once the chart of accounts is known-good and you want drift to stop a deploy. /// /// 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); }