补齐人工复核转换和列表筛选
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
convertReservationManualReviewTask,
|
||||
fetchReservationOrderDetail,
|
||||
fetchReservationOrders,
|
||||
fetchReservationTaskDetail,
|
||||
@@ -76,6 +77,31 @@ describe('reservationService real API mode', () => {
|
||||
expect(result.items[0]?.source_subject).toBe('Booking Request')
|
||||
})
|
||||
|
||||
it('passes task subtype and terminal task status filters to the backend', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
items: [],
|
||||
page: {
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
total: 0,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
await fetchReservationTaskList({
|
||||
task_subtype: 'RATE_CHANGE',
|
||||
task_status: 'FAILED',
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/reservation/tasks?hotel_id=HOTEL-TEST&task_subtype=RATE_CHANGE&task_status=FAILED&page_num=1&page_size=20',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('fetches the order list from the backend by default', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
@@ -117,6 +143,70 @@ describe('reservationService real API mode', () => {
|
||||
expect(result.items[0]?.next_processable_task_id).toBe('10002')
|
||||
})
|
||||
|
||||
it('passes order status and business-key filters to the backend', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
items: [],
|
||||
page: {
|
||||
page_num: 2,
|
||||
page_size: 10,
|
||||
total: 0,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
await fetchReservationOrders({
|
||||
order_status: 'LOGIC_DELETED',
|
||||
group_code: 'GRP-001',
|
||||
confirmation_number: 'CNF-001',
|
||||
keyword: 'VIP',
|
||||
page_num: 2,
|
||||
page_size: 10,
|
||||
})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/reservation/orders?hotel_id=HOTEL-TEST&order_status=LOGIC_DELETED&group_code=GRP-001&confirmation_number=CNF-001&keyword=VIP&page_num=2&page_size=10',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('converts a manual review task through the backend endpoint', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
task_id: '10001',
|
||||
order_id: '20002',
|
||||
original_order_id: '20001',
|
||||
system_task_type: 'UPDATE_BOOKING',
|
||||
task_card_type: 'UPDATE_BOOKING',
|
||||
task_status: 'PENDING_CONFIRM',
|
||||
original_order_logic_deleted: true,
|
||||
}),
|
||||
)
|
||||
|
||||
const result = await convertReservationManualReviewTask('10001', {
|
||||
target_task_type: 'UPDATE_BOOKING',
|
||||
target_order_id: '20002',
|
||||
reason: 'Matched by group code',
|
||||
})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/reservation/tasks/10001/manual-review-conversions',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: expect.objectContaining({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
target_task_type: 'UPDATE_BOOKING',
|
||||
target_order_id: '20002',
|
||||
reason: 'Matched by group code',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
expect(result.system_task_type).toBe('UPDATE_BOOKING')
|
||||
expect(result.original_order_logic_deleted).toBe(true)
|
||||
})
|
||||
|
||||
it('fetches order detail from the backend by default', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
|
||||
@@ -6,6 +6,9 @@ import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
import zhCN from '@/i18n/locales/zh-CN'
|
||||
import ReservationTaskDetailPanel from '@/components/reservation/ReservationTaskDetailPanel.vue'
|
||||
import type {
|
||||
ReservationManualReviewConversionResult,
|
||||
ReservationOrderListItem,
|
||||
ReservationOrderListResult,
|
||||
ReservationTaskAuditLogResult,
|
||||
ReservationTaskDetailResult,
|
||||
ReservationTaskFieldResult,
|
||||
@@ -18,6 +21,8 @@ vi.mock('@/services/reservationService', async (importOriginal) => {
|
||||
...actual,
|
||||
fetchReservationTaskDetail: vi.fn(),
|
||||
fetchReservationTaskAudits: vi.fn(),
|
||||
fetchReservationOrders: vi.fn(),
|
||||
convertReservationManualReviewTask: vi.fn(),
|
||||
saveReservationTaskDraft: vi.fn(),
|
||||
confirmReservationTask: vi.fn(),
|
||||
}
|
||||
@@ -70,6 +75,50 @@ function createMutationResult(taskStatus = 'PENDING_CONFIRM'): ReservationTaskPa
|
||||
}
|
||||
}
|
||||
|
||||
function createManualReviewConversionResult(
|
||||
overrides: Partial<ReservationManualReviewConversionResult> = {},
|
||||
): ReservationManualReviewConversionResult {
|
||||
return {
|
||||
task_id: '10001',
|
||||
order_id: '20001',
|
||||
original_order_id: '20001',
|
||||
system_task_type: 'NEW_BOOKING',
|
||||
task_card_type: 'NEW_BOOKING',
|
||||
task_status: 'PENDING_CONFIRM',
|
||||
original_order_logic_deleted: false,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function createOrderCandidate(overrides: Partial<ReservationOrderListItem> = {}): ReservationOrderListItem {
|
||||
return {
|
||||
order_id: '20002',
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
order_status: 'ACTIVE',
|
||||
display_order_key: 'GRP-001',
|
||||
temporary_order_no: null,
|
||||
confirmation_number: 'CNF-001',
|
||||
group_code: 'GRP-001',
|
||||
display_name: 'GRP-001',
|
||||
open_task_count: 1,
|
||||
next_processable_task_id: null,
|
||||
created_at: '2026-07-08T03:00:00Z',
|
||||
updated_at: '2026-07-08T03:10:00Z',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function createOrderCandidateResult(items: ReservationOrderListItem[]): ReservationOrderListResult {
|
||||
return {
|
||||
items,
|
||||
page: {
|
||||
page_num: 1,
|
||||
page_size: 8,
|
||||
total: items.length,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createAudit(auditId: string, action: string): ReservationTaskAuditLogResult {
|
||||
return {
|
||||
audit_id: auditId,
|
||||
@@ -162,6 +211,8 @@ describe('ReservationTaskDetailPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(service.fetchReservationTaskDetail).mockReset()
|
||||
vi.mocked(service.fetchReservationTaskAudits).mockReset()
|
||||
vi.mocked(service.fetchReservationOrders).mockReset()
|
||||
vi.mocked(service.convertReservationManualReviewTask).mockReset()
|
||||
vi.mocked(service.saveReservationTaskDraft).mockReset()
|
||||
vi.mocked(service.confirmReservationTask).mockReset()
|
||||
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(createTaskDetail())
|
||||
@@ -169,6 +220,8 @@ describe('ReservationTaskDetailPanel', () => {
|
||||
task_id: '10001',
|
||||
items: [],
|
||||
})
|
||||
vi.mocked(service.fetchReservationOrders).mockResolvedValue(createOrderCandidateResult([]))
|
||||
vi.mocked(service.convertReservationManualReviewTask).mockResolvedValue(createManualReviewConversionResult())
|
||||
})
|
||||
|
||||
it('shows draft save errors instead of leaving an unhandled rejection', async () => {
|
||||
@@ -335,4 +388,115 @@ describe('ReservationTaskDetailPanel', () => {
|
||||
expect(wrapper.text()).toContain('查看邮件会话')
|
||||
expect(wrapper.text()).not.toContain('接口待补')
|
||||
})
|
||||
|
||||
it('converts a manual review task to a new booking without a target order', async () => {
|
||||
vi.mocked(service.fetchReservationTaskDetail)
|
||||
.mockResolvedValueOnce(
|
||||
createTaskDetail({
|
||||
system_task_type: 'MANUAL_REVIEW',
|
||||
task_card_type: 'MANUAL_REVIEW',
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
createTaskDetail({
|
||||
system_task_type: 'NEW_BOOKING',
|
||||
task_card_type: 'NEW_BOOKING',
|
||||
}),
|
||||
)
|
||||
vi.mocked(service.fetchReservationTaskAudits)
|
||||
.mockResolvedValueOnce({
|
||||
task_id: '10001',
|
||||
items: [createAudit('audit-1', 'TASK_LOADED')],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
task_id: '10001',
|
||||
items: [createAudit('audit-2', 'MANUAL_REVIEW_CONVERT')],
|
||||
})
|
||||
|
||||
const wrapper = await mountPanel()
|
||||
expect(wrapper.text()).toContain('人工转换')
|
||||
|
||||
await wrapper.find('select[name="manual_target_task_type"]').setValue('NEW_BOOKING')
|
||||
await wrapper.find('textarea[name="manual_conversion_reason"]').setValue('识别为新预订')
|
||||
await findButton(wrapper, '转换任务').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(service.convertReservationManualReviewTask).toHaveBeenCalledWith('10001', {
|
||||
target_task_type: 'NEW_BOOKING',
|
||||
reason: '识别为新预订',
|
||||
})
|
||||
expect(service.fetchReservationTaskDetail).toHaveBeenCalledTimes(2)
|
||||
expect(service.fetchReservationTaskAudits).toHaveBeenCalledTimes(2)
|
||||
expect(wrapper.text()).toContain('人工转换已完成')
|
||||
expect(wrapper.text()).toContain('人工转换')
|
||||
expect(wrapper.text()).not.toContain('MANUAL_REVIEW_CONVERT')
|
||||
})
|
||||
|
||||
it('requires a non-deleted target order when converting manual review to update booking', async () => {
|
||||
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
|
||||
createTaskDetail({
|
||||
system_task_type: 'MANUAL_REVIEW',
|
||||
task_card_type: 'MANUAL_REVIEW',
|
||||
}),
|
||||
)
|
||||
vi.mocked(service.fetchReservationOrders).mockResolvedValue(
|
||||
createOrderCandidateResult([
|
||||
createOrderCandidate({
|
||||
order_id: '20002',
|
||||
display_order_key: 'GRP-ACTIVE',
|
||||
group_code: 'GRP-ACTIVE',
|
||||
confirmation_number: 'CNF-ACTIVE',
|
||||
order_status: 'ACTIVE',
|
||||
}),
|
||||
createOrderCandidate({
|
||||
order_id: '20003',
|
||||
display_order_key: 'GRP-DELETED',
|
||||
group_code: 'GRP-DELETED',
|
||||
confirmation_number: 'CNF-DELETED',
|
||||
order_status: 'LOGIC_DELETED',
|
||||
}),
|
||||
]),
|
||||
)
|
||||
vi.mocked(service.convertReservationManualReviewTask).mockResolvedValue(
|
||||
createManualReviewConversionResult({
|
||||
order_id: '20002',
|
||||
original_order_id: '20001',
|
||||
system_task_type: 'UPDATE_BOOKING',
|
||||
task_card_type: 'UPDATE_BOOKING',
|
||||
}),
|
||||
)
|
||||
|
||||
const wrapper = await mountPanel()
|
||||
|
||||
await wrapper.find('select[name="manual_target_task_type"]').setValue('UPDATE_BOOKING')
|
||||
await findButton(wrapper, '转换任务').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(service.convertReservationManualReviewTask).not.toHaveBeenCalled()
|
||||
expect(wrapper.text()).toContain('请选择目标订单')
|
||||
|
||||
await wrapper.find('input[name="manual_target_order_keyword"]').setValue('GRP')
|
||||
await findButton(wrapper, '搜索订单').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(service.fetchReservationOrders).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
keyword: 'GRP',
|
||||
page_num: 1,
|
||||
page_size: 8,
|
||||
}),
|
||||
)
|
||||
expect(wrapper.text()).toContain('GRP-ACTIVE')
|
||||
expect(wrapper.text()).toContain('GRP-DELETED')
|
||||
expect(wrapper.find('input[value="20003"]').attributes('disabled')).toBeDefined()
|
||||
|
||||
await wrapper.find('input[value="20002"]').setValue()
|
||||
await findButton(wrapper, '转换任务').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(service.convertReservationManualReviewTask).toHaveBeenCalledWith('10001', {
|
||||
target_task_type: 'UPDATE_BOOKING',
|
||||
target_order_id: '20002',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -310,6 +310,29 @@ describe('reservation P0 views', () => {
|
||||
expect(wrapper.text()).not.toContain('接口待接入')
|
||||
})
|
||||
|
||||
it('filters order list with order status and business-key fields', async () => {
|
||||
vi.mocked(service.fetchReservationOrders).mockResolvedValue(createOrderListResult())
|
||||
|
||||
const wrapper = await mountWithPlugins(ReservationOrderListView)
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
await wrapper.find('select[name="order_status"]').setValue('LOGIC_DELETED')
|
||||
await vi.dynamicImportSettled()
|
||||
await wrapper.find('input[name="group_code"]').setValue('GRP-009')
|
||||
await vi.dynamicImportSettled()
|
||||
await wrapper.find('input[name="confirmation_number"]').setValue('CNF-009')
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
expect(service.fetchReservationOrders).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
order_status: 'LOGIC_DELETED',
|
||||
group_code: 'GRP-009',
|
||||
confirmation_number: 'CNF-009',
|
||||
page_num: 1,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('paginates order list requests with backend page metadata', async () => {
|
||||
vi.mocked(service.fetchReservationOrders)
|
||||
.mockResolvedValueOnce(createOrderListResult('GRP-001', { page_num: 1, page_size: 20, total: 41 }))
|
||||
@@ -363,26 +386,31 @@ describe('reservation P0 views', () => {
|
||||
.mockResolvedValueOnce(createTaskListResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 }))
|
||||
.mockResolvedValueOnce(createTaskListResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 }))
|
||||
.mockResolvedValueOnce(createTaskListResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 }))
|
||||
.mockResolvedValueOnce(createTaskListResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 }))
|
||||
.mockResolvedValueOnce(createTaskListResult('GRP-002', 'Booking Update', { page_num: 2, page_size: 20, total: 45 }))
|
||||
|
||||
const wrapper = await mountWithPlugins(ReservationTaskListView)
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
const selects = wrapper.findAll('select')
|
||||
expect(wrapper.text()).not.toContain('订单状态筛选待后端接入')
|
||||
expect(selects[2]!.attributes('disabled')).toBeUndefined()
|
||||
expect(wrapper.find('select[name="order_status"]').attributes('disabled')).toBeUndefined()
|
||||
expect(wrapper.find('select[name="task_status"]').text()).toContain('执行中')
|
||||
expect(wrapper.find('select[name="task_status"]').text()).toContain('失败')
|
||||
|
||||
await selects[1]!.setValue('READY')
|
||||
await wrapper.find('select[name="task_status"]').setValue('FAILED')
|
||||
await vi.dynamicImportSettled()
|
||||
await selects[2]!.setValue('ENDED')
|
||||
await wrapper.find('select[name="task_subtype"]').setValue('RATE_CHANGE')
|
||||
await vi.dynamicImportSettled()
|
||||
await selects[3]!.setValue('true')
|
||||
await wrapper.find('select[name="order_status"]').setValue('ENDED')
|
||||
await vi.dynamicImportSettled()
|
||||
await wrapper.find('select[name="queue_participation"]').setValue('true')
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
const taskListCalls = vi.mocked(service.fetchReservationTaskList).mock.calls
|
||||
const filteredTaskRequest = taskListCalls[taskListCalls.length - 1]?.[0] ?? {}
|
||||
expect(filteredTaskRequest).toMatchObject({
|
||||
task_status: 'READY',
|
||||
task_status: 'FAILED',
|
||||
task_subtype: 'RATE_CHANGE',
|
||||
order_status: 'ENDED',
|
||||
queue_participation: true,
|
||||
page_num: 1,
|
||||
@@ -393,7 +421,8 @@ describe('reservation P0 views', () => {
|
||||
|
||||
expect(service.fetchReservationTaskList).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
task_status: 'READY',
|
||||
task_status: 'FAILED',
|
||||
task_subtype: 'RATE_CHANGE',
|
||||
order_status: 'ENDED',
|
||||
queue_participation: true,
|
||||
page_num: 2,
|
||||
|
||||
Reference in New Issue
Block a user