fix(api): stop PATCH from skipping the approval workflow

This commit is contained in:
Adam Moussa 2026-09-22 13:57:50 -04:00
parent d96fb4fc18
commit 963d48f140
No known key found for this signature in database
3 changed files with 25 additions and 9 deletions

View file

@ -64,8 +64,9 @@ patch:
example: "2026-09-01"
status:
type: string
enum: [pending_approval, approved, scheduled, paid, rejected, void]
example: approved
enum: [void]
description: The only status PATCH may set. Approval and payment statuses go through decision routes.
example: void
paymentMethod:
type: string
enum: [check, ach]

View file

@ -155,6 +155,19 @@ describe("invoice stubs", () => {
expect(body.items).toHaveLength(2);
});
it("rejects PATCH that sets approved without an approval decision", async () => {
const app = createApp(envFor("admin"), createFakeDb(), {
documents: createMemoryDocumentsStore(),
});
const response = await app.request(`/api/invoices/${SEED.invoice.id}`, {
method: "PATCH",
headers: jsonHeaders,
body: JSON.stringify({ status: "approved" }),
});
expect(response.status).toBe(400);
expectEnvelope(await response.json(), "VALIDATION_ERROR");
});
it("keeps existing lines when a replace insert fails", async () => {
const store = emptyStore();
const handle = createFakeDb(store);

View file

@ -23,7 +23,6 @@ import {
rowsOf,
} from "./helpers.js";
const STATUSES = ["pending_approval", "approved", "scheduled", "paid", "rejected", "void"] as const;
const PAYMENT_METHODS = ["check", "ach"] as const;
type InvoiceRow = typeof invoices.$inferSelect;
@ -37,10 +36,6 @@ type LineInput = {
departmentId: string | null;
};
function isStatus(value: string): value is (typeof STATUSES)[number] {
return (STATUSES as readonly string[]).includes(value);
}
function isPaymentMethod(value: string): value is (typeof PAYMENT_METHODS)[number] {
return (PAYMENT_METHODS as readonly string[]).includes(value);
}
@ -305,8 +300,15 @@ export function createInvoiceRoutes(handle: Db, store: DocumentsStore) {
}
if (body.status !== undefined) {
const status = asString(body.status);
if (!isStatus(status)) return errorJson(c, 400, "VALIDATION_ERROR", "Invalid status.");
patch.status = status;
if (status !== "void") {
return errorJson(
c,
400,
"VALIDATION_ERROR",
"PATCH may only set status to void. Approval and payment statuses go through decision routes.",
);
}
patch.status = "void";
}
if (body.paymentMethod !== undefined) {
const method = asString(body.paymentMethod);