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('财务处理结论') }) })