Merge pull request 'Make the GL account-code check report instead of refusing to start' (#105) from Dev into production

Reviewed-on: #105
This commit was merged in pull request #105.
This commit is contained in:
2026-10-07 06:14:37 +00:00
2 changed files with 46 additions and 8 deletions
@@ -26,8 +26,12 @@ public class GlAccountCodeValidatorTests
await validator.StartAsync(CancellationToken.None); 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] [Fact]
public async Task StartAsync_Throws_AndNamesEveryMissingCode() public async Task StartAsync_DoesNotThrow_WhenCodesAreMissing_ByDefault()
{ {
var validator = CreateValidator( var validator = CreateValidator(
new() new()
@@ -36,6 +40,22 @@ public class GlAccountCodeValidatorTests
["SalesDayEnd:GlCashAccountCode"] = "1000-01", ["SalesDayEnd:GlCashAccountCode"] = "1000-01",
["Adjustment:GlLossAccountCode"] = "5100" ["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"])); new StubGl(["1100"]));
var ex = await Assert.ThrowsAsync<InvalidOperationException>( var ex = await Assert.ThrowsAsync<InvalidOperationException>(
@@ -43,6 +63,7 @@ public class GlAccountCodeValidatorTests
Assert.Contains("1000-01", ex.Message); Assert.Contains("1000-01", ex.Message);
Assert.Contains("5100", ex.Message); Assert.Contains("5100", ex.Message);
Assert.Contains("2 of 3", ex.Message);
Assert.DoesNotContain("1100,", 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 /// 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. /// discovery, repeat. This asserts the whole set once, at boot.
/// <para> /// <para>
/// Deliberately asymmetric about failure: a <em>confirmed missing</em> code is fatal (the config is /// <b>Reports, does not block.</b> An earlier revision threw on a missing code, on the reasoning
/// wrong and no amount of waiting fixes it), but an <em>unreachable</em> GL only logs a warning. /// that the config is wrong and waiting won't fix it. In practice that took erpcore.service down
/// GL runs on the same host as ERPCore, so on a host reboot it may simply not be listening yet — /// with a core-dump on 2026-10-07: 11 of the 12 configured codes turned out never to have existed
/// hard-failing there would turn a transient ordering issue into a deploy crash-loop. /// 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> /// </para>
/// </summary> /// </summary>
public sealed class GlAccountCodeValidator( public sealed class GlAccountCodeValidator(
@@ -78,9 +85,19 @@ public sealed class GlAccountCodeValidator(
var missing = expected.Where(c => !present.Contains(c)).ToArray(); var missing = expected.Where(c => !present.Contains(c)).ToArray();
if (missing.Length > 0) if (missing.Length > 0)
throw new InvalidOperationException( {
$"General Ledger is missing {missing.Length} configured account code(s): {string.Join(", ", missing)}. " + var message =
"Seed them in GL's chart of accounts, or correct the Gl*Code settings in configuration."); $"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); logger.LogInformation("GL account codes verified: {Count} present.", expected.Length);
} }