优化优化
This commit is contained in:
@@ -1,48 +1,62 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/shared/api/http'
|
||||
import { adjustFinanceBill, createFinanceCost, createFinanceReceipt, getFinanceBill, issueFinanceBill, listFinanceBills, listFinanceCosts, listFinanceReceipts, updateFinanceBill, uploadFinanceVoucher, voidFinanceBill } from './finance'
|
||||
import { adjustFinanceBill, createFinanceCost, createFinanceReceipt, getFinanceBill, getFinanceCustomerAccount, getFinanceCustomerTerms, issueFinanceBill, listFinanceBillRevisions, listFinanceFeePool, listFinancePayableSettlements, listFinanceReceivableSettlements, listFinanceStatements, updateFinanceBill, updateFinanceCustomerTerms, uploadFinanceVoucher, voidFinanceBill } from './finance'
|
||||
|
||||
describe('finance api', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('uses the OpenAPI bill path and pageSize parameter', async () => {
|
||||
it('uses the customer statement read-model path', async () => {
|
||||
const get = vi.spyOn(http, 'get').mockResolvedValue({
|
||||
code: '200', message: 'ok', data: [{ id: 1, bill_no: 'B-1', 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: 2 }],
|
||||
code: '200', message: 'ok', data: [{ id: 1, customerId: 10001, billNo: 'B-1', customerName: '客户A', billDate: '2026-08-11', dueDate: '2026-09-10', totalAmount: 100, receivedAmount: 0, outstandingAmount: 100, status: 'DRAFT', version: 2 }],
|
||||
})
|
||||
|
||||
const result = await listFinanceBills({ customerId: '10001', status: 'DRAFT', page: 1, pageSize: 20 })
|
||||
const result = await listFinanceStatements('10001')
|
||||
|
||||
expect(get).toHaveBeenCalledWith('/platform/finance/v1/bills', { params: { customerId: '10001', status: 'DRAFT', page: 1, pageSize: 20 } })
|
||||
expect(result.records[0]).toMatchObject({ id: '1', billNo: 'B-1', currency: 'CNY', status: 'DRAFT', version: 2 })
|
||||
expect(get).toHaveBeenCalledWith('/platform/finance/v1/statements', { params: { customerId: '10001' } })
|
||||
expect(result[0]).toMatchObject({ id: '1', customerId: '10001', billNo: 'B-1', currency: 'CNY', status: 'DRAFT', version: 2 })
|
||||
})
|
||||
|
||||
it('uses the receipt and cost entry paths and fixes currency to CNY', async () => {
|
||||
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: [] })
|
||||
it('uses fixed read-model paths for all five finance pages', async () => {
|
||||
const get = vi.spyOn(http, 'get')
|
||||
.mockResolvedValueOnce({ code: '200', message: 'ok', data: { customerId: 10001, totalReceivable: 300, totalReceived: 100, outstandingAmount: 200, entries: [] } })
|
||||
.mockResolvedValueOnce({ code: '200', message: 'ok', data: [{ poolItemId: 9, direction: 'PAYABLE', sourceType: 'CONTAINER_COST', sourceId: 81, sourceVersion: 2, containerId: 81, feeItemName: '拖车费', amount: 680.5, status: 'OPEN' }] })
|
||||
.mockResolvedValueOnce({ code: '200', message: 'ok', data: [] })
|
||||
.mockResolvedValueOnce({ code: '200', message: 'ok', data: [{ id: 81, customerId: 10001, settlementNo: 'AR-81', receivableAmount: 300, receivedAmount: 100, outstandingAmount: 200, status: 'PARTIALLY_PAID', version: 2 }] })
|
||||
.mockResolvedValueOnce({ code: '200', message: 'ok', data: [{ id: 71, customerId: 10001, settlementNo: 'AP-71', costType: 'CONTAINER_SHARED', containerId: 81, feeItemName: '拖车费', payableAmount: 680.5, paidAmount: 0, outstandingAmount: 680.5, status: 'UNPAID', version: 1 }] })
|
||||
|
||||
const account = await getFinanceCustomerAccount('10001')
|
||||
const pool = await listFinanceFeePool('10001', 'PAYABLE')
|
||||
await listFinanceStatements('10001')
|
||||
const receivables = await listFinanceReceivableSettlements('10001')
|
||||
const payables = await listFinancePayableSettlements('10001')
|
||||
|
||||
expect(get.mock.calls.map(([path]) => path)).toEqual([
|
||||
'/platform/finance/v1/customer-accounts/10001',
|
||||
'/platform/finance/v1/fee-pool',
|
||||
'/platform/finance/v1/statements',
|
||||
'/platform/finance/v1/receivable-settlements',
|
||||
'/platform/finance/v1/payable-settlements',
|
||||
])
|
||||
expect(account).toMatchObject({ customerId: '10001', currency: 'CNY', accountMode: 'MANUAL_RECEIVABLE_LEDGER', outstandingAmount: 200 })
|
||||
expect(pool[0]).toMatchObject({ poolItemId: '9', sourceId: '81', sourceVersion: 2, containerId: '81', feeItemName: '拖车费', amount: 680.5 })
|
||||
expect(receivables[0]).toMatchObject({ id: '81', settlementNo: 'AR-81', currency: 'CNY' })
|
||||
expect(payables[0]).toMatchObject({ id: '71', settlementNo: 'AP-71', feeItemName: '拖车费', payableAmount: 680.5, currency: 'CNY' })
|
||||
})
|
||||
|
||||
it('keeps receipt and cost commands on CNY with real voucher ids', async () => {
|
||||
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: {} })
|
||||
|
||||
await listFinanceReceipts('10001')
|
||||
await listFinanceCosts('10001')
|
||||
await createFinanceReceipt({ customerId: '10001', amount: 100 }, ['90001'])
|
||||
await createFinanceCost({ customerId: '10001', amount: 100 }, ['90001'])
|
||||
|
||||
expect(get).toHaveBeenNthCalledWith(1, '/platform/finance/v1/receipts', { params: { customerId: '10001' } })
|
||||
expect(get).toHaveBeenNthCalledWith(2, '/platform/finance/v1/cost-entries', { params: { customerId: '10001' } })
|
||||
expect(post.mock.calls[0]?.[1]).toMatchObject({ currency: 'CNY', voucherFileIds: ['90001'] })
|
||||
expect(post.mock.calls[1]?.[1]).toMatchObject({ currency: 'CNY', voucherFileIds: ['90001'] })
|
||||
expect(post.mock.calls[0]?.[2]).toMatchObject({ headers: { 'Idempotency-Key': expect.any(String) } })
|
||||
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('uploads a real voucher through the Finance Controller proxy', async () => {
|
||||
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: { fileId: 90001, tenantId: 10001, fileName: 'receipt.png', contentType: 'image/png', sizeBytes: 128 } })
|
||||
const file = new File(['image'], 'receipt.png', { type: 'image/png' })
|
||||
@@ -56,17 +70,41 @@ describe('finance api', () => {
|
||||
expect(post.mock.calls[0]?.[2]).toEqual({ params: { customerId: '10001' }, headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
})
|
||||
|
||||
it('submits a non-empty container volume snapshot with the uploaded voucher id', async () => {
|
||||
it('lets Finance backend obtain the container snapshot from Firstleg', async () => {
|
||||
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: {} })
|
||||
const payload = { customerId: '10001', costType: 'CONTAINER_SHARED', containerId: '81', expenseName: '拖车费', amount: '600.000000', containerOrders: [{ orderId: '91', inboundId: '101', containerId: '81', volumeCbm: '2.500000' }] }
|
||||
const payload = { customerId: '10001', costType: 'CONTAINER_SHARED', containerId: '81', expenseName: '拖车费', amount: '600.000000' }
|
||||
|
||||
await createFinanceCost(payload, ['90001'])
|
||||
|
||||
expect(post.mock.calls[0]?.[1]).toMatchObject({ ...payload, currency: 'CNY', voucherFileIds: ['90001'], containerOrders: [{ orderId: '91', volumeCbm: '2.500000' }] })
|
||||
expect(post.mock.calls[0]?.[1]).toEqual({ ...payload, currency: 'CNY', voucherFileIds: ['90001'] })
|
||||
expect(post.mock.calls[0]?.[1]).not.toHaveProperty('containerOrders')
|
||||
})
|
||||
|
||||
it('gets and updates the customer credit terms', async () => {
|
||||
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: { tenantId: 10001, customerName: '客户A', creditDays: 45, status: 'ACTIVE' } })
|
||||
const put = vi.spyOn(http, 'put').mockResolvedValue({ code: '200', message: 'ok', data: { tenantId: 10001, customerName: '客户A', creditDays: 60, status: 'ACTIVE' } })
|
||||
|
||||
const terms = await getFinanceCustomerTerms('10001')
|
||||
const updated = await updateFinanceCustomerTerms('10001', { customerName: '客户A', creditDays: 60 })
|
||||
|
||||
expect(get).toHaveBeenCalledWith('/platform/finance/v1/customers/10001/terms')
|
||||
expect(put).toHaveBeenCalledWith('/platform/finance/v1/customers/10001/terms', { customerName: '客户A', creditDays: 60 }, { headers: { 'Idempotency-Key': expect.any(String) } })
|
||||
expect(terms.creditDays).toBe(45)
|
||||
expect(updated.creditDays).toBe(60)
|
||||
})
|
||||
|
||||
it('queries and normalizes real bill revision snapshots', async () => {
|
||||
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: [{ id: 301, bill_id: 81, version: 2, reason: '金额修正', bill_snapshot: JSON.stringify({ status: 'ISSUED', total_amount: '120.00', received_amount: '20.00', outstanding_amount: '100.00' }), line_snapshot: JSON.stringify([{ source_id: 91, source_version: 2, fee_item_name: '运费', amount: '120.00' }]), created_by: 7, created_at: '2026-08-11T12:00:00' }] })
|
||||
|
||||
const revisions = await listFinanceBillRevisions('81', '10001')
|
||||
|
||||
expect(get).toHaveBeenCalledWith('/platform/finance/v1/bills/81/revisions', { params: { customerId: '10001' } })
|
||||
expect(revisions[0]).toMatchObject({ id: '301', billId: '81', version: 2, reason: '金额修正', createdBy: '7', status: 'ISSUED', totalAmount: 120, receivedAmount: 20, outstandingAmount: 100, lines: [{ sourceFeeId: '91', sourceFeeVersion: 2, feeItemName: '运费', amount: 120 }] })
|
||||
})
|
||||
|
||||
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 }] }
|
||||
const statement = { id: 81, customerId: 10001, billNo: 'B-81', customerName: '客户A', billDate: '2026-08-11', dueDate: '2026-09-10', totalAmount: 100, receivedAmount: 0, outstandingAmount: 100, status: 'DRAFT', version: 3 }
|
||||
const raw = { statement, lines: [{ sourceId: 91, sourceVersion: 2, orderId: 92, inboundId: 93, feeItemName: '运费', 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 })
|
||||
@@ -76,7 +114,7 @@ describe('finance api', () => {
|
||||
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(detail).toMatchObject({ id: '81', customerId: '10001', version: 3, lines: [{ sourceFeeId: '91', sourceFeeVersion: 2, orderId: '92', inboundId: '93', 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) } })
|
||||
|
||||
@@ -1,49 +1,93 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type { FinanceBillDetail, FinanceBillLine, FinanceBillResource, FinanceCostResource, FinancePaymentMethod, FinanceReceiptResource, FinanceVoucherResource } from '../types'
|
||||
import type { FinanceAccountEntry, FinanceBillDetail, FinanceBillLine, FinanceBillResource, FinanceBillRevision, FinanceCostResource, FinanceCustomerAccountReadModel, FinanceCustomerTerms, FinanceFeePoolItem, FinancePayableSettlement, FinanceReceivableSettlement, FinanceReceiptResource, FinanceStatementReadModel, FinanceVoucherResource } from '../types'
|
||||
|
||||
const basePath = '/platform/finance/v1'
|
||||
function idempotencyKey(operation: string) { return `finance-${operation}-${Date.now()}-${Math.random().toString(36).slice(2)}` }
|
||||
type RawResource = Record<string, unknown>
|
||||
interface PageResult<T> { records: T[]; total: number; serverPaged: boolean }
|
||||
type RawPage = RawResource[] | { records?: RawResource[]; total?: number }
|
||||
|
||||
function stringValue(value: unknown) { return value == null ? '' : String(value) }
|
||||
function numberValue(value: unknown) { return Number(value ?? 0) }
|
||||
function fileIds(value: unknown): string[] {
|
||||
if (Array.isArray(value)) return value.map(String)
|
||||
if (typeof value !== 'string' || !value.trim()) return []
|
||||
try { const parsed = JSON.parse(value) as unknown; return Array.isArray(parsed) ? parsed.map(String) : [] } catch { return [] }
|
||||
function jsonRecord(value: unknown): RawResource {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) return value as RawResource
|
||||
if (typeof value !== 'string' || !value.trim()) return {}
|
||||
try { const parsed = JSON.parse(value) as unknown; return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as RawResource : {} } catch { return {} }
|
||||
}
|
||||
function normalizePage<T>(payload: RawPage, map: (item: RawResource) => T): PageResult<T> {
|
||||
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) }
|
||||
function jsonRecords(value: unknown): RawResource[] {
|
||||
if (Array.isArray(value)) return value.filter((item): item is RawResource => Boolean(item) && typeof item === 'object' && !Array.isArray(item))
|
||||
if (typeof value !== 'string' || !value.trim()) return []
|
||||
try { const parsed = JSON.parse(value) as unknown; return Array.isArray(parsed) ? parsed.filter((item): item is RawResource => Boolean(item) && typeof item === 'object' && !Array.isArray(item)) : [] } catch { return [] }
|
||||
}
|
||||
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),
|
||||
totalAmount: numberValue(item.total_amount ?? item.totalAmount), paidAmount: numberValue(item.received_amount ?? item.receivedAmount ?? 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,
|
||||
sourceFeeId: stringValue(item.source_id ?? item.sourceId ?? item.sourceFeeId), sourceFeeVersion: numberValue(item.source_version ?? item.sourceVersion ?? 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)) : [] }
|
||||
const statement = item.statement && typeof item.statement === 'object' ? item.statement as RawResource : item
|
||||
return { ...normalizeBill(statement), remark: stringValue(statement.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 getFinanceCustomerAccount(customerId: string): Promise<FinanceCustomerAccountReadModel> {
|
||||
const response = await http.get<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/customer-accounts/${customerId}`)
|
||||
const item = unwrapApiResponse(response)
|
||||
return {
|
||||
customerId: stringValue(item.customerId), currency: 'CNY', accountMode: 'MANUAL_RECEIVABLE_LEDGER',
|
||||
totalReceivable: numberValue(item.totalReceivable), totalReceived: numberValue(item.totalReceived), outstandingAmount: numberValue(item.outstandingAmount),
|
||||
entries: (Array.isArray(item.entries) ? item.entries : []).map((entry) => { const row = entry as RawResource; return { entryType: stringValue(row.entryType) as FinanceAccountEntry['entryType'], resourceId: stringValue(row.resourceId), resourceNo: stringValue(row.resourceNo), occurredAt: stringValue(row.occurredAt), debitAmount: numberValue(row.debitAmount), creditAmount: numberValue(row.creditAmount), currency: 'CNY', status: stringValue(row.status) } }),
|
||||
}
|
||||
}
|
||||
|
||||
export async function listFinanceFeePool(customerId: string, direction?: 'RECEIVABLE' | 'PAYABLE'): Promise<FinanceFeePoolItem[]> {
|
||||
const response = await http.get<ApiEnvelope<RawResource[]>, ApiEnvelope<RawResource[]>>(`${basePath}/fee-pool`, { params: { customerId, direction } })
|
||||
return unwrapApiResponse(response).map((item) => ({ poolItemId: stringValue(item.poolItemId), direction: stringValue(item.direction) as FinanceFeePoolItem['direction'], sourceType: stringValue(item.sourceType), sourceId: stringValue(item.sourceId), sourceVersion: numberValue(item.sourceVersion), orderId: item.orderId == null ? null : stringValue(item.orderId), inboundId: item.inboundId == null ? null : stringValue(item.inboundId), containerId: item.containerId == null ? null : stringValue(item.containerId), feeItemName: stringValue(item.feeItemName), amount: numberValue(item.amount), currency: 'CNY', status: stringValue(item.status), createdAt: stringValue(item.createdAt) }))
|
||||
}
|
||||
|
||||
export async function listFinanceStatements(customerId: string): Promise<FinanceStatementReadModel[]> {
|
||||
const response = await http.get<ApiEnvelope<RawResource[]>, ApiEnvelope<RawResource[]>>(`${basePath}/statements`, { params: { customerId } })
|
||||
return unwrapApiResponse(response).map((item) => ({ id: stringValue(item.id), customerId: stringValue(item.customerId), billNo: stringValue(item.billNo), customerName: stringValue(item.customerName), billDate: stringValue(item.billDate), dueDate: stringValue(item.dueDate), totalAmount: numberValue(item.totalAmount), receivedAmount: numberValue(item.receivedAmount), outstandingAmount: numberValue(item.outstandingAmount), currency: 'CNY', status: stringValue(item.status) as FinanceStatementReadModel['status'], version: numberValue(item.version) }))
|
||||
}
|
||||
|
||||
export async function listFinanceReceivableSettlements(customerId: string): Promise<FinanceReceivableSettlement[]> {
|
||||
const response = await http.get<ApiEnvelope<RawResource[]>, ApiEnvelope<RawResource[]>>(`${basePath}/receivable-settlements`, { params: { customerId } })
|
||||
return unwrapApiResponse(response).map((item) => ({ id: stringValue(item.id), customerId: stringValue(item.customerId), settlementNo: stringValue(item.settlementNo), billDate: stringValue(item.billDate), dueDate: stringValue(item.dueDate), receivableAmount: numberValue(item.receivableAmount), receivedAmount: numberValue(item.receivedAmount), outstandingAmount: numberValue(item.outstandingAmount), currency: 'CNY', status: stringValue(item.status) as FinanceReceivableSettlement['status'], version: numberValue(item.version) }))
|
||||
}
|
||||
|
||||
export async function listFinancePayableSettlements(customerId: string): Promise<FinancePayableSettlement[]> {
|
||||
const response = await http.get<ApiEnvelope<RawResource[]>, ApiEnvelope<RawResource[]>>(`${basePath}/payable-settlements`, { params: { customerId } })
|
||||
return unwrapApiResponse(response).map((item) => ({ id: stringValue(item.id), customerId: stringValue(item.customerId), settlementNo: stringValue(item.settlementNo), costType: stringValue(item.costType) as FinancePayableSettlement['costType'], orderId: item.orderId == null ? null : stringValue(item.orderId), inboundId: item.inboundId == null ? null : stringValue(item.inboundId), containerId: item.containerId == null ? null : stringValue(item.containerId), supplierName: stringValue(item.supplierName), feeItemName: stringValue(item.feeItemName), payableAmount: numberValue(item.payableAmount), paidAmount: numberValue(item.paidAmount), outstandingAmount: numberValue(item.outstandingAmount), currency: 'CNY', status: stringValue(item.status), createdAt: stringValue(item.createdAt), version: numberValue(item.version) }))
|
||||
}
|
||||
|
||||
function normalizeCustomerTerms(item: RawResource, customerId: string): FinanceCustomerTerms {
|
||||
return {
|
||||
customerId: stringValue(item.tenant_id ?? item.tenantId ?? customerId),
|
||||
customerName: stringValue(item.customer_name_snapshot ?? item.customerName),
|
||||
creditDays: numberValue(item.credit_days ?? item.creditDays ?? 30),
|
||||
status: stringValue(item.status) || 'ACTIVE',
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFinanceCustomerTerms(customerId: string) {
|
||||
const response = await http.get<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/customers/${customerId}/terms`)
|
||||
return normalizeCustomerTerms(unwrapApiResponse(response), customerId)
|
||||
}
|
||||
|
||||
export async function updateFinanceCustomerTerms(customerId: string, payload: { customerName: string; creditDays: number }) {
|
||||
const response = await http.put<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/customers/${customerId}/terms`, payload, mutationHeaders('customer-terms'))
|
||||
return normalizeCustomerTerms(unwrapApiResponse(response), customerId)
|
||||
}
|
||||
|
||||
export async function createFinanceBill(payload: Record<string, unknown>) {
|
||||
@@ -56,6 +100,21 @@ export async function getFinanceBill(billId: string) {
|
||||
return normalizeBillDetail(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function listFinanceBillRevisions(billId: string, customerId: string): Promise<FinanceBillRevision[]> {
|
||||
const response = await http.get<ApiEnvelope<RawResource[]>, ApiEnvelope<RawResource[]>>(`${basePath}/bills/${billId}/revisions`, { params: { customerId } })
|
||||
return unwrapApiResponse(response).map((item) => {
|
||||
const bill = jsonRecord(item.bill_snapshot ?? item.billSnapshot)
|
||||
const lines = jsonRecords(item.line_snapshot ?? item.lineSnapshot)
|
||||
return {
|
||||
id: stringValue(item.id), billId: stringValue(item.bill_id ?? item.billId ?? billId), version: numberValue(item.version),
|
||||
reason: stringValue(item.reason), createdBy: stringValue(item.created_by ?? item.createdBy), createdAt: stringValue(item.created_at ?? item.createdAt),
|
||||
status: stringValue(bill.status) as FinanceBillRevision['status'], totalAmount: numberValue(bill.total_amount ?? bill.totalAmount),
|
||||
receivedAmount: numberValue(bill.received_amount ?? bill.receivedAmount), outstandingAmount: numberValue(bill.outstanding_amount ?? bill.outstandingAmount),
|
||||
lines: lines.map(normalizeBillLine),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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))
|
||||
@@ -71,24 +130,6 @@ export async function voidFinanceBill(billId: string, expectedVersion: number, r
|
||||
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)
|
||||
const normalized = normalizePage<FinanceReceiptResource>(payload, (item) => ({
|
||||
id: stringValue(item.id), customerId: stringValue(item.tenant_id), customerName: `客户 ${stringValue(item.tenant_id)}`,
|
||||
receivedDate: stringValue(item.received_at).slice(0, 10), amount: numberValue(item.received_amount), currency: 'CNY',
|
||||
paymentMethod: stringValue(item.payment_method) as FinancePaymentMethod, voucherFileIds: fileIds(item.voucher_file_ids),
|
||||
allocationCount: item.bill_id == null ? 0 : 1,
|
||||
}))
|
||||
const grouped = new Map<string, FinanceReceiptResource>()
|
||||
for (const item of normalized.records) {
|
||||
const existing = grouped.get(item.id)
|
||||
if (existing) existing.allocationCount += item.allocationCount
|
||||
else grouped.set(item.id, { ...item })
|
||||
}
|
||||
return { ...normalized, records: [...grouped.values()], total: normalized.serverPaged ? normalized.total : grouped.size }
|
||||
}
|
||||
|
||||
export async function createFinanceReceipt(payload: Record<string, unknown>, voucherFileIds: string[]) {
|
||||
const response = await http.post<ApiEnvelope<FinanceReceiptResource>, ApiEnvelope<FinanceReceiptResource>>(`${basePath}/receipts`, { ...payload, currency: 'CNY', voucherFileIds }, { headers: { 'Idempotency-Key': idempotencyKey('receipt') } })
|
||||
return unwrapApiResponse(response)
|
||||
@@ -108,16 +149,6 @@ export async function uploadFinanceVoucher(customerId: string, file: File): Prom
|
||||
}
|
||||
}
|
||||
|
||||
export async function listFinanceCosts(customerId?: string) {
|
||||
const response = await http.get<ApiEnvelope<RawPage>, ApiEnvelope<RawPage>>(`${basePath}/cost-entries`, { params: { customerId: customerId || undefined } })
|
||||
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.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),
|
||||
}))
|
||||
}
|
||||
|
||||
export async function createFinanceCost(payload: Record<string, unknown>, voucherFileIds: string[]) {
|
||||
const response = await http.post<ApiEnvelope<FinanceCostResource>, ApiEnvelope<FinanceCostResource>>(`${basePath}/cost-entries`, { ...payload, currency: 'CNY', voucherFileIds }, { headers: { 'Idempotency-Key': idempotencyKey('cost') } })
|
||||
return unwrapApiResponse(response)
|
||||
|
||||
Reference in New Issue
Block a user