优化
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/shared/api/http'
|
||||
import { createFinanceCost, createFinanceReceipt, issueFinanceBill, listFinanceBills, listFinanceCosts, listFinanceReceipts } from './finance'
|
||||
|
||||
describe('finance api', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('uses the OpenAPI bill path and pageSize parameter', 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 }],
|
||||
})
|
||||
|
||||
const result = await listFinanceBills({ customerId: '10001', status: 'DRAFT', page: 1, pageSize: 20 })
|
||||
|
||||
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 })
|
||||
})
|
||||
|
||||
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: [] })
|
||||
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('issues a bill with optimistic version and an idempotency key', async () => {
|
||||
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: {} })
|
||||
|
||||
await issueFinanceBill('81001', 3)
|
||||
|
||||
expect(post).toHaveBeenCalledWith('/platform/finance/v1/bills/81001/issue', { expectedVersion: 3 }, { headers: { 'Idempotency-Key': expect.any(String) } })
|
||||
})
|
||||
})
|
||||
@@ -1,46 +1,80 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type { FinanceBillResource, FinanceCostResource, FinanceReceiptResource, FinanceReportResource } from '../types'
|
||||
import type { FinanceBillResource, FinanceCostResource, FinancePaymentMethod, FinanceReceiptResource } from '../types'
|
||||
|
||||
const basePath = '/platform/v1/finance'
|
||||
interface PageResult<T> { records: T[]; total: number; page: number; size: number }
|
||||
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 }
|
||||
|
||||
export async function listFinanceBills(params: Record<string, string | number | undefined>) {
|
||||
const response = await http.get<ApiEnvelope<PageResult<FinanceBillResource>>, ApiEnvelope<PageResult<FinanceBillResource>>>(`${basePath}/bills`, { params: { ...params, currency: 'CNY' } })
|
||||
return unwrapApiResponse(response)
|
||||
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 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) }
|
||||
}
|
||||
|
||||
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}`,
|
||||
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),
|
||||
}))
|
||||
}
|
||||
|
||||
export async function createFinanceBill(payload: Record<string, unknown>) {
|
||||
const response = await http.post<ApiEnvelope<FinanceBillResource>, ApiEnvelope<FinanceBillResource>>(`${basePath}/bills`, { ...payload, currency: 'CNY' })
|
||||
const response = await http.post<ApiEnvelope<FinanceBillResource>, ApiEnvelope<FinanceBillResource>>(`${basePath}/bills`, { ...payload }, { headers: { 'Idempotency-Key': idempotencyKey('bill') } })
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function listFinanceReceipts(params: Record<string, string | number | undefined>) {
|
||||
const response = await http.get<ApiEnvelope<PageResult<FinanceReceiptResource>>, ApiEnvelope<PageResult<FinanceReceiptResource>>>(`${basePath}/receipts`, { params: { ...params, currency: 'CNY' } })
|
||||
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)
|
||||
}
|
||||
|
||||
export async function createFinanceReceipt(payload: Record<string, unknown>, voucher?: File) {
|
||||
const body = new FormData()
|
||||
body.append('payload', JSON.stringify({ ...payload, currency: 'CNY' }))
|
||||
if (voucher) body.append('file', voucher)
|
||||
const response = await http.post<ApiEnvelope<FinanceReceiptResource>, ApiEnvelope<FinanceReceiptResource>>(`${basePath}/receipts`, body)
|
||||
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.expense_name), amount: numberValue(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)
|
||||
}
|
||||
|
||||
export async function listFinanceCosts(params: Record<string, string | number | undefined>) {
|
||||
const response = await http.get<ApiEnvelope<PageResult<FinanceCostResource>>, ApiEnvelope<PageResult<FinanceCostResource>>>(`${basePath}/costs`, { params: { ...params, currency: 'CNY' } })
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function createFinanceCost(payload: Record<string, unknown>, voucher?: File) {
|
||||
const body = new FormData()
|
||||
body.append('payload', JSON.stringify({ ...payload, currency: 'CNY' }))
|
||||
if (voucher) body.append('file', voucher)
|
||||
const response = await http.post<ApiEnvelope<FinanceCostResource>, ApiEnvelope<FinanceCostResource>>(`${basePath}/costs`, body)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function listFinanceReports(params: Record<string, string | number | undefined>) {
|
||||
const response = await http.get<ApiEnvelope<FinanceReportResource[]>, ApiEnvelope<FinanceReportResource[]>>(`${basePath}/reports`, { params: { ...params, currency: 'CNY' } })
|
||||
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') } })
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user