Files
admin-web/src/modules/overseas-business/procurementControlForm.test.ts
T

67 lines
9.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { buildControl, controlForm } from './procurementControlForm'
import { readPendingProcurement } from './procurementPending'
import type { PlatformProcurement, ProcurementControlNote, ProcurementFile } from './procurementTypes'
const base = { version: 7, idempotencyKey: 'control-one', reason: '内部处理依据' }
const proof: ProcurementFile = { id: '51', fileId: '501', category: 'EXCEPTION', fileName: 'agreement.pdf', contentType: 'application/pdf', sizeBytes: 10, createdAt: '' }
function detail(): PlatformProcurement {
return {
customerView: { order: { id: '1', orderNo: 'OP-1', customerId: '10001', customerName: '客户', purchaseCategory: '样品', productName: '外套', productSpecification: null, productUrl: null, purchaseQuantity: 10, budgetUnitPrice: '1.00', currency: 'CNY', receivingAddress: '深圳', contactName: '收件人', contactPhone: '13800000000', estimatedArrivalAt: null, plannedWarehouseName: '海外仓', actualWarehouseName: '海外仓', actualWarehouseCode: 'LAX', status: 'OVERSEAS_RECEIVING', version: 7, createdAt: '', updatedAt: '' }, budget: { totalAmount: '10.00', currency: 'CNY', authorizedAt: null }, purchased: true, purchaseConfirmed: true, purchaseNeedsAttention: false, progress: [], boxes: [], shipments: [{ shipmentNo: 'PS-1', leg: 'OVERSEAS', cartonNo: 'C1', quantity: 5, shippedAt: '2020-01-03T10:00:00', carrier: 'UPS', trackingNo: 'TRACK-1' }], fees: [], exceptionSummaries: [] },
purchaseFacts: { actual_warehouse_id: '20', purchased_quantity: 10, purchased_at: '2020-01-01T10:00:00', overseas_shipped_at: '2020-01-03T10:00:00' },
physical: { receipts: [{ id: '11', receiptNo: 'PR-11', leg: 'OVERSEAS', sourceCartonId: null, goodQuantity: 6, damagedQuantity: 1, extraQuantity: 2, receivedAt: '2020-01-02T10:00:00', active: true }, { id: '12', receiptNo: 'PR-12', leg: 'OVERSEAS', sourceCartonId: null, goodQuantity: 3, damagedQuantity: 0, extraQuantity: 0, receivedAt: '2020-01-02T12:00:00', active: true }], dispositions: [], packingVersion: 2, packingStatus: 'DRAFT', cartons: [{ id: '31', cartonNo: 'C1', quantity: 5, lengthCm: '10.00', widthCm: '20.00', heightCm: '30.00', grossWeightKg: '1.234', sources: [{ receiptId: '11', quantity: 5 }] }, { id: '32', cartonNo: 'C2', quantity: 4, lengthCm: '10.00', widthCm: '20.00', heightCm: '30.00', grossWeightKg: '1.234', sources: [{ receiptId: '11', quantity: 1 }, { receiptId: '12', quantity: 3 }] }] },
transport: { overseasCargo: [], domesticCargo: [], overseasLabels: null, domesticLabels: null, shipments: [] }, fees: [], feeCorrections: [], controls: { notes: [], remaining: [], closures: [], closureItems: [] }, files: [], reviewHistory: [], operations: [], actions: [{ key: 'EXCEPTION_CLOSE', label: '异常关闭', permission: 'platform:overseas-business:purchase:manage', allowed: true, reason: null }], internalRemark: null,
}
}
function note(): ProcurementControlNote { return { id: '71', note_no: 'PN-71', target_type: 'PURCHASE', target_id: null, blocking: true, reason: '待处理', client_summary: null, created_at: '', resolved_at: null, resolution_reason: null, voucher_file_id: '501', resolution_voucher_id: null, original_value_json: null, corrected_value_json: null } }
function closure() { const value = detail(); value.controls.remaining = [{ kind: 'PURCHASE_REMAINDER', sourceId: null, businessNo: 'OP-1', quantity: 3 }, { kind: 'GOOD', sourceId: '11', businessNo: 'PR-11', quantity: 6 }]; const form = controlForm(value); form.customerVoucherFileId = '501'; form.clientSummary = '客户同意停止寄送'; form.financialConclusion = '所有资金处理已结束'; for (const row of form.resolutions) Object.assign(row, { decision: row.kind === 'PURCHASE_REMAINDER' ? 'SUPPLIER_RETAINED' : 'CUSTOMER_AUTHORIZED_RETENTION', destination: '明确的接收方与留存地点', voucherFileId: '501' }); return { value, form } }
describe('procurement exception control requests', () => {
it('adds a purchase note without leaking the internal reason into a customer summary', () => {
const value = detail(); const form = controlForm(value); form.targetId = 'stale-receipt'
const payload = buildControl('control-notes', form, value, [proof], base, '', '501')
expect(payload).toEqual({ ...base, targetType: 'PURCHASE', blocking: true, voucherFileId: '501' })
expect(payload).not.toHaveProperty('clientSummary'); expect(payload).not.toHaveProperty('targetId')
expect(readPendingProcurement(JSON.stringify({ command: 'control-notes', payload }))).not.toBeNull()
})
it('requires actual order-owned facts and allows notes against historical receipts', () => {
const value = detail(); const form = controlForm(value); form.targetType = 'RECEIPT'; form.targetId = 'other'
expect(() => buildControl('control-notes', form, value, [proof], base, '', '501')).toThrow('本单实收')
form.targetId = '11'; value.physical.receipts[0].active = false; expect(buildControl('control-notes', form, value, [proof], base, '', '501').targetId).toBe('11')
value.transport.shipments = [{ id: '41', shipmentNo: 'PS-41', leg: 'OVERSEAS', shippedAt: '2020-01-03T10:00:00' }]; form.targetType = 'SHIPMENT'; form.targetId = '41'
expect(buildControl('control-notes', form, value, [proof], base, '', '501').targetId).toBe('41')
form.targetId = '42'; expect(() => buildControl('control-notes', form, value, [proof], base, '', '501')).toThrow('本单实际交寄')
})
it('only resolves a still-active blocker with order evidence', () => {
const value = detail(); value.controls.notes = [note()]; const form = controlForm(value); form.noteId = '71'
expect(buildControl('control-notes/resolve', form, value, [proof], base, '', '501')).toEqual({ ...base, noteId: '71', voucherFileId: '501' })
value.controls.notes[0].resolved_at = '2020-01-03T10:00:00'; expect(() => buildControl('control-notes/resolve', form, value, [proof], base, '', '501')).toThrow('尚未处理')
})
it('requires a distinct actual warehouse and sends no direct inventory mutation', () => {
const value = detail(); const form = controlForm(value)
expect(() => buildControl('actual-warehouse/correct', form, value, [proof], base, '20', '501')).toThrow('不同')
expect(buildControl('actual-warehouse/correct', form, value, [proof], base, '30', '501')).toEqual({ ...base, warehouseId: '30', voucherFileId: '501' })
})
it('closes with exact remaining sources, explicit destinations and separate private/public conclusions', () => {
const { value, form } = closure(); const payload = buildControl('exception-close', form, value, [proof], base, '', '')
expect(payload.resolutions?.map(row => [row.kind, row.sourceId, row.quantity])).toEqual([['PURCHASE_REMAINDER', null, 3], ['GOOD', '11', 6]])
expect(payload).toMatchObject({ customerVoucherFileId: '501', financialConclusion: '所有资金处理已结束', clientSummary: '客户同意停止寄送' })
expect(payload).not.toHaveProperty('amount'); expect(payload).not.toHaveProperty('status')
expect(readPendingProcurement(JSON.stringify({ command: 'exception-close', payload }))).toEqual({ command: 'exception-close', payload })
})
it('rejects omitted, duplicated or changed remaining quantities instead of forcing closure', () => {
let { value, form } = closure(); form.resolutions.pop(); expect(() => buildControl('exception-close', form, value, [proof], base, '', '')).toThrow('去向未明确')
;({ value, form } = closure()); form.resolutions.push({ ...form.resolutions[0] }); expect(() => buildControl('exception-close', form, value, [proof], base, '', '')).toThrow('不可重复')
;({ value, form } = closure()); form.resolutions[0].quantity = 2; expect(() => buildControl('exception-close', form, value, [proof], base, '', '')).toThrow('改数量')
})
it('rejects wrong source dispositions, missing evidence and financial blockers', () => {
let { value, form } = closure(); form.resolutions[0].decision = 'DISPOSED'; expect(() => buildControl('exception-close', form, value, [proof], base, '', '')).toThrow('来源不匹配')
;({ value, form } = closure()); form.customerVoucherFileId = ''; expect(() => buildControl('exception-close', form, value, [proof], base, '', '')).toThrow('客户确认凭证')
;({ value, form } = closure()); value.actions[0].allowed = false; value.actions[0].reason = '未决资金请求'; expect(() => buildControl('exception-close', form, value, [proof], base, '', '')).toThrow('未决资金请求')
;({ value, form } = closure()); value.controls.notes = [note()]; expect(() => buildControl('exception-close', form, value, [proof], base, '', '')).toThrow('未解除')
})
it('allows an empty remaining list only with the required customer and financial conclusions', () => {
const value = detail(); const form = controlForm(value); form.customerVoucherFileId = '501'; form.financialConclusion = '无未决费用或冻结'; form.clientSummary = '履约已停止'
expect(buildControl('exception-close', form, value, [proof], base, '', '').resolutions).toEqual([])
form.financialConclusion = ''; expect(() => buildControl('exception-close', form, value, [proof], base, '', '')).toThrow('财务处理结论')
})
})