seahaven-ap/src/mocks/invoices.ts

538 lines
13 KiB
TypeScript
Raw Normal View History

import { syncMockApprovalsForInvoice } from "@/mocks/approvals";
import { areMocksEnabled } from "@/mocks/env";
export type MockInvoiceStatus = "pending_approval" | "approved" | "scheduled" | "paid" | "rejected";
export type MockInvoiceLine = {
id: string;
description: string;
amount: number;
glAccountId: string;
departmentId: string;
};
export type MockInvoiceComment = {
id: string;
authorUserId: string;
authorName: string;
body: string;
createdAt: string;
};
export type MockInvoiceActivity = {
id: string;
actorUserId: string;
actorName: string;
message: string;
createdAt: string;
};
export type MockInvoice = {
id: string;
invoiceNumber: string;
invoiceNumbers: string;
vendorId: string;
vendorName: string;
amountDue: number;
amount: number;
dueDate: string;
payDate: string | null;
sendPaymentOn: string | null;
status: MockInvoiceStatus;
paymentMethod: "check" | "ach";
documentUrl: string;
memo: string;
lines: MockInvoiceLine[];
comments: MockInvoiceComment[];
activity: MockInvoiceActivity[];
};
export type MockInvoiceHeaderPatch = Partial<
Pick<
MockInvoice,
| "vendorName"
| "amountDue"
| "amount"
| "dueDate"
| "payDate"
| "sendPaymentOn"
| "paymentMethod"
| "memo"
>
>;
const SAMPLE_DOCUMENT_URL = "/fixtures/sample-invoice.pdf";
function cloneInvoice(invoice: MockInvoice): MockInvoice {
return {
...invoice,
lines: invoice.lines.map((line) => ({ ...line })),
comments: invoice.comments.map((comment) => ({ ...comment })),
activity: invoice.activity.map((entry) => ({ ...entry })),
};
}
const seedInvoices: MockInvoice[] = [
{
id: "inv-1",
invoiceNumber: "INV-1001",
invoiceNumbers: "INV-1001",
vendorId: "vendor-1",
vendorName: "Acme Supplies Co",
amountDue: 1250.5,
amount: 1250.5,
dueDate: "2026-08-20",
payDate: "2026-08-15",
sendPaymentOn: "2026-08-15",
status: "approved",
paymentMethod: "check",
documentUrl: SAMPLE_DOCUMENT_URL,
memo: "Office supplies restock",
lines: [
{
id: "inv-1-line-1",
description: "Printer paper (case)",
amount: 450.5,
glAccountId: "gl-1",
departmentId: "dept-4",
},
{
id: "inv-1-line-2",
description: "Toner cartridges",
amount: 800,
glAccountId: "gl-1",
departmentId: "dept-4",
},
],
comments: [
{
id: "inv-1-c1",
authorUserId: "user-1",
authorName: "Alex Rivera",
body: "Coded to Administration office supplies.",
createdAt: "2026-08-05T14:20:00.000Z",
},
],
activity: [
{
id: "inv-1-a1",
actorUserId: "user-1",
actorName: "Alex Rivera",
message: "Submitted for approval",
createdAt: "2026-08-05T15:00:00.000Z",
},
{
id: "inv-1-a2",
actorUserId: "user-2",
actorName: "Jordan Lee",
message: "Approved",
createdAt: "2026-08-06T10:12:00.000Z",
},
],
},
{
id: "inv-2",
invoiceNumber: "INV-1002",
invoiceNumbers: "INV-1002",
vendorId: "vendor-2",
vendorName: "Harbor Maintenance LLC",
amountDue: 4800,
amount: 4800,
dueDate: "2026-08-25",
payDate: null,
sendPaymentOn: "2026-08-20",
status: "pending_approval",
paymentMethod: "ach",
documentUrl: SAMPLE_DOCUMENT_URL,
memo: "Dock repair materials",
lines: [
{
id: "inv-2-line-1",
description: "Dock board replacement",
amount: 3200,
glAccountId: "gl-2",
departmentId: "dept-1",
},
{
id: "inv-2-line-2",
description: "Labor",
amount: 1600,
glAccountId: "gl-4",
departmentId: "dept-1",
},
],
comments: [
{
id: "inv-2-c1",
authorUserId: "user-1",
authorName: "Alex Rivera",
body: "Please confirm GL split before approve.",
createdAt: "2026-08-08T11:05:00.000Z",
},
],
activity: [
{
id: "inv-2-a1",
actorUserId: "user-1",
actorName: "Alex Rivera",
message: "Submitted for approval",
createdAt: "2026-08-08T11:10:00.000Z",
},
],
},
{
id: "inv-3",
invoiceNumber: "INV-1003",
invoiceNumbers: "INV-1003",
vendorId: "vendor-3",
vendorName: "Northwind Utilities",
amountDue: 312.75,
amount: 312.75,
dueDate: "2026-08-12",
payDate: "2026-08-10",
sendPaymentOn: "2026-08-10",
status: "approved",
paymentMethod: "check",
documentUrl: SAMPLE_DOCUMENT_URL,
memo: "July utilities",
lines: [
{
id: "inv-3-line-1",
description: "Electric service",
amount: 312.75,
glAccountId: "gl-3",
departmentId: "dept-1",
},
],
comments: [],
activity: [
{
id: "inv-3-a1",
actorUserId: "user-1",
actorName: "Alex Rivera",
message: "Submitted for approval",
createdAt: "2026-08-01T09:00:00.000Z",
},
{
id: "inv-3-a2",
actorUserId: "user-3",
actorName: "Sam Patel",
message: "Approved",
createdAt: "2026-08-02T16:40:00.000Z",
},
],
},
{
id: "inv-4",
invoiceNumber: "INV-1004",
invoiceNumbers: "INV-1004",
vendorId: "vendor-4",
vendorName: "Coastal Office Supply",
amountDue: 890.25,
amount: 900,
dueDate: "2026-09-01",
payDate: null,
sendPaymentOn: "2026-08-27",
status: "pending_approval",
paymentMethod: "ach",
documentUrl: SAMPLE_DOCUMENT_URL,
memo: "Desk chairs",
lines: [
{
id: "inv-4-line-1",
description: "Ergonomic chairs (3)",
amount: 900,
glAccountId: "gl-1",
departmentId: "dept-3",
},
],
comments: [],
activity: [
{
id: "inv-4-a1",
actorUserId: "user-1",
actorName: "Alex Rivera",
message: "Submitted for approval",
createdAt: "2026-08-09T13:22:00.000Z",
},
],
},
{
id: "inv-5",
invoiceNumber: "INV-1005",
invoiceNumbers: "INV-1005",
vendorId: "vendor-1",
vendorName: "Acme Supplies Co",
amountDue: 2100,
amount: 2100,
dueDate: "2026-07-30",
payDate: "2026-07-25",
sendPaymentOn: "2026-07-25",
status: "scheduled",
paymentMethod: "check",
documentUrl: SAMPLE_DOCUMENT_URL,
memo: "Quarterly stock",
lines: [
{
id: "inv-5-line-1",
description: "Bulk supplies",
amount: 2100,
glAccountId: "gl-1",
departmentId: "dept-2",
},
],
comments: [],
activity: [
{
id: "inv-5-a1",
actorUserId: "user-2",
actorName: "Jordan Lee",
message: "Approved",
createdAt: "2026-07-20T12:00:00.000Z",
},
{
id: "inv-5-a2",
actorUserId: "user-1",
actorName: "Alex Rivera",
message: "Scheduled for payment",
createdAt: "2026-07-22T09:30:00.000Z",
},
],
},
{
id: "inv-6",
invoiceNumber: "INV-1006",
invoiceNumbers: "INV-1006",
vendorId: "vendor-2",
vendorName: "Harbor Maintenance LLC",
amountDue: 0,
amount: 1550,
dueDate: "2026-07-15",
payDate: "2026-07-10",
sendPaymentOn: "2026-07-10",
status: "paid",
paymentMethod: "ach",
documentUrl: SAMPLE_DOCUMENT_URL,
memo: "Emergency pump repair",
lines: [
{
id: "inv-6-line-1",
description: "Pump repair",
amount: 1550,
glAccountId: "gl-2",
departmentId: "dept-1",
},
],
comments: [],
activity: [
{
id: "inv-6-a1",
actorUserId: "user-1",
actorName: "Alex Rivera",
message: "Marked paid",
createdAt: "2026-07-11T08:00:00.000Z",
},
],
},
{
id: "inv-7",
invoiceNumber: "INV-1007",
invoiceNumbers: "INV-1007",
vendorId: "vendor-3",
vendorName: "Northwind Utilities",
amountDue: 645.1,
amount: 645.1,
dueDate: "2026-08-28",
payDate: "2026-08-22",
sendPaymentOn: "2026-08-22",
status: "approved",
paymentMethod: "ach",
documentUrl: SAMPLE_DOCUMENT_URL,
memo: "August utilities",
lines: [
{
id: "inv-7-line-1",
description: "Water and sewer",
amount: 645.1,
glAccountId: "gl-3",
departmentId: "dept-1",
},
],
comments: [],
activity: [
{
id: "inv-7-a1",
actorUserId: "user-3",
actorName: "Sam Patel",
message: "Approved",
createdAt: "2026-08-18T17:05:00.000Z",
},
],
},
{
id: "inv-8",
invoiceNumber: "INV-1008",
invoiceNumbers: "INV-1008",
vendorId: "vendor-1",
vendorName: "Acme Supplies Co",
amountDue: 275.5,
amount: 275.5,
dueDate: "2026-08-30",
payDate: null,
sendPaymentOn: "2026-08-28",
status: "pending_approval",
paymentMethod: "check",
documentUrl: SAMPLE_DOCUMENT_URL,
memo: "Misc small order",
lines: [
{
id: "inv-8-line-1",
description: "Labels and binders",
amount: 275.5,
glAccountId: "gl-1",
departmentId: "dept-2",
},
],
comments: [
{
id: "inv-8-c1",
authorUserId: "user-1",
authorName: "Alex Rivera",
body: "Under threshold policy.",
createdAt: "2026-08-07T10:00:00.000Z",
},
],
activity: [
{
id: "inv-8-a1",
actorUserId: "user-1",
actorName: "Alex Rivera",
message: "Submitted for approval",
createdAt: "2026-08-07T10:05:00.000Z",
},
],
},
];
/** Mutable in-memory invoice store for mock POC interactions. */
let invoiceStore: MockInvoice[] = seedInvoices.map((invoice) => cloneInvoice(invoice));
function replaceInvoice(index: number, next: MockInvoice): MockInvoice {
invoiceStore = [...invoiceStore.slice(0, index), next, ...invoiceStore.slice(index + 1)];
return cloneInvoice(next);
}
export function getMockInvoices(): MockInvoice[] {
if (!areMocksEnabled()) {
return [];
}
return invoiceStore.map((invoice) => cloneInvoice(invoice));
}
export function getMockInvoice(id: string): MockInvoice | undefined {
if (!areMocksEnabled()) {
return undefined;
}
const found = invoiceStore.find((invoice) => invoice.id === id);
return found ? cloneInvoice(found) : undefined;
}
export function updateMockInvoicePayDate(id: string, payDate: string | null): MockInvoice {
const index = invoiceStore.findIndex((invoice) => invoice.id === id);
if (index === -1) {
throw new Error(`Mock invoice not found: ${id}`);
}
return replaceInvoice(index, { ...invoiceStore[index], payDate });
}
export function updateMockInvoiceHeader(id: string, patch: MockInvoiceHeaderPatch): MockInvoice {
const index = invoiceStore.findIndex((invoice) => invoice.id === id);
if (index === -1) {
throw new Error(`Mock invoice not found: ${id}`);
}
return replaceInvoice(index, { ...invoiceStore[index], ...patch });
}
export function updateMockInvoiceLines(id: string, lines: MockInvoiceLine[]): MockInvoice {
const index = invoiceStore.findIndex((invoice) => invoice.id === id);
if (index === -1) {
throw new Error(`Mock invoice not found: ${id}`);
}
return replaceInvoice(index, {
...invoiceStore[index],
lines: lines.map((line) => ({ ...line })),
});
}
export function addMockInvoiceComment(
id: string,
comment: Omit<MockInvoiceComment, "id" | "createdAt"> & {
id?: string;
createdAt?: string;
},
): MockInvoice {
const index = invoiceStore.findIndex((invoice) => invoice.id === id);
if (index === -1) {
throw new Error(`Mock invoice not found: ${id}`);
}
const current = invoiceStore[index];
const nextComment: MockInvoiceComment = {
id: comment.id ?? `comment-${Date.now()}`,
authorUserId: comment.authorUserId,
authorName: comment.authorName,
body: comment.body,
createdAt: comment.createdAt ?? new Date().toISOString(),
};
return replaceInvoice(index, {
...current,
comments: [...current.comments, nextComment],
});
}
export function transitionMockInvoiceStatus(
id: string,
status: MockInvoiceStatus,
activity: Omit<MockInvoiceActivity, "id" | "createdAt"> & {
id?: string;
createdAt?: string;
},
): MockInvoice {
const index = invoiceStore.findIndex((invoice) => invoice.id === id);
if (index === -1) {
throw new Error(`Mock invoice not found: ${id}`);
}
const current = invoiceStore[index];
const nextActivity: MockInvoiceActivity = {
id: activity.id ?? `activity-${Date.now()}`,
actorUserId: activity.actorUserId,
actorName: activity.actorName,
message: activity.message,
createdAt: activity.createdAt ?? new Date().toISOString(),
};
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. */
export function resetMockInvoices(): void {
invoiceStore = seedInvoices.map((invoice) => cloneInvoice(invoice));
}