The three synchronous importers (timesheets, fuel, plant hours) used to write status: "processed" on their audit row before a single insert ran, and never updated it. Production had 21 such phantom-processed batches across payroll and plant hours. resolveDocumentOutcome() computes the real status from actual insert counts, so no route can claim success it didn't earn. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
80 lines
2.6 KiB
TypeScript
80 lines
2.6 KiB
TypeScript
// src/lib/document-status.test.ts
|
|
import { describe, it, expect } from "vitest";
|
|
import { resolveDocumentOutcome } from "./document-status";
|
|
|
|
const NOW = new Date("2026-09-11T10:00:00.000Z");
|
|
|
|
describe("resolveDocumentOutcome", () => {
|
|
it("marks a clean import processed with the real row count", () => {
|
|
const r = resolveDocumentOutcome(
|
|
{ rowsTotal: 40, rowsCreated: 40, rowErrors: [] },
|
|
NOW,
|
|
);
|
|
expect(r.processing_status).toBe("processed");
|
|
expect(r.rows_created).toBe(40);
|
|
expect(r.rows_failed).toBe(0);
|
|
expect(r.last_error).toBe("");
|
|
expect(r.processed_at).toBe(NOW.toISOString());
|
|
});
|
|
|
|
it("marks a partial import processed but records the failures", () => {
|
|
const r = resolveDocumentOutcome(
|
|
{
|
|
rowsTotal: 10,
|
|
rowsCreated: 7,
|
|
rowErrors: [
|
|
{ row: 3, reason: "unknown vehicle qr=1234" },
|
|
{ row: 8, reason: "invalid date" },
|
|
{ row: 9, reason: "invalid litres" },
|
|
],
|
|
},
|
|
NOW,
|
|
);
|
|
expect(r.processing_status).toBe("processed");
|
|
expect(r.rows_created).toBe(7);
|
|
expect(r.rows_failed).toBe(3);
|
|
expect(r.last_error).toBe("3 of 10 rows failed");
|
|
expect(r.row_errors).toHaveLength(3);
|
|
});
|
|
|
|
// THE REGRESSION GUARD.
|
|
// Production had 19 plant_hours batches marked `processed` against a
|
|
// single plant_hours_log row, because status was hardcoded at create
|
|
// time before any insert ran. A document that created nothing is not
|
|
// processed, no matter what the caller thinks.
|
|
it("never reports processed when nothing was created", () => {
|
|
const r = resolveDocumentOutcome(
|
|
{
|
|
rowsTotal: 12,
|
|
rowsCreated: 0,
|
|
rowErrors: [{ row: 1, reason: "unknown vehicle qr=9999" }],
|
|
},
|
|
NOW,
|
|
);
|
|
expect(r.processing_status).toBe("failed");
|
|
expect(r.rows_created).toBe(0);
|
|
expect(r.last_error).toBe("unknown vehicle qr=9999");
|
|
});
|
|
|
|
it("fails a file that parsed to no rows at all", () => {
|
|
const r = resolveDocumentOutcome(
|
|
{ rowsTotal: 0, rowsCreated: 0, rowErrors: [] },
|
|
NOW,
|
|
);
|
|
expect(r.processing_status).toBe("failed");
|
|
expect(r.last_error).toBe("no data rows found in file");
|
|
});
|
|
|
|
it("caps row_errors so a pathological sheet cannot blow the json field", () => {
|
|
const many = Array.from({ length: 500 }, (_, i) => ({
|
|
row: i,
|
|
reason: "bad",
|
|
}));
|
|
const r = resolveDocumentOutcome(
|
|
{ rowsTotal: 500, rowsCreated: 0, rowErrors: many },
|
|
NOW,
|
|
);
|
|
expect(r.row_errors).toHaveLength(200);
|
|
expect(r.rows_failed).toBe(500);
|
|
});
|
|
});
|