276 lines
31 KiB
TypeScript
276 lines
31 KiB
TypeScript
import ElementPlus from 'element-plus'
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
import { mount, flushPromises, enableAutoUnmount } from '@vue/test-utils'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { useAuthStore } from '@/core/auth/auth'
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
|
import { ApiResponseError } from '@/shared/api/http'
|
|
import AppDialog from '@/shared/components/AppDialog.vue'
|
|
import ProcurementNodeActions from './ProcurementNodeActions.vue'
|
|
import { listOverseasPurchaseWarehouseOptions } from '../api/platformOverseasBusiness'
|
|
import { submitProcurementCommand, uploadProcurementFile } from '../api/procurementCommands'
|
|
import { readPendingProcurement } from '../procurementPending'
|
|
import type { PlatformProcurement } from '../procurementTypes'
|
|
vi.mock('../api/platformOverseasBusiness', () => ({ listOverseasPurchaseWarehouseOptions: vi.fn() }))
|
|
vi.mock('../api/procurementCommands', () => ({ submitProcurementCommand: vi.fn(), uploadProcurementFile: vi.fn() }))
|
|
vi.mock('@/core/unsaved-work/useUnsavedWorkRegistration', () => ({ useUnsavedWorkRegistration: () => ({ captureBaseline: vi.fn() }) }))
|
|
const storageKey = 'procurement-command:7:10001:OP-100'
|
|
function storage(): Storage { const values = new Map<string, string>(); return { get length() { return values.size }, clear: () => values.clear(), getItem: key => values.get(key) ?? null, key: index => [...values.keys()][index] ?? null, removeItem: key => values.delete(key), setItem: (key, value) => values.set(key, value) } }
|
|
function fixture(key = 'REVIEW', allowed = true): PlatformProcurement {
|
|
const keys: Record<string, [string, string]> = { REVIEW: ['审核采购', 'review'], PURCHASE: ['登记购买完成', 'buy'], AUTHORIZE_BUDGET: ['核验预算上限', 'review'], PLAN_WAREHOUSE: ['修改计划仓', 'review'], CANCEL: ['购买前取消', 'cancel'], RECEIVE_OVERSEAS: ['登记海外实收', 'receive-overseas'], FINISH_DOMESTIC: ['确认国内收货完成', 'receive-domestic'], PACK: ['保存装箱草稿', 'pack'], MANAGE: ['事实更正及异常处置', 'manage'], LABELS: ['维护两段面单', 'label'], ESTIMATE: ['核对预估运费', 'estimate'], SHIP_OVERSEAS: ['确认海外交寄', 'ship-overseas'], CONFIRM_CHARGES: ['确认国内费用', 'charge'], FINANCE_ADJUST: ['费用更正与退款', 'finance-adjust'], EXCEPTION_CLOSE: ['异常关闭', 'manage'] }
|
|
const [label, suffix] = keys[key]
|
|
return {
|
|
customerView: { order: { id: '1', orderNo: 'OP-100', customerId: '10001', customerName: '测试客户', purchaseCategory: '家居', productName: '样品商品', productSpecification: '黑色', productUrl: null, purchaseQuantity: 2, budgetUnitPrice: '10.00', currency: 'USD', receivingAddress: '深圳', contactName: '收件人', contactPhone: '13800000000', estimatedArrivalAt: null, plannedWarehouseName: null, actualWarehouseCode: null, actualWarehouseName: null, status: 'PENDING_REVIEW', version: 3, createdAt: '', updatedAt: '' }, budget: { totalAmount: '20.00', currency: 'USD', authorizedAt: null }, purchased: false, purchaseConfirmed: false, purchaseNeedsAttention: false, progress: [], boxes: [], shipments: [], fees: [], exceptionSummaries: [] },
|
|
purchaseFacts: {}, physical: { dispositions: [], receipts: [], cartons: [], packingVersion: null, packingStatus: null }, transport: { overseasCargo: [], domesticCargo: [], overseasLabels: null, domesticLabels: null, shipments: [] }, fees: [], feeCorrections: [], controls: { notes: [], remaining: [], closures: [], closureItems: [] }, files: [], reviewHistory: [], operations: [], actions: [{ key, label, permission: `platform:overseas-business:purchase:${suffix}`, allowed, reason: allowed ? null : '业务条件不满足' }], internalRemark: null,
|
|
}
|
|
}
|
|
function render(detail = fixture(), permissions = ['review', 'buy', 'cancel', 'receive-overseas', 'receive-domestic', 'pack', 'manage', 'label', 'estimate', 'ship-overseas', 'charge', 'finance-adjust']) {
|
|
const pinia = createPinia(); setActivePinia(pinia)
|
|
const auth = useAuthStore(); auth.user = { id: '7', name: '测试操作员', account: 'tester', roles: [] }; auth.token = 'test-token'; auth.permissions = permissions.map(item => `platform:overseas-business:purchase:${item}`); useAccessPreviewStore().initializeAfterLogin()
|
|
return mount(ProcurementNodeActions, { attachTo: document.body, props: { detail, customerId: '10001' }, global: { plugins: [pinia, ElementPlus] } })
|
|
}
|
|
async function open(wrapper: ReturnType<typeof render>) { await wrapper.find('.purchase-actions button').trigger('click'); await flushPromises(); return wrapper.findComponent(AppDialog) }
|
|
async function explain(wrapper: ReturnType<typeof render>, value = '已核对客户资料') { await wrapper.findComponent(AppDialog).findAllComponents({ name: 'ElFormItem' }).find(row => row.props('label') === '操作说明')!.find('textarea').setValue(value) }
|
|
async function confirm(wrapper: ReturnType<typeof render>) { wrapper.findComponent(AppDialog).vm.$emit('confirm'); await flushPromises() }
|
|
enableAutoUnmount(afterEach)
|
|
afterEach(() => vi.unstubAllGlobals())
|
|
beforeEach(() => {
|
|
vi.stubGlobal('sessionStorage', storage()); vi.stubGlobal('localStorage', storage()); document.body.innerHTML = ''
|
|
vi.mocked(listOverseasPurchaseWarehouseOptions).mockReset().mockResolvedValue([{ warehouseId: '20001', warehouseCode: 'LAX', warehouseName: '海外收货仓', countryCode: 'US' }])
|
|
vi.mocked(submitProcurementCommand).mockReset().mockResolvedValue(undefined)
|
|
vi.mocked(uploadProcurementFile).mockReset().mockResolvedValue({ id: '11', fileId: '101', category: 'PURCHASE', fileName: 'purchase.pdf', contentType: 'application/pdf', sizeBytes: 3, createdAt: '' })
|
|
})
|
|
describe('procurement purchase node operations', () => {
|
|
it('saves and restores a separate correction while the immutable original request is pending, scoped to operator/customer/order', async () => {
|
|
const value = fixture('FINANCE_ADJUST', false)
|
|
value.fees = [{ id: '11', feeNo: 'PF-11', purpose: 'SAMPLE_PAYMENT', confirmationOrigin: 'BUSINESS', feeItemCode: 'SAMPLE_PAYMENT', feeItemName: '样品货款', feeVersion: 1, active: true, determination: 'POSITIVE', amount: '70.00', currency: 'CNY', originalAmount: '10.00', originalCurrency: 'USD', exchangeRate: '7', exchangeRateMonth: '2020-01', businessDate: '2020-01-01', parentFeeId: null, sourceFeeId: null, voucherFileId: '501', voucherFileName: 'invoice.pdf', walletStatus: 'PENDING', walletLedgerId: null, walletErrorCode: null, walletErrorMessage: null, remark: null }]
|
|
const original = JSON.stringify({ command: 'fees/correct-target', payload: { version: 3, idempotencyKey: 'original-request', feeId: '11', targetOriginalAmount: '11.00', reason: '原请求', voucherFileId: '501' } })
|
|
sessionStorage.setItem(storageKey, original)
|
|
let wrapper = render(value)
|
|
const click = async (label: string) => { await wrapper.findAll('button').find(button => button.text() === label)!.trigger('click'); await flushPromises() }
|
|
await click('准备费用更正草稿')
|
|
const dialog = wrapper.findComponent(AppDialog)
|
|
dialog.findAllComponents({ name: 'ElFormItem' }).find(row => row.props('label') === '原费用')!.findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', '11')
|
|
await dialog.find('input[placeholder="填写原费用链正确总额,最多两位小数"]').setValue('12.00')
|
|
await explain(wrapper, '准备下一版本')
|
|
await click('保存本地草稿')
|
|
expect(submitProcurementCommand).not.toHaveBeenCalled()
|
|
expect(sessionStorage.getItem(storageKey)).toBe(original)
|
|
expect(JSON.parse(sessionStorage.getItem(`${storageKey}:correction-draft`)!)).toMatchObject({ targetOriginalAmount: '12.00', feeId: '11', reason: '准备下一版本' })
|
|
expect(wrapper.findAll('button').find(button => button.text() === '核对并继续确认')!.attributes('disabled')).toBeDefined()
|
|
wrapper.unmount(); wrapper = render(value)
|
|
await click('恢复费用更正草稿')
|
|
expect((wrapper.findComponent(AppDialog).find('input[placeholder="填写原费用链正确总额,最多两位小数"]').element as HTMLInputElement).value).toBe('12.00')
|
|
await click('确认上次提交结果')
|
|
expect(submitProcurementCommand).toHaveBeenCalledWith('OP-100', '10001', 'fees/correct-target', JSON.parse(original).payload)
|
|
expect(sessionStorage.getItem(`${storageKey}:correction-draft`)).not.toBeNull()
|
|
const latest = structuredClone(value)
|
|
latest.actions[0].allowed = true; latest.customerView.order.version = 9
|
|
latest.files = [{ id: '51', fileId: '501', category: 'VOUCHER', fileName: 'invoice.pdf', contentType: 'application/pdf', sizeBytes: 3, createdAt: '' }]
|
|
await wrapper.setProps({ detail: latest })
|
|
await click('恢复费用更正草稿'); await click('核对并继续确认')
|
|
wrapper.findComponent(AppDialog).findAllComponents({ name: 'ElFormItem' }).find(row => row.props('label') === '本单凭证')!.findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', '501')
|
|
await confirm(wrapper)
|
|
expect(submitProcurementCommand).toHaveBeenCalledTimes(1)
|
|
expect(wrapper.findComponent(AppDialog).text()).toContain('原实际费用及来源关联尚未完成')
|
|
latest.fees[0].walletStatus = 'SETTLED'; latest.fees[0].walletLedgerId = 'LEDGER-1'
|
|
latest.customerView.fees = [{ feeNo: 'PF-11', purpose: 'SAMPLE_PAYMENT', name: '样品货款', version: 1, active: true, determination: 'POSITIVE', amount: '70.00', currency: 'CNY', originalAmount: '10.00', originalCurrency: 'USD', exchangeRate: '7', exchangeRateMonth: '2020-01', businessDate: '2020-01-01', parentFeeNo: null, walletStatus: 'SETTLED', sourceLinked: true }]
|
|
await wrapper.setProps({ detail: structuredClone(latest) })
|
|
await confirm(wrapper)
|
|
expect(submitProcurementCommand).toHaveBeenLastCalledWith('OP-100', '10001', 'fees/correct-target', expect.objectContaining({ version: 9, targetOriginalAmount: '12.00', feeId: '11' }))
|
|
expect(sessionStorage.getItem(`${storageKey}:correction-draft`)).toBeNull()
|
|
// Restore a saved draft before changing the scope to ensure isolation is meaningful.
|
|
sessionStorage.setItem(`${storageKey}:correction-draft`, JSON.stringify({ feeId: '11', targetOriginalAmount: '13.00', reason: '下一目标', voucherFileId: '501', orderVersion: 9 }))
|
|
await wrapper.setProps({ customerId: 'OTHER' }); expect(wrapper.text()).not.toContain('恢复费用更正草稿')
|
|
await wrapper.setProps({ customerId: '10001', detail: { ...value, customerView: { ...value.customerView, order: { ...value.customerView.order, orderNo: 'OTHER' } } } }); expect(wrapper.text()).not.toContain('恢复费用更正草稿')
|
|
useAuthStore().user = { id: '8', name: '另一操作员', account: 'other', roles: [] }
|
|
await wrapper.setProps({ detail: value }); expect(wrapper.text()).not.toContain('恢复费用更正草稿')
|
|
})
|
|
it('requires explicit remaining-goods outcomes and keeps private closure reasons separate', async () => {
|
|
const value = fixture('EXCEPTION_CLOSE'); value.purchaseFacts.purchased_at = '2020-01-01T10:00:00'
|
|
value.controls.remaining = [{ kind: 'PURCHASE_REMAINDER', sourceId: null, businessNo: 'OP-100', quantity: 2 }]
|
|
value.files = [{ id: '51', fileId: '501', category: 'EXCEPTION', fileName: 'agreement.pdf', contentType: 'application/pdf', sizeBytes: 3, createdAt: '' }]
|
|
const wrapper = render(value); const dialog = await open(wrapper); await explain(wrapper, '内部客户沟通及供应商处理依据')
|
|
await confirm(wrapper); expect(submitProcurementCommand).not.toHaveBeenCalled()
|
|
const field = (label: string) => dialog.findAllComponents({ name: 'ElFormItem' }).find(row => row.props('label') === label)!
|
|
field('实际处置').findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', 'SUPPLIER_RETAINED')
|
|
field('本项处置凭证').findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', '501')
|
|
await field('实际去向及接收方').find('input').setValue('供应商原仓留存')
|
|
field('客户同意停止履约凭证').findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', '501')
|
|
await field('财务处理结论(仅平台可见)').find('textarea').setValue('全部退款及冻结释放已处理')
|
|
await field('对客关闭摘要(客户可见)').find('textarea').setValue('货物由供应商保留,停止后续寄送')
|
|
await confirm(wrapper)
|
|
expect(submitProcurementCommand).toHaveBeenCalledWith('OP-100', '10001', 'exception-close', expect.objectContaining({ resolutions: [{ kind: 'PURCHASE_REMAINDER', sourceId: null, quantity: 2, decision: 'SUPPLIER_RETAINED', destination: '供应商原仓留存', voucherFileId: '501' }], customerVoucherFileId: '501', financialConclusion: '全部退款及冻结释放已处理', clientSummary: '货物由供应商保留,停止后续寄送' }))
|
|
expect(dialog.findAllComponents({ name: 'ElInputNumber' })).toHaveLength(0)
|
|
})
|
|
it('confirms three explicit zero determinations and marks other charges not applicable in one request', async () => {
|
|
const value = fixture('CONFIRM_CHARGES'); value.purchaseFacts.domestic_received_at = '2020-01-04T10:00:00'
|
|
const wrapper = render(value); const dialog = await open(wrapper); await explain(wrapper, '整单费用核对完成')
|
|
await confirm(wrapper); expect(submitProcurementCommand).not.toHaveBeenCalled()
|
|
const groups = dialog.findAllComponents({ name: 'ProcurementChargeFields' }); expect(groups).toHaveLength(3)
|
|
for (const group of groups) {
|
|
const determination = group.findAllComponents({ name: 'ElSelect' })[0]; determination.vm.$emit('update:modelValue', 'ZERO'); determination.vm.$emit('change', 'ZERO')
|
|
group.findComponent({ name: 'ElDatePicker' }).vm.$emit('update:modelValue', '2020-01-04')
|
|
await group.find('input[placeholder="说明计费依据;零元或不适用也需说明"]').setValue('本项费用免收')
|
|
}
|
|
await confirm(wrapper)
|
|
expect(submitProcurementCommand).toHaveBeenCalledTimes(1)
|
|
const [number, customer, command, payload] = vi.mocked(submitProcurementCommand).mock.calls[0]
|
|
expect([number, customer, command]).toEqual(['OP-100', '10001', 'charges/confirm'])
|
|
for (const selection of [payload.internationalFreight, payload.duty, payload.domesticFreight]) expect(selection).toEqual({ value: { determination: 'ZERO', amount: '0.00', currency: 'CNY', businessDate: '2020-01-04', reason: '本项费用免收' } })
|
|
expect(payload.otherDetermination).toBe('NOT_APPLICABLE'); expect(payload.otherFees).toEqual([])
|
|
})
|
|
it('lets finance replace a terminated unpaid sample without modifying its original purchase fact', async () => {
|
|
const value = fixture('FINANCE_ADJUST')
|
|
value.fees = [{ id: '11', feeNo: 'PF-11', purpose: 'SAMPLE_PAYMENT', confirmationOrigin: 'BUSINESS', feeItemCode: 'SAMPLE_PAYMENT', feeItemName: '样品货款', feeVersion: 1, active: false, determination: 'POSITIVE', amount: '70.00', currency: 'CNY', originalAmount: '10.00', originalCurrency: 'USD', exchangeRate: '7', exchangeRateMonth: '2020-01', businessDate: '2020-01-01', parentFeeId: null, sourceFeeId: null, voucherFileId: '501', voucherFileName: 'invoice.pdf', walletStatus: 'TERMINATED', walletLedgerId: null, walletErrorCode: null, walletErrorMessage: null, remark: null }]
|
|
value.files = [{ id: '51', fileId: '501', category: 'VOUCHER', fileName: 'invoice.pdf', contentType: 'application/pdf', sizeBytes: 3, createdAt: '' }]
|
|
const wrapper = render(value, ['finance-adjust']); const dialog = await open(wrapper)
|
|
const field = (label: string) => dialog.findAllComponents({ name: 'ElFormItem' }).find(row => row.props('label') === label)!
|
|
field('财务处理方式').findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', 'fees/replace-failed'); await flushPromises()
|
|
field('原费用').findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', '11'); await flushPromises()
|
|
await dialog.find('input[placeholder="填写原费用链正确总额,最多两位小数"]').setValue('12.00')
|
|
field('本单凭证').findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', '501'); await explain(wrapper, '原资金已终止,按真实凭证重新核定')
|
|
await confirm(wrapper)
|
|
expect(submitProcurementCommand).toHaveBeenCalledWith('OP-100', '10001', 'fees/replace-failed', expect.objectContaining({ feeId: '11', targetOriginalAmount: '12.00', voucherFileId: '501' }))
|
|
expect(value.fees[0].originalAmount).toBe('10.00'); expect(value.fees[0].active).toBe(false)
|
|
})
|
|
it('lets a buyer retry a failed sample payment without granting ordinary charge or finance-adjust access', async () => {
|
|
const value = fixture('PURCHASE')
|
|
value.fees = [{ id: '11', feeNo: 'PF-11', purpose: 'SAMPLE_PAYMENT', confirmationOrigin: 'BUSINESS', feeItemCode: 'SAMPLE_PAYMENT', feeItemName: '样品货款', feeVersion: 1, active: true, determination: 'POSITIVE', amount: '70.00', currency: 'CNY', originalAmount: '10.00', originalCurrency: 'USD', exchangeRate: '7', exchangeRateMonth: '2020-01', businessDate: '2020-01-01', parentFeeId: null, sourceFeeId: null, voucherFileId: '501', voucherFileName: 'invoice.pdf', walletStatus: 'FREEZE_FAILED', walletLedgerId: null, walletErrorCode: null, walletErrorMessage: null, remark: null }]
|
|
const wrapper = render(value, ['buy']); await wrapper.findAll('button').find(button => button.text() === '重试失败资金请求')!.trigger('click'); await flushPromises(); await explain(wrapper, '客户已补足余额,重试原资金请求')
|
|
wrapper.findComponent(AppDialog).findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', '11'); await confirm(wrapper)
|
|
expect(submitProcurementCommand).toHaveBeenCalledWith('OP-100', '10001', 'fees/retry', expect.objectContaining({ feeId: '11', version: 3 }))
|
|
expect(vi.mocked(submitProcurementCommand).mock.calls[0][3]).not.toHaveProperty('amount')
|
|
wrapper.unmount(); const restricted = render(value, ['charge']); expect(restricted.text()).not.toContain('重试失败资金请求')
|
|
})
|
|
it('uploads an external label and submits a per-carton association with receiving details', async () => {
|
|
const value = fixture('LABELS'); value.physical.packingStatus = 'CONFIRMED'; value.physical.packingVersion = 2
|
|
value.transport.overseasCargo = [{ cartonId: '31', cartonNo: 'BOX-31', quantity: 2, lengthCm: '10.00', widthCm: '20.00', heightCm: '30.00', grossWeightKg: '1.234', packagingRecordId: null, sources: [{ receiptId: '11', quantity: 2 }] }]
|
|
vi.mocked(uploadProcurementFile).mockResolvedValue({ id: '51', fileId: '501', category: 'LABEL', fileName: 'label.pdf', contentType: 'application/pdf', sizeBytes: 3, createdAt: '' })
|
|
const wrapper = render(value); const dialog = await open(wrapper); await explain(wrapper, '国内客服制作国际面单')
|
|
const field = (label: string) => dialog.findAllComponents({ name: 'ElFormItem' }).find(row => row.props('label') === label)!
|
|
await field('国内接收人').find('input').setValue('国内收货人'); await field('联系电话').find('input').setValue('13800000001'); await field('完整收件地址').find('input').setValue('上海国内收货地址')
|
|
const input = dialog.find('input[aria-label="上传快递面单"]'); const file = new File(['pdf'], 'label.pdf', { type: 'application/pdf' }); Object.defineProperty(input.element, 'files', { value: [file] }); await input.trigger('change'); await flushPromises()
|
|
field('本单面单文件').findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', '501'); await field('承运商').find('input').setValue('UPS'); await field('本箱运单号').find('input').setValue('TRACK-31')
|
|
await confirm(wrapper)
|
|
expect(uploadProcurementFile).toHaveBeenCalledWith('OP-100', '10001', 'LABEL', file)
|
|
expect(submitProcurementCommand).toHaveBeenCalledWith('OP-100', '10001', 'labels', expect.objectContaining({ leg: 'OVERSEAS', packingVersion: 2, receiver: { name: '国内收货人', phone: '13800000001', address: '上海国内收货地址' }, labels: [{ cartonId: '31', fileId: '501', page: 1, carrier: 'UPS', trackingNo: 'TRACK-31' }] }))
|
|
expect(submitProcurementCommand).toHaveBeenCalledTimes(1)
|
|
})
|
|
it('records a partial overseas receipt separately from completing receipt', async () => {
|
|
const value = fixture('RECEIVE_OVERSEAS'); value.purchaseFacts = { purchased_at: '2020-01-01T10:00:00', purchased_quantity: 2 }
|
|
const wrapper = render(value); const dialog = await open(wrapper); await explain(wrapper, '首批样品到仓')
|
|
dialog.findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', '20001')
|
|
dialog.findAllComponents({ name: 'ElInputNumber' })[0].vm.$emit('update:modelValue', 1)
|
|
dialog.findComponent({ name: 'ElDatePicker' }).vm.$emit('update:modelValue', '2020-01-02T10:00:00')
|
|
await confirm(wrapper)
|
|
expect(submitProcurementCommand).toHaveBeenCalledWith('OP-100', '10001', 'overseas-receipts', expect.objectContaining({ version: 3, leg: 'OVERSEAS', warehouseId: '20001', goodQuantity: 1, damagedQuantity: 0, extraQuantity: 0, receivedAt: '2020-01-02T10:00:00' }))
|
|
expect(submitProcurementCommand).toHaveBeenCalledTimes(1)
|
|
expect(vi.mocked(submitProcurementCommand).mock.calls[0][3]).not.toHaveProperty('sourceCartonId')
|
|
})
|
|
it('saves structured packing sources without invoking packing confirmation', async () => {
|
|
const value = fixture('PACK')
|
|
value.physical.receipts = [{ id: '11', receiptNo: 'PR-11', leg: 'OVERSEAS', sourceCartonId: null, goodQuantity: 2, damagedQuantity: 0, extraQuantity: 0, receivedAt: '2020-01-02T10:00:00', active: true }]
|
|
const wrapper = render(value); const dialog = await open(wrapper); await explain(wrapper, '核对装箱草稿')
|
|
const values = ['BOX-ONE', '10.12', '20', '30', '1.234']
|
|
for (const [index, input] of dialog.findAll('.packing-grid input').entries()) await input.setValue(values[index])
|
|
dialog.findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', '11')
|
|
dialog.findComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 2)
|
|
await confirm(wrapper)
|
|
expect(submitProcurementCommand).toHaveBeenCalledWith('OP-100', '10001', 'packing', expect.objectContaining({ cartons: [{ cartonNo: 'BOX-ONE', lengthCm: '10.12', widthCm: '20', heightCm: '30', grossWeightKg: '1.234', sources: [{ receiptId: '11', quantity: 2 }] }] }))
|
|
expect(submitProcurementCommand).toHaveBeenCalledTimes(1)
|
|
})
|
|
it('uses exception evidence and source quantities for damaged goods disposition', async () => {
|
|
const value = fixture('MANAGE'); value.purchaseFacts.purchased_quantity = 2
|
|
value.physical.receipts = [{ id: '11', receiptNo: 'PR-11', leg: 'OVERSEAS', sourceCartonId: null, goodQuantity: 1, damagedQuantity: 1, extraQuantity: 0, receivedAt: '2020-01-02T10:00:00', active: true }]
|
|
vi.mocked(uploadProcurementFile).mockResolvedValue({ id: '12', fileId: '102', category: 'EXCEPTION', fileName: 'damage.png', contentType: 'image/png', sizeBytes: 3, createdAt: '' })
|
|
const wrapper = render(value); const dialog = await open(wrapper); await explain(wrapper, '破损样品已经处理,客户已确认')
|
|
const field = (label: string) => dialog.findAllComponents({ name: 'ElFormItem' }).find(row => row.props('label') === label)!
|
|
field('差异及处置').findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', 'DAMAGED'); await flushPromises()
|
|
field('对应实收').findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', '11')
|
|
dialog.findComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 1)
|
|
const input = dialog.find('input[type="file"]'); const file = new File(['img'], 'damage.png', { type: 'image/png' }); Object.defineProperty(input.element, 'files', { value: [file] }); await input.trigger('change'); await flushPromises()
|
|
await confirm(wrapper)
|
|
expect(uploadProcurementFile).toHaveBeenCalledWith('OP-100', '10001', 'EXCEPTION', file)
|
|
expect(submitProcurementCommand).toHaveBeenCalledWith('OP-100', '10001', 'dispositions', expect.objectContaining({ receiptId: '11', kind: 'DAMAGED', decision: 'DISPOSE_DAMAGED', quantity: 1, voucherFileId: '102' }))
|
|
})
|
|
it('requires an authoritative warehouse on approval and sends the current order version', async () => {
|
|
const wrapper = render(); const dialog = await open(wrapper); await explain(wrapper)
|
|
await confirm(wrapper); expect(submitProcurementCommand).not.toHaveBeenCalled(); expect(document.body.textContent).toContain('请选择有效采购仓')
|
|
dialog.findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', '20001'); await confirm(wrapper)
|
|
expect(submitProcurementCommand).toHaveBeenCalledWith('OP-100', '10001', 'review', { version: 3, idempotencyKey: expect.any(String), decision: 'APPROVE', remark: '已核对客户资料', purchaseWarehouseId: '20001' })
|
|
expect(wrapper.emitted('changed')).toHaveLength(1); expect(sessionStorage.getItem(storageKey)).toBeNull()
|
|
})
|
|
it('rejects without a warehouse and never substitutes an invented candidate', async () => {
|
|
vi.mocked(listOverseasPurchaseWarehouseOptions).mockRejectedValue(new Error('unavailable'))
|
|
const wrapper = render(); const dialog = await open(wrapper); await explain(wrapper, '购买对象不符合要求')
|
|
dialog.findComponent({ name: 'ElRadioGroup' }).vm.$emit('update:modelValue', 'REJECT'); await confirm(wrapper)
|
|
expect(submitProcurementCommand).toHaveBeenCalledWith('OP-100', '10001', 'review', expect.objectContaining({ decision: 'REJECT' }))
|
|
expect(vi.mocked(submitProcurementCommand).mock.calls[0][3]).not.toHaveProperty('purchaseWarehouseId')
|
|
})
|
|
it('records explicit zero purchase with the exact product snapshot and purchase-purpose evidence', async () => {
|
|
const wrapper = render(fixture('PURCHASE')); const dialog = await open(wrapper); await explain(wrapper, '供应商免费提供样品')
|
|
await confirm(wrapper); expect(submitProcurementCommand).not.toHaveBeenCalled()
|
|
const amount = dialog.findAll('.el-form-item').find(item => item.text().startsWith('供应商实付总额'))!.find('input'); await amount.setValue('0.00')
|
|
const dates = dialog.findAllComponents({ name: 'ElDatePicker' }); dates[0].vm.$emit('update:modelValue', '2026-10-08T10:00:00'); dates[1].vm.$emit('update:modelValue', '2026-10-15T10:00:00')
|
|
const input = dialog.find('input[type="file"]'); const file = new File(['pdf'], 'purchase.pdf', { type: 'application/pdf' }); Object.defineProperty(input.element, 'files', { value: [file] }); await input.trigger('change'); await flushPromises()
|
|
expect(uploadProcurementFile).toHaveBeenCalledWith('OP-100', '10001', 'PURCHASE', file)
|
|
await confirm(wrapper)
|
|
expect(submitProcurementCommand).toHaveBeenCalledWith('OP-100', '10001', 'purchase', expect.objectContaining({ productName: '样品商品', specification: '黑色', quantity: 2, amount: '0.00', currency: 'USD', voucherFileId: '101', purchasedAt: '2026-10-08T10:00:00' }))
|
|
})
|
|
it('recovers an uncertain request after reload without changing its body or key', async () => {
|
|
vi.mocked(submitProcurementCommand).mockRejectedValueOnce(new Error('timeout'))
|
|
let wrapper = render(fixture('CANCEL')); await open(wrapper); await explain(wrapper, '客户取消'); await confirm(wrapper)
|
|
const original = vi.mocked(submitProcurementCommand).mock.calls[0][3]; expect(sessionStorage.getItem(storageKey)).not.toBeNull(); wrapper.unmount()
|
|
wrapper = render(fixture('CANCEL')); await flushPromises()
|
|
const retry = wrapper.findAll('button').find(button => button.text() === '确认上次提交结果')!; await retry.trigger('click'); await flushPromises()
|
|
expect(vi.mocked(submitProcurementCommand).mock.calls[1][3]).toEqual(original); expect(sessionStorage.getItem(storageKey)).toBeNull()
|
|
})
|
|
it('preserves an earlier uncertain financial or physical command when replay is rejected', async () => {
|
|
vi.mocked(submitProcurementCommand).mockRejectedValueOnce(new Error('timeout')).mockRejectedValueOnce(new ApiResponseError('资源正在处理中', 'A0409'))
|
|
const wrapper = render(fixture('CANCEL')); await open(wrapper); await explain(wrapper); await confirm(wrapper)
|
|
const saved = sessionStorage.getItem(storageKey); expect(saved).not.toBeNull()
|
|
await confirm(wrapper)
|
|
expect(sessionStorage.getItem(storageKey)).toBe(saved)
|
|
expect(vi.mocked(submitProcurementCommand).mock.calls[1][3]).toEqual(vi.mocked(submitProcurementCommand).mock.calls[0][3])
|
|
expect(wrapper.emitted('changed')).toBeUndefined()
|
|
})
|
|
it('keeps a version conflict visible, releases the failed command and does not report success', async () => {
|
|
vi.mocked(submitProcurementCommand).mockRejectedValue(new ApiResponseError('订单版本冲突,请刷新', 'A0409'))
|
|
const wrapper = render(fixture('CANCEL')); await open(wrapper); await explain(wrapper); await confirm(wrapper)
|
|
expect(sessionStorage.getItem(storageKey)).toBeNull(); expect(document.body.textContent).toContain('订单版本冲突'); expect(wrapper.emitted('changed')).toBeUndefined()
|
|
})
|
|
it('hides unauthorized actions and preserves blocked node reasons without a submit button', async () => {
|
|
let wrapper = render(fixture(), []); expect(wrapper.find('.purchase-actions button').exists()).toBe(false); wrapper.unmount()
|
|
wrapper = render(fixture('REVIEW', false))
|
|
expect(wrapper.find('.purchase-actions button').exists()).toBe(false)
|
|
expect(wrapper.find('details').text()).toContain('审核采购')
|
|
expect(wrapper.find('details').text()).toContain('业务条件不满足')
|
|
expect(submitProcurementCommand).not.toHaveBeenCalled()
|
|
})
|
|
it('keeps executable workflow actions in front and financial exceptions in a closed group', () => {
|
|
const detail = fixture('PURCHASE')
|
|
detail.actions.push(...fixture('FINANCE_ADJUST').actions, ...fixture('PACK', false).actions)
|
|
const wrapper = render(detail)
|
|
const primary = wrapper.get('[data-testid="current-procurement-actions"]')
|
|
expect(primary.text()).toContain('登记购买完成')
|
|
expect(primary.text()).not.toContain('费用更正与退款')
|
|
expect(primary.text()).not.toContain('保存装箱草稿')
|
|
const groups = wrapper.findAll('details')
|
|
expect(groups[0].text()).toContain('费用更正与退款')
|
|
expect(groups[0].attributes('open')).toBeUndefined()
|
|
expect(groups[1].text()).toContain('业务条件不满足')
|
|
})
|
|
it('prevents a second submit while the first command is pending', async () => {
|
|
let complete!: () => void; vi.mocked(submitProcurementCommand).mockImplementation(() => new Promise(resolve => { complete = resolve }))
|
|
const wrapper = render(fixture('CANCEL')); await open(wrapper); await explain(wrapper)
|
|
wrapper.findComponent(AppDialog).vm.$emit('confirm'); wrapper.findComponent(AppDialog).vm.$emit('confirm'); await flushPromises()
|
|
expect(submitProcurementCommand).toHaveBeenCalledTimes(1); complete(); await flushPromises()
|
|
})
|
|
it('rejects malformed recovery records instead of allowing a fresh competing operation', async () => {
|
|
sessionStorage.setItem(storageKey, '{bad')
|
|
const wrapper = render(); expect(wrapper.text()).toContain('上次操作记录无法读取'); expect(wrapper.find('.purchase-actions button').attributes('disabled')).toBeDefined()
|
|
expect(() => readPendingProcurement(JSON.stringify({ command: 'purchase', payload: { version: 3, idempotencyKey: 'a', amount: '-2' } }))).toThrow()
|
|
})
|
|
})
|