补齐人工复核转换和列表筛选

This commit is contained in:
andy
2026-07-09 18:38:29 +08:00
parent e12bfd77e1
commit 1282d4aede
12 changed files with 983 additions and 15 deletions

View File

@@ -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({

View File

@@ -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',
})
})
})

View File

@@ -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,