mirror of
https://github.com/Sea-Haven-Industries/seahaven-ap.git
synced 2026-10-02 02:43:21 +00:00
fix(api): replace invoice lines in a single transaction
This commit is contained in:
parent
280014e89d
commit
2d3c3cb099
3 changed files with 57 additions and 10 deletions
|
|
@ -1,9 +1,9 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createApp } from "../app.js";
|
||||
import { createMemoryDocumentsStore } from "../documents.js";
|
||||
import { loadEnv } from "../env.js";
|
||||
import type { ErrorEnvelope } from "../http.js";
|
||||
import { createFakeDb, SEED } from "../test/fake-db.js";
|
||||
import { createFakeDb, emptyStore, SEED } from "../test/fake-db.js";
|
||||
|
||||
function expectEnvelope(body: unknown, code: string) {
|
||||
const envelope = body as ErrorEnvelope;
|
||||
|
|
@ -114,6 +114,35 @@ describe("invoice stubs", () => {
|
|||
expect(body.items).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("keeps existing lines when a replace insert fails", async () => {
|
||||
const store = emptyStore();
|
||||
const handle = createFakeDb(store);
|
||||
const db = handle.db as { insert: ReturnType<typeof vi.fn> };
|
||||
db.insert.mockImplementation(() => ({
|
||||
values: () => ({
|
||||
returning: async () => {
|
||||
throw new Error("insert failed");
|
||||
},
|
||||
}),
|
||||
}));
|
||||
const app = createApp(envFor("admin"), handle, {
|
||||
documents: createMemoryDocumentsStore(),
|
||||
});
|
||||
const response = await app.request(`/api/invoices/${SEED.invoice.id}/lines`, {
|
||||
method: "PUT",
|
||||
headers: jsonHeaders,
|
||||
body: JSON.stringify({
|
||||
items: [
|
||||
{ description: "Labor", amount: "1000.00" },
|
||||
{ description: "Parts", amount: "250.00" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
expect(response.status).toBe(500);
|
||||
expect(store.invoiceLines).toHaveLength(1);
|
||||
expect(store.invoiceLines[0]?.id).toBe(SEED.line.id);
|
||||
});
|
||||
|
||||
it("presigns a document and confirms after upload", async () => {
|
||||
const documents = createMemoryDocumentsStore();
|
||||
const app = createApp(envFor("admin"), createFakeDb(), { documents });
|
||||
|
|
|
|||
|
|
@ -379,15 +379,17 @@ export function createInvoiceRoutes(handle: Db, store: DocumentsStore) {
|
|||
if (!linesSumToAmount(parsedLines, invoice.amount)) {
|
||||
return errorJson(c, 400, "VALIDATION_ERROR", "Line amounts must sum to the invoice amount.");
|
||||
}
|
||||
await handle.db.delete(invoiceLines).where(eq(invoiceLines.invoiceId, id));
|
||||
const created: LineRow[] = [];
|
||||
for (const line of parsedLines) {
|
||||
const [row] = await handle.db
|
||||
.insert(invoiceLines)
|
||||
.values({ invoiceId: id, ...line })
|
||||
.returning();
|
||||
created.push(row);
|
||||
}
|
||||
await handle.db.transaction(async (tx) => {
|
||||
await tx.delete(invoiceLines).where(eq(invoiceLines.invoiceId, id));
|
||||
for (const line of parsedLines) {
|
||||
const [row] = await tx
|
||||
.insert(invoiceLines)
|
||||
.values({ invoiceId: id, ...line })
|
||||
.returning();
|
||||
created.push(row);
|
||||
}
|
||||
});
|
||||
return c.json({ items: created.map(toLine) });
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -233,6 +233,22 @@ export function createFakeDb(store: Store = emptyStore()): Db {
|
|||
rows.splice(0, rows.length);
|
||||
}),
|
||||
})),
|
||||
transaction: vi.fn(async (callback: (tx: never) => Promise<unknown>) => {
|
||||
const snapshot = structuredClone(store) as Store;
|
||||
try {
|
||||
return await callback(db as never);
|
||||
} catch (error) {
|
||||
(Object.keys(store) as Array<keyof Store>).forEach((key) => {
|
||||
const rows = store[key] as unknown as Array<Record<string, unknown>>;
|
||||
rows.splice(
|
||||
0,
|
||||
rows.length,
|
||||
...(snapshot[key] as unknown as Array<Record<string, unknown>>),
|
||||
);
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue