优化
This commit is contained in:
@@ -10,37 +10,51 @@ type WireScalar = string | number
|
|||||||
|
|
||||||
export interface FinanceBillWire {
|
export interface FinanceBillWire {
|
||||||
id: WireScalar
|
id: WireScalar
|
||||||
bill_no: string
|
customerId: WireScalar
|
||||||
customer_name_snapshot: string
|
billNo: string
|
||||||
currency_code: string
|
customerName: string
|
||||||
bill_date: string | null
|
currency: string
|
||||||
due_date: string | null
|
billDate: string | null
|
||||||
|
dueDate: string | null
|
||||||
status: FinanceBillStatus
|
status: FinanceBillStatus
|
||||||
total_amount: WireScalar
|
totalAmount: WireScalar
|
||||||
received_amount: WireScalar
|
receivedAmount: WireScalar
|
||||||
outstanding_amount: WireScalar
|
outstandingAmount: WireScalar
|
||||||
lines?: FinanceBillLineWire[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FinanceBillLineWire {
|
export interface FinanceBillLineWire {
|
||||||
id: WireScalar
|
id: WireScalar
|
||||||
source_no: string | null
|
sourceType: string
|
||||||
order_id: WireScalar | null
|
sourceId: WireScalar
|
||||||
fee_item_name: string
|
sourceVersion: number
|
||||||
|
orderId: WireScalar | null
|
||||||
|
inboundId: WireScalar | null
|
||||||
|
feeItemCode: string
|
||||||
|
feeItemName: string
|
||||||
amount: WireScalar
|
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 {
|
export interface FinanceReceiptWire {
|
||||||
id: WireScalar
|
id: WireScalar
|
||||||
receipt_no: string
|
receiptNo: string
|
||||||
received_amount: WireScalar
|
receivedAmount: WireScalar
|
||||||
received_at: string
|
receivedAt: string
|
||||||
payment_method: string | null
|
paymentMethod: string | null
|
||||||
voucher_no: string | null
|
currency: string
|
||||||
status: string
|
status: string
|
||||||
bill_id: WireScalar | null
|
allocations: FinanceReceiptAllocationWire[]
|
||||||
applied_amount: WireScalar | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function text(value: WireScalar) {
|
function text(value: WireScalar) {
|
||||||
@@ -50,14 +64,14 @@ function text(value: WireScalar) {
|
|||||||
function mapBillSummary(bill: FinanceBillWire): FinanceBillSummary {
|
function mapBillSummary(bill: FinanceBillWire): FinanceBillSummary {
|
||||||
return {
|
return {
|
||||||
id: text(bill.id),
|
id: text(bill.id),
|
||||||
billNo: bill.bill_no,
|
billNo: bill.billNo,
|
||||||
customerName: bill.customer_name_snapshot,
|
customerName: bill.customerName,
|
||||||
currencyCode: 'CNY',
|
currencyCode: 'CNY',
|
||||||
totalAmount: text(bill.total_amount),
|
totalAmount: text(bill.totalAmount),
|
||||||
receivedAmount: text(bill.received_amount),
|
receivedAmount: text(bill.receivedAmount),
|
||||||
outstandingAmount: text(bill.outstanding_amount),
|
outstandingAmount: text(bill.outstandingAmount),
|
||||||
issueDate: bill.bill_date,
|
issueDate: bill.billDate,
|
||||||
dueDate: bill.due_date,
|
dueDate: bill.dueDate,
|
||||||
status: bill.status,
|
status: bill.status,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,9 +79,9 @@ function mapBillSummary(bill: FinanceBillWire): FinanceBillSummary {
|
|||||||
function mapBillLine(line: FinanceBillLineWire): FinanceBillLine {
|
function mapBillLine(line: FinanceBillLineWire): FinanceBillLine {
|
||||||
return {
|
return {
|
||||||
id: text(line.id),
|
id: text(line.id),
|
||||||
orderId: line.order_id == null ? null : text(line.order_id),
|
orderId: line.orderId == null ? null : text(line.orderId),
|
||||||
feeNo: line.source_no,
|
feeNo: `${line.sourceType}:${text(line.sourceId)}/v${line.sourceVersion}`,
|
||||||
feeName: line.fee_item_name,
|
feeName: line.feeItemName,
|
||||||
amount: text(line.amount),
|
amount: text(line.amount),
|
||||||
currencyCode: 'CNY',
|
currencyCode: 'CNY',
|
||||||
}
|
}
|
||||||
@@ -79,27 +93,26 @@ export function mapFinanceBills(bills: FinanceBillWire[]) {
|
|||||||
.map(mapBillSummary)
|
.map(mapBillSummary)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapFinanceBillDetail(bill: FinanceBillWire): FinanceBillDetail {
|
export function mapFinanceBillDetail(detail: FinanceBillDetailWire): FinanceBillDetail {
|
||||||
return {
|
return {
|
||||||
...mapBillSummary(bill),
|
...mapBillSummary(detail.statement),
|
||||||
lines: (bill.lines ?? []).map(mapBillLine),
|
lines: (detail.lines ?? []).map(mapBillLine),
|
||||||
receipts: [],
|
receipts: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapFinanceReceipts(receipts: FinanceReceiptWire[], billId: string): FinanceReceiptRecord[] {
|
export function mapFinanceReceipts(receipts: FinanceReceiptWire[], billId: string): FinanceReceiptRecord[] {
|
||||||
return receipts
|
return receipts.flatMap((receipt) => receipt.allocations
|
||||||
.filter((receipt) => receipt.bill_id != null && text(receipt.bill_id) === billId)
|
.filter((allocation) => text(allocation.billId) === billId)
|
||||||
.map((receipt) => ({
|
.map((allocation) => ({
|
||||||
id: text(receipt.id),
|
id: text(receipt.id),
|
||||||
receiptNo: receipt.receipt_no,
|
receiptNo: receipt.receiptNo,
|
||||||
receivedAt: receipt.received_at,
|
receivedAt: receipt.receivedAt,
|
||||||
amount: text(receipt.received_amount),
|
amount: text(receipt.receivedAmount),
|
||||||
currencyCode: 'CNY',
|
currencyCode: 'CNY' as const,
|
||||||
method: receipt.payment_method,
|
method: receipt.paymentMethod,
|
||||||
voucherNo: receipt.voucher_no,
|
allocatedAmount: text(allocation.appliedAmount),
|
||||||
allocatedAmount: receipt.applied_amount == null ? '0' : text(receipt.applied_amount),
|
})))
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sumDecimalStrings(values: string[]) {
|
export function sumDecimalStrings(values: string[]) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
mapFinanceBills,
|
mapFinanceBills,
|
||||||
mapFinanceReceipts,
|
mapFinanceReceipts,
|
||||||
sumDecimalStrings,
|
sumDecimalStrings,
|
||||||
|
type FinanceBillDetailWire,
|
||||||
type FinanceBillWire,
|
type FinanceBillWire,
|
||||||
type FinanceReceiptWire,
|
type FinanceReceiptWire,
|
||||||
} from './adapters'
|
} from './adapters'
|
||||||
@@ -39,7 +40,7 @@ export async function getMerchantFinanceBills(params: FinanceBillQuery = {}): Pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getMerchantFinanceBill(id: string): Promise<FinanceBillDetail> {
|
export async function getMerchantFinanceBill(id: string): Promise<FinanceBillDetail> {
|
||||||
const response = await apiRequest<FinanceBillWire>({
|
const response = await apiRequest<FinanceBillDetailWire>({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `/client/finance/v1/bills/${encodeURIComponent(id)}`,
|
url: `/client/finance/v1/bills/${encodeURIComponent(id)}`,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export interface FinanceReceiptRecord {
|
|||||||
amount: string
|
amount: string
|
||||||
currencyCode: 'CNY'
|
currencyCode: 'CNY'
|
||||||
method: string | null
|
method: string | null
|
||||||
voucherNo: string | null
|
|
||||||
allocatedAmount: string
|
allocatedAmount: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,11 +38,16 @@ test('uses client finance read APIs without browser-selected customer scope', ()
|
|||||||
assert.doesNotMatch(apiSource, /params:\s*\{\s*billId/)
|
assert.doesNotMatch(apiSource, /params:\s*\{\s*billId/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('adapts the finance snake-case wire contract and filters receipts locally by bill id', () => {
|
test('adapts the Finance Java DTO contract and filters nested receipt allocations 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']) {
|
for (const field of ['billNo', 'customerName', 'billDate', 'totalAmount', 'outstandingAmount', 'feeItemName', 'orderId', 'receiptNo', 'allocations', 'billId', 'appliedAmount']) {
|
||||||
assert.match(adapterSource, new RegExp(field))
|
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.match(viewSource, /loadDetail\(row\.id\)/)
|
||||||
assert.doesNotMatch(viewSource, /loadDetail\(row\.billNo\)/)
|
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', () => {
|
test('does not expose draft or voided bills returned by the server', () => {
|
||||||
const bill = (id, status) => ({
|
const bill = (id, status) => ({
|
||||||
id,
|
id,
|
||||||
bill_no: `B-${id}`,
|
customerId: 10001,
|
||||||
customer_name_snapshot: '测试客户',
|
billNo: `B-${id}`,
|
||||||
currency_code: 'CNY',
|
customerName: '测试客户',
|
||||||
bill_date: '2026-08-11',
|
currency: 'CNY',
|
||||||
due_date: '2026-09-10',
|
billDate: '2026-08-11',
|
||||||
|
dueDate: '2026-09-10',
|
||||||
status,
|
status,
|
||||||
total_amount: '100.00',
|
totalAmount: '100.00',
|
||||||
received_amount: '0.00',
|
receivedAmount: '0.00',
|
||||||
outstanding_amount: '100.00',
|
outstandingAmount: '100.00',
|
||||||
})
|
})
|
||||||
const visible = financeAdapters.mapFinanceBills([
|
const visible = financeAdapters.mapFinanceBills([
|
||||||
bill(1, 'DRAFT'),
|
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'])
|
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', () => {
|
test('requires both finance statement authorities for the page permission', () => {
|
||||||
const allowed = (codes) => permissionContracts.hasMappedMerchantPermission('finance:bill:view', new Set(codes))
|
const allowed = (codes) => permissionContracts.hasMappedMerchantPermission('finance:bill:view', new Set(codes))
|
||||||
assert.equal(allowed([]), false)
|
assert.equal(allowed([]), false)
|
||||||
|
|||||||
Reference in New Issue
Block a user