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 = { 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 = { 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 = { 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( (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; }