接入M002V4订单任务多卡前端

This commit is contained in:
andy
2026-07-19 13:48:07 +07:00
parent 13364e7edf
commit cae169cefd
19 changed files with 4084 additions and 343 deletions

View File

@@ -0,0 +1,466 @@
import { flushPromises, mount } 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 { useAuthStore } from '@/stores/authStore'
import type {
ReservationV4OrderTaskDetailResult,
ReservationV4SourceNotificationDetailResult,
ReservationV4TaskCardResult,
} from '@/types/reservation'
import ReservationV4OrderTaskDetailView from '@/views/reservation/ReservationV4OrderTaskDetailView.vue'
import ReservationV4SourceNotificationDetailView from '@/views/reservation/ReservationV4SourceNotificationDetailView.vue'
vi.mock('@/services/reservationService', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/services/reservationService')>()
return {
...actual,
ackReservationV4SourceNotification: vi.fn(),
confirmReservationV4OrderTaskCard: vi.fn(),
fetchReservationV4OrderTaskDetail: vi.fn(),
fetchReservationV4SourceNotificationDetail: vi.fn(),
resolveReservationV4OrderTaskCardReview: vi.fn(),
}
})
const service = await import('@/services/reservationService')
describe('reservation V4 pages', () => {
beforeEach(() => {
sessionStorage.clear()
vi.mocked(service.ackReservationV4SourceNotification).mockReset()
vi.mocked(service.confirmReservationV4OrderTaskCard).mockReset()
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockReset()
vi.mocked(service.fetchReservationV4SourceNotificationDetail).mockReset()
vi.mocked(service.resolveReservationV4OrderTaskCardReview).mockReset()
})
it('renders V4 order task cards and confirms only editable basic information fields', async () => {
const detail = createOrderTaskDetail()
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
vi.mocked(service.confirmReservationV4OrderTaskCard).mockResolvedValue({
...detail,
basic_information_card: {
...detail.basic_information_card!,
card_status: 'CONFIRMED',
},
})
const wrapper = await mountWithPlugins(
ReservationV4OrderTaskDetailView,
'/reservation/order-tasks/9001',
)
await flushPromises()
expect(wrapper.text()).toContain('V4 订单任务详情')
expect(wrapper.text()).toContain('Booking Request')
expect(wrapper.text()).toContain('基础信息卡')
expect(wrapper.text()).toContain('业务任务卡')
expect(wrapper.text()).toContain('Backend contract issue')
await wrapper.find('select').setValue('HANATOUR')
await wrapper.findAll('button').find((button) => button.text().includes('确认卡片'))?.trigger('click')
await flushPromises()
expect(service.confirmReservationV4OrderTaskCard).toHaveBeenCalledWith('9001', 'card-basic', {
version: 7,
confirmed_payload: {
basic_information: {
account_code: 'HANATOUR',
},
},
})
})
it('submits V4 review resolution with field_pointer overrides and confirmed order id', async () => {
const detail = createOrderTaskDetail({
businessCardStatus: 'REVIEW_REQUIRED',
businessCardAvailability: {
confirmable: false,
reviewable: true,
},
})
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
const resolvedBusinessCard = detail.business_cards[0]!
vi.mocked(service.resolveReservationV4OrderTaskCardReview).mockResolvedValue({
...detail,
business_cards: [
{
...resolvedBusinessCard,
card_status: 'PENDING_CONFIRM',
review_status: 'RESOLVED',
},
],
})
const wrapper = await mountWithPlugins(
ReservationV4OrderTaskDetailView,
'/reservation/order-tasks/9001',
)
await flushPromises()
const selects = wrapper.findAll('select')
await selects[1]!.setValue('TWN')
const inputs = wrapper.findAll('input')
await inputs[0]!.setValue('order-2001')
await wrapper.find('textarea').setValue('confirmed by email evidence')
await wrapper.findAll('button').find((button) => button.text().includes('提交复核'))?.trigger('click')
await flushPromises()
expect(service.resolveReservationV4OrderTaskCardReview).toHaveBeenCalledWith('9001', 'card-room', {
version: 5,
confirmed_order_id: 'order-2001',
reason: 'confirmed by email evidence',
field_overrides: [
{
field_pointer: '/business_fields/after/room_items/0/room_type_code',
value: 'TWN',
},
],
})
})
it('requires confirmed order id before resolving an unresolved V4 review card', async () => {
const detail = createOrderTaskDetail({
businessCardStatus: 'REVIEW_REQUIRED',
businessCardAvailability: {
confirmable: false,
reviewable: true,
},
})
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
const wrapper = await mountWithPlugins(
ReservationV4OrderTaskDetailView,
'/reservation/order-tasks/9001',
)
await flushPromises()
await wrapper.findAll('button').find((button) => button.text().includes('提交复核'))?.trigger('click')
await flushPromises()
expect(service.resolveReservationV4OrderTaskCardReview).not.toHaveBeenCalled()
expect(wrapper.text()).toContain(zhCN.taskV4.confirmedOrderRequired)
})
it('renders V4 source notification detail and acknowledges it without business actions', async () => {
const detail = createSourceNotificationDetail()
vi.mocked(service.fetchReservationV4SourceNotificationDetail).mockResolvedValue(detail)
vi.mocked(service.ackReservationV4SourceNotification).mockResolvedValue({
...detail,
notification: {
...detail.notification,
notification_status: 'ACKED',
},
availability: {
...detail.availability,
ackable: false,
},
})
const wrapper = await mountWithPlugins(
ReservationV4SourceNotificationDetailView,
'/reservation/source-notifications/7001',
)
await flushPromises()
expect(wrapper.text()).toContain('来源通知详情')
expect(wrapper.text()).toContain('FYI only')
expect(wrapper.text()).toContain('该通知不关联订单、业务卡、字段编辑、复核或 OPERA 操作')
expect(wrapper.text()).not.toContain('业务任务卡')
expect(wrapper.text()).not.toContain('确认卡片')
await wrapper.find('textarea').setValue('handled')
await wrapper.findAll('button').find((button) => button.text().includes('确认已读'))?.trigger('click')
await flushPromises()
expect(service.ackReservationV4SourceNotification).toHaveBeenCalledWith('7001', {
version: 3,
reason: 'handled',
})
})
})
async function mountWithPlugins(component: object, initialPath: string) {
const pinia = createPinia()
setActivePinia(pinia)
const authStore = useAuthStore()
authStore.applyLoginResult({
access_token: 'test-token',
token_type: 'Bearer',
expires_at: '2026-07-08T04:00:00Z',
user: {
id: 'u1',
username: 'agent',
display_name: '系统管理员',
super_admin: false,
},
default_hotel_id: 'HOTEL-TEST',
hotels: [
{
hotel_id: 'HOTEL-TEST',
hotel_name: '测试酒店',
time_zone: 'Asia/Bangkok',
default_hotel: true,
},
],
permissions: [
'RESERVATION_TASK_READ',
'RESERVATION_TASK_CONFIRM',
'RESERVATION_MANUAL_REVIEW_RESOLVE',
'SOURCE_MESSAGE_ORIGINAL_READ',
],
menus: [],
})
const i18n = createI18n({
legacy: false,
locale: 'zh-CN',
messages: {
'zh-CN': zhCN,
},
})
const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', component: { template: '<div />' } },
{ path: '/reservation/order-tasks/:orderTaskId', component: { template: '<div />' } },
{ path: '/reservation/source-notifications/:notificationId', component: { template: '<div />' } },
{ path: '/reservation/source-messages/:sourceMessageId/conversation', component: { template: '<div />' } },
{ path: '/reservation/orders/:orderId', component: { template: '<div />' } },
],
})
await router.push(initialPath)
await router.isReady()
return mount(component, {
global: {
plugins: [pinia, i18n, router],
stubs: {
RouterLink: {
template: '<a><slot /></a>',
},
},
},
})
}
function createOrderTaskDetail(options: {
businessCardStatus?: string
businessCardAvailability?: Partial<ReservationV4TaskCardResult['availability']>
} = {}): ReservationV4OrderTaskDetailResult {
const sourceCard = createCard('card-source', 'SOURCE_MESSAGE_DISPLAY', 'READONLY', {
fields: [],
display_payload: {
subject: 'Booking Request',
sender_summary: 'guest@example.test',
relevant_message_excerpt: 'Please book one twin room.',
attachments: [
{
name: 'booking.pdf',
content_type: 'application/pdf',
},
],
},
})
const basicCard = createCard('card-basic', 'BASIC_INFORMATION', 'PENDING_CONFIRM', {
version: 7,
fields: [
createField('/basic_information/account_code', {
display_name: 'Account',
value: '',
options_source: 'RESERVATION_V4_ACCOUNT_CATALOG',
control_type: 'SELECT',
}),
createField('/basic_information/read_only_marker', {
display_name: 'Read only marker',
value: 'VISIBLE',
editable: true,
raw_readonly: true,
}),
],
})
const businessCard = createCard('card-room', 'ROOM_INFORMATION', options.businessCardStatus ?? 'PENDING_CONFIRM', {
version: 5,
availability: createAvailability({
confirmable: options.businessCardAvailability?.confirmable ?? true,
reviewable: options.businessCardAvailability?.reviewable ?? false,
}),
fields: [
createField('/business_fields/after/room_items/0/room_type_code', {
display_name: 'Room type',
value: '',
options_source: 'RESERVATION_V4_ROOM_TYPE_CATALOG',
control_type: 'SELECT',
edit_scope: options.businessCardStatus === 'REVIEW_REQUIRED' ? 'MANUAL_REVIEW_ONLY' : 'NORMAL_TASK',
write_target: options.businessCardStatus === 'REVIEW_REQUIRED'
? 'REVIEW_RESOLUTION_FIELD_OVERRIDES'
: 'CONFIRMED_PAYLOAD_JSON',
}),
],
})
return {
order_task: {
order_task_id: '9001',
hotel_id: 'HOTEL-TEST',
source_message_id: '30001',
ai_batch_id: 'batch-1',
order_ref: 'GRP-001',
order_context_index: 0,
order_id: null,
target_booking_type: 'GROUP',
target_locator_type: 'GROUP_CODE',
target_locator_value: 'GRP-001',
target_resolution_status: 'UNRESOLVED',
order_task_status: 'OPEN',
display_order_key: 'GRP-001',
source_received_at: '2026-07-08T03:00:00Z',
version: 11,
created_at: '2026-07-08T03:00:00Z',
updated_at: '2026-07-08T03:00:00Z',
},
source_message_summary: {
source_message_id: '30001',
hotel_id: 'HOTEL-TEST',
external_message_id: 'm1',
external_conversation_id: 'thread-1',
subject: 'Booking Request',
sender_summary: 'guest@example.test',
received_at: '2026-07-08T03:00:00Z',
source_sent_at: null,
conversation_message_count: 2,
},
bound_order: null,
source_message_card: sourceCard,
basic_information_card: basicCard,
business_cards: [businessCard],
card_counts: {
total_count: 3,
readonly_count: 1,
pending_confirm_count: 2,
review_required_count: options.businessCardStatus === 'REVIEW_REQUIRED' ? 1 : 0,
confirmed_count: 0,
},
adapter_contract_errors: [
{
transition_id: 'trans-1',
route_code: 'R02_NEW_GROUP_BLOCK_NORMAL',
result_type: 'ADAPTER_CONTRACT_ERROR',
adapter_error_code: 'ADAPTER_CONTRACT_ERROR',
adapter_error_message: 'Backend contract issue',
payload_fragment: null,
},
],
availability: createAvailability(),
}
}
function createSourceNotificationDetail(): ReservationV4SourceNotificationDetailResult {
return {
notification: {
notification_id: '7001',
hotel_id: 'HOTEL-TEST',
source_message_id: '30002',
ai_batch_id: 'batch-2',
ai_transition_id: 'transition-2',
route_code: 'S10',
notification_status: 'ACK_REQUIRED',
source_received_at: '2026-07-08T03:00:00Z',
ack_by: null,
ack_at: null,
version: 3,
created_at: '2026-07-08T03:00:00Z',
updated_at: '2026-07-08T03:00:00Z',
},
conversation_summary: {
source_message_id: '30002',
hotel_id: 'HOTEL-TEST',
external_message_id: 'm2',
external_conversation_id: 'thread-2',
subject: 'FYI only',
sender_summary: 'guest@example.test',
received_at: '2026-07-08T03:00:00Z',
source_sent_at: null,
conversation_message_count: 1,
},
source_message_card: createCard('card-source-notification', 'SOURCE_MESSAGE_NOTIFICATION', 'ACK_REQUIRED', {
display_payload: {
subject: 'FYI only',
relevant_message_excerpt: 'Thanks for the update.',
},
}),
availability: createAvailability({
ackable: true,
confirmable: false,
editable: false,
}),
}
}
function createCard(
card_id: string,
card_type: string,
card_status: string,
overrides: Partial<ReservationV4TaskCardResult> = {},
): ReservationV4TaskCardResult {
return {
card_id,
card_type,
event_type: null,
source_event_index: null,
card_sort_order: null,
card_status,
review_status: null,
display_payload: overrides.display_payload ?? null,
confirmed_payload: null,
review_resolution: null,
validation_errors: null,
fields: overrides.fields ?? [],
confirmed_by: null,
confirmed_at: null,
version: overrides.version ?? 1,
created_at: '2026-07-08T03:00:00Z',
updated_at: '2026-07-08T03:00:00Z',
availability: createAvailability(overrides.availability),
}
}
function createField(
field_pointer: string,
overrides: Partial<ReservationV4TaskCardResult['fields'][number]> = {},
): ReservationV4TaskCardResult['fields'][number] {
return {
field_path: field_pointer.replace(/^\//, '').replace(/\//g, '.'),
field_pointer,
display_name: overrides.display_name ?? field_pointer,
value: overrides.value ?? '',
editable: overrides.editable ?? true,
required: overrides.required ?? false,
control_type: overrides.control_type ?? 'TEXT',
edit_scope: overrides.edit_scope ?? 'NORMAL_TASK',
write_target: overrides.write_target ?? 'CONFIRMED_PAYLOAD_JSON',
options_source: overrides.options_source ?? null,
raw_readonly: overrides.raw_readonly ?? false,
validation_errors: overrides.validation_errors ?? [],
control_hint: overrides.control_hint ?? null,
}
}
function createAvailability(
overrides: Partial<ReservationV4TaskCardResult['availability']> = {},
): ReservationV4TaskCardResult['availability'] {
return {
blocked: overrides.blocked ?? false,
read_only: overrides.read_only ?? false,
editable: overrides.editable ?? true,
confirmable: overrides.confirmable ?? true,
reviewable: overrides.reviewable ?? false,
ackable: overrides.ackable ?? false,
readonly_reason_code: overrides.readonly_reason_code ?? null,
blocked_by_order_task_id: overrides.blocked_by_order_task_id ?? null,
blocked_by_card_id: overrides.blocked_by_card_id ?? null,
blocked_reason: overrides.blocked_reason ?? null,
}
}