优化
This commit is contained in:
@@ -28,7 +28,7 @@ describe('delivery order API', () => {
|
||||
await listDeliveryReturnInboundOptions('DEL/1', '1001')
|
||||
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1/return-inbound-options', { params: { customerId: '1001' } })
|
||||
|
||||
const payload = { inboundOrderId: '9', reason: '客户拒收', returnDirection: 'OVERSEAS_WAREHOUSE', packageCount: 2 }
|
||||
const payload = { inboundOrderId: '9', reason: '客户拒收', returnDirection: 'OVERSEAS_WAREHOUSE' as const, items: [{ outboundPalletItemId: '9007199254740995', inventoryPositionId: '9007199254740996', packageCount: 2 }], idempotencyKey: 'return-1' }
|
||||
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', data: { id: '19', exceptionNo: 'EXC-1', status: 'PENDING' } })
|
||||
await markDeliveryReturn('DEL/1', '1001', payload)
|
||||
expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1/mark-return', payload, { params: { customerId: '1001' } })
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { overseasHttp } from '@/shared/api/overseasHttp'
|
||||
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
|
||||
import type { ExceptionRelation } from './exceptionOrders'
|
||||
import type { DeliveryDocumentSummary } from './deliveryDocuments'
|
||||
|
||||
export interface DeliveryOrderQuery {
|
||||
@@ -63,6 +62,29 @@ export interface DeliveryFeeRow { id: string; feeDirection: string; feeType: str
|
||||
export interface DeliveryOperationRow { id: string; operatedAt: string; operationNode: string; operatorName?: string | null; result: string }
|
||||
export interface DeliveryOrderDetail { order: DeliveryOrderRow; skus: DeliverySkuRow[]; fees: DeliveryFeeRow[]; operations: DeliveryOperationRow[] }
|
||||
|
||||
export interface DeliveryReturnSource {
|
||||
outboundPalletItemId: string
|
||||
inventoryPositionId: string
|
||||
palletNo?: string | null
|
||||
skuCode?: string | null
|
||||
maxReturnPackages: number
|
||||
}
|
||||
export interface DeliveryReturnRelation {
|
||||
inboundOrderId: string
|
||||
inboundCode?: string | null
|
||||
lastMileOrderNo?: string | null
|
||||
customerId: string
|
||||
returnSources: DeliveryReturnSource[]
|
||||
}
|
||||
export interface MarkDeliveryReturnPayload {
|
||||
inboundOrderId: string
|
||||
reason: string
|
||||
returnDirection: 'OVERSEAS_WAREHOUSE' | 'CUSTOMER' | 'OTHER'
|
||||
returnLogisticsNo?: string
|
||||
items: Array<{ outboundPalletItemId: string; inventoryPositionId: string; packageCount: number }>
|
||||
idempotencyKey: string
|
||||
}
|
||||
|
||||
export async function listDeliveryOrders(query: DeliveryOrderQuery) {
|
||||
const { customerId, ...params } = query
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<DeliveryOrderRow>>, ApiEnvelope<OverseasPagePayload<DeliveryOrderRow>>>(
|
||||
@@ -79,13 +101,13 @@ export async function getDeliveryOrder(deliveryNo: string, customerId: string) {
|
||||
}
|
||||
|
||||
export async function listDeliveryReturnInboundOptions(deliveryNo: string, customerId: string) {
|
||||
const response = await overseasHttp.get<ApiEnvelope<ExceptionRelation[]>, ApiEnvelope<ExceptionRelation[]>>(
|
||||
const response = await overseasHttp.get<ApiEnvelope<DeliveryReturnRelation[]>, ApiEnvelope<DeliveryReturnRelation[]>>(
|
||||
`/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}/return-inbound-options`, { params: { customerId } },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function markDeliveryReturn(deliveryNo: string, customerId: string, payload: { inboundOrderId: string; reason: string; returnDirection: string; returnLogisticsNo?: string; packageCount?: number; skuCode?: string }) {
|
||||
export async function markDeliveryReturn(deliveryNo: string, customerId: string, payload: MarkDeliveryReturnPayload) {
|
||||
const response = await overseasHttp.post<ApiEnvelope<{ id: string; exceptionNo: string; status: string }>, ApiEnvelope<{ id: string; exceptionNo: string; status: string }>>(
|
||||
`/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}/mark-return`, payload, { params: { customerId } },
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import ElementPlus from 'element-plus'
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { DOMWrapper, flushPromises, mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import DeliveryDocumentsDialog from './DeliveryDocumentsDialog.vue'
|
||||
import { commonPhase5Stubs } from '../pages/phase5TestStubs'
|
||||
@@ -50,6 +50,41 @@ describe('delivery document drafts', () => {
|
||||
expect(wrapper.emitted('saved')).toHaveLength(1)
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('uses real upload controls to add, replace, delete and reload the saved three-category snapshot', async () => {
|
||||
const receipt = { ...originalFile, fileId: 'receipt-1', fileName: 'pod.pdf' }
|
||||
const courier = { ...originalFile, fileId: 'receipt-2', fileName: 'courier.pdf' }
|
||||
const replacement = { ...originalFile, fileId: 'bol-2', fileName: 'signed-bol.pdf' }
|
||||
vi.mocked(uploadDeliveryDocument).mockResolvedValueOnce(receipt).mockResolvedValueOnce(courier).mockResolvedValueOnce(replacement)
|
||||
const wrapper = mount(DeliveryDocumentsDialog, { attachTo: document.body, props: { modelValue: true, deliveryNo: 'DL/1', customerId: '100' }, global: { plugins: [ElementPlus] } })
|
||||
const body = new DOMWrapper(document.body)
|
||||
try {
|
||||
await flushPromises()
|
||||
const dialog = body.find('[role="dialog"]')
|
||||
expect(dialog.findAll('[data-category]')).toHaveLength(3)
|
||||
await choose(dialog.find('[data-category="RECEIPT"] header input[type="file"]'), new File(['pod'], 'pod.pdf'))
|
||||
await choose(dialog.find('[data-category="RECEIPT"] header input[type="file"]'), new File(['courier'], 'courier.pdf'))
|
||||
expect(dialog.text()).toContain('2/3 已上传')
|
||||
await choose(dialog.find('[data-category="BOL"] .document-file input[type="file"]'), new File(['signed'], 'signed-bol.pdf'))
|
||||
await dialog.find('[data-category="RECEIPT"] .document-file').findAll('button').find(button => button.text() === '删除')!.trigger('click')
|
||||
expect(dialog.text()).not.toContain('pod.pdf')
|
||||
expect(dialog.text()).toContain('courier.pdf')
|
||||
expect(saveDeliveryDocuments).not.toHaveBeenCalled()
|
||||
await dialog.findAll('button').find(button => button.text() === '保存资料')!.trigger('click'); await flushPromises()
|
||||
expect(saveDeliveryDocuments).toHaveBeenCalledWith('DL/1', '100', { expectedVersion: 9, idempotencyKey: expect.any(String), bolNo: 'BOL-1', documents: [{ category: 'LOADING_PHOTO', fileIds: [] }, { category: 'BOL', fileIds: ['bol-2'] }, { category: 'RECEIPT', fileIds: ['receipt-2'] }] })
|
||||
expect(wrapper.emitted('saved')).toHaveLength(1)
|
||||
const persisted = response()
|
||||
persisted.version = 10; persisted.uploadedCategoryCount = 2
|
||||
persisted.categories = persisted.categories.map(group => ({ ...group, files: group.category === 'BOL' ? [replacement] : group.category === 'RECEIPT' ? [courier] : [], fileCount: group.category === 'LOADING_PHOTO' ? 0 : 1, status: group.category === 'LOADING_PHOTO' ? 'MISSING' : 'UPLOADED' }))
|
||||
vi.mocked(getDeliveryDocuments).mockResolvedValue(persisted)
|
||||
await wrapper.setProps({ modelValue: false }); await wrapper.setProps({ modelValue: true }); await flushPromises()
|
||||
const reopened = body.find('[role="dialog"]')
|
||||
expect(reopened.text()).toContain('2/3 已上传')
|
||||
expect(reopened.text()).toContain('signed-bol.pdf')
|
||||
expect(reopened.text()).toContain('courier.pdf')
|
||||
expect(reopened.text()).not.toContain('original.pdf')
|
||||
expect(getDeliveryDocuments).toHaveBeenCalledTimes(2)
|
||||
} finally { wrapper.unmount() }
|
||||
})
|
||||
it('discards unsaved uploads on cancel and reloads persisted categories on reopening', async () => {
|
||||
const wrapper = mountDialog(); await flushPromises()
|
||||
await choose(wrapper.find('[data-category="RECEIPT"] input[type="file"]'), new File(['proof'], 'new.pdf'))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import ElementPlus from 'element-plus'
|
||||
import { defineComponent, h, type PropType } from 'vue'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { DOMWrapper, flushPromises, mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getDeliveryOrder, listDeliveryOrders, listDeliveryReturnInboundOptions, markDeliveryReturn } from '../api/deliveryOrders'
|
||||
import { updateDeliveryOrderStatus } from '../api/customerVisits'
|
||||
@@ -17,7 +17,7 @@ vi.mock('../api/pallets', () => ({ listReceivingOverseasWarehouses: vi.fn() }))
|
||||
vi.mock('../api/deliveryDocuments', async importOriginal => ({ ...await importOriginal<typeof import('../api/deliveryDocuments')>(), getDeliveryDocuments: vi.fn() }))
|
||||
|
||||
const row = {
|
||||
version: 2, allowedActions: ['START_DELIVERY', 'COMPLETE_DELIVERY', 'REJECT_DELIVERY', 'CANCEL_DELIVERY'],
|
||||
version: 2, allowedActions: ['START_DELIVERY', 'COMPLETE_DELIVERY', 'REJECT_DELIVERY', 'CANCEL_DELIVERY', 'MARK_RETURN'],
|
||||
id: '1', customerId: '100', deliveryNo: 'DEL/1', outboundNo: 'OUT-1', dispatchNo: 'PC-1',
|
||||
status: 'WAIT_DELIVERY', palletCount: 1, packageCount: 2, createdAt: '2026-09-01T10:00:00',
|
||||
}
|
||||
@@ -32,7 +32,7 @@ describe('DeliveryOrdersView permissions and commands', () => {
|
||||
vi.mocked(getDeliveryDocuments).mockReset()
|
||||
vi.mocked(getDeliveryOrder).mockReset().mockResolvedValue({ order: { ...row, version: 7 }, skus: [], fees: [], operations: [] })
|
||||
vi.mocked(listDeliveryOrders).mockReset().mockResolvedValue({ records: [row], page: 1, size: 10, total: 1, pages: 1 })
|
||||
vi.mocked(listDeliveryReturnInboundOptions).mockReset().mockResolvedValue([{ inboundOrderId: '9', inboundCode: 'IN-1', lastMileOrderNo: 'LM-1', customerId: '100' }])
|
||||
vi.mocked(listDeliveryReturnInboundOptions).mockReset().mockResolvedValue([{ inboundOrderId: '9', inboundCode: 'IN-1', lastMileOrderNo: 'LM-1', customerId: '100', returnSources: [{ outboundPalletItemId: '9007199254740995', inventoryPositionId: '9007199254740996', palletNo: 'PL-1', skuCode: 'SKU-1', maxReturnPackages: 3 }] }])
|
||||
vi.mocked(markDeliveryReturn).mockReset().mockResolvedValue({ id: '19', exceptionNo: 'EXC-1', status: 'PENDING' })
|
||||
vi.mocked(updateDeliveryOrderStatus).mockReset().mockResolvedValue({ deliveryNo: 'DEL/1', deliveryStatus: 'WAIT_DELIVERY', replayed: false })
|
||||
})
|
||||
@@ -63,11 +63,13 @@ describe('DeliveryOrdersView permissions and commands', () => {
|
||||
await flushPromises()
|
||||
expect(listDeliveryReturnInboundOptions).toHaveBeenCalledWith('DEL/1', '100')
|
||||
await wrapper.find('[data-dialog="标记退货"] textarea').setValue(' 客户拒收 ')
|
||||
await wrapper.find('[data-dialog="标记退货"] input[type="checkbox"]').setValue(true)
|
||||
await wrapper.find('[data-dialog="标记退货"] input[type="number"]').setValue(2)
|
||||
await wrapper.find('[data-dialog="标记退货"] .dialog-confirm').trigger('click')
|
||||
await flushPromises()
|
||||
expect(markDeliveryReturn).toHaveBeenCalledWith('DEL/1', '100', {
|
||||
inboundOrderId: '9', reason: '客户拒收', returnDirection: 'OVERSEAS_WAREHOUSE',
|
||||
returnLogisticsNo: undefined, packageCount: undefined, skuCode: undefined,
|
||||
returnLogisticsNo: undefined, items: [{ outboundPalletItemId: '9007199254740995', inventoryPositionId: '9007199254740996', packageCount: 2 }], idempotencyKey: expect.any(String),
|
||||
})
|
||||
wrapper.unmount()
|
||||
})
|
||||
@@ -116,6 +118,71 @@ describe('DeliveryOrdersView permissions and commands', () => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('keeps return hidden without the server action even when the user has permission', async () => {
|
||||
canPermission.mockImplementation(code => code.endsWith(':return'))
|
||||
vi.mocked(listDeliveryOrders).mockResolvedValue({ records: [{ ...row, status: 'RETURNED', allowedActions: [] }], page: 1, size: 10, total: 1, pages: 1 })
|
||||
const wrapper = mountView(); await flushPromises()
|
||||
expect(wrapper.find('[data-testid="data-table"]').text()).not.toContain('标记退货')
|
||||
expect(listDeliveryReturnInboundOptions).not.toHaveBeenCalled()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('validates selected return sources and reuses the idempotency key after a failed request', async () => {
|
||||
canPermission.mockImplementation(code => code.endsWith(':return'))
|
||||
const wrapper = mountView(); await flushPromises()
|
||||
try {
|
||||
await wrapper.findAll('button').find(button => button.text().includes('标记退货'))!.trigger('click'); await flushPromises()
|
||||
const dialog = wrapper.find('[data-dialog="标记退货"]')
|
||||
expect(dialog.text()).toContain('PL-1 · SKU-1(最多 3 箱)')
|
||||
expect(dialog.text()).not.toContain('9007199254740995')
|
||||
expect(dialog.text()).not.toContain('可自由填写')
|
||||
await dialog.find('textarea').setValue('拒收')
|
||||
await dialog.find('.dialog-confirm').trigger('click')
|
||||
expect(markDeliveryReturn).not.toHaveBeenCalled()
|
||||
await dialog.find('input[type="checkbox"]').setValue(true)
|
||||
for (const invalid of [0, 4, 1.5]) {
|
||||
await dialog.find('input[type="number"]').setValue(invalid)
|
||||
await dialog.find('.dialog-confirm').trigger('click')
|
||||
expect(markDeliveryReturn).not.toHaveBeenCalled()
|
||||
}
|
||||
await dialog.find('input[type="number"]').setValue(2)
|
||||
let reject!: (error: Error) => void
|
||||
vi.mocked(markDeliveryReturn).mockReturnValueOnce(new Promise((_resolve, fail) => { reject = fail }))
|
||||
await dialog.find('.dialog-confirm').trigger('click'); await dialog.find('.dialog-confirm').trigger('click')
|
||||
expect(markDeliveryReturn).toHaveBeenCalledTimes(1)
|
||||
const first = vi.mocked(markDeliveryReturn).mock.calls[0]![2]
|
||||
reject(new Error('网络异常')); await flushPromises()
|
||||
expect(dialog.text()).toContain('标记退货失败')
|
||||
expect((dialog.find('textarea').element as HTMLTextAreaElement).value).toBe('拒收')
|
||||
await dialog.find('.dialog-confirm').trigger('click'); await flushPromises()
|
||||
expect(vi.mocked(markDeliveryReturn).mock.calls[1]![2]).toEqual(first)
|
||||
expect(first.items).toEqual([{ outboundPalletItemId: '9007199254740995', inventoryPositionId: '9007199254740996', packageCount: 2 }])
|
||||
expect(first).not.toHaveProperty('skuCode')
|
||||
expect(first).not.toHaveProperty('packageCount')
|
||||
} finally { wrapper.unmount() }
|
||||
})
|
||||
|
||||
it('clears the selected source when a different inbound order is chosen', async () => {
|
||||
canPermission.mockImplementation(code => code.endsWith(':return'))
|
||||
vi.mocked(listDeliveryReturnInboundOptions).mockResolvedValue([
|
||||
{ inboundOrderId: '9', inboundCode: 'IN-1', customerId: '100', returnSources: [{ outboundPalletItemId: '11', inventoryPositionId: '12', palletNo: 'PL-1', skuCode: 'SKU-1', maxReturnPackages: 3 }] },
|
||||
{ inboundOrderId: '10', inboundCode: 'IN-2', customerId: '100', returnSources: [] },
|
||||
])
|
||||
const wrapper = mountView(); await flushPromises()
|
||||
try {
|
||||
await wrapper.findAll('button').find(button => button.text().includes('标记退货'))!.trigger('click'); await flushPromises()
|
||||
const dialog = wrapper.find('[data-dialog="标记退货"]')
|
||||
await dialog.find('select').setValue('9')
|
||||
await dialog.find('input[type="checkbox"]').setValue(true)
|
||||
await dialog.find('input[type="number"]').setValue(2)
|
||||
await dialog.find('textarea').setValue('拒收')
|
||||
await dialog.find('select').setValue('10')
|
||||
expect(dialog.find('input[type="checkbox"]').exists()).toBe(false)
|
||||
await dialog.find('.dialog-confirm').trigger('click')
|
||||
expect(markDeliveryReturn).not.toHaveBeenCalled()
|
||||
} finally { wrapper.unmount() }
|
||||
})
|
||||
|
||||
it('hides status editing when the server supplies no actions', async () => {
|
||||
canPermission.mockReturnValue(true)
|
||||
vi.mocked(listDeliveryOrders).mockResolvedValue({ records: [{ ...row, status: 'DELIVERED', allowedActions: [] }], page: 1, size: 10, total: 1, pages: 1 })
|
||||
@@ -125,6 +192,39 @@ describe('DeliveryOrdersView permissions and commands', () => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('selects completion through the real dialog and select, then retries a conflict with the refreshed version', async () => {
|
||||
canPermission.mockImplementation(code => code.endsWith(':status'))
|
||||
vi.mocked(getDeliveryOrder).mockResolvedValue({ order: { ...row, version: 7, status: 'DELIVERING', allowedActions: ['COMPLETE_DELIVERY'] }, skus: [], fees: [], operations: [] })
|
||||
vi.mocked(updateDeliveryOrderStatus).mockRejectedValueOnce({ isAxiosError: true, response: { status: 409, data: { code: 'A0409', message: '版本冲突' } } })
|
||||
const wrapper = mount(DeliveryOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus], stubs: { ...commonPhase5Stubs, AppDialog: false, ElInput: false, ElSelect: false, ElOption: false } } })
|
||||
const body = new DOMWrapper(document.body)
|
||||
try {
|
||||
await flushPromises()
|
||||
await wrapper.findAll('button').find(button => button.text().includes('编辑状态'))!.trigger('click')
|
||||
await flushPromises()
|
||||
const dialog = body.find('[role="dialog"][aria-label="编辑派送状态"]')
|
||||
const save = () => dialog.findAll('button').find(button => button.text() === '保存状态')!
|
||||
expect(save().attributes('disabled')).toBeDefined()
|
||||
await dialog.find('.el-select__wrapper').trigger('click')
|
||||
await flushPromises()
|
||||
const options = body.findAll('.el-select-dropdown__item')
|
||||
expect(options.map(option => option.text())).toEqual(['派送完成'])
|
||||
await options[0]!.trigger('click')
|
||||
await dialog.find('textarea').setValue('已交付')
|
||||
await save().trigger('click'); await flushPromises()
|
||||
expect(updateDeliveryOrderStatus).toHaveBeenLastCalledWith('DEL/1', '100', { status: 'DELIVERED', expectedVersion: 7, remark: '已交付' })
|
||||
expect(save().attributes('disabled')).toBeDefined()
|
||||
expect(dialog.text()).toContain('请刷新最新状态并重新确认')
|
||||
vi.mocked(getDeliveryOrder).mockResolvedValue({ order: { ...row, version: 8, status: 'DELIVERING', allowedActions: ['COMPLETE_DELIVERY'] }, skus: [], fees: [], operations: [] })
|
||||
await dialog.findAll('button').find(button => button.text().includes('重新加载'))!.trigger('click')
|
||||
await flushPromises()
|
||||
expect((dialog.find('textarea').element as HTMLTextAreaElement).value).toBe('已交付')
|
||||
await save().trigger('click'); await flushPromises()
|
||||
expect(updateDeliveryOrderStatus).toHaveBeenLastCalledWith('DEL/1', '100', { status: 'DELIVERED', expectedVersion: 8, remark: '已交付' })
|
||||
expect(listDeliveryOrders).toHaveBeenCalledTimes(2)
|
||||
} finally { wrapper.unmount() }
|
||||
})
|
||||
|
||||
it('renders category counts from the list without per-row document requests', async () => {
|
||||
canPermission.mockReturnValue(true)
|
||||
const documentSummary = { uploadedCategoryCount: 1, totalCategoryCount: 3, categories: deliveryDocumentCategories.map(item => ({ category: item.category, status: item.category === 'RECEIPT' ? 'UPLOADED' as const : 'MISSING' as const, fileCount: item.category === 'RECEIPT' ? 3 : 0 })) }
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user