1265 lines
44 KiB
TypeScript
1265 lines
44 KiB
TypeScript
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { createI18n } from 'vue-i18n'
|
|
import { createMemoryHistory, createRouter } from 'vue-router'
|
|
|
|
import zhCN from '@/i18n/locales/zh-CN'
|
|
import ReservationTaskDetailPanel from '@/components/reservation/ReservationTaskDetailPanel.vue'
|
|
import { useAuthStore } from '@/stores/authStore'
|
|
import type {
|
|
ReservationManualReviewConversionResult,
|
|
ReservationManualReviewResolutionResult,
|
|
ReservationOrderListItem,
|
|
ReservationOrderListResult,
|
|
ReservationTaskAuditLogResult,
|
|
ReservationTaskDetailResult,
|
|
ReservationTaskFieldResult,
|
|
ReservationTaskPayloadMutationResult,
|
|
} from '@/types/reservation'
|
|
|
|
vi.mock('@/services/reservationService', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('@/services/reservationService')>()
|
|
return {
|
|
...actual,
|
|
fetchReservationTaskDetail: vi.fn(),
|
|
fetchReservationTaskAudits: vi.fn(),
|
|
fetchReservationOrders: vi.fn(),
|
|
convertReservationManualReviewTask: vi.fn(),
|
|
resolveReservationManualReviewTask: vi.fn(),
|
|
saveReservationTaskDraft: vi.fn(),
|
|
confirmReservationTask: vi.fn(),
|
|
}
|
|
})
|
|
|
|
const service = await import('@/services/reservationService')
|
|
|
|
function createTaskDetail(overrides: Partial<ReservationTaskDetailResult> = {}): ReservationTaskDetailResult {
|
|
const detail: ReservationTaskDetailResult = {
|
|
task_id: '10001',
|
|
order_id: '20001',
|
|
source_message_id: '30001',
|
|
source_subject: 'Booking Request',
|
|
source_sender_summary: 'guest@example.test',
|
|
source_received_at: '2026-07-08T03:00:00Z',
|
|
external_conversation_id: 'thread-30001',
|
|
conversation_message_count: 2,
|
|
system_task_type: 'NEW_BOOKING',
|
|
task_card_type: 'NEW_BOOKING',
|
|
task_status: 'PENDING_CONFIRM',
|
|
field_contract_version: '20260708-3.0',
|
|
draft_payload: null,
|
|
confirmed_payload: null,
|
|
availability: {
|
|
blocked: false,
|
|
read_only: false,
|
|
editable: true,
|
|
confirmable: true,
|
|
executable: false,
|
|
blocked_by_task_id: null,
|
|
blocked_reason: null,
|
|
},
|
|
fields: [],
|
|
opera_operations: [],
|
|
}
|
|
return {
|
|
...detail,
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
function createSourceMessageOnlyTaskDetail(
|
|
overrides: Partial<ReservationTaskDetailResult> = {},
|
|
): ReservationTaskDetailResult {
|
|
return createTaskDetail({
|
|
result_type: 'source_message_review_notification',
|
|
ai_task_type: 'Message Notification',
|
|
task_subtype: 'S10',
|
|
route_code: 'S10',
|
|
system_process_category: 'SOURCE_MESSAGE_NOTIFICATION',
|
|
system_task_type: 'MESSAGE_NOTIFICATION',
|
|
task_card_type: 'MESSAGE_NOTIFICATION',
|
|
task_status: 'COMPLETED',
|
|
fields: [],
|
|
opera_operations: [],
|
|
availability: {
|
|
blocked: false,
|
|
read_only: true,
|
|
editable: false,
|
|
confirmable: false,
|
|
executable: false,
|
|
blocked_by_task_id: null,
|
|
blocked_reason: null,
|
|
},
|
|
source_message_only_result: {
|
|
entry_result_code: 'S10',
|
|
entry_result_meaning: 'PURE_INFORMATION',
|
|
entry_result_description: '纯信息类邮件',
|
|
entry_result_source_message_id: '30001',
|
|
result_type: 'source_message_review_notification',
|
|
route_code: 'S10',
|
|
agent_assessment: {
|
|
intent: 'informational',
|
|
},
|
|
notification: {
|
|
current_message_summary: '仅同步信息,不需要订单动作。',
|
|
},
|
|
manual_review: null,
|
|
raw_answer: 'S10,30001',
|
|
},
|
|
...overrides,
|
|
})
|
|
}
|
|
|
|
function createMutationResult(taskStatus = 'PENDING_CONFIRM'): ReservationTaskPayloadMutationResult {
|
|
return {
|
|
task_id: '10001',
|
|
order_id: '20001',
|
|
task_status: taskStatus,
|
|
draft_payload: {},
|
|
confirmed_payload: taskStatus === 'READY' ? {} : null,
|
|
opera_operations: [],
|
|
}
|
|
}
|
|
|
|
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 createManualReviewResolutionResult(
|
|
overrides: Partial<ReservationManualReviewResolutionResult> = {},
|
|
): ReservationManualReviewResolutionResult {
|
|
return {
|
|
task_id: '10001',
|
|
order_id: '20001',
|
|
task_status: 'READY',
|
|
review_status: 'RESOLVED',
|
|
review_resolution: {
|
|
confirmed_order_id: '20001',
|
|
field_overrides: [
|
|
{
|
|
field_pointer: '/extracted_fields/room_items/0/pms_room_type_code',
|
|
field_path: 'extracted_fields.room_items.0.pms_room_type_code',
|
|
legacy_field_path: 'extracted_fields.pms_room_type_code',
|
|
value: 'RM3',
|
|
},
|
|
],
|
|
},
|
|
confirmed_payload: {
|
|
field_values: {
|
|
'extracted_fields.room_items.0.pms_room_type_code': 'RM3',
|
|
},
|
|
legacy_field_values: {
|
|
'extracted_fields.pms_room_type_code': 'RM3',
|
|
},
|
|
},
|
|
opera_operations: [],
|
|
...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,
|
|
open_work_item_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,
|
|
order_id: '20001',
|
|
task_id: '10001',
|
|
operation_id: null,
|
|
actor_type: 'USER',
|
|
actor_id: 'andy',
|
|
action,
|
|
reason: null,
|
|
before_snapshot: null,
|
|
after_snapshot: null,
|
|
occurred_at: '2026-07-08T03:00:00Z',
|
|
}
|
|
}
|
|
|
|
function createOperaOperation(): ReservationTaskDetailResult['opera_operations'][number] {
|
|
return {
|
|
operation_id: 'op-1',
|
|
task_id: '10001',
|
|
operation_sequence: 1,
|
|
operation_code: 'CREATE_RESERVATION',
|
|
operation_name: 'Create reservation',
|
|
operation_status: 'PENDING',
|
|
attempt_count: 0,
|
|
last_error_message: null,
|
|
attempts: [],
|
|
}
|
|
}
|
|
|
|
function createRequiredField(overrides: Partial<ReservationTaskFieldResult> = {}): ReservationTaskFieldResult {
|
|
return {
|
|
row_number: 1,
|
|
card_name: 'New Booking',
|
|
display_area: '客人信息',
|
|
field_path: 'guest.name',
|
|
display_name: '客人姓名',
|
|
visible: 'Y',
|
|
editable: 'Y',
|
|
input_editable: 'Y',
|
|
select_editable: 'N',
|
|
date_picker: 'N',
|
|
number_input: 'N',
|
|
file_display: 'N',
|
|
table_editable: 'N',
|
|
enum_options: null,
|
|
required_rule: 'Y',
|
|
display_condition: null,
|
|
validation_rule: null,
|
|
write_path: null,
|
|
opera_write_participation: 'N',
|
|
opera_parameter_mapping: null,
|
|
notes: null,
|
|
value: '',
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
function createAuthPayload(permissions: string[]) {
|
|
return {
|
|
access_token: 'session-token',
|
|
token_type: 'Bearer',
|
|
expires_at: '2026-07-09T12:00:00Z',
|
|
user: {
|
|
id: '1900000000000000001',
|
|
username: 'admin',
|
|
display_name: '系统管理员',
|
|
super_admin: true,
|
|
},
|
|
default_hotel_id: 'HOTEL-TEST',
|
|
hotels: [
|
|
{
|
|
hotel_id: 'HOTEL-TEST',
|
|
hotel_name: '测试酒店',
|
|
time_zone: 'Asia/Bangkok',
|
|
default_hotel: true,
|
|
},
|
|
],
|
|
permissions,
|
|
menus: [],
|
|
}
|
|
}
|
|
|
|
const defaultTaskPermissions = [
|
|
'RESERVATION_TASK_READ',
|
|
'RESERVATION_TASK_EDIT',
|
|
'RESERVATION_TASK_CONFIRM',
|
|
'RESERVATION_OPERA_SIM_EXECUTE',
|
|
'RESERVATION_AUDIT_READ',
|
|
]
|
|
|
|
async function mountPanel(permissions = defaultTaskPermissions) {
|
|
const i18n = createI18n({
|
|
legacy: false,
|
|
locale: 'zh-CN',
|
|
messages: {
|
|
'zh-CN': zhCN,
|
|
},
|
|
})
|
|
const router = createRouter({
|
|
history: createMemoryHistory(),
|
|
routes: [
|
|
{ path: '/reservation/tasks/:taskId', component: { template: '<div />' } },
|
|
{ path: '/reservation/orders/:orderId', component: { template: '<div />' } },
|
|
{ path: '/reservation/source-messages/:sourceMessageId/conversation', component: { template: '<div />' } },
|
|
],
|
|
})
|
|
const pinia = createPinia()
|
|
setActivePinia(pinia)
|
|
useAuthStore().applyLoginResult(createAuthPayload(permissions))
|
|
await router.push('/reservation/tasks/10001')
|
|
await router.isReady()
|
|
|
|
const wrapper = mount(ReservationTaskDetailPanel, {
|
|
props: {
|
|
taskId: '10001',
|
|
},
|
|
global: {
|
|
plugins: [i18n, router, pinia],
|
|
stubs: {
|
|
RouterLink: {
|
|
template: '<a><slot /></a>',
|
|
},
|
|
},
|
|
},
|
|
})
|
|
await flushPromises()
|
|
return wrapper
|
|
}
|
|
|
|
function findButton(wrapper: VueWrapper, label: string) {
|
|
const button = wrapper.findAll('button').find((item) => item.text().includes(label))
|
|
if (!button) {
|
|
throw new Error(`Button ${label} was not found.`)
|
|
}
|
|
return button
|
|
}
|
|
|
|
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.resolveReservationManualReviewTask).mockReset()
|
|
vi.mocked(service.saveReservationTaskDraft).mockReset()
|
|
vi.mocked(service.confirmReservationTask).mockReset()
|
|
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(createTaskDetail())
|
|
vi.mocked(service.fetchReservationTaskAudits).mockResolvedValue({
|
|
task_id: '10001',
|
|
items: [],
|
|
})
|
|
vi.mocked(service.fetchReservationOrders).mockResolvedValue(createOrderCandidateResult([]))
|
|
vi.mocked(service.convertReservationManualReviewTask).mockResolvedValue(createManualReviewConversionResult())
|
|
vi.mocked(service.resolveReservationManualReviewTask).mockResolvedValue(createManualReviewResolutionResult())
|
|
sessionStorage.clear()
|
|
})
|
|
|
|
it('shows draft save errors instead of leaving an unhandled rejection', async () => {
|
|
vi.mocked(service.saveReservationTaskDraft).mockRejectedValue(new Error('Draft save failed'))
|
|
|
|
const wrapper = await mountPanel()
|
|
await findButton(wrapper, '保存草稿').trigger('click')
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).toContain('Draft save failed')
|
|
expect(findButton(wrapper, '保存草稿').attributes('disabled')).toBeUndefined()
|
|
})
|
|
|
|
it('shows confirm errors instead of leaving an unhandled rejection', async () => {
|
|
vi.mocked(service.confirmReservationTask).mockRejectedValue(new Error('Confirm failed'))
|
|
|
|
const wrapper = await mountPanel()
|
|
await findButton(wrapper, '确认').trigger('click')
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).toContain('Confirm failed')
|
|
expect(findButton(wrapper, '确认').attributes('disabled')).toBeUndefined()
|
|
})
|
|
|
|
it('blocks confirmation when frontend field validation fails', async () => {
|
|
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
|
|
createTaskDetail({
|
|
fields: [createRequiredField()],
|
|
}),
|
|
)
|
|
|
|
const wrapper = await mountPanel()
|
|
await findButton(wrapper, '确认').trigger('click')
|
|
await flushPromises()
|
|
|
|
expect(service.confirmReservationTask).not.toHaveBeenCalled()
|
|
expect(wrapper.text()).toContain('请先修正字段')
|
|
expect(wrapper.text()).toContain('客人姓名为必填项')
|
|
})
|
|
|
|
it('keeps remaining validation errors and clears the summary after fields are fixed', async () => {
|
|
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
|
|
createTaskDetail({
|
|
fields: [
|
|
createRequiredField(),
|
|
createRequiredField({
|
|
field_path: 'room.type',
|
|
display_name: '房型',
|
|
}),
|
|
],
|
|
}),
|
|
)
|
|
|
|
const wrapper = await mountPanel()
|
|
await findButton(wrapper, '确认').trigger('click')
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).toContain('请先修正字段')
|
|
expect(wrapper.text()).toContain('客人姓名为必填项')
|
|
expect(wrapper.text()).toContain('房型为必填项')
|
|
|
|
const textareas = wrapper.findAll('textarea')
|
|
await textareas[0]!.setValue('王建国')
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).toContain('请先修正字段')
|
|
expect(wrapper.text()).not.toContain('客人姓名为必填项')
|
|
expect(wrapper.text()).toContain('房型为必填项')
|
|
|
|
await textareas[1]!.setValue('豪华 Q1A')
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).not.toContain('请先修正字段')
|
|
expect(wrapper.text()).not.toContain('客人姓名为必填项')
|
|
expect(wrapper.text()).not.toContain('房型为必填项')
|
|
})
|
|
|
|
it('hydrates code-v1 legacy payload values but saves future drafts with P0 field paths', async () => {
|
|
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
|
|
createTaskDetail({
|
|
field_contract_version: 'code-v1',
|
|
draft_payload: {
|
|
field_values: {
|
|
'extracted_fields.room_items.0.pms_room_type_code': 'P0-SHOULD-NOT-WIN',
|
|
},
|
|
legacy_field_values: {
|
|
'extracted_fields.pms_room_type_code': 'LEGACY-RM',
|
|
},
|
|
},
|
|
fields: [
|
|
createRequiredField({
|
|
field_path: 'extracted_fields.room_items.0.pms_room_type_code',
|
|
field_pointer: '/extracted_fields/room_items/0/pms_room_type_code',
|
|
legacy_field_path: 'extracted_fields.pms_room_type_code',
|
|
display_name: 'PMS 房型代码',
|
|
value: null,
|
|
}),
|
|
],
|
|
}),
|
|
)
|
|
vi.mocked(service.saveReservationTaskDraft).mockResolvedValue(createMutationResult())
|
|
|
|
const wrapper = await mountPanel()
|
|
const pmsInput = wrapper.find('textarea')
|
|
|
|
expect((pmsInput.element as HTMLTextAreaElement).value).toBe('LEGACY-RM')
|
|
expect(wrapper.text()).not.toContain('P0-SHOULD-NOT-WIN')
|
|
|
|
await pmsInput.setValue('RM4')
|
|
await findButton(wrapper, '保存草稿').trigger('click')
|
|
await flushPromises()
|
|
|
|
expect(service.saveReservationTaskDraft).toHaveBeenCalledWith('10001', {
|
|
field_values: {
|
|
'extracted_fields.room_items.0.pms_room_type_code': 'RM4',
|
|
},
|
|
})
|
|
})
|
|
|
|
it('refreshes audit records after saving a draft', async () => {
|
|
vi.mocked(service.fetchReservationTaskAudits)
|
|
.mockResolvedValueOnce({
|
|
task_id: '10001',
|
|
items: [createAudit('audit-1', 'TASK_LOADED')],
|
|
})
|
|
.mockResolvedValueOnce({
|
|
task_id: '10001',
|
|
items: [createAudit('audit-2', 'DRAFT_SAVED')],
|
|
})
|
|
vi.mocked(service.saveReservationTaskDraft).mockResolvedValue(createMutationResult())
|
|
|
|
const wrapper = await mountPanel()
|
|
expect(wrapper.text()).toContain('任务读取')
|
|
|
|
await findButton(wrapper, '保存草稿').trigger('click')
|
|
await flushPromises()
|
|
|
|
expect(service.fetchReservationTaskAudits).toHaveBeenCalledTimes(2)
|
|
expect(wrapper.text()).toContain('草稿保存')
|
|
expect(wrapper.text()).not.toContain('任务读取')
|
|
expect(wrapper.text()).not.toContain('DRAFT_SAVED')
|
|
expect(wrapper.text()).not.toContain('TASK_LOADED')
|
|
})
|
|
|
|
it('saves only editable active fields to match backend task field validation', async () => {
|
|
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
|
|
createTaskDetail({
|
|
fields: [
|
|
createRequiredField({
|
|
field_path: 'visible_reason',
|
|
display_name: '生成原因',
|
|
editable: 'N',
|
|
input_editable: 'N',
|
|
required_rule: 'Y',
|
|
value: 'AI extracted this task.',
|
|
}),
|
|
createRequiredField({
|
|
field_path: 'case_keys.group_code',
|
|
display_name: 'Group Code',
|
|
display_condition: '对象为Group Block或Allotment时展示',
|
|
value: 'GRP-001',
|
|
}),
|
|
createRequiredField({
|
|
field_path: 'extracted_fields.booking_object_type',
|
|
display_name: '订单对象类型',
|
|
select_editable: 'Y',
|
|
input_editable: 'N',
|
|
enum_options: 'Group Block, FIT Reservation',
|
|
value: 'Group Block',
|
|
}),
|
|
],
|
|
}),
|
|
)
|
|
vi.mocked(service.saveReservationTaskDraft).mockResolvedValue(createMutationResult())
|
|
|
|
const wrapper = await mountPanel()
|
|
await findButton(wrapper, '保存草稿').trigger('click')
|
|
await flushPromises()
|
|
|
|
expect(service.saveReservationTaskDraft).toHaveBeenCalledWith(
|
|
'10001',
|
|
{
|
|
field_values: {
|
|
'case_keys.group_code': 'GRP-001',
|
|
'extracted_fields.booking_object_type': 'Group Block',
|
|
},
|
|
},
|
|
)
|
|
})
|
|
|
|
it('renders a CP9 group block detail without assuming FIT or attachment fields exist', async () => {
|
|
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
|
|
createTaskDetail({
|
|
field_contract_version: '20260711-p0',
|
|
ai_task_type: 'New Booking',
|
|
task_subtype: 'new_group_block',
|
|
fields: [
|
|
createRequiredField({
|
|
field_path: 'extracted_fields.booking_object_type',
|
|
display_name: '订单对象类型',
|
|
select_editable: 'Y',
|
|
input_editable: 'N',
|
|
enum_options: 'Group Block, FIT Reservation',
|
|
value: 'Group Block',
|
|
}),
|
|
createRequiredField({
|
|
field_path: 'case_keys.group_code',
|
|
field_pointer: '/case_keys/group_code',
|
|
display_name: 'Group Code',
|
|
value: 'GRP-CP9',
|
|
}),
|
|
],
|
|
}),
|
|
)
|
|
vi.mocked(service.saveReservationTaskDraft).mockResolvedValue(createMutationResult())
|
|
|
|
const wrapper = await mountPanel()
|
|
await findButton(wrapper, '保存草稿').trigger('click')
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).toContain('Group Code')
|
|
expect((wrapper.find('textarea').element as HTMLTextAreaElement).value).toBe('GRP-CP9')
|
|
expect(wrapper.text()).not.toContain('Confirmation No.')
|
|
expect(wrapper.text()).not.toContain('相关附件')
|
|
expect(service.saveReservationTaskDraft).toHaveBeenCalledWith('10001', {
|
|
field_values: {
|
|
'extracted_fields.booking_object_type': 'Group Block',
|
|
'case_keys.group_code': 'GRP-CP9',
|
|
},
|
|
})
|
|
})
|
|
|
|
it('saves drafts only with editable fields opened by backend control metadata', async () => {
|
|
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
|
|
createTaskDetail({
|
|
fields: [
|
|
createRequiredField({
|
|
field_path: 'case_keys.group_code',
|
|
field_pointer: '/case_keys/group_code',
|
|
display_name: 'Group Code',
|
|
value: 'GRP-OPEN',
|
|
control_type: 'text',
|
|
edit_scope: 'normal_and_manual_review',
|
|
write_target: 'field_values',
|
|
raw_readonly: false,
|
|
} as Partial<ReservationTaskFieldResult>),
|
|
createRequiredField({
|
|
field_path: 'route_code',
|
|
display_name: '路由代码',
|
|
value: 'R01_NEW_BOOKING_NORMAL',
|
|
editable: 'Y',
|
|
input_editable: 'Y',
|
|
control_type: 'workflow_state',
|
|
edit_scope: 'system_only',
|
|
write_target: 'none',
|
|
raw_readonly: false,
|
|
} as Partial<ReservationTaskFieldResult>),
|
|
createRequiredField({
|
|
field_path: 'source_message.attachments',
|
|
display_name: '来源附件',
|
|
value: [{ name: 'rooming-list.pdf' }],
|
|
editable: 'Y',
|
|
input_editable: 'Y',
|
|
control_type: 'file',
|
|
edit_scope: 'never',
|
|
write_target: 'none',
|
|
raw_readonly: true,
|
|
} as Partial<ReservationTaskFieldResult>),
|
|
],
|
|
}),
|
|
)
|
|
vi.mocked(service.saveReservationTaskDraft).mockResolvedValue(createMutationResult())
|
|
|
|
const wrapper = await mountPanel()
|
|
await findButton(wrapper, '保存草稿').trigger('click')
|
|
await flushPromises()
|
|
|
|
expect(service.saveReservationTaskDraft).toHaveBeenCalledWith('10001', {
|
|
field_values: {
|
|
'case_keys.group_code': 'GRP-OPEN',
|
|
},
|
|
})
|
|
})
|
|
|
|
it('opens the source conversation by source message id without external conversation id', async () => {
|
|
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
|
|
createTaskDetail({
|
|
external_conversation_id: null,
|
|
}),
|
|
)
|
|
|
|
const wrapper = await mountPanel()
|
|
|
|
expect(wrapper.text()).toContain('查看邮件会话')
|
|
expect(wrapper.text()).not.toContain('接口待补')
|
|
})
|
|
|
|
it('renders source email preview with excerpt, attachments and full conversation entry', async () => {
|
|
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
|
|
createTaskDetail({
|
|
fields: [
|
|
createRequiredField({
|
|
field_path: 'relevant_message_excerpt',
|
|
display_name: '邮件片段',
|
|
value: 'LLT260509FA213 11-13/05/2026 2N 14 TWN +1 DBL',
|
|
editable: 'N',
|
|
input_editable: 'N',
|
|
required_rule: null,
|
|
control_type: 'evidence_text',
|
|
edit_scope: 'never',
|
|
write_target: 'none',
|
|
raw_readonly: true,
|
|
} as Partial<ReservationTaskFieldResult>),
|
|
createRequiredField({
|
|
field_path: 'source_message.attachments',
|
|
display_name: '来源附件',
|
|
value: [
|
|
{
|
|
id: 'att-1',
|
|
name: 'WYNDHAM LIANTAI 2026 UPDATE BOOKING 12-05-2026 NO.1.xlsx',
|
|
content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
size_bytes: 2048,
|
|
},
|
|
],
|
|
editable: 'N',
|
|
input_editable: 'N',
|
|
file_display: 'Y',
|
|
required_rule: null,
|
|
control_type: 'file',
|
|
edit_scope: 'never',
|
|
write_target: 'none',
|
|
raw_readonly: true,
|
|
} as Partial<ReservationTaskFieldResult>),
|
|
],
|
|
}),
|
|
)
|
|
|
|
const wrapper = await mountPanel()
|
|
|
|
expect(wrapper.text()).toContain('来源邮件预览')
|
|
expect(wrapper.text()).toContain('Booking Request')
|
|
expect(wrapper.text()).toContain('guest@example.test')
|
|
expect(wrapper.text()).toContain('2026-07-08 10:00:00')
|
|
expect(wrapper.text()).toContain('LLT260509FA213 11-13/05/2026 2N 14 TWN +1 DBL')
|
|
expect(wrapper.text()).toContain('WYNDHAM LIANTAI 2026 UPDATE BOOKING 12-05-2026 NO.1.xlsx')
|
|
expect(wrapper.text()).toContain('查看完整邮件会话')
|
|
expect(wrapper.text()).not.toContain('{"id":"att-1"')
|
|
})
|
|
|
|
it('does not surface source email excerpt or attachments from unexposed backend context', async () => {
|
|
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
|
|
createTaskDetail({
|
|
fields: [],
|
|
context_used: {
|
|
relevant_message_excerpt: 'Hidden context excerpt',
|
|
attachments: [
|
|
{
|
|
name: 'hidden-context-attachment.pdf',
|
|
},
|
|
],
|
|
},
|
|
manual_review: {
|
|
attachments: [
|
|
{
|
|
name: 'hidden-review-attachment.pdf',
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
)
|
|
|
|
const wrapper = await mountPanel()
|
|
const sourceEmailPreview = wrapper.find('.task-source-email')
|
|
|
|
expect(sourceEmailPreview.text()).toContain('暂无邮件片段')
|
|
expect(sourceEmailPreview.text()).not.toContain('Hidden context excerpt')
|
|
expect(sourceEmailPreview.text()).not.toContain('hidden-context-attachment.pdf')
|
|
expect(sourceEmailPreview.text()).not.toContain('hidden-review-attachment.pdf')
|
|
expect(sourceEmailPreview.text()).not.toContain('相关附件')
|
|
})
|
|
|
|
it('renders source-message-only entry result without editable or OPERA actions', async () => {
|
|
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(createSourceMessageOnlyTaskDetail())
|
|
|
|
const wrapper = await mountPanel()
|
|
|
|
expect(wrapper.text()).toContain('来源消息只读')
|
|
expect(wrapper.text()).toContain('来源消息入口结果')
|
|
expect(wrapper.text()).toContain('该任务仅用于展示来源消息入口结果')
|
|
expect(wrapper.text()).toContain('S10')
|
|
expect(wrapper.text()).toContain('PURE_INFORMATION')
|
|
expect(wrapper.text()).toContain('纯信息类邮件')
|
|
expect(wrapper.text()).toContain('S10,30001')
|
|
expect(wrapper.text()).toContain('来源消息通知')
|
|
expect(wrapper.text()).toContain('查看邮件会话')
|
|
expect(wrapper.text()).not.toContain('查看订单')
|
|
expect(wrapper.text()).not.toContain('保存草稿')
|
|
expect(wrapper.text()).not.toContain('确认任务')
|
|
expect(wrapper.text()).not.toContain('人工转换')
|
|
expect(wrapper.text()).not.toContain('OPERA 模拟操作')
|
|
expect(wrapper.text()).not.toContain('确认任务后生成 OPERA 模拟操作')
|
|
expect(wrapper.find('button[title="执行"]').exists()).toBe(false)
|
|
expect(wrapper.find('button[title="重试"]').exists()).toBe(false)
|
|
})
|
|
|
|
it('renders R08 parent group manual review with identity candidates on the same task card', async () => {
|
|
vi.mocked(service.fetchReservationTaskDetail)
|
|
.mockResolvedValueOnce(
|
|
createTaskDetail({
|
|
result_type: 'manual_review',
|
|
ai_task_type: 'Cancel Allotment',
|
|
task_subtype: 'cancel_allotment_control_block',
|
|
route_code: 'R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_REVIEW',
|
|
system_process_category: 'BUSINESS_TASK',
|
|
system_task_type: 'CANCEL_BOOKING',
|
|
task_card_type: 'CANCEL_ALLOTMENT',
|
|
task_status: 'BLOCKED',
|
|
review_status: 'PENDING',
|
|
manual_review: {
|
|
reason_code: 'target_object_unclear',
|
|
visible_reason: 'Parent Group 的 group_code 与 block_code 原始候选冲突,请人工确认目标。',
|
|
missing_fields: ['/case_keys/group_code', '/case_keys/block_code'],
|
|
blocking_points: ['Parent Group identity cannot be safely normalized.'],
|
|
conflicting_points: ['PARENT-2608-CONFLICT-A', 'PARENT-2608-CONFLICT-B'],
|
|
suggested_human_actions: ['confirm_parent_group_identity'],
|
|
evidence_to_check: ['parent_identity_candidates'],
|
|
},
|
|
context_used: {
|
|
parent_identity_candidates: [
|
|
{
|
|
field: 'group_code',
|
|
value: 'PARENT-2608-CONFLICT-A',
|
|
evidence_source: 'subject',
|
|
},
|
|
{
|
|
field: 'block_code',
|
|
value: 'PARENT-2608-CONFLICT-B',
|
|
evidence_source: 'attachment',
|
|
normalized_value: 'PARENT-2608-CONFLICT-B',
|
|
},
|
|
{
|
|
candidate_field: 'parent_group_code',
|
|
raw_value: 'PARENT-2608-CONFLICT-C',
|
|
origin: 'email_body',
|
|
},
|
|
],
|
|
},
|
|
availability: {
|
|
blocked: true,
|
|
read_only: true,
|
|
editable: false,
|
|
confirmable: false,
|
|
executable: false,
|
|
blocked_by_task_id: null,
|
|
blocked_reason: 'MANUAL_REVIEW_REQUIRED',
|
|
},
|
|
fields: [
|
|
createRequiredField({
|
|
field_path: 'case_keys.group_code',
|
|
field_pointer: '/case_keys/group_code',
|
|
display_name: 'Parent Group 团队号',
|
|
value: null,
|
|
}),
|
|
createRequiredField({
|
|
field_path: 'case_keys.block_code',
|
|
field_pointer: '/case_keys/block_code',
|
|
display_name: 'Parent Group Block Code',
|
|
value: null,
|
|
}),
|
|
],
|
|
}),
|
|
)
|
|
.mockResolvedValueOnce(
|
|
createTaskDetail({
|
|
result_type: 'manual_review',
|
|
ai_task_type: 'Cancel Allotment',
|
|
task_subtype: 'cancel_allotment_control_block',
|
|
route_code: 'R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_REVIEW',
|
|
system_process_category: 'BUSINESS_TASK',
|
|
system_task_type: 'CANCEL_BOOKING',
|
|
task_card_type: 'CANCEL_ALLOTMENT',
|
|
task_status: 'READY',
|
|
review_status: 'RESOLVED',
|
|
review_resolution: {
|
|
confirmed_order_id: '20001',
|
|
field_overrides: [
|
|
{
|
|
field_pointer: '/case_keys/group_code',
|
|
field_path: 'case_keys.group_code',
|
|
value: 'PARENT-2608-CONFLICT-A',
|
|
},
|
|
{
|
|
field_pointer: '/case_keys/block_code',
|
|
field_path: 'case_keys.block_code',
|
|
value: 'PARENT-2608-CONFLICT-A',
|
|
},
|
|
],
|
|
},
|
|
availability: {
|
|
blocked: false,
|
|
read_only: false,
|
|
editable: false,
|
|
confirmable: false,
|
|
executable: true,
|
|
blocked_by_task_id: null,
|
|
blocked_reason: null,
|
|
},
|
|
opera_operations: [createOperaOperation()],
|
|
}),
|
|
)
|
|
vi.mocked(service.fetchReservationTaskAudits)
|
|
.mockResolvedValueOnce({
|
|
task_id: '10001',
|
|
items: [createAudit('audit-1', 'TASK_LOADED')],
|
|
})
|
|
.mockResolvedValueOnce({
|
|
task_id: '10001',
|
|
items: [createAudit('audit-2', 'MANUAL_REVIEW_RESOLVED')],
|
|
})
|
|
|
|
const wrapper = await mountPanel()
|
|
|
|
expect(wrapper.text()).toContain('Parent Group')
|
|
expect(wrapper.text()).toContain('取消 Parent Group 配额')
|
|
expect(wrapper.text()).toContain('取消 Parent Group 整块配额')
|
|
expect(wrapper.text()).toContain('R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_REVIEW')
|
|
expect(wrapper.text()).toContain('人工复核证据')
|
|
expect(wrapper.text()).toContain('target_object_unclear')
|
|
expect(wrapper.text()).toContain('Parent Group 的 group_code 与 block_code 原始候选冲突')
|
|
expect(wrapper.text()).toContain('PARENT-2608-CONFLICT-A')
|
|
expect(wrapper.text()).toContain('PARENT-2608-CONFLICT-B')
|
|
expect(wrapper.text()).toContain('parent_group_code')
|
|
expect(wrapper.text()).toContain('PARENT-2608-CONFLICT-C')
|
|
expect(wrapper.text()).toContain('subject')
|
|
expect(wrapper.text()).toContain('attachment')
|
|
expect(wrapper.text()).toContain('email_body')
|
|
expect(wrapper.text()).toContain('normalized_value')
|
|
expect(wrapper.text()).not.toContain('适配契约异常')
|
|
expect(wrapper.text()).not.toContain('人工转换')
|
|
expect(wrapper.text()).not.toContain('保存草稿')
|
|
expect(wrapper.text()).not.toContain('确认任务')
|
|
|
|
await wrapper.find('input[name="manual_resolution_field_override_0"]').setValue('PARENT-2608-CONFLICT-A')
|
|
await wrapper.find('input[name="manual_resolution_field_override_1"]').setValue('PARENT-2608-CONFLICT-A')
|
|
await wrapper.find('textarea[name="manual_resolution_reason"]').setValue('确认 Parent Group identity')
|
|
await findButton(wrapper, '提交复核结果').trigger('click')
|
|
await flushPromises()
|
|
|
|
expect(service.resolveReservationManualReviewTask).toHaveBeenCalledWith('10001', {
|
|
confirmed_order_id: '20001',
|
|
reason: '确认 Parent Group identity',
|
|
field_overrides: [
|
|
{
|
|
field_pointer: '/case_keys/group_code',
|
|
field_path: 'case_keys.group_code',
|
|
value: 'PARENT-2608-CONFLICT-A',
|
|
},
|
|
{
|
|
field_pointer: '/case_keys/block_code',
|
|
field_path: 'case_keys.block_code',
|
|
value: 'PARENT-2608-CONFLICT-A',
|
|
},
|
|
],
|
|
})
|
|
expect(wrapper.text()).toContain('人工复核已解阻')
|
|
})
|
|
|
|
it('shows missing manual review fields as backend unopened when detail.fields omits them', async () => {
|
|
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
|
|
createTaskDetail({
|
|
result_type: 'manual_review',
|
|
ai_task_type: 'NEW_BOOKING',
|
|
task_subtype: 'NEW_BOOKING',
|
|
route_code: 'NEW_BOOKING_MANUAL_REVIEW',
|
|
system_process_category: 'BUSINESS_TASK',
|
|
task_status: 'BLOCKED',
|
|
review_status: 'PENDING',
|
|
manual_review: {
|
|
reason_code: 'missing_room_type_code',
|
|
missing_fields: [
|
|
'/extracted_fields/room_items/0/pms_room_type_code',
|
|
'/case_keys/confirmation_number',
|
|
],
|
|
review_instruction: '请补充缺失字段。',
|
|
},
|
|
availability: {
|
|
blocked: true,
|
|
read_only: true,
|
|
editable: false,
|
|
confirmable: false,
|
|
executable: false,
|
|
blocked_by_task_id: null,
|
|
blocked_reason: 'MANUAL_REVIEW_REQUIRED',
|
|
},
|
|
fields: [
|
|
createRequiredField({
|
|
field_path: 'extracted_fields.room_items.0.pms_room_type_code',
|
|
field_pointer: '/extracted_fields/room_items/0/pms_room_type_code',
|
|
legacy_field_path: 'extracted_fields.pms_room_type_code',
|
|
display_name: 'PMS 房型代码',
|
|
value: null,
|
|
}),
|
|
],
|
|
}),
|
|
)
|
|
|
|
const wrapper = await mountPanel()
|
|
|
|
expect(wrapper.text()).toContain('PMS 房型代码')
|
|
expect(wrapper.text()).toContain('/case_keys/confirmation_number')
|
|
expect(wrapper.text()).toContain('当前字段后端未开放编辑')
|
|
expect(wrapper.findAll('input[name^="manual_resolution_field_override_"]')).toHaveLength(1)
|
|
expect(findButton(wrapper, '提交复核结果').attributes('disabled')).toBeDefined()
|
|
})
|
|
|
|
it('resolves type-known manual review on the same task card', async () => {
|
|
vi.mocked(service.fetchReservationTaskDetail)
|
|
.mockResolvedValueOnce(
|
|
createTaskDetail({
|
|
result_type: 'manual_review',
|
|
ai_task_type: 'NEW_BOOKING',
|
|
task_subtype: 'NEW_BOOKING',
|
|
route_code: 'NEW_BOOKING_MANUAL_REVIEW',
|
|
system_process_category: 'BUSINESS_TASK',
|
|
task_status: 'BLOCKED',
|
|
review_status: 'PENDING',
|
|
manual_review: {
|
|
reason_code: 'missing_room_type_code',
|
|
missing_fields: ['/extracted_fields/room_items/0/pms_room_type_code'],
|
|
review_instruction: '请补充 PMS 房型代码。',
|
|
},
|
|
availability: {
|
|
blocked: true,
|
|
read_only: true,
|
|
editable: false,
|
|
confirmable: false,
|
|
executable: false,
|
|
blocked_by_task_id: null,
|
|
blocked_reason: 'MANUAL_REVIEW_REQUIRED',
|
|
},
|
|
fields: [
|
|
createRequiredField({
|
|
field_path: 'extracted_fields.room_items.0.pms_room_type_code',
|
|
field_pointer: '/extracted_fields/room_items/0/pms_room_type_code',
|
|
legacy_field_path: 'extracted_fields.pms_room_type_code',
|
|
display_name: 'PMS 房型代码',
|
|
editable: 'N',
|
|
input_editable: 'N',
|
|
value: null,
|
|
}),
|
|
createRequiredField({
|
|
field_path: 'extracted_fields.room_items.0.room_quantity',
|
|
field_pointer: '/extracted_fields/room_items/0/room_quantity',
|
|
legacy_field_path: 'extracted_fields.room_quantity',
|
|
display_name: '房量',
|
|
control_type: 'number',
|
|
edit_scope: 'normal_and_manual_review',
|
|
write_target: 'draft_payload_and_review_resolution',
|
|
raw_readonly: false,
|
|
value: null,
|
|
}),
|
|
],
|
|
}),
|
|
)
|
|
.mockResolvedValueOnce(
|
|
createTaskDetail({
|
|
result_type: 'manual_review',
|
|
ai_task_type: 'NEW_BOOKING',
|
|
task_subtype: 'NEW_BOOKING',
|
|
route_code: 'NEW_BOOKING_MANUAL_REVIEW',
|
|
system_process_category: 'BUSINESS_TASK',
|
|
task_status: 'READY',
|
|
review_status: 'RESOLVED',
|
|
review_resolution: {
|
|
confirmed_order_id: '20001',
|
|
field_overrides: [
|
|
{
|
|
field_pointer: '/extracted_fields/room_items/0/pms_room_type_code',
|
|
field_path: 'extracted_fields.room_items.0.pms_room_type_code',
|
|
legacy_field_path: 'extracted_fields.pms_room_type_code',
|
|
value: 'RM3',
|
|
},
|
|
],
|
|
},
|
|
availability: {
|
|
blocked: false,
|
|
read_only: false,
|
|
editable: false,
|
|
confirmable: false,
|
|
executable: true,
|
|
blocked_by_task_id: null,
|
|
blocked_reason: null,
|
|
},
|
|
opera_operations: [createOperaOperation()],
|
|
}),
|
|
)
|
|
vi.mocked(service.fetchReservationTaskAudits)
|
|
.mockResolvedValueOnce({
|
|
task_id: '10001',
|
|
items: [createAudit('audit-1', 'TASK_LOADED')],
|
|
})
|
|
.mockResolvedValueOnce({
|
|
task_id: '10001',
|
|
items: [createAudit('audit-2', 'MANUAL_REVIEW_RESOLVED')],
|
|
})
|
|
|
|
const wrapper = await mountPanel()
|
|
|
|
expect(wrapper.text()).toContain('路由与处理信息')
|
|
expect(wrapper.text()).toContain('系统任务类型')
|
|
expect(wrapper.text()).toContain('任务卡类型')
|
|
expect(wrapper.text()).toContain('NEW_BOOKING_MANUAL_REVIEW')
|
|
expect(wrapper.text()).toContain('人工复核解阻')
|
|
expect(wrapper.text()).toContain('/extracted_fields/room_items/0/pms_room_type_code')
|
|
expect(wrapper.text()).toContain('PMS 房型代码')
|
|
expect(wrapper.text()).toContain('/extracted_fields/room_items/0/room_quantity')
|
|
expect(wrapper.text()).toContain('房量')
|
|
expect(wrapper.text()).not.toContain('人工转换')
|
|
expect(wrapper.text()).not.toContain('保存草稿')
|
|
expect(wrapper.text()).not.toContain('确认任务')
|
|
expect(wrapper.text()).not.toContain('OPERA 模拟操作')
|
|
|
|
await wrapper.find('input[name="manual_resolution_field_override_0"]').setValue('RM3')
|
|
await wrapper.find('input[name="manual_resolution_field_override_1"]').setValue('2')
|
|
await wrapper.find('textarea[name="manual_resolution_reason"]').setValue('确认 PMS 房型代码')
|
|
await findButton(wrapper, '提交复核结果').trigger('click')
|
|
await flushPromises()
|
|
|
|
expect(service.resolveReservationManualReviewTask).toHaveBeenCalledWith('10001', {
|
|
confirmed_order_id: '20001',
|
|
reason: '确认 PMS 房型代码',
|
|
field_overrides: [
|
|
{
|
|
field_pointer: '/extracted_fields/room_items/0/pms_room_type_code',
|
|
field_path: 'extracted_fields.room_items.0.pms_room_type_code',
|
|
value: 'RM3',
|
|
},
|
|
{
|
|
field_pointer: '/extracted_fields/room_items/0/room_quantity',
|
|
field_path: 'extracted_fields.room_items.0.room_quantity',
|
|
value: '2',
|
|
},
|
|
],
|
|
})
|
|
expect(service.fetchReservationTaskDetail).toHaveBeenCalledTimes(2)
|
|
expect(service.fetchReservationTaskAudits).toHaveBeenCalledTimes(2)
|
|
expect(wrapper.text()).toContain('人工复核已解阻')
|
|
expect(wrapper.text()).toContain('人工复核解阻')
|
|
expect(wrapper.text()).toContain('OPERA 模拟操作')
|
|
expect(wrapper.text()).toContain('CREATE_RESERVATION')
|
|
expect(wrapper.find('button[title="执行"]').exists()).toBe(true)
|
|
expect(wrapper.text()).not.toContain('MANUAL_REVIEW_RESOLVED')
|
|
})
|
|
|
|
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: 'FALLBACK_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('人工转换')
|
|
expect(wrapper.text()).not.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: 'FALLBACK_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',
|
|
})
|
|
})
|
|
|
|
it('hides task processing buttons and audit timeline without permissions', async () => {
|
|
vi.mocked(service.fetchReservationTaskAudits).mockResolvedValue({
|
|
task_id: '10001',
|
|
items: [createAudit('audit-1', 'TASK_LOADED')],
|
|
})
|
|
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
|
|
createTaskDetail({
|
|
availability: {
|
|
blocked: false,
|
|
read_only: false,
|
|
editable: true,
|
|
confirmable: true,
|
|
executable: true,
|
|
blocked_by_task_id: null,
|
|
blocked_reason: null,
|
|
},
|
|
opera_operations: [
|
|
{
|
|
operation_id: 'op-1',
|
|
task_id: '10001',
|
|
operation_sequence: 1,
|
|
operation_code: 'CREATE_RESERVATION',
|
|
operation_name: 'Create reservation',
|
|
operation_status: 'PENDING',
|
|
attempt_count: 0,
|
|
last_error_message: null,
|
|
attempts: [],
|
|
},
|
|
],
|
|
}),
|
|
)
|
|
|
|
const wrapper = await mountPanel(['RESERVATION_TASK_READ'])
|
|
|
|
expect(wrapper.text()).not.toContain('保存草稿')
|
|
expect(wrapper.text()).not.toContain('确认任务')
|
|
expect(wrapper.text()).not.toContain('审计时间线')
|
|
expect(wrapper.find('button[title="执行"]').exists()).toBe(false)
|
|
expect(wrapper.find('button[title="重试"]').exists()).toBe(false)
|
|
})
|
|
})
|