mirror of
https://github.com/Sea-Haven-Industries/seahaven-ap.git
synced 2026-10-01 09:43:17 +00:00
* 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.
106 lines
3.1 KiB
TypeScript
106 lines
3.1 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|