mirror of
https://github.com/Sea-Haven-Industries/seahaven-ap.git
synced 2026-09-30 05:43:18 +00:00
* 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
303 lines
11 KiB
TypeScript
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/,
|
|
);
|
|
});
|
|
});
|