fix(frontend): sync approvals; fix invoice not-found query (AP-6)

Keep approvals inbox rows aligned when detail stubs transition status, and return null from useInvoice so missing ids hit the not-found UI instead of a query error.
This commit is contained in:
Adam Moussa 2026-08-10 18:17:32 -04:00
parent 532c0f7f85
commit b00a3311f5
No known key found for this signature in database
6 changed files with 44 additions and 3 deletions

View file

@ -43,4 +43,10 @@ test.describe("invoice detail 3-pane shell", () => {
await expect(page).toHaveURL(/\/invoices\/inv-/);
await expect(page.getByTestId("invoice-document-viewer")).toBeVisible();
});
test("unknown invoice id shows not-found warning", async ({ page }) => {
await page.goto("/invoices/missing-invoice");
await expect(page.getByTestId("invoice-not-found")).toBeVisible();
await expect(page.getByTestId("invoice-document-viewer")).toHaveCount(0);
});
});

View file

@ -13,6 +13,7 @@ import {
type MockInvoiceLine,
type MockInvoiceStatus,
} from "@/mocks";
import { approvalQueryKeys } from "@/domain/approvals/query-keys";
import { invoiceQueryKeys } from "@/domain/invoices/query-keys";
async function invalidateInvoiceQueries(
@ -37,9 +38,9 @@ export function useInvoice(id: string | undefined) {
queryKey: invoiceQueryKeys.detail(id ?? ""),
queryFn: () => {
if (!id) {
return undefined;
return null;
}
return getMockInvoice(id);
return getMockInvoice(id) ?? null;
},
enabled: Boolean(id),
});
@ -117,6 +118,7 @@ export function useTransitionInvoiceStatus() {
}) => Promise.resolve(transitionMockInvoiceStatus(id, status, activity)),
onSuccess: async (_data, variables) => {
await invalidateInvoiceQueries(queryClient, variables.id);
await queryClient.invalidateQueries({ queryKey: approvalQueryKeys.all });
},
});
}

View file

@ -69,6 +69,23 @@ export function updateMockApprovalStatus(
return { ...updated };
}
/** Keep inbox rows aligned when invoice detail stubs transition status. */
export function syncMockApprovalsForInvoice(
invoiceId: string,
status: MockApprovalStatus,
): MockApprovalItem[] {
const updated: MockApprovalItem[] = [];
approvalStore = approvalStore.map((item) => {
if (item.invoiceId !== invoiceId) {
return item;
}
const next = { ...item, status };
updated.push({ ...next });
return next;
});
return updated;
}
export function resetMockApprovals(): void {
approvalStore = seedApprovals.map((item) => ({ ...item }));
}

View file

@ -124,6 +124,12 @@ describe("mocks module", () => {
});
expect(approved.status).toBe("approved");
expect(approved.activity.some((entry) => entry.message === "Approved")).toBe(true);
expect(getMockPendingApprovals().some((item) => item.invoiceId === "inv-2")).toBe(false);
expect(getMockApprovals().find((item) => item.invoiceId === "inv-2")?.status).toBe("approved");
});
it("returns undefined for unknown invoice ids", () => {
expect(getMockInvoice("missing-invoice")).toBeUndefined();
});
it("switches mock session user", () => {

View file

@ -64,6 +64,7 @@ export {
getMockApprovals,
getMockPendingApprovals,
resetMockApprovals,
syncMockApprovalsForInvoice,
updateMockApprovalStatus,
type MockApprovalItem,
type MockApprovalStatus,

View file

@ -1,3 +1,4 @@
import { syncMockApprovalsForInvoice } from "@/mocks/approvals";
import { areMocksEnabled } from "@/mocks/env";
export type MockInvoiceStatus = "pending_approval" | "approved" | "scheduled" | "paid" | "rejected";
@ -515,11 +516,19 @@ export function transitionMockInvoiceStatus(
createdAt: activity.createdAt ?? new Date().toISOString(),
};
return replaceInvoice(index, {
const updated = replaceInvoice(index, {
...current,
status,
activity: [...current.activity, nextActivity],
});
if (status === "approved" || status === "rejected") {
syncMockApprovalsForInvoice(id, status);
} else if (status === "pending_approval") {
syncMockApprovalsForInvoice(id, "pending");
}
return updated;
}
/** Test helper to restore seed data between unit tests. */