diff --git a/README.md b/README.md index c357ae6..71f4f8e 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,13 @@ Dates use strict `YYYY-MM-DD` calendar values, so impossible dates such as with code `INVALID_FIXTURE_SCHEMA` and a row plus field diagnostic; invalid amounts use `INVALID_FIXTURE_AMOUNT`. +Collections may otherwise be empty, but scenarios that modify an existing +entity have explicit minimums: `duplicate-invoice` requires at least one +invoice and `vendor-bank-swap` requires at least one vendor. Missing scenario +seed entities throw `LedgerpetError` with code +`SCENARIO_FIXTURE_REQUIREMENT`; the other scenarios generate their own rows +and do not require a non-empty collection. + ## Development ```sh diff --git a/src/scenarios.js b/src/scenarios.js index 317156c..d283792 100644 --- a/src/scenarios.js +++ b/src/scenarios.js @@ -2,6 +2,11 @@ import { SCENARIOS } from "./constants.js"; import { LedgerpetError } from "./errors.js"; import { validateFixture } from "./fixtures.js"; +const SCENARIO_REQUIREMENTS = { + "duplicate-invoice": { collection: "invoices", entity: "invoice" }, + "vendor-bank-swap": { collection: "vendors", entity: "vendor" } +}; + export function listScenarios() { return [...SCENARIOS]; } @@ -11,6 +16,7 @@ export function generateScenario(fixture, scenario = "duplicate-invoice") { throw new LedgerpetError(`Unknown scenario '${scenario}'. Try one of: ${SCENARIOS.join(", ")}`, "UNKNOWN_SCENARIO"); } validateFixture(fixture); + requireScenarioSeed(fixture, scenario); const clone = structuredClone(fixture); const findings = []; if (scenario === "duplicate-invoice") duplicateInvoice(clone, findings); @@ -22,6 +28,16 @@ export function generateScenario(fixture, scenario = "duplicate-invoice") { return { fixture: clone, expectedFindings: findings }; } +function requireScenarioSeed(fixture, scenario) { + const requirement = SCENARIO_REQUIREMENTS[scenario]; + if (requirement && fixture[requirement.collection].length === 0) { + throw new LedgerpetError( + `Scenario '${scenario}' requires at least one ${requirement.entity} in the fixture`, + "SCENARIO_FIXTURE_REQUIREMENT" + ); + } +} + function duplicateInvoice(fixture, findings) { const invoice = fixture.invoices.find((row) => row.invoice_id === "INV-1003") ?? fixture.invoices[0]; const duplicate = { ...invoice, invoice_id: `${invoice.invoice_id}-DUP`, due_date: shiftDate(invoice.due_date, 2), note: "Injected duplicate invoice" }; diff --git a/tests/scenarios.test.js b/tests/scenarios.test.js index 2c5c003..2f22ce3 100644 --- a/tests/scenarios.test.js +++ b/tests/scenarios.test.js @@ -15,3 +15,25 @@ test("unknown scenario fails loudly", async () => { const base = await loadFixture("fixtures/sample"); assert.throws(() => generateScenario(base, "nope"), /Unknown scenario/); }); + +test("duplicate-invoice requires a seed invoice", async () => { + const base = await loadFixture("fixtures/sample"); + base.invoices = []; + assert.throws( + () => generateScenario(base, "duplicate-invoice"), + (error) => error.name === "LedgerpetError" && + error.code === "SCENARIO_FIXTURE_REQUIREMENT" && + error.message === "Scenario 'duplicate-invoice' requires at least one invoice in the fixture" + ); +}); + +test("vendor-bank-swap requires a seed vendor", async () => { + const base = await loadFixture("fixtures/sample"); + base.vendors = []; + assert.throws( + () => generateScenario(base, "vendor-bank-swap"), + (error) => error.name === "LedgerpetError" && + error.code === "SCENARIO_FIXTURE_REQUIREMENT" && + error.message === "Scenario 'vendor-bank-swap' requires at least one vendor in the fixture" + ); +});