feat(shared): add payment ladder and CSV domain package (AP-13) (#11)

* feat(shared): add payment ladder and CSV domain package

Introduce @seahaven-ap/shared via npm workspaces so FE and future API share
payments-dashboard-compatible status ladders, pay-date helpers, and CSV headers.

* fix(web): build shared package before vite dev

Ensure fresh npm ci && npm run dev (and Playwright webServer) can resolve
@seahaven-ap/shared dist exports that are gitignored.
This commit is contained in:
Adam Moussa 2026-08-10 19:28:02 -04:00 • committed by GitHub
parent 20d6f56f7e
commit 46b570352a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 549 additions and 8 deletions

1
.gitignore vendored
View file

@ -3,6 +3,7 @@ node_modules/
# Build # Build
dist/ dist/
packages/*/dist/
build/ build/
coverage/ coverage/
test-results/ test-results/

View file

@ -1,4 +1,5 @@
dist dist
packages/*/dist
build build
node_modules node_modules
coverage coverage

View file

@ -2,6 +2,14 @@
Internal accounts payable automation for Sea Haven Industries (`ap.seahaven.com`). 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 ## Local development
```bash ```bash

View file

@ -7,11 +7,33 @@ import tseslint from "typescript-eslint";
export default tseslint.config( 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, js.configs.recommended,
...tseslint.configs.recommended, ...tseslint.configs.recommended,
eslintConfigPrettier, 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"], files: ["src/**/*.{ts,tsx}", "vite.config.ts", "vitest.config.ts", "playwright.config.ts"],
languageOptions: { languageOptions: {

23
package-lock.json generated
View file

@ -1,12 +1,15 @@
{ {
"name": "seahaven-ap", "name": "@seahaven-ap/web",
"version": "0.1.0", "version": "0.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "seahaven-ap", "name": "@seahaven-ap/web",
"version": "0.1.0", "version": "0.1.0",
"workspaces": [
"packages/*"
],
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
@ -16,6 +19,7 @@
"@mui/icons-material": "^7.3.2", "@mui/icons-material": "^7.3.2",
"@mui/material": "^7.3.2", "@mui/material": "^7.3.2",
"@mui/x-data-grid": "^8.11.0", "@mui/x-data-grid": "^8.11.0",
"@seahaven-ap/shared": "workspace:*",
"@tanstack/react-query": "^5.85.5", "@tanstack/react-query": "^5.85.5",
"@tanstack/react-query-devtools": "^5.85.5", "@tanstack/react-query-devtools": "^5.85.5",
"ky": "^1.10.0", "ky": "^1.10.0",
@ -2080,6 +2084,10 @@
"win32" "win32"
] ]
}, },
"node_modules/@seahaven-ap/shared": {
"resolved": "packages/shared",
"link": true
},
"node_modules/@tanstack/query-core": { "node_modules/@tanstack/query-core": {
"version": "5.101.4", "version": "5.101.4",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz",
@ -5570,6 +5578,17 @@
"peerDependencies": { "peerDependencies": {
"zod": "^3.25.0 || ^4.0.0" "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"
}
} }
} }
} }

View file

@ -1,14 +1,18 @@
{ {
"name": "seahaven-ap", "name": "@seahaven-ap/web",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"type": "module", "type": "module",
"workspaces": [
"packages/*"
],
"scripts": { "scripts": {
"dev": "vite", "dev": "npm run build:shared && vite",
"generate:router": "node scripts/generate-router.mjs", "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", "preview": "vite preview",
"test": "vitest run", "test": "npm run test -w @seahaven-ap/shared && vitest run",
"test:watch": "vitest", "test:watch": "vitest",
"test:e2e": "playwright test", "test:e2e": "playwright test",
"lint": "eslint . --max-warnings=0", "lint": "eslint . --max-warnings=0",
@ -30,6 +34,7 @@
"@mui/icons-material": "^7.3.2", "@mui/icons-material": "^7.3.2",
"@mui/material": "^7.3.2", "@mui/material": "^7.3.2",
"@mui/x-data-grid": "^8.11.0", "@mui/x-data-grid": "^8.11.0",
"@seahaven-ap/shared": "workspace:*",
"@tanstack/react-query": "^5.85.5", "@tanstack/react-query": "^5.85.5",
"@tanstack/react-query-devtools": "^5.85.5", "@tanstack/react-query-devtools": "^5.85.5",
"ky": "^1.10.0", "ky": "^1.10.0",

View file

@ -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"
}
}

View file

@ -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");
});
});

View file

@ -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];

View file

@ -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";

View file

@ -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");
});
});

View file

@ -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";
}

View file

@ -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);
});
});

View file

@ -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;
}

View file

@ -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);
});
});

View file

@ -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<Record<PaymentStatus, number>> = {
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<Record<PaymentStatus, PaymentDisplayStatus>> = {
scheduled: "Scheduled",
payment_submitted: "Payment Submitted",
issued: "Issued",
outstanding: "Outstanding",
cleared: "Cleared",
};
const DISPLAY_TO_INTERNAL: Readonly<Record<string, PaymentStatus>> = 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;
}

View file

@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false
},
"exclude": ["src/**/*.test.ts"]
}

View file

@ -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"]
}

View file

@ -0,0 +1,8 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "node",
include: ["src/**/*.test.ts"],
},
});

View file

@ -11,6 +11,7 @@ import {
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import type { GridRowParams, GridRowSelectionModel } from "@mui/x-data-grid"; import type { GridRowParams, GridRowSelectionModel } from "@mui/x-data-grid";
import { isReadyToPay } from "@seahaven-ap/shared";
import { CreatePayRunButton } from "@/dashboard/CreatePayRunButton"; import { CreatePayRunButton } from "@/dashboard/CreatePayRunButton";
import { InvoiceDataGrid } from "@/dashboard/InvoiceDataGrid"; import { InvoiceDataGrid } from "@/dashboard/InvoiceDataGrid";
import { pendingApprovalColumns, readyToPayColumns } from "@/dashboard/columns"; import { pendingApprovalColumns, readyToPayColumns } from "@/dashboard/columns";
@ -46,7 +47,7 @@ export function PaymentProcessingGrids() {
}; };
const selectRows = useMemo( const selectRows = useMemo(
() => invoices.filter((invoice) => invoice.status === "approved"), () => invoices.filter((invoice) => isReadyToPay(invoice.status)),
[invoices], [invoices],
); );
const pendingRows = useMemo( const pendingRows = useMemo(