Files

170 lines
4.8 KiB
TypeScript

export const BILLING_STATUS_VALUES = ["paid", "partial", "pending", "overdue"] as const;
export type BillingStatus = (typeof BILLING_STATUS_VALUES)[number];
export const BILLING_STATUS_LABELS: Record<BillingStatus, string> = {
paid: "已结清",
partial: "部分收款",
pending: "待收款",
overdue: "已逾期",
};
export const PAYMENT_METHOD_VALUES = ["wechat", "alipay", "bank_transfer", "card", "cash"] as const;
export type PaymentMethod = (typeof PAYMENT_METHOD_VALUES)[number];
export const PAYMENT_METHOD_LABELS: Record<PaymentMethod, string> = {
wechat: "微信支付",
alipay: "支付宝",
bank_transfer: "银行转账",
card: "银行卡",
cash: "现金",
};
export const INVOICE_STATUS_VALUES = ["not_requested", "pending", "issued"] as const;
export type InvoiceStatus = (typeof INVOICE_STATUS_VALUES)[number];
export const INVOICE_STATUS_LABELS: Record<InvoiceStatus, string> = {
not_requested: "未开票",
pending: "待开票",
issued: "已开票",
};
export type BillingCharge = {
id: string;
category: string;
description: string;
quantity: number;
serviceDate: string;
unitPriceCents: number;
};
export type BillingPayment = {
id: string;
amountCents: number;
method: PaymentMethod;
paidAt: string;
reference: string;
operator: string;
note: string;
};
export type BillingStatement = {
id: string;
elderId: string;
elderName: string;
roomLabel: string;
contactName: string;
contactPhone: string;
period: string;
dueAt: string;
charges: BillingCharge[];
payments: BillingPayment[];
invoiceStatus: InvoiceStatus;
invoiceNumber?: string;
receiptCount: number;
};
export type BillingStatementView = BillingStatement & {
totalCents: number;
paidCents: number;
outstandingCents: number;
status: BillingStatus;
};
export type BillingSummary = {
receivableCents: number;
collectedCents: number;
outstandingCents: number;
overdueCount: number;
};
export function getChargeAmountCents(charge: BillingCharge): number {
return Math.round(charge.quantity * charge.unitPriceCents);
}
export function getStatementTotalCents(statement: BillingStatement): number {
return statement.charges.reduce((total, charge) => total + getChargeAmountCents(charge), 0);
}
export function getStatementPaidCents(statement: BillingStatement): number {
return statement.payments.reduce((total, payment) => total + payment.amountCents, 0);
}
export function getStatementOutstandingCents(statement: BillingStatement): number {
return Math.max(0, getStatementTotalCents(statement) - getStatementPaidCents(statement));
}
export function getBillingStatus(statement: BillingStatement, referenceDate = new Date()): BillingStatus {
const outstandingCents = getStatementOutstandingCents(statement);
if (outstandingCents === 0) {
return "paid";
}
if (new Date(statement.dueAt).getTime() < referenceDate.getTime()) {
return "overdue";
}
return statement.payments.length > 0 ? "partial" : "pending";
}
export function toBillingStatementView(
statement: BillingStatement,
referenceDate = new Date(),
): BillingStatementView {
const totalCents = getStatementTotalCents(statement);
const paidCents = getStatementPaidCents(statement);
return {
...statement,
totalCents,
paidCents,
outstandingCents: Math.max(0, totalCents - paidCents),
status: getBillingStatus(statement, referenceDate),
};
}
export function calculateBillingSummary(
statements: readonly BillingStatement[],
referenceDate = new Date(),
): BillingSummary {
return statements.reduce<BillingSummary>(
(summary, statement) => {
const view = toBillingStatementView(statement, referenceDate);
return {
receivableCents: summary.receivableCents + view.totalCents,
collectedCents: summary.collectedCents + view.paidCents,
outstandingCents: summary.outstandingCents + view.outstandingCents,
overdueCount: summary.overdueCount + (view.status === "overdue" ? 1 : 0),
};
},
{ receivableCents: 0, collectedCents: 0, outstandingCents: 0, overdueCount: 0 },
);
}
export function addBillingPayment(statement: BillingStatement, payment: BillingPayment): BillingStatement {
return {
...statement,
payments: [payment, ...statement.payments],
};
}
export function addBillingCharge(statement: BillingStatement, charge: BillingCharge): BillingStatement {
return {
...statement,
charges: [...statement.charges, charge],
invoiceStatus: "not_requested",
invoiceNumber: undefined,
};
}
export function formatCny(cents: number): string {
return new Intl.NumberFormat("zh-CN", {
style: "currency",
currency: "CNY",
minimumFractionDigits: 2,
}).format(cents / 100);
}
export function formatBillingPeriod(period: string): string {
const [year, month] = period.split("-");
return year && month ? `${year}${month}` : period;
}