|
|
|
@@ -39,6 +39,90 @@ beforeEach(() => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('ContainerFeesPanel contract', () => {
|
|
|
|
|
it.each(['success', 'failure'])('ignores a closed editor request that finishes late: %s', async (outcome) => {
|
|
|
|
|
const item = { id: '7', name: '操作费', feeCode: 'OPERATION', defaultPrice: 60, supplierCategory: null, warehouseReceivable: true, warehousePayable: false, pickupReceivable: false, pickupPayable: false }
|
|
|
|
|
let resolveOld!: (items: typeof item[]) => void
|
|
|
|
|
let rejectOld!: (reason: Error) => void
|
|
|
|
|
vi.mocked(listApplicableFeeItems).mockImplementationOnce(() => new Promise((resolve, reject) => { resolveOld = resolve; rejectOld = reject }))
|
|
|
|
|
vi.mocked(listApplicableFeeItems).mockResolvedValueOnce([{ ...item, defaultPrice: 80 }])
|
|
|
|
|
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view'])
|
|
|
|
|
const state = (wrapper.vm.$ as unknown as { setupState: {
|
|
|
|
|
openCreate(): void
|
|
|
|
|
editorOpen: boolean
|
|
|
|
|
form: { amount: number }
|
|
|
|
|
feeItems: typeof item[]
|
|
|
|
|
feeItemsLoading: boolean
|
|
|
|
|
feeItemsError: unknown
|
|
|
|
|
} }).setupState
|
|
|
|
|
state.openCreate(); await flushPromises()
|
|
|
|
|
state.editorOpen = false; await flushPromises()
|
|
|
|
|
state.openCreate(); await flushPromises()
|
|
|
|
|
expect(state.feeItems[0]?.defaultPrice).toBe(80)
|
|
|
|
|
if (outcome === 'success') resolveOld([item])
|
|
|
|
|
else rejectOld(new Error('旧窗口请求失败'))
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(state.feeItemsError).toBeNull()
|
|
|
|
|
expect(state.feeItemsLoading).toBe(false)
|
|
|
|
|
const select = wrapper.findAllComponents({ name: 'ElFormItem' }).find((field) => field.props('prop') === 'feeItemId')!.findComponent({ name: 'ElSelect' })
|
|
|
|
|
select.vm.$emit('update:modelValue', '7'); select.vm.$emit('change', '7')
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(state.form.amount).toBe(80)
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('does not let an old completion clear the current loading state or refill a closed editor', async () => {
|
|
|
|
|
const item = { id: '7', name: '操作费', feeCode: 'OPERATION', defaultPrice: 60, supplierCategory: null, warehouseReceivable: true, warehousePayable: false, pickupReceivable: false, pickupPayable: false }
|
|
|
|
|
let resolveOld!: (items: typeof item[]) => void
|
|
|
|
|
let resolveCurrent!: (items: typeof item[]) => void
|
|
|
|
|
vi.mocked(listApplicableFeeItems)
|
|
|
|
|
.mockImplementationOnce(() => new Promise((resolve) => { resolveOld = resolve }))
|
|
|
|
|
.mockImplementationOnce(() => new Promise((resolve) => { resolveCurrent = resolve }))
|
|
|
|
|
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view'])
|
|
|
|
|
const state = (wrapper.vm.$ as unknown as { setupState: {
|
|
|
|
|
openCreate(): void; editorOpen: boolean; feeItems: unknown[]; feeItemsLoading: boolean; feeItemsError: unknown
|
|
|
|
|
} }).setupState
|
|
|
|
|
state.openCreate(); await flushPromises()
|
|
|
|
|
state.editorOpen = false
|
|
|
|
|
state.openCreate(); await flushPromises()
|
|
|
|
|
resolveOld([item]); await flushPromises()
|
|
|
|
|
expect(state.feeItems).toEqual([])
|
|
|
|
|
expect(state.feeItemsLoading).toBe(true)
|
|
|
|
|
state.editorOpen = false; await flushPromises()
|
|
|
|
|
resolveCurrent([{ ...item, defaultPrice: 80 }]); await flushPromises()
|
|
|
|
|
expect(state.feeItems).toEqual([])
|
|
|
|
|
expect(state.feeItemsLoading).toBe(false)
|
|
|
|
|
expect(state.feeItemsError).toBeNull()
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('prefills the selected default, clears stale prices and retains manual overrides', async () => {
|
|
|
|
|
const base = { name: '操作费', feeCode: 'OPERATION', supplierCategory: null, warehouseReceivable: true, warehousePayable: false, pickupReceivable: false, pickupPayable: false }
|
|
|
|
|
vi.mocked(listApplicableFeeItems).mockResolvedValue([
|
|
|
|
|
{ ...base, id: '7', defaultPrice: 60 }, { ...base, id: '8', defaultPrice: null }, { ...base, id: '9', defaultPrice: 0 },
|
|
|
|
|
])
|
|
|
|
|
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view'])
|
|
|
|
|
const state = (wrapper.vm.$ as unknown as { setupState: {
|
|
|
|
|
openCreate(): void; save(): Promise<void>
|
|
|
|
|
resetForm(row: Record<string, unknown>): void
|
|
|
|
|
form: { feeItemId: string; amount: number }
|
|
|
|
|
} }).setupState
|
|
|
|
|
state.resetForm({ feeItemId: '7', direction: 'RECEIVABLE', businessDate: '2026-08-12', amount: 100 })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(state.form.amount).toBe(100)
|
|
|
|
|
state.openCreate(); await flushPromises()
|
|
|
|
|
const select = wrapper.findAllComponents({ name: 'ElFormItem' }).find((item) => item.props('prop') === 'feeItemId')!.findComponent({ name: 'ElSelect' })
|
|
|
|
|
for (const [id, price] of [['7', 60], ['8', 0], ['9', 0]] as const) {
|
|
|
|
|
select.vm.$emit('update:modelValue', id); select.vm.$emit('change', id)
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(state.form.amount).toBe(price)
|
|
|
|
|
}
|
|
|
|
|
select.vm.$emit('update:modelValue', '7'); select.vm.$emit('change', '7')
|
|
|
|
|
await flushPromises()
|
|
|
|
|
state.form.amount = 75
|
|
|
|
|
await state.save(); await flushPromises()
|
|
|
|
|
expect(createContainerFee).toHaveBeenCalledWith('9', '11', expect.objectContaining({ feeItemId: '7', amount: 75 }))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps fee controls and table utilities in compact aligned toolbars', () => {
|
|
|
|
|
expect(panel).toContain('class="fees-header__actions"')
|
|
|
|
|
expect(panel).toContain('class="customer-context"')
|
|
|
|
@@ -179,7 +263,7 @@ describe('ContainerFeesPanel contract', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps receivable usable when supplier permission is missing', async () => {
|
|
|
|
|
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '操作费', feeCode: 'OPERATION', supplierCategory: 'TRUCKING', warehouseReceivable: true, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
|
|
|
|
|
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '操作费', feeCode: 'OPERATION', defaultPrice: null, supplierCategory: 'TRUCKING', warehouseReceivable: true, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
|
|
|
|
|
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view'])
|
|
|
|
|
const state = (wrapper.vm.$ as unknown as { setupState: { openCreate(): void; form: { direction: string; supplierId: string | null } } }).setupState
|
|
|
|
|
state.openCreate(); await flushPromises()
|
|
|
|
@@ -190,7 +274,7 @@ describe('ContainerFeesPanel contract', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('isolates a supplier load failure and clears supplier fields for receivable', async () => {
|
|
|
|
|
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '操作费', feeCode: 'TRUCKING', supplierCategory: 'TRUCKING', warehouseReceivable: true, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
|
|
|
|
|
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '操作费', feeCode: 'TRUCKING', defaultPrice: null, supplierCategory: 'TRUCKING', warehouseReceivable: true, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
|
|
|
|
|
vi.mocked(listContainerFeeSupplierOptions).mockRejectedValue(new Error('suppliers unavailable'))
|
|
|
|
|
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view', 'platform:supplier:list'])
|
|
|
|
|
const state = (wrapper.vm.$ as unknown as { setupState: { openCreate(): void; form: { direction: string; feeItemId: string; supplierId: string | null }; feeItems: unknown[] } }).setupState
|
|
|
|
@@ -203,7 +287,7 @@ describe('ContainerFeesPanel contract', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('submits only the selected supplier ID for payable fees', async () => {
|
|
|
|
|
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '操作费', feeCode: 'TRUCKING', supplierCategory: 'TRUCKING', warehouseReceivable: true, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
|
|
|
|
|
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '操作费', feeCode: 'TRUCKING', defaultPrice: null, supplierCategory: 'TRUCKING', warehouseReceivable: true, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
|
|
|
|
|
vi.mocked(listContainerFeeSupplierOptions).mockResolvedValue([{ id: '41', supplierName: '承运商A', category: 'TRUCKING' }])
|
|
|
|
|
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view', 'platform:supplier:list'])
|
|
|
|
|
const state = (wrapper.vm.$ as unknown as { setupState: { openCreate(): void; save(): Promise<void>; form: { direction: string; feeItemId: string; supplierId: string | null; settlementMode: string | null; amount: number } } }).setupState
|
|
|
|
@@ -215,7 +299,7 @@ describe('ContainerFeesPanel contract', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('allows any loaded ACTIVE supplier when the payable fee item has no supplier category', async () => {
|
|
|
|
|
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '未映射费用', feeCode: 'UNMAPPED', supplierCategory: null, warehouseReceivable: false, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
|
|
|
|
|
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '未映射费用', feeCode: 'UNMAPPED', defaultPrice: null, supplierCategory: null, warehouseReceivable: false, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
|
|
|
|
|
vi.mocked(listContainerFeeSupplierOptions).mockResolvedValue([{ id: '41', supplierName: '报关行', category: 'CUSTOMS' }])
|
|
|
|
|
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view', 'platform:supplier:list'])
|
|
|
|
|
const state = (wrapper.vm.$ as unknown as { setupState: { openCreate(): void; save(): Promise<void>; form: { direction: string; feeItemId: string; supplierId: string | null; settlementMode: string | null; amount: number } } }).setupState
|
|
|
|
@@ -227,7 +311,7 @@ describe('ContainerFeesPanel contract', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('fails closed when a supplier candidate does not match the fee-item category', async () => {
|
|
|
|
|
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '拖车费', feeCode: 'TRUCKING', supplierCategory: 'TRUCKING', warehouseReceivable: false, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
|
|
|
|
|
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '拖车费', feeCode: 'TRUCKING', defaultPrice: null, supplierCategory: 'TRUCKING', warehouseReceivable: false, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
|
|
|
|
|
vi.mocked(listContainerFeeSupplierOptions).mockResolvedValue([{ id: '41', supplierName: '报关行', category: 'CUSTOMS' }])
|
|
|
|
|
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view', 'platform:supplier:list'])
|
|
|
|
|
const state = (wrapper.vm.$ as unknown as { setupState: { openCreate(): void; save(): Promise<void>; form: { direction: string; feeItemId: string; supplierId: string | null; amount: number } } }).setupState
|
|
|
|
@@ -249,7 +333,7 @@ describe('ContainerFeesPanel contract', () => {
|
|
|
|
|
|
|
|
|
|
it('clears paid inputs for RECEIVABLE and ON_ACCOUNT and shows backend settlement semantics', async () => {
|
|
|
|
|
vi.mocked(listContainerFees).mockResolvedValue([{ id: '2', loadingPlanId: '9', containerBookingId: '99', containerNo: null, feeItemId: '7', feeItemName: '已付拖车费', direction: 'PAYABLE', supplierId: '41', supplierName: '承运商A', supplierCategory: 'TRUCKING', settlementMode: 'DIRECT_PAID', paidAt: '2026-08-12T10:00:00', paymentMethod: 'BANK_TRANSFER', paymentReferenceNo: 'BANK-1', paymentVoucherFileIds: ['9001'], businessDate: '2026-08-12', amount: 100, currency: 'CNY', reviewStatus: 'PENDING', sourceStatus: 'ACTIVE', sourceVersion: 1, remark: null, reviewerName: null, reviewedAt: null, reviewReason: null }])
|
|
|
|
|
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '操作费', feeCode: 'TRUCKING', supplierCategory: 'TRUCKING', warehouseReceivable: true, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
|
|
|
|
|
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '操作费', feeCode: 'TRUCKING', defaultPrice: null, supplierCategory: 'TRUCKING', warehouseReceivable: true, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
|
|
|
|
|
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view', 'platform:supplier:list'])
|
|
|
|
|
expect(wrapper.text()).toContain('已付成本'); expect(wrapper.text()).toContain('银行转账'); expect(wrapper.text()).toContain('BANK-1')
|
|
|
|
|
const state = (wrapper.vm.$ as unknown as { setupState: { openCreate(): void; form: { direction: string; settlementMode: string | null; paidAt: string | null; paymentMethod: string | null; paymentReferenceNo: string | null; paymentVoucherFileIds: string[] | null } } }).setupState
|
|
|
|
@@ -259,7 +343,7 @@ describe('ContainerFeesPanel contract', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('fails closed without DIRECT_PAID evidence and submits a scoped uploaded voucher', async () => {
|
|
|
|
|
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '操作费', feeCode: 'TRUCKING', supplierCategory: 'TRUCKING', warehouseReceivable: false, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
|
|
|
|
|
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '操作费', feeCode: 'TRUCKING', defaultPrice: null, supplierCategory: 'TRUCKING', warehouseReceivable: false, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
|
|
|
|
|
vi.mocked(listContainerFeeSupplierOptions).mockResolvedValue([{ id: '41', supplierName: '承运商A', category: 'TRUCKING' }])
|
|
|
|
|
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view', 'platform:supplier:list'])
|
|
|
|
|
const state = (wrapper.vm.$ as unknown as { setupState: { openCreate(): void; save(): Promise<void>; selectVoucher(event: Event): Promise<void>; form: { direction: string; feeItemId: string; supplierId: string | null; settlementMode: string | null; paidAt: string | null; paymentMethod: string | null; paymentReferenceNo: string | null; amount: number } } }).setupState
|
|
|
|
|