diff --git a/src/modules/finance/api/adapters.ts b/src/modules/finance/api/adapters.ts index a17f880..b8bed3d 100644 --- a/src/modules/finance/api/adapters.ts +++ b/src/modules/finance/api/adapters.ts @@ -10,37 +10,51 @@ type WireScalar = string | number export interface FinanceBillWire { id: WireScalar - bill_no: string - customer_name_snapshot: string - currency_code: string - bill_date: string | null - due_date: string | null + customerId: WireScalar + billNo: string + customerName: string + currency: string + billDate: string | null + dueDate: string | null status: FinanceBillStatus - total_amount: WireScalar - received_amount: WireScalar - outstanding_amount: WireScalar - lines?: FinanceBillLineWire[] + totalAmount: WireScalar + receivedAmount: WireScalar + outstandingAmount: WireScalar } export interface FinanceBillLineWire { id: WireScalar - source_no: string | null - order_id: WireScalar | null - fee_item_name: string + sourceType: string + sourceId: WireScalar + sourceVersion: number + orderId: WireScalar | null + inboundId: WireScalar | null + feeItemCode: string + feeItemName: string amount: WireScalar - currency_code: string + currency: string +} + +export interface FinanceBillDetailWire { + statement: FinanceBillWire + lines: FinanceBillLineWire[] +} + +export interface FinanceReceiptAllocationWire { + billId: WireScalar + billNo: string + appliedAmount: WireScalar } export interface FinanceReceiptWire { id: WireScalar - receipt_no: string - received_amount: WireScalar - received_at: string - payment_method: string | null - voucher_no: string | null + receiptNo: string + receivedAmount: WireScalar + receivedAt: string + paymentMethod: string | null + currency: string status: string - bill_id: WireScalar | null - applied_amount: WireScalar | null + allocations: FinanceReceiptAllocationWire[] } function text(value: WireScalar) { @@ -50,14 +64,14 @@ function text(value: WireScalar) { function mapBillSummary(bill: FinanceBillWire): FinanceBillSummary { return { id: text(bill.id), - billNo: bill.bill_no, - customerName: bill.customer_name_snapshot, + billNo: bill.billNo, + customerName: bill.customerName, currencyCode: 'CNY', - totalAmount: text(bill.total_amount), - receivedAmount: text(bill.received_amount), - outstandingAmount: text(bill.outstanding_amount), - issueDate: bill.bill_date, - dueDate: bill.due_date, + totalAmount: text(bill.totalAmount), + receivedAmount: text(bill.receivedAmount), + outstandingAmount: text(bill.outstandingAmount), + issueDate: bill.billDate, + dueDate: bill.dueDate, status: bill.status, } } @@ -65,9 +79,9 @@ function mapBillSummary(bill: FinanceBillWire): FinanceBillSummary { function mapBillLine(line: FinanceBillLineWire): FinanceBillLine { return { id: text(line.id), - orderId: line.order_id == null ? null : text(line.order_id), - feeNo: line.source_no, - feeName: line.fee_item_name, + orderId: line.orderId == null ? null : text(line.orderId), + feeNo: `${line.sourceType}:${text(line.sourceId)}/v${line.sourceVersion}`, + feeName: line.feeItemName, amount: text(line.amount), currencyCode: 'CNY', } @@ -79,27 +93,26 @@ export function mapFinanceBills(bills: FinanceBillWire[]) { .map(mapBillSummary) } -export function mapFinanceBillDetail(bill: FinanceBillWire): FinanceBillDetail { +export function mapFinanceBillDetail(detail: FinanceBillDetailWire): FinanceBillDetail { return { - ...mapBillSummary(bill), - lines: (bill.lines ?? []).map(mapBillLine), + ...mapBillSummary(detail.statement), + lines: (detail.lines ?? []).map(mapBillLine), receipts: [], } } export function mapFinanceReceipts(receipts: FinanceReceiptWire[], billId: string): FinanceReceiptRecord[] { - return receipts - .filter((receipt) => receipt.bill_id != null && text(receipt.bill_id) === billId) - .map((receipt) => ({ + return receipts.flatMap((receipt) => receipt.allocations + .filter((allocation) => text(allocation.billId) === billId) + .map((allocation) => ({ id: text(receipt.id), - receiptNo: receipt.receipt_no, - receivedAt: receipt.received_at, - amount: text(receipt.received_amount), - currencyCode: 'CNY', - method: receipt.payment_method, - voucherNo: receipt.voucher_no, - allocatedAmount: receipt.applied_amount == null ? '0' : text(receipt.applied_amount), - })) + receiptNo: receipt.receiptNo, + receivedAt: receipt.receivedAt, + amount: text(receipt.receivedAmount), + currencyCode: 'CNY' as const, + method: receipt.paymentMethod, + allocatedAmount: text(allocation.appliedAmount), + }))) } export function sumDecimalStrings(values: string[]) { diff --git a/src/modules/finance/api/finance.ts b/src/modules/finance/api/finance.ts index 17df23c..14138af 100644 --- a/src/modules/finance/api/finance.ts +++ b/src/modules/finance/api/finance.ts @@ -5,6 +5,7 @@ import { mapFinanceBills, mapFinanceReceipts, sumDecimalStrings, + type FinanceBillDetailWire, type FinanceBillWire, type FinanceReceiptWire, } from './adapters' @@ -39,7 +40,7 @@ export async function getMerchantFinanceBills(params: FinanceBillQuery = {}): Pr } export async function getMerchantFinanceBill(id: string): Promise { - const response = await apiRequest({ + const response = await apiRequest({ method: 'GET', url: `/client/finance/v1/bills/${encodeURIComponent(id)}`, }) diff --git a/src/modules/finance/contracts.ts b/src/modules/finance/contracts.ts index ab56bb7..59e5b0c 100644 --- a/src/modules/finance/contracts.ts +++ b/src/modules/finance/contracts.ts @@ -38,7 +38,6 @@ export interface FinanceReceiptRecord { amount: string currencyCode: 'CNY' method: string | null - voucherNo: string | null allocatedAmount: string } diff --git a/src/modules/finance/views/FinanceBillsView.test.mjs b/src/modules/finance/views/FinanceBillsView.test.mjs index e1f0635..fc471e7 100644 --- a/src/modules/finance/views/FinanceBillsView.test.mjs +++ b/src/modules/finance/views/FinanceBillsView.test.mjs @@ -38,11 +38,16 @@ test('uses client finance read APIs without browser-selected customer scope', () assert.doesNotMatch(apiSource, /params:\s*\{\s*billId/) }) -test('adapts the finance snake-case wire contract and filters receipts locally by bill id', () => { - for (const field of ['bill_no', 'customer_name_snapshot', 'bill_date', 'total_amount', 'outstanding_amount', 'fee_item_name', 'order_id', 'receipt_no', 'bill_id', 'applied_amount']) { +test('adapts the Finance Java DTO contract and filters nested receipt allocations by bill id', () => { + for (const field of ['billNo', 'customerName', 'billDate', 'totalAmount', 'outstandingAmount', 'feeItemName', 'orderId', 'receiptNo', 'allocations', 'billId', 'appliedAmount']) { assert.match(adapterSource, new RegExp(field)) } - assert.match(adapterSource, /filter\(\(receipt\).*receipt\.bill_id/s) + assert.match(adapterSource, /flatMap\(\(receipt\).*receipt\.allocations/s) + assert.match(adapterSource, /detail\.statement/) + assert.match(adapterSource, /detail\.lines/) + assert.doesNotMatch(adapterSource, /bill_no|customer_name|bill_date|total_amount|outstanding_amount|receipt_no|bill_id|applied_amount/) + assert.doesNotMatch(adapterSource, /receipt\.billId|receipt\.appliedAmount/) + assert.doesNotMatch(adapterSource, /voucherNo|voucher_no/) assert.match(viewSource, /loadDetail\(row\.id\)/) assert.doesNotMatch(viewSource, /loadDetail\(row\.billNo\)/) }) @@ -50,15 +55,16 @@ test('adapts the finance snake-case wire contract and filters receipts locally b test('does not expose draft or voided bills returned by the server', () => { const bill = (id, status) => ({ id, - bill_no: `B-${id}`, - customer_name_snapshot: '测试客户', - currency_code: 'CNY', - bill_date: '2026-08-11', - due_date: '2026-09-10', + customerId: 10001, + billNo: `B-${id}`, + customerName: '测试客户', + currency: 'CNY', + billDate: '2026-08-11', + dueDate: '2026-09-10', status, - total_amount: '100.00', - received_amount: '0.00', - outstanding_amount: '100.00', + totalAmount: '100.00', + receivedAmount: '0.00', + outstandingAmount: '100.00', }) const visible = financeAdapters.mapFinanceBills([ bill(1, 'DRAFT'), @@ -70,6 +76,26 @@ test('does not expose draft or voided bills returned by the server', () => { assert.deepEqual(visible.map((item) => item.status), ['ISSUED', 'PARTIALLY_PAID', 'PAID']) }) +test('maps nested bill detail and customer-safe receipt allocations', () => { + const detail = financeAdapters.mapFinanceBillDetail({ + statement: { + id: 2, customerId: 10001, billNo: 'B-2', customerName: '测试客户', currency: 'CNY', + billDate: '2026-08-11', dueDate: '2026-09-10', status: 'ISSUED', totalAmount: '100.00', + receivedAmount: '40.00', outstandingAmount: '60.00', + }, + lines: [{ id: 21, sourceType: 'INBOUND_FEE', sourceId: 81, sourceVersion: 3, orderId: 31, inboundId: 41, feeItemCode: 'FREIGHT', feeItemName: '运费', amount: '100.00', currency: 'CNY' }], + }) + const receipts = financeAdapters.mapFinanceReceipts([{ + id: 51, receiptNo: 'RCPT-51', receivedAmount: '40.00', receivedAt: '2026-08-12T10:00:00', + paymentMethod: 'BANK_TRANSFER', currency: 'CNY', status: 'CONFIRMED', + allocations: [{ billId: 2, billNo: 'B-2', appliedAmount: '40.00' }], + }], '2') + + assert.equal(detail.billNo, 'B-2') + assert.deepEqual(detail.lines[0], { id: '21', orderId: '31', feeNo: 'INBOUND_FEE:81/v3', feeName: '运费', amount: '100.00', currencyCode: 'CNY' }) + assert.deepEqual(receipts[0], { id: '51', receiptNo: 'RCPT-51', receivedAt: '2026-08-12T10:00:00', amount: '40.00', currencyCode: 'CNY', method: 'BANK_TRANSFER', allocatedAmount: '40.00' }) +}) + test('requires both finance statement authorities for the page permission', () => { const allowed = (codes) => permissionContracts.hasMappedMerchantPermission('finance:bill:view', new Set(codes)) assert.equal(allowed([]), false)