diff --git a/.gitignore b/.gitignore index 555eb8b..6ecc818 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules/ # Build dist/ +packages/*/dist/ build/ coverage/ test-results/ diff --git a/.prettierignore b/.prettierignore index ba9857d..8d4ad70 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,5 @@ dist +packages/*/dist build node_modules coverage diff --git a/README.md b/README.md index 9b90376..e48e9ce 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,14 @@ Internal accounts payable automation for Sea Haven Industries (`ap.seahaven.com`). +## Workspace layout + +npm workspaces: + +- `@seahaven-ap/web` — Vite/React SPA (repo root) +- `@seahaven-ap/shared` — payment ladder, invoice helpers, pay-date parsers, CSV constants ([`packages/shared`](packages/shared)) +- `@seahaven-ap/api` — reserved for AP-14 (not present yet) + ## Local development ```bash diff --git a/eslint.config.js b/eslint.config.js index 4e0c20e..78339bd 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -7,11 +7,33 @@ import tseslint from "typescript-eslint"; export default tseslint.config( { - ignores: ["dist/**", "build/**", "node_modules/**", "coverage/**", "src/router.ts"], + ignores: [ + "dist/**", + "packages/*/dist/**", + "build/**", + "node_modules/**", + "coverage/**", + "src/router.ts", + ], }, js.configs.recommended, ...tseslint.configs.recommended, eslintConfigPrettier, + { + files: ["packages/shared/src/**/*.ts"], + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + globals: globals.node, + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + }, + }, { files: ["src/**/*.{ts,tsx}", "vite.config.ts", "vitest.config.ts", "playwright.config.ts"], languageOptions: { diff --git a/package-lock.json b/package-lock.json index 2528648..4a1f161 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,15 @@ { - "name": "seahaven-ap", + "name": "@seahaven-ap/web", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "seahaven-ap", + "name": "@seahaven-ap/web", "version": "0.1.0", + "workspaces": [ + "packages/*" + ], "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", @@ -16,6 +19,7 @@ "@mui/icons-material": "^7.3.2", "@mui/material": "^7.3.2", "@mui/x-data-grid": "^8.11.0", + "@seahaven-ap/shared": "workspace:*", "@tanstack/react-query": "^5.85.5", "@tanstack/react-query-devtools": "^5.85.5", "ky": "^1.10.0", @@ -2080,6 +2084,10 @@ "win32" ] }, + "node_modules/@seahaven-ap/shared": { + "resolved": "packages/shared", + "link": true + }, "node_modules/@tanstack/query-core": { "version": "5.101.4", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz", @@ -5570,6 +5578,17 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "packages/shared": { + "name": "@seahaven-ap/shared", + "version": "0.1.0", + "devDependencies": { + "typescript": "^5.9.2", + "vitest": "^3.2.4" + }, + "engines": { + "node": ">=22.22.1" + } } } } diff --git a/package.json b/package.json index e3d2df5..5ef0b3f 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,18 @@ { - "name": "seahaven-ap", + "name": "@seahaven-ap/web", "version": "0.1.0", "private": true, "type": "module", + "workspaces": [ + "packages/*" + ], "scripts": { - "dev": "vite", + "dev": "npm run build:shared && vite", "generate:router": "node scripts/generate-router.mjs", - "build": "npm run generate:router && tsc -b && vite build", + "build:shared": "npm run build -w @seahaven-ap/shared", + "build": "npm run build:shared && npm run generate:router && tsc -b && vite build", "preview": "vite preview", - "test": "vitest run", + "test": "npm run test -w @seahaven-ap/shared && vitest run", "test:watch": "vitest", "test:e2e": "playwright test", "lint": "eslint . --max-warnings=0", @@ -30,6 +34,7 @@ "@mui/icons-material": "^7.3.2", "@mui/material": "^7.3.2", "@mui/x-data-grid": "^8.11.0", + "@seahaven-ap/shared": "workspace:*", "@tanstack/react-query": "^5.85.5", "@tanstack/react-query-devtools": "^5.85.5", "ky": "^1.10.0", diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..3b49aba --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,31 @@ +{ + "name": "@seahaven-ap/shared", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Shared AP domain: payment ladder, invoice helpers, pay-date parsers, CSV constants", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc --project tsconfig.build.json", + "typecheck": "tsc --noEmit --project tsconfig.json", + "test": "vitest run", + "test:watch": "vitest" + }, + "devDependencies": { + "typescript": "^5.9.2", + "vitest": "^3.2.4" + }, + "engines": { + "node": ">=22.22.1" + } +} diff --git a/packages/shared/src/csv.test.ts b/packages/shared/src/csv.test.ts new file mode 100644 index 0000000..c40eb82 --- /dev/null +++ b/packages/shared/src/csv.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest"; +import { PAYMENT_CSV_HEADER, PAYMENT_CSV_HEADERS, PAYMENT_CSV_METHODS } from "./csv.js"; + +describe("payment CSV constants", () => { + it("freezes the consumer header contract and order", () => { + expect([...PAYMENT_CSV_HEADERS]).toEqual([ + "Check Number", + "Method", + "Status", + "Send Payment On", + "Amount in USD", + "Amount", + "Payee", + "Invoice Numbers", + "Company/Subsidiary", + ]); + expect(PAYMENT_CSV_HEADER.checkNumber).toBe("Check Number"); + expect(PAYMENT_CSV_HEADER.companySubsidiary).toBe("Company/Subsidiary"); + }); + + it("exports Check and ACH method display values", () => { + expect(PAYMENT_CSV_METHODS.check).toBe("Check"); + expect(PAYMENT_CSV_METHODS.ach).toBe("ACH"); + }); +}); diff --git a/packages/shared/src/csv.ts b/packages/shared/src/csv.ts new file mode 100644 index 0000000..15e4ca6 --- /dev/null +++ b/packages/shared/src/csv.ts @@ -0,0 +1,41 @@ +/** + * Fixed CSV header contract for payments-dashboard ProcessPaymentCsv compatibility. + * Column names and export order must not drift. + */ + +export const PAYMENT_CSV_HEADER = { + checkNumber: "Check Number", + method: "Method", + status: "Status", + sendPaymentOn: "Send Payment On", + amountInUsd: "Amount in USD", + amount: "Amount", + payee: "Payee", + invoiceNumbers: "Invoice Numbers", + companySubsidiary: "Company/Subsidiary", +} as const; + +export type PaymentCsvHeaderKey = keyof typeof PAYMENT_CSV_HEADER; + +/** Ordered headers for export / golden-file alignment (AP-17). */ +export const PAYMENT_CSV_HEADERS = [ + PAYMENT_CSV_HEADER.checkNumber, + PAYMENT_CSV_HEADER.method, + PAYMENT_CSV_HEADER.status, + PAYMENT_CSV_HEADER.sendPaymentOn, + PAYMENT_CSV_HEADER.amountInUsd, + PAYMENT_CSV_HEADER.amount, + PAYMENT_CSV_HEADER.payee, + PAYMENT_CSV_HEADER.invoiceNumbers, + PAYMENT_CSV_HEADER.companySubsidiary, +] as const; + +export type PaymentCsvHeader = (typeof PAYMENT_CSV_HEADERS)[number]; + +/** Method display values the consumer branches on (Check vs ACH). */ +export const PAYMENT_CSV_METHODS = { + check: "Check", + ach: "ACH", +} as const; + +export type PaymentCsvMethod = (typeof PAYMENT_CSV_METHODS)[keyof typeof PAYMENT_CSV_METHODS]; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..0f8133e --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,42 @@ +export { + CANCEL_STATUSES, + PAYMENT_DISPLAY_STATUSES, + PAYMENT_STATUSES, + PAYMENT_STATUS_RANK, + canTransitionPaymentStatus, + isCancelStatus, + isPaymentStatus, + parsePaymentStatus, + paymentStatusRank, + toPaymentDisplayStatus, + type CancelStatus, + type PaymentDisplayStatus, + type PaymentStatus, +} from "./payment-status.js"; + +export { + INVOICE_STATUSES, + READY_TO_PAY_STATUSES, + isInvoiceStatus, + isReadyToPay, + statusAfterPayRun, + statusAfterPaymentCleared, + type InvoiceStatus, +} from "./invoice-status.js"; + +export { + isPlausibleSendYear, + parseMDY, + toCanonicalMDY, + toISODate, + type ParsedMDY, +} from "./pay-date.js"; + +export { + PAYMENT_CSV_HEADER, + PAYMENT_CSV_HEADERS, + PAYMENT_CSV_METHODS, + type PaymentCsvHeader, + type PaymentCsvHeaderKey, + type PaymentCsvMethod, +} from "./csv.js"; diff --git a/packages/shared/src/invoice-status.test.ts b/packages/shared/src/invoice-status.test.ts new file mode 100644 index 0000000..a37f144 --- /dev/null +++ b/packages/shared/src/invoice-status.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { isReadyToPay, statusAfterPayRun, statusAfterPaymentCleared } from "./invoice-status.js"; + +describe("invoice status helpers", () => { + it("treats approved as Ready-to-Pay", () => { + expect(isReadyToPay("approved")).toBe(true); + expect(isReadyToPay("pending_approval")).toBe(false); + expect(isReadyToPay("scheduled")).toBe(false); + expect(isReadyToPay("paid")).toBe(false); + expect(isReadyToPay("rejected")).toBe(false); + expect(isReadyToPay("void")).toBe(false); + }); + + it("exposes post pay-run and cleared invoice statuses", () => { + expect(statusAfterPayRun()).toBe("scheduled"); + expect(statusAfterPaymentCleared()).toBe("paid"); + }); +}); diff --git a/packages/shared/src/invoice-status.ts b/packages/shared/src/invoice-status.ts new file mode 100644 index 0000000..be6efca --- /dev/null +++ b/packages/shared/src/invoice-status.ts @@ -0,0 +1,36 @@ +/** + * Invoice status helpers for Ready-to-Pay and post pay-run alignment. + * Thin surface for AP-17; full pay-run logic stays in the API. + */ + +export const INVOICE_STATUSES = [ + "pending_approval", + "approved", + "scheduled", + "paid", + "rejected", + "void", +] as const; + +export type InvoiceStatus = (typeof INVOICE_STATUSES)[number]; + +/** Invoices eligible for Select to Pay / pay-run selection. */ +export const READY_TO_PAY_STATUSES = ["approved"] as const satisfies readonly InvoiceStatus[]; + +export function isInvoiceStatus(value: string): value is InvoiceStatus { + return (INVOICE_STATUSES as readonly string[]).includes(value); +} + +export function isReadyToPay(status: InvoiceStatus | string): boolean { + return (READY_TO_PAY_STATUSES as readonly string[]).includes(status); +} + +/** Status after a successful pay run groups the invoice into the payments queue. */ +export function statusAfterPayRun(): InvoiceStatus { + return "scheduled"; +} + +/** Terminal paid status once the payment ladder reaches cleared (AP-17 wiring). */ +export function statusAfterPaymentCleared(): InvoiceStatus { + return "paid"; +} diff --git a/packages/shared/src/pay-date.test.ts b/packages/shared/src/pay-date.test.ts new file mode 100644 index 0000000..4341548 --- /dev/null +++ b/packages/shared/src/pay-date.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { isPlausibleSendYear, parseMDY, toCanonicalMDY, toISODate } from "./pay-date.js"; + +describe("pay-date helpers", () => { + it("parses M/D/YY and MM/DD/YYYY", () => { + expect(parseMDY("7/4/26")).toEqual({ year: 2026, month: 7, day: 4 }); + expect(parseMDY("07/04/2026")).toEqual({ year: 2026, month: 7, day: 4 }); + }); + + it("rejects invalid calendar dates and garbage", () => { + expect(parseMDY("2/30/2026")).toBeNull(); + expect(parseMDY("2026-07-04")).toBeNull(); + expect(parseMDY("")).toBeNull(); + expect(parseMDY(null)).toBeNull(); + }); + + it("formats ISO and canonical MDY", () => { + expect(toISODate("7/4/26")).toBe("2026-07-04"); + expect(toCanonicalMDY("7/4/26")).toBe("07/04/2026"); + expect(toISODate("bad")).toBeNull(); + }); + + it("enforces the plausibility window relative to nowYear", () => { + expect(isPlausibleSendYear("01/01/2019", 2026)).toBe(true); + expect(isPlausibleSendYear("01/01/2018", 2026)).toBe(false); + expect(isPlausibleSendYear("01/01/2028", 2026)).toBe(true); + expect(isPlausibleSendYear("01/01/2029", 2026)).toBe(false); + expect(isPlausibleSendYear("not-a-date", 2026)).toBe(false); + }); +}); diff --git a/packages/shared/src/pay-date.ts b/packages/shared/src/pay-date.ts new file mode 100644 index 0000000..e55721e --- /dev/null +++ b/packages/shared/src/pay-date.ts @@ -0,0 +1,53 @@ +/** + * Shared date parsing for Stampli M/D/YY and MM/DD/YYYY formats. + * Ported from payments-dashboard/src/dates.js — both formats must parse; + * anything else returns null (never pass through garbage). + */ + +export type ParsedMDY = { + year: number; + month: number; + day: number; +}; + +/** Parse "M/D/YY" or "MM/DD/YYYY" into { year, month, day }, or null. */ +export function parseMDY(value: string | null | undefined): ParsedMDY | null { + const match = /^(\d{1,2})\/(\d{1,2})\/(\d{2}|\d{4})$/.exec(String(value ?? "").trim()); + if (!match) return null; + const month = Number.parseInt(match[1]!, 10); + const day = Number.parseInt(match[2]!, 10); + let year = Number.parseInt(match[3]!, 10); + if (match[3]!.length === 2) year += 2000; + const dt = new Date(year, month - 1, day); + if (dt.getFullYear() !== year || dt.getMonth() !== month - 1 || dt.getDate() !== day) { + return null; + } + return { year, month, day }; +} + +/** "YYYY-MM-DD" or null. */ +export function toISODate(value: string | null | undefined): string | null { + const p = parseMDY(value); + if (!p) return null; + return `${p.year}-${String(p.month).padStart(2, "0")}-${String(p.day).padStart(2, "0")}`; +} + +/** Canonical "MM/DD/YYYY" or null. Stored form for send_payment_on / CSV. */ +export function toCanonicalMDY(value: string | null | undefined): string | null { + const p = parseMDY(value); + if (!p) return null; + return `${String(p.month).padStart(2, "0")}/${String(p.day).padStart(2, "0")}/${p.year}`; +} + +/** + * Plausibility window for ingested send dates. + * Relative to the current year (7 years back, 2 years forward). + */ +export function isPlausibleSendYear( + value: string | null | undefined, + nowYear: number = new Date().getFullYear(), +): boolean { + const p = parseMDY(value); + if (p === null) return false; + return p.year >= nowYear - 7 && p.year <= nowYear + 2; +} diff --git a/packages/shared/src/payment-status.test.ts b/packages/shared/src/payment-status.test.ts new file mode 100644 index 0000000..f18de50 --- /dev/null +++ b/packages/shared/src/payment-status.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from "vitest"; +import { + CANCEL_STATUSES, + PAYMENT_STATUS_RANK, + canTransitionPaymentStatus, + isCancelStatus, + parsePaymentStatus, + toPaymentDisplayStatus, +} from "./payment-status.js"; + +describe("payment status ladder", () => { + it("ranks the ladder in consumer order", () => { + expect(PAYMENT_STATUS_RANK.scheduled).toBe(1); + expect(PAYMENT_STATUS_RANK.payment_submitted).toBe(2); + expect(PAYMENT_STATUS_RANK.issued).toBe(3); + expect(PAYMENT_STATUS_RANK.outstanding).toBe(4); + expect(PAYMENT_STATUS_RANK.cleared).toBe(5); + }); + + it("maps internal statuses to CSV display strings", () => { + expect(toPaymentDisplayStatus("payment_submitted")).toBe("Payment Submitted"); + expect(toPaymentDisplayStatus("cleared")).toBe("Cleared"); + }); + + it("parses display and internal status strings", () => { + expect(parsePaymentStatus("Payment Submitted")).toBe("payment_submitted"); + expect(parsePaymentStatus("OUTSTANDING")).toBe("outstanding"); + expect(parsePaymentStatus("scheduled")).toBe("scheduled"); + expect(parsePaymentStatus("voided")).toBeNull(); + }); + + it("allows forward transitions and same-rank no-ops", () => { + expect(canTransitionPaymentStatus("scheduled", "payment_submitted")).toBe(true); + expect(canTransitionPaymentStatus("issued", "Outstanding")).toBe(true); + expect(canTransitionPaymentStatus("outstanding", "outstanding")).toBe(true); + expect(canTransitionPaymentStatus("outstanding", "cleared")).toBe(true); + }); + + it("blocks non-cancel rank regression", () => { + expect(canTransitionPaymentStatus("issued", "scheduled")).toBe(false); + expect(canTransitionPaymentStatus("cleared", "outstanding")).toBe(false); + expect(canTransitionPaymentStatus("payment_submitted", "Scheduled")).toBe(false); + }); + + it("allows void from ranks below cleared", () => { + for (const status of ["scheduled", "payment_submitted", "issued", "outstanding"] as const) { + expect(canTransitionPaymentStatus(status, "voided")).toBe(true); + expect(canTransitionPaymentStatus(status, "Cancelled")).toBe(true); + } + }); + + it("blocks all transitions once cleared, including void", () => { + expect(canTransitionPaymentStatus("cleared", "voided")).toBe(false); + expect(canTransitionPaymentStatus("cleared", "cleared")).toBe(false); + expect(canTransitionPaymentStatus("cleared", "outstanding")).toBe(false); + }); + + it("recognizes cancel vocabulary case-insensitively", () => { + for (const status of CANCEL_STATUSES) { + expect(isCancelStatus(status)).toBe(true); + expect(isCancelStatus(status.toUpperCase())).toBe(true); + } + expect(isCancelStatus("Marked As Void")).toBe(true); + expect(isCancelStatus("Cleared")).toBe(false); + }); +}); diff --git a/packages/shared/src/payment-status.ts b/packages/shared/src/payment-status.ts new file mode 100644 index 0000000..c244efb --- /dev/null +++ b/packages/shared/src/payment-status.ts @@ -0,0 +1,106 @@ +/** + * Payment status ladder aligned with payments-dashboard ProcessPaymentCsv. + * Internal snake_case for DB/API; Title Case display for CSV consumer rows. + */ + +export const PAYMENT_STATUSES = [ + "scheduled", + "payment_submitted", + "issued", + "outstanding", + "cleared", +] as const; + +export type PaymentStatus = (typeof PAYMENT_STATUSES)[number]; + +export const PAYMENT_STATUS_RANK: Readonly> = { + scheduled: 1, + payment_submitted: 2, + issued: 3, + outstanding: 4, + cleared: 5, +}; + +export const PAYMENT_DISPLAY_STATUSES = [ + "Scheduled", + "Payment Submitted", + "Issued", + "Outstanding", + "Cleared", +] as const; + +export type PaymentDisplayStatus = (typeof PAYMENT_DISPLAY_STATUSES)[number]; + +const INTERNAL_TO_DISPLAY: Readonly> = { + scheduled: "Scheduled", + payment_submitted: "Payment Submitted", + issued: "Issued", + outstanding: "Outstanding", + cleared: "Cleared", +}; + +const DISPLAY_TO_INTERNAL: Readonly> = Object.fromEntries( + Object.entries(INTERNAL_TO_DISPLAY).map(([internal, display]) => [ + display.toLowerCase(), + internal as PaymentStatus, + ]), +); + +/** Cancel / void vocabulary accepted by the payments-dashboard consumer (case-insensitive). */ +export const CANCEL_STATUSES = ["voided", "cancelled", "canceled", "marked as void"] as const; + +export type CancelStatus = (typeof CANCEL_STATUSES)[number]; + +export function isPaymentStatus(value: string): value is PaymentStatus { + return (PAYMENT_STATUSES as readonly string[]).includes(value); +} + +export function isCancelStatus(value: string | null | undefined): boolean { + if (value == null) return false; + return (CANCEL_STATUSES as readonly string[]).includes(value.trim().toLowerCase()); +} + +export function paymentStatusRank(status: PaymentStatus): number { + return PAYMENT_STATUS_RANK[status]; +} + +/** + * Parse a CSV/display or internal status string into PaymentStatus. + * Cancel vocabulary returns null (use isCancelStatus separately). + */ +export function parsePaymentStatus(value: string | null | undefined): PaymentStatus | null { + if (value == null) return null; + const normalized = value.trim().toLowerCase(); + if (isPaymentStatus(normalized)) return normalized; + return DISPLAY_TO_INTERNAL[normalized] ?? null; +} + +export function toPaymentDisplayStatus(status: PaymentStatus): PaymentDisplayStatus { + return INTERNAL_TO_DISPLAY[status]; +} + +/** + * Whether a payment may move from `from` to `to`. + * - Non-cancel: rank must not regress; cleared is terminal. + * - Cancel: allowed only when current rank < cleared. + * - `to` may be a PaymentStatus or a cancel vocabulary string. + */ +export function canTransitionPaymentStatus( + from: PaymentStatus, + to: PaymentStatus | string, +): boolean { + const fromRank = PAYMENT_STATUS_RANK[from]; + if (fromRank === PAYMENT_STATUS_RANK.cleared) { + return false; + } + + if (isCancelStatus(to)) { + return fromRank < PAYMENT_STATUS_RANK.cleared; + } + + const next = parsePaymentStatus(to); + if (next == null) return false; + + const toRank = PAYMENT_STATUS_RANK[next]; + return toRank >= fromRank; +} diff --git a/packages/shared/tsconfig.build.json b/packages/shared/tsconfig.build.json new file mode 100644 index 0000000..cf0eba4 --- /dev/null +++ b/packages/shared/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false + }, + "exclude": ["src/**/*.test.ts"] +} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..8c02f2f --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "noEmitOnError": true, + "noEmit": true + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/shared/vitest.config.ts b/packages/shared/vitest.config.ts new file mode 100644 index 0000000..c1433e6 --- /dev/null +++ b/packages/shared/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +}); diff --git a/src/dashboard/PaymentProcessingGrids.tsx b/src/dashboard/PaymentProcessingGrids.tsx index edcbc70..603de42 100644 --- a/src/dashboard/PaymentProcessingGrids.tsx +++ b/src/dashboard/PaymentProcessingGrids.tsx @@ -11,6 +11,7 @@ import { Typography, } from "@mui/material"; import type { GridRowParams, GridRowSelectionModel } from "@mui/x-data-grid"; +import { isReadyToPay } from "@seahaven-ap/shared"; import { CreatePayRunButton } from "@/dashboard/CreatePayRunButton"; import { InvoiceDataGrid } from "@/dashboard/InvoiceDataGrid"; import { pendingApprovalColumns, readyToPayColumns } from "@/dashboard/columns"; @@ -46,7 +47,7 @@ export function PaymentProcessingGrids() { }; const selectRows = useMemo( - () => invoices.filter((invoice) => invoice.status === "approved"), + () => invoices.filter((invoice) => isReadyToPay(invoice.status)), [invoices], ); const pendingRows = useMemo(