seahaven-ap/src/mocks/index.test.ts
Adam Moussa c6867e6bec
feat(web): expand mock invoice fields and multi-line cells (AP-47) (#16)
* feat(web): fix shell chrome, logo home, and live nav badges

Refs: AP-42

* feat(web): add invoice grid lines and bold headers

Refs: AP-44

* feat(web): support auto row height for multi-line invoice cells

Refs: AP-44

* feat(web): expand mock invoice fields for Stampli column catalog

Refs: AP-47

* feat(web): add multi-line Stampli invoice cell renderers

Refs: AP-47

* fix(web): address review feedback

* ci: remove the pr policy caller

* fix(web): keep mock catalog dates and updates in sync

* fix(web): derive mock action labels from invoice status

* fix(ci): drop the unused catalog refresh index

* fix(ci): format invoice mock helpers
2026-09-25 21:51:59 +00:00

303 lines
11 KiB
TypeScript

import { afterEach, describe, expect, it } from "vitest";
import {
addMockInvoiceComment,
areMocksEnabled,
getMockApprovalPolicies,
getMockApprovals,
getMockDepartments,
getMockGlAccounts,
getMockInvoice,
getMockInvoices,
getMockPendingApprovals,
getMockSessionUser,
getMockUsers,
getMockVendors,
resetMockApprovalPolicies,
resetMockApprovals,
resetMockDepartments,
resetMockGlAccounts,
resetMockInvoices,
resetMockSession,
resetMockUsers,
resetMockVendors,
setMockSessionUserId,
transitionMockInvoiceStatus,
updateMockApprovalStatus,
updateMockInvoiceHeader,
updateMockInvoiceLines,
updateMockInvoicePayDate,
upsertMockDepartment,
upsertMockGlAccount,
upsertMockVendor,
upsertMockUser,
upsertMockApprovalPolicy,
} from "@/mocks";
describe("mocks module", () => {
afterEach(() => {
resetMockInvoices();
resetMockVendors();
resetMockGlAccounts();
resetMockDepartments();
resetMockUsers();
resetMockApprovalPolicies();
resetMockApprovals();
resetMockSession();
});
it("defaults to mocks enabled", () => {
expect(areMocksEnabled()).toBe(true);
});
it("exposes sample invoices and vendors", () => {
expect(getMockVendors().length).toBeGreaterThan(0);
expect(getMockInvoices().length).toBeGreaterThanOrEqual(6);
});
it("seeds Stampli catalog fields on every invoice (AP-47)", () => {
const invoices = getMockInvoices();
expect(invoices.length).toBeGreaterThan(0);
for (const invoice of invoices) {
expect(typeof invoice.hasBolt).toBe("boolean");
expect(typeof invoice.hasBell).toBe("boolean");
expect(typeof invoice.hasEye).toBe("boolean");
expect(invoice.actionLabel.length).toBeGreaterThan(0);
expect(invoice.lastUpdate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
expect(invoice.lastUpdateMessage.length).toBeGreaterThan(0);
expect(invoice.lastUpdateActorName.length).toBeGreaterThan(0);
expect(invoice.lastUpdateAt.length).toBeGreaterThan(0);
expect(invoice.invoiceDate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
expect(invoice.invoiceDate).not.toBe(invoice.dueDate);
expect(invoice.processingBegan).toMatch(/^\d{4}-\d{2}-\d{2}$/);
expect(typeof invoice.tagging).toBe("string");
expect(typeof invoice.fs).toBe("string");
expect(typeof invoice.description).toBe("string");
expect(typeof invoice.invoiceType).toBe("string");
expect(typeof invoice.location).toBe("string");
expect(typeof invoice.paymentAccount).toBe("string");
expect(invoice.relatedInvoice === null || typeof invoice.relatedInvoice === "string").toBe(
true,
);
expect(invoice.rowCount).toBe(invoice.lines.length);
}
const pending = invoices.filter((invoice) => invoice.status === "pending_approval");
expect(pending.every((invoice) => invoice.actionLabel === "Approve invoice")).toBe(true);
const nonPending = invoices.filter((invoice) => invoice.status !== "pending_approval");
expect(nonPending.every((invoice) => invoice.actionLabel !== "Approve invoice")).toBe(true);
expect(
invoices
.filter((invoice) => invoice.status === "approved")
.every((invoice) => invoice.actionLabel === "Approved"),
).toBe(true);
expect(
invoices
.filter((invoice) => invoice.status === "paid")
.every((invoice) => invoice.actionLabel === "Paid"),
).toBe(true);
expect(
invoices
.filter((invoice) => invoice.status === "scheduled")
.every((invoice) => invoice.actionLabel === "Scheduled"),
).toBe(true);
expect(
invoices
.filter((invoice) => invoice.status === "rejected")
.every((invoice) => invoice.actionLabel === "Rejected"),
).toBe(true);
});
it("seeds at least two invoices per dashboard tab", () => {
const invoices = getMockInvoices();
const pending = invoices.filter((invoice) => invoice.status === "pending_approval");
const ready = invoices.filter((invoice) => invoice.status === "approved");
expect(pending.length).toBeGreaterThanOrEqual(2);
expect(ready.length).toBeGreaterThanOrEqual(2);
expect(invoices.every((invoice) => invoice.invoiceNumbers && invoice.amount >= 0)).toBe(true);
});
it("updates pay date in the mutable mock store", () => {
const before = getMockInvoices().find((invoice) => invoice.id === "inv-1");
expect(before).toBeDefined();
const updated = updateMockInvoicePayDate("inv-1", "2026-08-18");
expect(updated.payDate).toBe("2026-08-18");
expect(updated.lastUpdateMessage).toBe("Pay date updated");
expect(updated.lastUpdateAt).not.toBe(before?.lastUpdateAt);
const after = getMockInvoices().find((invoice) => invoice.id === "inv-1");
expect(after?.payDate).toBe("2026-08-18");
});
it("throws when updating an unknown invoice", () => {
expect(() => updateMockInvoicePayDate("missing", "2026-08-18")).toThrow(
/Mock invoice not found/,
);
});
it("returns invoice detail with lines, comments, and activity", () => {
const invoice = getMockInvoice("inv-2");
expect(invoice).toBeDefined();
expect(invoice?.documentUrl).toBe("/fixtures/sample-invoice.pdf");
expect(invoice?.lines.length).toBeGreaterThan(0);
expect(invoice?.comments.length).toBeGreaterThan(0);
expect(invoice?.activity.length).toBeGreaterThan(0);
});
it("updates header and lines coding against mocks", () => {
const updated = updateMockInvoiceHeader("inv-2", { memo: "Updated memo", amountDue: 4700 });
expect(updated.memo).toBe("Updated memo");
expect(updated.amountDue).toBe(4700);
expect(updated.description).toBe("Updated memo");
const withLines = updateMockInvoiceLines("inv-2", [
{
id: "inv-2-line-1",
description: "Updated line",
amount: 4700,
glAccountId: "gl-2",
departmentId: "dept-1",
},
]);
expect(withLines.lines).toHaveLength(1);
expect(withLines.lines[0].description).toBe("Updated line");
expect(withLines.rowCount).toBe(1);
expect(getMockInvoice("inv-2")?.lines[0].description).toBe("Updated line");
expect(getMockInvoice("inv-2")?.rowCount).toBe(1);
});
it("adds comments and transitions status with activity", () => {
const withComment = addMockInvoiceComment("inv-2", {
authorUserId: "user-1",
authorName: "Alex Rivera",
body: "Need another look",
createdAt: "2026-08-11T09:00:00.000Z",
});
expect(withComment.comments.some((comment) => comment.body === "Need another look")).toBe(true);
const approved = transitionMockInvoiceStatus("inv-2", "approved", {
actorUserId: "user-2",
actorName: "Jordan Lee",
message: "Approved",
createdAt: "2026-08-12T09:00:00.000Z",
});
expect(approved.status).toBe("approved");
expect(approved.activity.some((entry) => entry.message === "Approved")).toBe(true);
expect(approved.lastUpdateMessage).toBe("Approved");
expect(approved.lastUpdateActorName).toBe("Jordan Lee");
expect(approved.lastUpdateAt).toBe("2026-08-12T09:00:00.000Z");
expect(approved.lastUpdate).toBe("2026-08-12");
expect(approved.actionLabel).not.toBe("Approve invoice");
expect(getMockPendingApprovals().some((item) => item.invoiceId === "inv-2")).toBe(false);
expect(getMockApprovals().find((item) => item.invoiceId === "inv-2")?.status).toBe("approved");
});
it("refreshes last update from a comment and payment account from the method", () => {
const commented = addMockInvoiceComment("inv-1", {
authorUserId: "user-1",
authorName: "Alex Rivera",
body: "Checked the total",
createdAt: "2026-09-01T15:00:00.000Z",
});
expect(commented.lastUpdateAt).toBe("2026-09-01T15:00:00.000Z");
expect(commented.lastUpdate).toBe("2026-09-01");
expect(commented.lastUpdateMessage).toBe("Checked the total");
expect(commented.lastUpdateActorName).toBe("Alex Rivera");
const method = updateMockInvoiceHeader("inv-1", { paymentMethod: "ach" });
expect(method.paymentAccount).toBe("Operating ACH");
expect(getMockInvoice("inv-1")?.paymentAccount).toBe("Operating ACH");
});
it("refreshes description and rowCount when header or lines change", () => {
const before = getMockInvoice("inv-1");
expect(before?.rowCount).toBe(2);
expect(before?.description).toBe("Office supplies restock");
const header = updateMockInvoiceHeader("inv-1", { memo: "Revised supplies memo" });
expect(header.description).toBe("Revised supplies memo");
const lines = updateMockInvoiceLines("inv-1", [
{
id: "inv-1-line-1",
description: "Only line",
amount: 100,
glAccountId: "gl-1",
departmentId: "dept-4",
},
]);
expect(lines.rowCount).toBe(1);
expect(getMockInvoice("inv-1")?.rowCount).toBe(1);
});
it("returns undefined for unknown invoice ids", () => {
expect(getMockInvoice("missing-invoice")).toBeUndefined();
});
it("switches mock session user", () => {
expect(getMockSessionUser().id).toBe("user-1");
const next = setMockSessionUserId("user-2");
expect(next.role).toBe("approver");
expect(getMockSessionUser().id).toBe("user-2");
});
it("includes inv-8 referenced by approvals", () => {
expect(getMockInvoice("inv-8")).toBeDefined();
expect(getMockApprovals().some((item) => item.invoiceId === "inv-8")).toBe(true);
});
it("seeds settings entities and supports upsert", () => {
expect(getMockVendors().length).toBeGreaterThanOrEqual(4);
expect(getMockGlAccounts().length).toBeGreaterThanOrEqual(3);
expect(getMockDepartments().length).toBeGreaterThanOrEqual(3);
expect(getMockUsers().length).toBeGreaterThanOrEqual(3);
expect(getMockApprovalPolicies().length).toBeGreaterThanOrEqual(3);
const vendor = upsertMockVendor({
id: "vendor-new",
name: "New Vendor",
email: "new@example.com",
defaultPaymentMethod: "ach",
});
expect(getMockVendors().some((row) => row.id === vendor.id)).toBe(true);
upsertMockGlAccount({ id: "gl-new", code: "9999", name: "Misc" });
expect(getMockGlAccounts().some((row) => row.id === "gl-new")).toBe(true);
upsertMockDepartment({ id: "dept-new", code: "NEW", name: "New Dept" });
expect(getMockDepartments().some((row) => row.id === "dept-new")).toBe(true);
upsertMockUser({
id: "user-new",
name: "New User",
email: "new.user@seahaven.example",
role: "approver",
});
expect(getMockUsers().some((row) => row.id === "user-new")).toBe(true);
upsertMockApprovalPolicy({
id: "policy-new",
name: "Ad hoc",
amountThreshold: 250,
approverRole: "admin",
});
expect(getMockApprovalPolicies().some((row) => row.id === "policy-new")).toBe(true);
});
it("lists pending approvals and updates status", () => {
const pending = getMockPendingApprovals();
expect(pending.length).toBeGreaterThanOrEqual(2);
expect(getMockApprovals().length).toBeGreaterThanOrEqual(pending.length);
const first = pending[0];
const updated = updateMockApprovalStatus(first.id, "approved");
expect(updated.status).toBe("approved");
expect(getMockPendingApprovals().some((row) => row.id === first.id)).toBe(false);
});
it("throws when updating an unknown approval", () => {
expect(() => updateMockApprovalStatus("missing", "rejected")).toThrow(
/Mock approval not found/,
);
});
});