This commit is contained in:
2026-08-11 11:41:47 +08:00
parent af30c0c16c
commit e3757cc9f9
5 changed files with 247 additions and 58 deletions
+26 -1
View File
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/shared/api/http'
import { createFinanceCost, createFinanceReceipt, issueFinanceBill, listFinanceBills, listFinanceCosts, listFinanceReceipts } from './finance'
import { adjustFinanceBill, createFinanceCost, createFinanceReceipt, getFinanceBill, issueFinanceBill, listFinanceBills, listFinanceCosts, listFinanceReceipts, updateFinanceBill, voidFinanceBill } from './finance'
describe('finance api', () => {
beforeEach(() => {
@@ -35,6 +35,31 @@ describe('finance api', () => {
expect(post.mock.calls[1]?.[2]).toMatchObject({ headers: { 'Idempotency-Key': expect.any(String) } })
})
it('maps the real cost fee_item_name and cost_amount fields', async () => {
vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: [{ id: 71, tenant_id: 10001, cost_type: 'CONTAINER_SHARED', container_id: 81, fee_item_name: '拖车费', cost_amount: '680.500000', voucher_file_ids: '["90001"]', created_at: '2026-08-11T10:00:00' }] })
const result = await listFinanceCosts('10001')
expect(result.records[0]).toMatchObject({ id: '71', customerId: '10001', containerId: '81', expenseName: '拖车费', amount: 680.5, voucherFileIds: ['90001'] })
})
it('normalizes bill details and uses update, adjust and void contracts', async () => {
const raw = { id: 81, tenant_id: 10001, bill_no: 'B-81', customer_name_snapshot: '客户A', bill_date: '2026-08-11', due_date: '2026-09-10', total_amount: 100, received_amount: 0, outstanding_amount: 100, status: 'DRAFT', version: 3, lines: [{ source_id: 91, source_version: 2, fee_item_name: '运费', amount: 100 }] }
vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: raw })
const put = vi.spyOn(http, 'put').mockResolvedValue({ code: '200', message: 'ok', data: raw })
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: raw })
const detail = await getFinanceBill('81')
await updateFinanceBill('81', { customerId: '10001', expectedVersion: 3, lines: [] })
await adjustFinanceBill('81', { expectedVersion: 3, reason: '金额修正', lines: [] })
await voidFinanceBill('81', 3, '重复账单')
expect(detail).toMatchObject({ id: '81', customerId: '10001', version: 3, lines: [{ sourceFeeId: '91', sourceFeeVersion: 2, feeItemName: '运费', amount: 100 }] })
expect(put).toHaveBeenCalledWith('/platform/finance/v1/bills/81', expect.objectContaining({ expectedVersion: 3 }), { headers: { 'Idempotency-Key': expect.any(String) } })
expect(post).toHaveBeenNthCalledWith(1, '/platform/finance/v1/bills/81/adjust', expect.objectContaining({ expectedVersion: 3, reason: '金额修正' }), { headers: { 'Idempotency-Key': expect.any(String) } })
expect(post).toHaveBeenNthCalledWith(2, '/platform/finance/v1/bills/81/void', { expectedVersion: 3, reason: '重复账单' }, { headers: { 'Idempotency-Key': expect.any(String) } })
})
it('issues a bill with optimistic version and an idempotency key', async () => {
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: {} })
+46 -11
View File
@@ -1,5 +1,5 @@
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type { FinanceBillResource, FinanceCostResource, FinancePaymentMethod, FinanceReceiptResource } from '../types'
import type { FinanceBillDetail, FinanceBillLine, FinanceBillResource, FinanceCostResource, FinancePaymentMethod, FinanceReceiptResource } from '../types'
const basePath = '/platform/finance/v1'
function idempotencyKey(operation: string) { return `finance-${operation}-${Date.now()}-${Math.random().toString(36).slice(2)}` }
@@ -18,24 +18,59 @@ function normalizePage<T>(payload: RawPage, map: (item: RawResource) => T): Page
const records = Array.isArray(payload) ? payload : payload.records || []
return { records: records.map(map), total: Array.isArray(payload) ? records.length : Number(payload.total ?? records.length), serverPaged: !Array.isArray(payload) }
}
export async function listFinanceBills(params: { customerId: string; status?: string; page: number; pageSize: number }) {
const response = await http.get<ApiEnvelope<RawPage>, ApiEnvelope<RawPage>>(`${basePath}/bills`, { params })
return normalizePage<FinanceBillResource>(unwrapApiResponse(response), (item) => ({
id: stringValue(item.id), billNo: stringValue(item.bill_no ?? item.billNo), customerId: params.customerId,
customerName: stringValue(item.customer_name_snapshot ?? item.customerName) || `客户 ${params.customerId}`,
function normalizeBill(item: RawResource, customerId = stringValue(item.tenant_id ?? item.customerId)): FinanceBillResource {
return {
id: stringValue(item.id), billNo: stringValue(item.bill_no ?? item.billNo), customerId,
customerName: stringValue(item.customer_name_snapshot ?? item.customerName) || `客户 ${customerId}`,
billDate: stringValue(item.bill_date ?? item.billDate), dueDate: stringValue(item.due_date ?? item.dueDate), currency: 'CNY',
totalAmount: numberValue(item.total_amount ?? item.totalAmount), paidAmount: numberValue(item.received_amount ?? item.paidAmount),
outstandingAmount: numberValue(item.outstanding_amount ?? item.outstandingAmount), status: stringValue(item.status) as FinanceBillResource['status'],
version: numberValue(item.version),
}))
}
}
function normalizeBillLine(item: RawResource): FinanceBillLine {
return {
sourceFeeId: stringValue(item.source_id ?? item.sourceFeeId), sourceFeeVersion: numberValue(item.source_version ?? item.sourceFeeVersion) || 1,
orderId: item.order_id == null && item.orderId == null ? null : stringValue(item.order_id ?? item.orderId),
inboundId: item.inbound_id == null && item.inboundId == null ? null : stringValue(item.inbound_id ?? item.inboundId),
feeItemName: stringValue(item.fee_item_name ?? item.feeItemName), amount: numberValue(item.amount),
}
}
function normalizeBillDetail(item: RawResource): FinanceBillDetail {
return { ...normalizeBill(item), remark: stringValue(item.remark), lines: Array.isArray(item.lines) ? item.lines.map((line) => normalizeBillLine(line as RawResource)) : [] }
}
function mutationHeaders(operation: string) { return { headers: { 'Idempotency-Key': idempotencyKey(operation) } } }
export async function listFinanceBills(params: { customerId: string; status?: string; page: number; pageSize: number }) {
const response = await http.get<ApiEnvelope<RawPage>, ApiEnvelope<RawPage>>(`${basePath}/bills`, { params })
return normalizePage<FinanceBillResource>(unwrapApiResponse(response), (item) => normalizeBill(item, params.customerId))
}
export async function createFinanceBill(payload: Record<string, unknown>) {
const response = await http.post<ApiEnvelope<FinanceBillResource>, ApiEnvelope<FinanceBillResource>>(`${basePath}/bills`, { ...payload }, { headers: { 'Idempotency-Key': idempotencyKey('bill') } })
const response = await http.post<ApiEnvelope<FinanceBillResource>, ApiEnvelope<FinanceBillResource>>(`${basePath}/bills`, { ...payload }, mutationHeaders('bill'))
return unwrapApiResponse(response)
}
export async function getFinanceBill(billId: string) {
const response = await http.get<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/bills/${billId}`)
return normalizeBillDetail(unwrapApiResponse(response))
}
export async function updateFinanceBill(billId: string, payload: Record<string, unknown>) {
const response = await http.put<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/bills/${billId}`, payload, mutationHeaders('bill-update'))
return normalizeBillDetail(unwrapApiResponse(response))
}
export async function adjustFinanceBill(billId: string, payload: Record<string, unknown>) {
const response = await http.post<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/bills/${billId}/adjust`, payload, mutationHeaders('bill-adjust'))
return normalizeBillDetail(unwrapApiResponse(response))
}
export async function voidFinanceBill(billId: string, expectedVersion: number, reason: string) {
const response = await http.post<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/bills/${billId}/void`, { expectedVersion, reason }, mutationHeaders('bill-void'))
return normalizeBillDetail(unwrapApiResponse(response))
}
export async function listFinanceReceipts(customerId?: string) {
const response = await http.get<ApiEnvelope<RawPage>, ApiEnvelope<RawPage>>(`${basePath}/receipts`, { params: { customerId: customerId || undefined } })
const payload = unwrapApiResponse(response)
@@ -64,7 +99,7 @@ export async function listFinanceCosts(customerId?: string) {
return normalizePage<FinanceCostResource>(unwrapApiResponse(response), (item) => ({
id: stringValue(item.id), customerId: stringValue(item.tenant_id), costType: stringValue(item.cost_type) as FinanceCostResource['costType'],
orderId: item.order_id == null ? null : stringValue(item.order_id), containerId: item.container_id == null ? null : stringValue(item.container_id),
expenseName: stringValue(item.expense_name), amount: numberValue(item.amount), currency: 'CNY',
expenseName: stringValue(item.fee_item_name ?? item.expense_name), amount: numberValue(item.cost_amount ?? item.amount), currency: 'CNY',
voucherFileIds: fileIds(item.voucher_file_ids), createdAt: stringValue(item.created_at),
}))
}
@@ -75,6 +110,6 @@ export async function createFinanceCost(payload: Record<string, unknown>, vouche
}
export async function issueFinanceBill(billId: string, expectedVersion: number) {
const response = await http.post<ApiEnvelope<FinanceBillResource>, ApiEnvelope<FinanceBillResource>>(`${basePath}/bills/${billId}/issue`, { expectedVersion }, { headers: { 'Idempotency-Key': idempotencyKey('bill-issue') } })
const response = await http.post<ApiEnvelope<FinanceBillResource>, ApiEnvelope<FinanceBillResource>>(`${basePath}/bills/${billId}/issue`, { expectedVersion }, mutationHeaders('bill-issue'))
return unwrapApiResponse(response)
}