feat(freight-backoffice): EIMS cancel/receipt/memo actions in filing card

Adds cancel, sales/withholding receipt filing + listing + PDF download, and
credit/debit memo issuance to EimsFilingCard, each gated on its own
permission.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Hagernesh
2026-08-15 06:45:00 +00:00
parent 8e70352407
commit 47c2e82839
8 changed files with 593 additions and 38 deletions

View File

@@ -1,11 +1,30 @@
import { Alert, Badge, Button, Card, Group, SimpleGrid, Stack, Text } from "@mantine/core";
import { useState } from "react";
import {
Alert,
Badge,
Button,
Card,
Group,
Modal,
NumberInput,
Radio,
Select,
SimpleGrid,
Stack,
Table,
Text,
Textarea,
TextInput,
} from "@mantine/core";
import { useMutation, useQuery } from "@tanstack/react-query";
import { AlertTriangle, RefreshCw, Send, ShieldCheck } from "lucide-react";
import { AlertTriangle, Ban, Download, FileText, RefreshCw, Send, ShieldCheck } from "lucide-react";
import { useAuth } from "@/auth/useAuth";
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
import { api } from "@/services/api";
import type { EimsInvoiceStatus } from "@/types/eims";
import { eimsService } from "@/services/eims.service";
import { openPdfBlob } from "@/components/warehouses/pdf";
import { EIMS_MODE_OF_PAYMENT, type EimsInvoiceStatus, type EimsModeOfPayment } from "@/types/eims";
import { useToast } from "@/hooks/use-toast";
const STATUS_COLOR: Record<EimsInvoiceStatus, string> = {
@@ -14,6 +33,7 @@ const STATUS_COLOR: Record<EimsInvoiceStatus, string> = {
REGISTERED: "edr-green",
FAILED: "red",
UNKNOWN: "orange",
CANCELLED: "gray",
};
const STATUS_LABEL: Record<EimsInvoiceStatus, string> = {
@@ -22,6 +42,7 @@ const STATUS_LABEL: Record<EimsInvoiceStatus, string> = {
REGISTERED: "Filed",
FAILED: "Rejected",
UNKNOWN: "Unacknowledged",
CANCELLED: "Cancelled",
};
function Field({ label, value }: { label: string; value?: string | number | null }) {
@@ -37,6 +58,367 @@ function Field({ label, value }: { label: string; value?: string | number | null
);
}
/** Reason codes from the collection docs, e.g. "1" (Duplicate), "6" (Calculation Error). */
const CANCEL_REASON_CODES = [
{ value: "1", label: "1 — Duplicate" },
{ value: "2", label: "2 — Buyer request" },
{ value: "3", label: "3 — Data entry error" },
{ value: "6", label: "6 — Calculation error" },
];
function CancelModal({
invoiceId,
opened,
onClose,
}: {
invoiceId: string;
opened: boolean;
onClose: () => void;
}) {
const { toast } = useToast();
const [reasonCode, setReasonCode] = useState<string | null>(null);
const [remark, setRemark] = useState("");
const cancel = useMutation(
api.invoices.eimsCancel.mutationOptions({
onSuccess: () => {
onClose();
toast({ title: "Cancelled with MoR" });
},
onError: (error) => toast({ title: "Could not cancel", description: error.message, variant: "destructive" }),
}),
);
return (
<Modal opened={opened} onClose={onClose} title="Cancel EIMS registration" centered>
<Stack gap="md">
<Text size="sm" c="dimmed">
Cancels this invoice&apos;s registered document at MoR. Irreversible an already-cancelled
invoice refuses a second attempt.
</Text>
<Select
label="Reason code"
withAsterisk
data={CANCEL_REASON_CODES}
value={reasonCode}
onChange={setReasonCode}
placeholder="Select a reason"
/>
<Textarea
label="Remark"
placeholder="Optional note"
value={remark}
onChange={(e) => setRemark(e.currentTarget.value)}
autosize
minRows={2}
/>
<Button
color="red"
leftSection={<Ban size={16} />}
loading={cancel.isPending}
disabled={!reasonCode}
onClick={() => cancel.mutate({ id: invoiceId, reasonCode: reasonCode!, remark: remark.trim() || undefined })}
>
Cancel with MoR
</Button>
</Stack>
</Modal>
);
}
function SalesReceiptModal({
invoiceId,
opened,
onClose,
}: {
invoiceId: string;
opened: boolean;
onClose: () => void;
}) {
const { toast } = useToast();
const [modeOfPayment, setModeOfPayment] = useState<EimsModeOfPayment | null>(null);
const [collectedAmount, setCollectedAmount] = useState<number | "">("");
const [reason, setReason] = useState("");
const register = useMutation(
api.invoices.eimsRegisterSalesReceipt.mutationOptions({
onSuccess: (receipt) => {
onClose();
toast({ title: "Sales receipt filed", description: `RRN ${receipt.rrn ?? "—"}` });
},
onError: (error) => toast({ title: "Could not file receipt", description: error.message, variant: "destructive" }),
}),
);
return (
<Modal opened={opened} onClose={onClose} title="File sales receipt" centered>
<Stack gap="md">
<Select
label="Mode of payment"
withAsterisk
data={EIMS_MODE_OF_PAYMENT.map((v) => ({ value: v, label: v }))}
value={modeOfPayment}
onChange={(v) => setModeOfPayment(v as EimsModeOfPayment)}
placeholder="Select"
/>
<NumberInput
label="Collected amount"
placeholder="Defaults to the invoice's paid amount"
min={0}
decimalScale={2}
value={collectedAmount}
onChange={(v) => setCollectedAmount(v === "" ? "" : Number(v))}
/>
<TextInput
label="Reason"
placeholder='Defaults to "Payment received"'
value={reason}
onChange={(e) => setReason(e.currentTarget.value)}
/>
<Button
color="edr-green"
leftSection={<Send size={16} />}
loading={register.isPending}
disabled={!modeOfPayment}
onClick={() =>
register.mutate({
id: invoiceId,
modeOfPayment: modeOfPayment!,
collectedAmount: collectedAmount === "" ? undefined : collectedAmount,
reason: reason.trim() || undefined,
})
}
>
File with MoR
</Button>
</Stack>
</Modal>
);
}
function WithholdingReceiptModal({
invoiceId,
opened,
onClose,
}: {
invoiceId: string;
opened: boolean;
onClose: () => void;
}) {
const { toast } = useToast();
const [type, setType] = useState("TWHT");
const [preTaxAmount, setPreTaxAmount] = useState<number | "">("");
const [withholdingAmount, setWithholdingAmount] = useState<number | "">("");
const [reason, setReason] = useState("");
const register = useMutation(
api.invoices.eimsRegisterWithholdingReceipt.mutationOptions({
onSuccess: (receipt) => {
onClose();
toast({ title: "Withholding receipt filed", description: `RRN ${receipt.rrn ?? "—"}` });
},
onError: (error) => toast({ title: "Could not file receipt", description: error.message, variant: "destructive" }),
}),
);
const valid = preTaxAmount !== "" && withholdingAmount !== "";
return (
<Modal opened={opened} onClose={onClose} title="File withholding receipt" centered>
<Stack gap="md">
<TextInput label="Type" withAsterisk value={type} onChange={(e) => setType(e.currentTarget.value)} />
<NumberInput
label="Pre-tax amount"
withAsterisk
min={0}
decimalScale={2}
value={preTaxAmount}
onChange={(v) => setPreTaxAmount(v === "" ? "" : Number(v))}
/>
<NumberInput
label="Withholding amount"
withAsterisk
min={0}
decimalScale={2}
value={withholdingAmount}
onChange={(v) => setWithholdingAmount(v === "" ? "" : Number(v))}
/>
<TextInput
label="Reason"
placeholder='Defaults to "Withholding"'
value={reason}
onChange={(e) => setReason(e.currentTarget.value)}
/>
<Button
color="edr-green"
leftSection={<Send size={16} />}
loading={register.isPending}
disabled={!valid}
onClick={() =>
register.mutate({
id: invoiceId,
type,
preTaxAmount: preTaxAmount as number,
withholdingAmount: withholdingAmount as number,
reason: reason.trim() || undefined,
})
}
>
File with MoR
</Button>
</Stack>
</Modal>
);
}
function MemoModal({
invoiceId,
opened,
onClose,
}: {
invoiceId: string;
opened: boolean;
onClose: () => void;
}) {
const { toast } = useToast();
const [type, setType] = useState<"CRE" | "DEB">("CRE");
const [reason, setReason] = useState("");
const issue = useMutation(
api.invoices.issueMemo.mutationOptions({
onSuccess: (memo) => {
onClose();
toast({ title: "Memo issued", description: `${memo.invoiceNumber} — file it with MoR separately` });
},
onError: (error) => toast({ title: "Could not issue memo", description: error.message, variant: "destructive" }),
}),
);
return (
<Modal opened={opened} onClose={onClose} title="Issue credit/debit memo" centered>
<Stack gap="md">
<Text size="sm" c="dimmed">
Creates a new invoice linked to this one, with every line copied verbatim. Filing it with
MoR is a separate step it does not happen automatically here.
</Text>
<Radio.Group value={type} onChange={(v) => setType(v as "CRE" | "DEB")} label="Type">
<Stack gap="xs" mt="xs">
<Radio value="CRE" label="Credit memo" description="Reduces what the buyer owes; created settled." />
<Radio value="DEB" label="Debit memo" description="An additional charge; created as a new open invoice." />
</Stack>
</Radio.Group>
<Textarea
label="Reason"
withAsterisk
placeholder="Why this memo is being issued"
value={reason}
onChange={(e) => setReason(e.currentTarget.value)}
autosize
minRows={2}
/>
<Button
color="edr-green"
loading={issue.isPending}
disabled={!reason.trim()}
onClick={() => issue.mutate({ id: invoiceId, type, reason: reason.trim() })}
>
Issue memo
</Button>
</Stack>
</Modal>
);
}
function ReceiptsSection({ invoiceId, canFile }: { invoiceId: string; canFile: boolean }) {
const { toast } = useToast();
const { data: receipts } = useQuery(api.invoices.eimsReceipts.queryOptions({ input: { id: invoiceId } }));
const [salesOpen, setSalesOpen] = useState(false);
const [withholdingOpen, setWithholdingOpen] = useState(false);
const [downloadingId, setDownloadingId] = useState<string | null>(null);
const download = async (receiptId: string, receiptNumber: string) => {
setDownloadingId(receiptId);
try {
const { data } = await eimsService.downloadReceiptDocument(invoiceId, receiptId);
openPdfBlob(data, `${receiptNumber}.pdf`);
} catch (error) {
toast({
title: "Could not download receipt",
description: error instanceof Error ? error.message : undefined,
variant: "destructive",
});
} finally {
setDownloadingId(null);
}
};
return (
<Stack gap="sm">
<Group justify="space-between">
<Text fw={600} size="sm" c="edr-text">
Receipts
</Text>
{canFile && (
<Group gap="xs">
<Button size="xs" variant="light" onClick={() => setSalesOpen(true)}>
File sales receipt
</Button>
<Button size="xs" variant="light" onClick={() => setWithholdingOpen(true)}>
File withholding receipt
</Button>
</Group>
)}
</Group>
{receipts && receipts.length > 0 ? (
<Table striped withTableBorder={false} verticalSpacing="xs">
<Table.Thead>
<Table.Tr>
<Table.Th>Kind</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>RRN</Table.Th>
<Table.Th />
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{receipts.map((r) => (
<Table.Tr key={r.id}>
<Table.Td>{r.kind}</Table.Td>
<Table.Td>
<Badge color={STATUS_COLOR[r.status] ?? "gray"} variant="light" size="sm">
{STATUS_LABEL[r.status] ?? r.status}
</Badge>
</Table.Td>
<Table.Td style={{ fontFamily: "monospace" }}>{r.rrn ?? "—"}</Table.Td>
<Table.Td>
{r.status === "REGISTERED" && (
<Button
size="xs"
variant="subtle"
leftSection={<Download size={14} />}
loading={downloadingId === r.id}
onClick={() => void download(r.id, r.receiptNumber)}
>
PDF
</Button>
)}
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
) : (
<Text size="sm" c="dimmed">
No receipts filed yet.
</Text>
)}
<SalesReceiptModal invoiceId={invoiceId} opened={salesOpen} onClose={() => setSalesOpen(false)} />
<WithholdingReceiptModal invoiceId={invoiceId} opened={withholdingOpen} onClose={() => setWithholdingOpen(false)} />
</Stack>
);
}
/**
* MoR EIMS filing state for one invoice, with the manual actions.
*
@@ -48,6 +430,12 @@ export function EimsFilingCard({ invoiceId }: { invoiceId: string }) {
const { user } = useAuth();
const { toast } = useToast();
const canFile = hasPermission(user, FREIGHT_PERMS.invoices.eimsRegister);
const canCancel = hasPermission(user, FREIGHT_PERMS.invoices.eimsCancel);
const canFileReceipt = hasPermission(user, FREIGHT_PERMS.invoices.eimsReceiptRegister);
const canIssueMemo = hasPermission(user, FREIGHT_PERMS.invoices.memoIssue);
const [cancelOpen, setCancelOpen] = useState(false);
const [memoOpen, setMemoOpen] = useState(false);
const { data: eims, isLoading } = useQuery(
api.invoices.eimsStatus.queryOptions({ input: { id: invoiceId }, enabled: Boolean(invoiceId) }),
@@ -118,39 +506,78 @@ export function EimsFilingCard({ invoiceId }: { invoiceId: string }) {
</Alert>
)}
{canFile && (
<Group gap="sm">
{/* UNKNOWN is never re-filed from here: resubmitting risks a duplicate registration. */}
{status !== "REGISTERED" && status !== "UNKNOWN" && (
<Button
size="xs"
variant="light"
radius="md"
loading={register.isPending}
disabled={busy}
leftSection={status === "FAILED" ? <RefreshCw size={14} /> : <Send size={14} />}
onClick={() => register.mutate({ id: invoiceId })}
>
{status === "FAILED" ? "File again" : "File with MoR"}
</Button>
)}
{eims.eimsIrn && (
<Button
size="xs"
variant="light"
radius="md"
loading={verify.isPending}
disabled={busy}
leftSection={<ShieldCheck size={14} />}
onClick={() => verify.mutate({ id: invoiceId })}
>
Verify with MoR
</Button>
)}
</Group>
{status === "CANCELLED" && (
<Alert color="gray" icon={<Ban size={16} />} title="Cancelled with MoR">
{eims.eimsCancellationDate ? `Confirmed ${eims.eimsCancellationDate}. ` : ""}
{eims.eimsCancellationRemark}
</Alert>
)}
<Group gap="sm">
{canFile && (
<>
{/* UNKNOWN is never re-filed from here: resubmitting risks a duplicate registration. */}
{status !== "REGISTERED" && status !== "UNKNOWN" && status !== "CANCELLED" && (
<Button
size="xs"
variant="light"
radius="md"
loading={register.isPending}
disabled={busy}
leftSection={status === "FAILED" ? <RefreshCw size={14} /> : <Send size={14} />}
onClick={() => register.mutate({ id: invoiceId })}
>
{status === "FAILED" ? "File again" : "File with MoR"}
</Button>
)}
{eims.eimsIrn && (
<Button
size="xs"
variant="light"
radius="md"
loading={verify.isPending}
disabled={busy}
leftSection={<ShieldCheck size={14} />}
onClick={() => verify.mutate({ id: invoiceId })}
>
Verify with MoR
</Button>
)}
</>
)}
{canCancel && eims.eimsIrn && status !== "CANCELLED" && (
<Button
size="xs"
variant="light"
color="red"
radius="md"
leftSection={<Ban size={14} />}
onClick={() => setCancelOpen(true)}
>
Cancel with MoR
</Button>
)}
{canIssueMemo && status === "REGISTERED" && (
<Button
size="xs"
variant="light"
radius="md"
leftSection={<FileText size={14} />}
onClick={() => setMemoOpen(true)}
>
Issue credit/debit memo
</Button>
)}
</Group>
{eims.eimsIrn && <ReceiptsSection invoiceId={invoiceId} canFile={canFileReceipt} />}
</Stack>
<CancelModal invoiceId={invoiceId} opened={cancelOpen} onClose={() => setCancelOpen(false)} />
<MemoModal invoiceId={invoiceId} opened={memoOpen} onClose={() => setMemoOpen(false)} />
</Card>
);
}

View File

@@ -96,6 +96,7 @@ export const QUERY_KEYS = {
offlineUsd: (filter?: InvoiceListFilter) =>
["invoices", "offline-usd", filter ?? {}] as const,
eimsStatus: (id: string) => ["invoices", "eims", id] as const,
eimsReceipts: (id: string) => ["invoices", "eims", id, "receipts"] as const,
},
BOOKINGS: {

View File

@@ -136,6 +136,7 @@ export const URL_CONSTANTS = {
INVOICES: "/billing/invoices",
INVOICE_BY_ID: (id: string) => `/billing/invoices/${id}`,
INVOICE_DOCUMENT: (id: string) => `/billing/invoices/${id}/document`,
INVOICE_MEMO: (id: string) => `/billing/invoices/${id}/memo`,
OFFLINE_USD: "/billing/offline-usd",
CONFIRM_OFFLINE: (id: string) => `/billing/invoices/${id}/confirm-offline`,
},
@@ -146,6 +147,12 @@ export const URL_CONSTANTS = {
REGISTER: (id: string) => `/invoices/${id}/eims/register`,
VERIFY: (id: string) => `/invoices/${id}/eims/verify`,
RESOLVE: (id: string) => `/invoices/${id}/eims/resolve`,
CANCEL: (id: string) => `/invoices/${id}/eims/cancel`,
RECEIPT_SALES: (id: string) => `/invoices/${id}/eims/receipt/sales`,
RECEIPT_WITHHOLDING: (id: string) => `/invoices/${id}/eims/receipt/withholding`,
RECEIPTS: (id: string) => `/invoices/${id}/eims/receipts`,
RECEIPT_DOCUMENT: (id: string, receiptId: string) =>
`/invoices/${id}/eims/receipts/${receiptId}/document`,
},
CUSTOMERS_API: {

View File

@@ -145,10 +145,16 @@ export const FREIGHT_PERMS = {
view: "edr_freight_app:invoices:view",
export: "edr_freight_app:invoices:export",
confirmOffline: "edr_freight_app:invoices:confirm_offline",
// Filing with MoR EIMS. Held by named admins rather than a role preset: registration is
// irreversible at the tax authority, and resolving clears a system-wide filing block.
// Filing with MoR EIMS. Off the general Finance role — automatic filing needs no permission
// at all (the cron sweep runs as the system); these are the manual, exceptional-operations
// actions, granted to the `chief` position (maker-checker, same as shipping-line credit
// mark-paid/cancel approval) rather than every Finance user.
eimsRegister: "edr_freight_app:invoices:eims_register",
eimsResolve: "edr_freight_app:invoices:eims_resolve",
eimsCancel: "edr_freight_app:invoices:eims_cancel",
eimsReceiptRegister: "edr_freight_app:invoices:eims_receipt_register",
// Issuing a credit/debit memo is filing-equivalent — same restricted grant as the eims_* keys.
memoIssue: "edr_freight_app:invoices:memo_issue",
},
firstMile: {
view: "edr_freight_app:first_mile:view",

View File

@@ -173,7 +173,7 @@ import { customersService } from "./customers.service";
import { shippingLineCompaniesService } from "./shippingLineCompanies.service";
import { shippingLineCreditsService } from "./shippingLineCredits.service";
import { eimsService } from "./eims.service";
import type { EimsInvoiceStatusView, EimsVerifyResult } from "@/types/eims";
import type { EimsInvoiceStatusView, EimsModeOfPayment, EimsReceiptView, EimsVerifyResult } from "@/types/eims";
import { invoicesService } from "./invoices.service";
import { dropdownSettingsService } from "./dropdownSettings.service";
import { fileUploadSettingsService } from "./fileUploadSettings.service";
@@ -3189,6 +3189,51 @@ export const api = {
undefined,
({ id }) => [QUERY_KEYS.INVOICES.eimsStatus(id), QUERY_KEYS.INVOICES.byId(id)],
),
eimsCancel: endpoint<{ id: string; reasonCode: string; remark?: string }, EimsInvoiceStatusView>(
"invoices",
"eimsCancel",
({ id, reasonCode, remark }) => eimsService.cancel(id, { reasonCode, remark }),
undefined,
({ id }) => [QUERY_KEYS.INVOICES.eimsStatus(id), QUERY_KEYS.INVOICES.byId(id)],
),
eimsReceipts: endpoint<{ id: string }, EimsReceiptView[]>(
"invoices",
"eimsReceipts",
({ id }) => eimsService.listReceipts(id),
({ id }) => QUERY_KEYS.INVOICES.eimsReceipts(id),
),
eimsRegisterSalesReceipt: endpoint<
{ id: string; modeOfPayment: EimsModeOfPayment; reason?: string; collectedAmount?: number },
EimsReceiptView
>(
"invoices",
"eimsRegisterSalesReceipt",
({ id, ...input }) => eimsService.registerSalesReceipt(id, input),
undefined,
({ id }) => [QUERY_KEYS.INVOICES.eimsReceipts(id)],
),
eimsRegisterWithholdingReceipt: endpoint<
{ id: string; type: string; preTaxAmount: number; withholdingAmount: number; reason?: string },
EimsReceiptView
>(
"invoices",
"eimsRegisterWithholdingReceipt",
({ id, ...input }) => eimsService.registerWithholdingReceipt(id, input),
undefined,
({ id }) => [QUERY_KEYS.INVOICES.eimsReceipts(id)],
),
issueMemo: endpoint<{ id: string; type: "CRE" | "DEB"; reason: string }, Invoice>(
"invoices",
"issueMemo",
({ id, ...input }) => invoicesService.issueMemo(id, input),
undefined,
() => [QUERY_KEYS.INVOICES.ROOT],
),
},
overview: {

View File

@@ -1,6 +1,11 @@
import { api as apiClient } from "@/auth/http";
import { URL_CONSTANTS } from "@/constants/URLS";
import type { EimsInvoiceStatusView, EimsVerifyResult } from "@/types/eims";
import type {
EimsInvoiceStatusView,
EimsModeOfPayment,
EimsReceiptView,
EimsVerifyResult,
} from "@/types/eims";
/**
* MoR EIMS filing actions on an invoice.
@@ -36,4 +41,45 @@ export const eimsService = {
.post<EimsInvoiceStatusView>(URL_CONSTANTS.EIMS.RESOLVE(invoiceId), input)
.then((r) => r.data);
},
/** Cancel the invoice's registered EIMS document. Refuses (409) an already-cancelled one. */
cancel(
invoiceId: string,
input: { reasonCode: string; remark?: string },
): Promise<EimsInvoiceStatusView> {
return apiClient
.post<EimsInvoiceStatusView>(URL_CONSTANTS.EIMS.CANCEL(invoiceId), input)
.then((r) => r.data);
},
registerSalesReceipt(
invoiceId: string,
input: { modeOfPayment: EimsModeOfPayment; reason?: string; collectedAmount?: number },
): Promise<EimsReceiptView> {
return apiClient
.post<EimsReceiptView>(URL_CONSTANTS.EIMS.RECEIPT_SALES(invoiceId), input)
.then((r) => r.data);
},
registerWithholdingReceipt(
invoiceId: string,
input: { type: string; preTaxAmount: number; withholdingAmount: number; reason?: string },
): Promise<EimsReceiptView> {
return apiClient
.post<EimsReceiptView>(URL_CONSTANTS.EIMS.RECEIPT_WITHHOLDING(invoiceId), input)
.then((r) => r.data);
},
listReceipts(invoiceId: string): Promise<EimsReceiptView[]> {
return apiClient
.get<EimsReceiptView[]>(URL_CONSTANTS.EIMS.RECEIPTS(invoiceId))
.then((r) => r.data);
},
/** Blob download — same pattern as `invoicesService.downloadDocument`. */
downloadReceiptDocument(invoiceId: string, receiptId: string) {
return apiClient.get<Blob>(URL_CONSTANTS.EIMS.RECEIPT_DOCUMENT(invoiceId, receiptId), {
responseType: "blob",
});
},
};

View File

@@ -35,6 +35,16 @@ export const invoicesService = {
});
},
/** Issue a credit/debit memo against a registered invoice (MoR DEB/CRE) — filing-equivalent. */
issueMemo(
id: string,
input: { type: "CRE" | "DEB"; reason: string },
): Promise<Invoice> {
return apiClient
.post<Invoice>(URL_CONSTANTS.BILLING.INVOICE_MEMO(id), input)
.then((r) => r.data);
},
/** Finance worklist: USD invoices awaiting bank-transfer confirmation. */
listOfflineUsd(
filter: InvoiceListFilter,

View File

@@ -65,3 +65,16 @@ export interface EimsVerifyResult {
[section: string]: unknown;
};
}
/** MoR `ModeOfPayment` enum — confirmed by a live schema error, verbatim spelling/casing. */
export const EIMS_MODE_OF_PAYMENT = [
"CASH",
"CHEQUE",
"CPO",
"Local Bank Transfer",
"SWIFT",
"Wire Transfer",
"Letter of Credit",
"Card",
] as const;
export type EimsModeOfPayment = (typeof EIMS_MODE_OF_PAYMENT)[number];