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);
}