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

72 lines
9.8 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { buildTransport, transportForm } from './procurementTransportForm'
import { readPendingProcurement } from './procurementPending'
import type { PlatformProcurement, ProcurementFile, ProcurementLabelSet } from './procurementTypes'
const base = { version: 7, idempotencyKey: 'transport-one', reason: '逐箱核对' }
const files: ProcurementFile[] = [{ id: '51', fileId: '501', category: 'LABEL', fileName: 'labels.pdf', contentType: 'application/pdf', sizeBytes: 10, createdAt: '' }, { id: '52', fileId: '502', category: 'EXCEPTION', fileName: 'customer.pdf', contentType: 'application/pdf', sizeBytes: 10, createdAt: '' }, { id: '53', fileId: '503', category: 'RECEIPT', fileName: 'repack.png', contentType: 'image/png', sizeBytes: 10, createdAt: '' }]
function labels(): ProcurementLabelSet { return { id: '71', version: 2, manifestId: '61', cargoHash: 'cargo', sender: { name: '海外发件人', phone: '13800000000', address: 'US warehouse' }, receiver: { name: '国内收货人', phone: '13800000001', address: '上海接收地址' }, labels: [{ cartonId: '31', fileId: '501', page: 1, carrier: 'carrier', trackingNo: 'TRACK-1', appliedAt: '2020-01-02T12:00:00' }, { cartonId: '32', fileId: '501', page: 2, carrier: 'carrier', trackingNo: 'TRACK-2', appliedAt: '2020-01-02T13:00:00' }] } }
function detail(): PlatformProcurement {
const cargo = [{ cartonId: '31', cartonNo: 'BOX-1', quantity: 6, lengthCm: '10.00', widthCm: '20.00', heightCm: '30.00', grossWeightKg: '1.234', packagingRecordId: null, sources: [{ receiptId: '11', quantity: 6 }] }, { cartonId: '32', cartonNo: 'BOX-2', quantity: 4, lengthCm: '10.00', widthCm: '20.00', heightCm: '30.00', grossWeightKg: '1.111', packagingRecordId: null, sources: [{ receiptId: '12', quantity: 4 }] }]
return { customerView: { order: { id: '1', orderNo: 'OP-1', customerId: '9', customerName: '客户', purchaseCategory: '样品', productName: '外套', productSpecification: null, productUrl: null, purchaseQuantity: 10, budgetUnitPrice: '1.00', currency: 'CNY', receivingAddress: '深圳客户地址', contactName: '客户收件人', contactPhone: '13800000002', estimatedArrivalAt: null, plannedWarehouseName: '海外仓', actualWarehouseName: '海外仓', actualWarehouseCode: 'LAX', status: 'WAIT_OVERSEAS_SHIPMENT', version: 7, createdAt: '', updatedAt: '' }, budget: { totalAmount: '10.00', currency: 'CNY', authorizedAt: null }, purchased: true, purchaseConfirmed: true, purchaseNeedsAttention: false, progress: [], boxes: [], shipments: [], fees: [], exceptionSummaries: [] }, purchaseFacts: { overseas_received_at: '2020-01-02T10:00:00', domestic_received_at: '2020-01-04T10:00:00' }, physical: { receipts: [], dispositions: [], packingVersion: 3, packingStatus: 'CONFIRMED', cartons: [] }, transport: { overseasCargo: cargo, domesticCargo: [{ ...cargo[0], quantity: 5, lengthCm: '15.00', grossWeightKg: '1.987', packagingRecordId: '81', sources: [{ receiptId: '21', quantity: 5 }] }], overseasLabels: labels(), domesticLabels: null, shipments: [] }, fees: [], feeCorrections: [], controls: { notes: [], remaining: [], closures: [], closureItems: [] }, files: [...files], reviewHistory: [], operations: [], actions: [{ key: 'SHIP_OVERSEAS', label: '海外交寄', permission: 'ship-overseas', allowed: true, reason: null }, { key: 'SHIP_CUSTOMER', label: '寄客户', permission: 'ship-customer', allowed: true, reason: null }], internalRemark: null }
}
describe('procurement transport form rules', () => {
it('binds the full batch to files, pages and the exact packing version', () => {
const value = detail(); const form = transportForm(value)
const payload = buildTransport('labels', form, value, files, base, '')
expect(payload).toMatchObject({ leg: 'OVERSEAS', packingVersion: 3, labels: [{ cartonId: '31', fileId: '501', page: 1 }, { cartonId: '32', fileId: '501', page: 2 }] })
expect(payload).not.toHaveProperty('sender')
expect(readPendingProcurement(JSON.stringify({ command: 'labels', payload }))).toEqual({ command: 'labels', payload })
form.labels[0].trackingNo = 'changed-after-submit'; expect(payload.labels?.[0].trackingNo).toBe('TRACK-1')
})
it('rejects missing or duplicate cartons, wrong-purpose files and invalid image pages', () => {
const value = detail(); let form = transportForm(value); form.labels.pop(); expect(() => buildTransport('labels', form, value, files, base, '')).toThrow('完整覆盖')
form = transportForm(value); form.labels[1].cartonId = '31'; expect(() => buildTransport('labels', form, value, files, base, '')).toThrow('完整覆盖')
form = transportForm(value); form.labels[0].fileId = '502'; expect(() => buildTransport('labels', form, value, files, base, '')).toThrow('每箱选择本单')
form = transportForm(value); expect(() => buildTransport('labels', form, value, [{ ...files[0], contentType: 'image/png' }], base, '')).toThrow('第 1 页')
})
it('uses domestic good quantities and replacement measurements, requiring proof for recipient changes', () => {
const value = detail(); const form = transportForm(value, 'DOMESTIC'); form.labels[0] = { cartonId: '31', fileId: '501', page: 1, carrier: 'SF', trackingNo: 'SF-1' }
expect(form.receiver).toEqual({ name: '客户收件人', phone: '13800000002', address: '深圳客户地址' })
expect(value.transport.domesticCargo[0]).toMatchObject({ quantity: 5, lengthCm: '15.00', grossWeightKg: '1.987' })
expect(buildTransport('labels', form, value, files, base, '')).not.toHaveProperty('addressVoucherFileId')
form.receiver.address = '客户确认的新地址'; expect(() => buildTransport('labels', form, value, files, base, '')).toThrow('客户确认凭证')
form.addressVoucherFileId = '502'; expect(buildTransport('labels', form, value, files, base, '').addressVoucherFileId).toBe('502')
})
it('requires confirmed packing, domestic receipt completion and an unshipped leg for label changes', () => {
const value = detail(); const form = transportForm(value); value.physical.packingStatus = 'DRAFT'; expect(() => buildTransport('labels', form, value, files, base, '')).toThrow('确认装箱')
value.physical.packingStatus = 'CONFIRMED'; value.purchaseFacts.overseas_shipped_at = '2020-01-03T10:00:00'; expect(() => buildTransport('labels', form, value, files, base, '')).toThrow('本段已交寄')
value.purchaseFacts.domestic_received_at = null; expect(() => buildTransport('labels', transportForm(value, 'DOMESTIC'), value, files, base, '')).toThrow('国内收货完成')
})
it('makes zero estimate explicit without sending actual-charge fields', () => {
const value = detail(); const form = transportForm(value); form.businessDate = '2020-01-02'
expect(() => buildTransport('international-estimate', form, value, files, base, '')).toThrow('明确填写')
form.amount = '0.00'; const payload = buildTransport('international-estimate', form, value, files, base, '')
expect(payload).toEqual({ ...base, amount: '0.00', businessDate: '2020-01-02', labelVersion: 2 })
expect(readPendingProcurement(JSON.stringify({ command: 'international-estimate', payload }))).not.toBeNull()
})
it('allows partial application only for current unapplied labels with a real time', () => {
const value = detail(); value.transport.overseasLabels!.labels[0].appliedAt = null; const form = transportForm(value); form.cartonIds = ['31']; form.occurredAt = '2020-01-02T11:00:00'
expect(buildTransport('labels/apply', form, value, files, base, '')).toEqual({ ...base, labelVersion: 2, cartonIds: ['31'], appliedAt: '2020-01-02T11:00:00' })
form.cartonIds = ['32']; expect(() => buildTransport('labels/apply', form, value, files, base, '')).toThrow('尚未确认贴单')
form.cartonIds = ['31']; form.occurredAt = '2020-01-01T10:00:00'; expect(() => buildTransport('labels/apply', form, value, files, base, '')).toThrow('早于上一节点')
})
it('ships the entire authoritative batch and checks label time and server readiness', () => {
const value = detail(); const form = transportForm(value); form.cartonIds = ['31']; form.occurredAt = '2020-01-03T10:00:00'
const payload = buildTransport('overseas-shipment', form, value, files, base, '')
expect(payload).toEqual({ ...base, leg: 'OVERSEAS', labelVersion: 2, cartonIds: ['31', '32'], shippedAt: '2020-01-03T10:00:00' })
expect(readPendingProcurement(JSON.stringify({ command: 'overseas-shipment', payload }))).not.toBeNull()
form.occurredAt = '2020-01-02T12:30:00'; expect(() => buildTransport('overseas-shipment', form, value, files, base, '')).toThrow('早于上一节点')
value.actions[0].allowed = false; value.actions[0].reason = '预估冻结未完成'; expect(() => buildTransport('overseas-shipment', form, value, files, base, '')).toThrow('预估冻结未完成')
})
it('keeps repacking one-to-one and never changes quantity or the original source', () => {
const value = detail(); const form = transportForm(value, 'DOMESTIC'); Object.assign(form, { cartonId: '31', lengthCm: '16.00', widthCm: '20.00', heightCm: '30.00', grossWeightKg: '2.123' })
const payload = buildTransport('domestic-repack', form, value, files, base, '503')
expect(payload).toEqual({ ...base, cartonId: '31', lengthCm: '16.00', widthCm: '20.00', heightCm: '30.00', grossWeightKg: '2.123', voucherFileId: '503' })
expect(readPendingProcurement(JSON.stringify({ command: 'domestic-repack', payload }))).not.toBeNull()
form.cartonId = 'other'; expect(() => buildTransport('domestic-repack', form, value, files, base, '503')).toThrow('可寄原箱')
})
it('rejects corrupted recovered shipment identity before any request', () => {
expect(() => readPendingProcurement(JSON.stringify({ command: 'customer-shipment', payload: { ...base, labelVersion: 1, cartonIds: ['31', '31'], leg: 'DOMESTIC', shippedAt: '2020-01-03T10:00:00' } }))).toThrow('transport cartons')
})
})