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[]; hasBolt: boolean; hasBell: boolean; hasEye: boolean; actionLabel: string; lastUpdate: string; lastUpdateMessage: string; lastUpdateActorName: string; lastUpdateAt: string; invoiceDate: string; processingBegan: string; tagging: string; fs: string; description: string; invoiceType: string; location: string; paymentAccount: string; relatedInvoice: string | null; rowCount: number; }; export type MockInvoiceHeaderPatch = Partial< Pick< MockInvoice, | "vendorName" | "amountDue" | "amount" | "dueDate" | "payDate" | "sendPaymentOn" | "paymentMethod" | "memo" > >; const SAMPLE_DOCUMENT_URL = "/fixtures/sample-invoice.pdf"; type MockInvoiceCore = Omit< MockInvoice, | "hasBolt" | "hasBell" | "hasEye" | "actionLabel" | "lastUpdate" | "lastUpdateMessage" | "lastUpdateActorName" | "lastUpdateAt" | "invoiceDate" | "processingBegan" | "tagging" | "fs" | "description" | "invoiceType" | "location" | "paymentAccount" | "relatedInvoice" | "rowCount" >; const ACTION_LABEL_BY_STATUS: Record = { pending_approval: "Approve invoice", approved: "Approved", scheduled: "Scheduled", paid: "Paid", rejected: "Rejected", }; function actionLabelForStatus(status: MockInvoiceStatus): string { return ACTION_LABEL_BY_STATUS[status]; } function derivedActivityFields(invoice: Pick) { const events = [ ...invoice.activity.map((entry) => ({ at: entry.createdAt, message: entry.message, actor: entry.actorName, })), ...invoice.comments.map((comment) => ({ at: comment.createdAt, message: comment.body, actor: comment.authorName, })), ]; const latest = events.reduce<(typeof events)[number] | undefined>((current, event) => { if (!current || event.at > current.at) { return event; } return current; }, undefined); const lastUpdateAt = latest?.at ?? `${invoice.dueDate}T12:00:00.000Z`; return { lastUpdate: lastUpdateAt.slice(0, 10), lastUpdateMessage: latest?.message ?? "Invoice updated", lastUpdateActorName: latest?.actor ?? "System", lastUpdateAt, }; } function paymentAccountFor(paymentMethod: MockInvoice["paymentMethod"]): string { return paymentMethod === "ach" ? "Operating ACH" : "Operating Check"; } /** Invoice date is independent of the due date. Offset varies per seed. */ function invoiceDateBeforeDue(dueDate: string, index: number): string { const [year, month, day] = dueDate.split("-").map(Number); const date = new Date(Date.UTC(year, month - 1, day)); date.setUTCDate(date.getUTCDate() - (14 + index)); return date.toISOString().slice(0, 10); } /** Keep catalog-derived fields in sync after mock mutations. */ function withRefreshedCatalogFields(invoice: MockInvoice): MockInvoice { return { ...invoice, actionLabel: actionLabelForStatus(invoice.status), ...derivedActivityFields(invoice), description: invoice.memo, paymentAccount: paymentAccountFor(invoice.paymentMethod), rowCount: invoice.lines.length, }; } function enrichInvoice(core: MockInvoiceCore, index: number): MockInvoice { return withRefreshedCatalogFields({ ...core, hasBolt: index % 3 === 0, hasBell: index % 2 === 0, hasEye: true, actionLabel: actionLabelForStatus(core.status), lastUpdate: "", lastUpdateMessage: "", lastUpdateActorName: "", lastUpdateAt: "", invoiceDate: invoiceDateBeforeDue(core.dueDate, index), processingBegan: core.activity[0]?.createdAt?.slice(0, 10) ?? core.dueDate, tagging: index % 2 === 0 ? "Ops" : "", fs: index % 2 === 0 ? "synced" : "", description: core.memo, invoiceType: "Standard", location: "Ronkonkoma", paymentAccount: paymentAccountFor(core.paymentMethod), relatedInvoice: null, rowCount: core.lines.length, }); } 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 seedInvoiceCores: MockInvoiceCore[] = [ { 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", }, ], }, ]; const seedInvoices: MockInvoice[] = seedInvoiceCores.map((core, index) => enrichInvoice(core, index), ); /** 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}`); } const current = invoiceStore[index]; return replaceInvoice( index, withRefreshedCatalogFields({ ...current, payDate, activity: [ ...current.activity, { id: `activity-pay-date-${Date.now()}`, actorUserId: "user-1", actorName: "System", message: "Pay date updated", createdAt: new Date().toISOString(), }, ], }), ); } 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, withRefreshedCatalogFields({ ...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, withRefreshedCatalogFields({ ...invoiceStore[index], lines: lines.map((line) => ({ ...line })), }), ); } export function addMockInvoiceComment( id: string, comment: Omit & { 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, withRefreshedCatalogFields({ ...current, comments: [...current.comments, nextComment], }), ); } export function transitionMockInvoiceStatus( id: string, status: MockInvoiceStatus, activity: Omit & { 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, withRefreshedCatalogFields({ ...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)); }