mirror of
https://github.com/Sea-Haven-Industries/seahaven-ap.git
synced 2026-10-02 03:53:23 +00:00
fix(api): create invoices and lines in one transaction
This commit is contained in:
parent
2d3c3cb099
commit
7e186e060b
2 changed files with 70 additions and 21 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import { getTableName } from "drizzle-orm";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createApp } from "../app.js";
|
||||
import { createMemoryDocumentsStore } from "../documents.js";
|
||||
|
|
@ -62,6 +63,46 @@ describe("invoice stubs", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("does not persist a partial invoice when a line insert fails", async () => {
|
||||
const store = emptyStore();
|
||||
const handle = createFakeDb(store);
|
||||
const db = handle.db as { insert: ReturnType<typeof vi.fn> };
|
||||
const originalInsert = db.insert.getMockImplementation() ?? db.insert;
|
||||
db.insert.mockImplementation((table: unknown) => {
|
||||
if (getTableName(table as never) === "invoice_lines") {
|
||||
return {
|
||||
values: () => ({
|
||||
returning: async () => {
|
||||
throw new Error("insert failed");
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
return originalInsert(table);
|
||||
});
|
||||
const app = createApp(envFor("admin"), handle, {
|
||||
documents: createMemoryDocumentsStore(),
|
||||
});
|
||||
const response = await app.request("/api/invoices", {
|
||||
method: "POST",
|
||||
headers: jsonHeaders,
|
||||
body: JSON.stringify({
|
||||
vendorId: SEED.vendor.id,
|
||||
invoiceNumber: "INV-2003",
|
||||
amount: "100.00",
|
||||
dueDate: "2026-10-01",
|
||||
lines: [
|
||||
{ description: "Half", amount: "40.00" },
|
||||
{ description: "Rest", amount: "60.00" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
expect(response.status).toBe(500);
|
||||
expect(store.invoices.map((row) => row.invoiceNumber)).toEqual(["INV-1001"]);
|
||||
expect(store.invoiceLines).toHaveLength(1);
|
||||
expect(store.invoiceLines[0]?.id).toBe(SEED.line.id);
|
||||
});
|
||||
|
||||
it("rejects a duplicate active vendor and invoice number", async () => {
|
||||
const app = createApp(envFor("admin"), createFakeDb(), {
|
||||
documents: createMemoryDocumentsStore(),
|
||||
|
|
|
|||
|
|
@ -215,21 +215,33 @@ export function createInvoiceRoutes(handle: Db, store: DocumentsStore) {
|
|||
"An active invoice already uses this vendor and invoice number.",
|
||||
);
|
||||
}
|
||||
let row: InvoiceRow;
|
||||
let latest: InvoiceRow | undefined;
|
||||
let currentLines: LineRow[] = [];
|
||||
try {
|
||||
[row] = await handle.db
|
||||
.insert(invoices)
|
||||
.values({
|
||||
vendorId,
|
||||
invoiceNumber,
|
||||
amount,
|
||||
amountDue: amount,
|
||||
dueDate,
|
||||
paymentMethod: method,
|
||||
memo: asString(body.memo),
|
||||
status: "pending_approval",
|
||||
})
|
||||
.returning();
|
||||
await handle.db.transaction(async (tx) => {
|
||||
const scoped = { ...handle, db: tx } as typeof handle;
|
||||
const [row] = await tx
|
||||
.insert(invoices)
|
||||
.values({
|
||||
vendorId,
|
||||
invoiceNumber,
|
||||
amount,
|
||||
amountDue: amount,
|
||||
dueDate,
|
||||
paymentMethod: method,
|
||||
memo: asString(body.memo),
|
||||
status: "pending_approval",
|
||||
})
|
||||
.returning();
|
||||
for (const line of parsedLines) {
|
||||
await tx
|
||||
.insert(invoiceLines)
|
||||
.values({ invoiceId: row.id, ...line })
|
||||
.returning();
|
||||
}
|
||||
latest = await applyMatchingPolicy(scoped, row, caller(c).id);
|
||||
currentLines = await linesFor(scoped, row.id);
|
||||
});
|
||||
} catch (error) {
|
||||
if (isUniqueViolation(error)) {
|
||||
return errorJson(
|
||||
|
|
@ -241,14 +253,10 @@ export function createInvoiceRoutes(handle: Db, store: DocumentsStore) {
|
|||
}
|
||||
throw error;
|
||||
}
|
||||
for (const line of parsedLines) {
|
||||
await handle.db
|
||||
.insert(invoiceLines)
|
||||
.values({ invoiceId: row.id, ...line })
|
||||
.returning();
|
||||
if (!latest) {
|
||||
return errorJson(c, 500, "INTERNAL_ERROR", "Invoice create did not complete.");
|
||||
}
|
||||
const latest = await applyMatchingPolicy(handle, row, caller(c).id);
|
||||
return c.json(toInvoice(latest, await linesFor(handle, row.id)), 201);
|
||||
return c.json(toInvoice(latest, currentLines), 201);
|
||||
});
|
||||
|
||||
routes.patch("/invoices/:id", async (c) => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue