接入M002V4订单任务多卡前端
This commit is contained in:
1 parent
13364e7edf
commit
cae169cefd
19 files changed
+4084
-343
No files matched your search
@@ -2,12 +2,19 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
convertReservationManualReviewTask,
|
||||
ackReservationV4SourceNotification,
|
||||
confirmReservationV4OrderTaskCard,
|
||||
fetchReservationOrderDetail,
|
||||
fetchReservationOrders,
|
||||
fetchReservationTaskDetail,
|
||||
fetchReservationTaskList,
|
||||
fetchReservationV4OrderTaskDetail,
|
||||
fetchReservationV4OrderTasks,
|
||||
fetchReservationV4SourceNotificationDetail,
|
||||
fetchReservationV4WorkbenchItems,
|
||||
fetchSourceMessageConversation,
|
||||
resolveReservationManualReviewTask,
|
||||
resolveReservationV4OrderTaskCardReview,
|
||||
} from '@/services/reservationService'
|
||||
import { setReservationHotelIdProvider } from '@/config/reservationConfig'
|
||||
|
||||
@@ -129,6 +136,164 @@ describe('reservationService real API mode', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('fetches V4 workbench items from the backend with supported filters only', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
items: [],
|
||||
page: {
|
||||
page_num: 2,
|
||||
page_size: 10,
|
||||
total: 0,
|
||||
},
|
||||
}),
|
||||
)
|
||||
setReservationHotelIdProvider(() => 'HOTEL-BKK')
|
||||
|
||||
await fetchReservationV4WorkbenchItems({
|
||||
item_type: 'SOURCE_NOTIFICATION',
|
||||
keyword: 'S10',
|
||||
page_num: 2,
|
||||
page_size: 10,
|
||||
})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/reservation/workbench-items?hotel_id=HOTEL-BKK&item_type=SOURCE_NOTIFICATION&keyword=S10&page_num=2&page_size=10',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('fetches V4 order tasks with order task and card status filters', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
items: [],
|
||||
page: {
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
total: 0,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
await fetchReservationV4OrderTasks({
|
||||
order_task_status: 'OPEN',
|
||||
card_status: 'REVIEW_REQUIRED',
|
||||
keyword: 'GRP',
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/reservation/order-tasks?order_task_status=OPEN&card_status=REVIEW_REQUIRED&keyword=GRP&page_num=1&page_size=20',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('fetches V4 order task and source notification details by id', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch')
|
||||
.mockResolvedValueOnce(mockJsonResponse({ order_task: { order_task_id: '9001' } }))
|
||||
.mockResolvedValueOnce(mockJsonResponse({ notification: { notification_id: '7001' } }))
|
||||
|
||||
await fetchReservationV4OrderTaskDetail('9001')
|
||||
await fetchReservationV4SourceNotificationDetail('7001')
|
||||
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/api/reservation/order-tasks/9001',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/api/reservation/source-notifications/7001',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('confirms a V4 task card with version and current-card payload', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
order_task: {
|
||||
order_task_id: '9001',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
await confirmReservationV4OrderTaskCard('9001', '9101', {
|
||||
version: 3,
|
||||
confirmed_payload: {
|
||||
basic_information: {
|
||||
account_code: 'QBD_TRAVEL',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/reservation/order-tasks/9001/cards/9101/confirm',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 3,
|
||||
confirmed_payload: {
|
||||
basic_information: {
|
||||
account_code: 'QBD_TRAVEL',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('resolves a V4 review card and acknowledges source notifications', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch')
|
||||
.mockResolvedValueOnce(mockJsonResponse({ order_task: { order_task_id: '9001' } }))
|
||||
.mockResolvedValueOnce(mockJsonResponse({ notification: { notification_id: '7001' } }))
|
||||
|
||||
await resolveReservationV4OrderTaskCardReview('9001', '9102', {
|
||||
version: 4,
|
||||
confirmed_order_id: '20001',
|
||||
reason: '人工确认',
|
||||
field_overrides: [
|
||||
{
|
||||
field_pointer: '/business_fields/room_items/0/room_type_code',
|
||||
value: 'TWN',
|
||||
},
|
||||
],
|
||||
})
|
||||
await ackReservationV4SourceNotification('7001', {
|
||||
version: 1,
|
||||
reason: '已读',
|
||||
})
|
||||
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/api/reservation/order-tasks/9001/cards/9102/review-resolution',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 4,
|
||||
confirmed_order_id: '20001',
|
||||
reason: '人工确认',
|
||||
field_overrides: [
|
||||
{
|
||||
field_pointer: '/business_fields/room_items/0/room_type_code',
|
||||
value: 'TWN',
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/api/reservation/source-notifications/7001/ack',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
reason: '已读',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('fetches the order list from the backend by default', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { ReservationV4TaskCardFieldResult } from '@/types/reservation'
|
||||
import {
|
||||
buildReservationV4ConfirmedPayload,
|
||||
buildReservationV4InitialFieldValues,
|
||||
buildReservationV4ReviewOverrides,
|
||||
isReservationV4ConfirmWritableField,
|
||||
isReservationV4ReviewWritableField,
|
||||
stringifyReservationV4SafeDisplayValue,
|
||||
validateReservationV4Fields,
|
||||
} from '@/utils/reservationV4FieldRules'
|
||||
|
||||
function createV4Field(
|
||||
field_pointer: string,
|
||||
overrides: Partial<ReservationV4TaskCardFieldResult> = {},
|
||||
): ReservationV4TaskCardFieldResult {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
describe('reservationV4FieldRules', () => {
|
||||
it('builds initial values from backend fields using field pointers', () => {
|
||||
const fields = [
|
||||
createV4Field('/basic_information/account_code', { value: 'HANATOUR' }),
|
||||
createV4Field('/business_fields/after/room_items/0/room_type_code', { value: 'TWN' }),
|
||||
]
|
||||
|
||||
expect(buildReservationV4InitialFieldValues(fields)).toEqual({
|
||||
'/basic_information/account_code': 'HANATOUR',
|
||||
'/business_fields/after/room_items/0/room_type_code': 'TWN',
|
||||
})
|
||||
})
|
||||
|
||||
it('submits only editable confirm fields and keeps nested pointer structure', () => {
|
||||
const fields = [
|
||||
createV4Field('/basic_information/account_code', { value: 'OLD' }),
|
||||
createV4Field('/basic_information/read_only_label', {
|
||||
value: 'VISIBLE_ONLY',
|
||||
editable: true,
|
||||
raw_readonly: true,
|
||||
}),
|
||||
createV4Field('/business_fields/after/room_items/0/room_type_code', {
|
||||
value: 'KING',
|
||||
edit_scope: 'MANUAL_REVIEW_ONLY',
|
||||
write_target: 'REVIEW_RESOLUTION_FIELD_OVERRIDES',
|
||||
}),
|
||||
]
|
||||
|
||||
const payload = buildReservationV4ConfirmedPayload(fields, {
|
||||
'/basic_information/account_code': 'HANATOUR',
|
||||
'/basic_information/read_only_label': 'SHOULD_NOT_SEND',
|
||||
'/business_fields/after/room_items/0/room_type_code': 'TWN',
|
||||
})
|
||||
|
||||
expect(payload).toEqual({
|
||||
basic_information: {
|
||||
account_code: 'HANATOUR',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('uses field_pointer for review resolution overrides', () => {
|
||||
const fields = [
|
||||
createV4Field('/business_fields/after/room_items/0/room_type_code', {
|
||||
value: '',
|
||||
edit_scope: 'MANUAL_REVIEW_ONLY',
|
||||
write_target: 'REVIEW_RESOLUTION_FIELD_OVERRIDES',
|
||||
}),
|
||||
createV4Field('/business_fields/after/room_items/0/room_type_raw', {
|
||||
value: 'TWN',
|
||||
editable: false,
|
||||
write_target: 'NONE',
|
||||
}),
|
||||
]
|
||||
|
||||
expect(buildReservationV4ReviewOverrides(fields, {
|
||||
'/business_fields/after/room_items/0/room_type_code': 'TWN',
|
||||
'/business_fields/after/room_items/0/room_type_raw': 'Twin',
|
||||
})).toEqual([
|
||||
{
|
||||
field_pointer: '/business_fields/after/room_items/0/room_type_code',
|
||||
value: 'TWN',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('does not submit fields with missing V4 write metadata', () => {
|
||||
const confirmField = createV4Field('/business_fields/after/room_items/0/rate_code')
|
||||
confirmField.edit_scope = null
|
||||
confirmField.write_target = null
|
||||
const reviewField = createV4Field('/business_fields/after/room_items/0/room_type_code', {
|
||||
edit_scope: 'MANUAL_REVIEW_ONLY',
|
||||
})
|
||||
reviewField.write_target = null
|
||||
|
||||
expect(isReservationV4ConfirmWritableField(confirmField)).toBe(false)
|
||||
expect(isReservationV4ReviewWritableField(reviewField)).toBe(false)
|
||||
expect(buildReservationV4ConfirmedPayload([confirmField], {
|
||||
'/business_fields/after/room_items/0/rate_code': 'BAR',
|
||||
})).toEqual({})
|
||||
expect(buildReservationV4ReviewOverrides([reviewField], {
|
||||
'/business_fields/after/room_items/0/room_type_code': 'TWN',
|
||||
})).toEqual([])
|
||||
})
|
||||
|
||||
it('allows review overrides only for basic information and business fields', () => {
|
||||
const fields = [
|
||||
createV4Field('/source_message/subject', {
|
||||
edit_scope: 'MANUAL_REVIEW_ONLY',
|
||||
write_target: 'REVIEW_RESOLUTION_FIELD_OVERRIDES',
|
||||
}),
|
||||
createV4Field('/business_fields/after/room_items/0/room_type_code', {
|
||||
edit_scope: 'MANUAL_REVIEW_ONLY',
|
||||
write_target: 'REVIEW_RESOLUTION_FIELD_OVERRIDES',
|
||||
}),
|
||||
]
|
||||
|
||||
expect(buildReservationV4ReviewOverrides(fields, {
|
||||
'/source_message/subject': 'SHOULD_NOT_SEND',
|
||||
'/business_fields/after/room_items/0/room_type_code': 'TWN',
|
||||
})).toEqual([
|
||||
{
|
||||
field_pointer: '/business_fields/after/room_items/0/room_type_code',
|
||||
value: 'TWN',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('redacts nested raw body, evidence and URL values from safe display text', () => {
|
||||
const text = stringifyReservationV4SafeDisplayValue({
|
||||
business_fields: {
|
||||
group_code: 'GRP-001',
|
||||
raw_evidence: 'raw email body',
|
||||
nested: {
|
||||
room_type_code: 'TWN',
|
||||
oss_url: 'https://oss.example/private-file.pdf',
|
||||
},
|
||||
safe_url_value: 'https://oss.example/also-hidden.pdf',
|
||||
},
|
||||
}, 'empty', 'hidden')
|
||||
|
||||
expect(text).toContain('GRP-001')
|
||||
expect(text).toContain('TWN')
|
||||
expect(text).not.toContain('raw email body')
|
||||
expect(text).not.toContain('https://oss.example')
|
||||
expect(text).not.toContain('oss_url')
|
||||
})
|
||||
|
||||
it('redacts direct URL-like display values', () => {
|
||||
expect(stringifyReservationV4SafeDisplayValue(
|
||||
'https://oss.example/private-file.pdf',
|
||||
'empty',
|
||||
'hidden',
|
||||
)).toBe('hidden')
|
||||
})
|
||||
|
||||
it('validates only editable visible fields', () => {
|
||||
const fields = [
|
||||
createV4Field('/basic_information/account_code', {
|
||||
display_name: 'Account',
|
||||
required: true,
|
||||
}),
|
||||
createV4Field('/business_fields/after/room_items/0/room_type_code', {
|
||||
display_name: 'Room type',
|
||||
required: true,
|
||||
editable: false,
|
||||
}),
|
||||
]
|
||||
|
||||
expect(validateReservationV4Fields(fields, {
|
||||
'/basic_information/account_code': '',
|
||||
'/business_fields/after/room_items/0/room_type_code': '',
|
||||
})).toEqual({
|
||||
'/basic_information/account_code': 'Account is required',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,11 @@ import ReservationOrderListView from '@/views/reservation/ReservationOrderListVi
|
||||
import ReservationSourceMessageConversationView from '@/views/reservation/ReservationSourceMessageConversationView.vue'
|
||||
import ReservationTaskListView from '@/views/reservation/ReservationTaskListView.vue'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import type { ReservationTaskListResult, SourceMessageConversationResult } from '@/types/reservation'
|
||||
import type {
|
||||
ReservationV4OrderTaskListResult,
|
||||
ReservationV4WorkbenchListResult,
|
||||
SourceMessageConversationResult,
|
||||
} from '@/types/reservation'
|
||||
|
||||
vi.mock('@/services/reservationService', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/services/reservationService')>()
|
||||
@@ -19,6 +23,8 @@ vi.mock('@/services/reservationService', async (importOriginal) => {
|
||||
fetchReservationOrderDetail: vi.fn(),
|
||||
fetchReservationOrders: vi.fn(),
|
||||
fetchReservationTaskList: vi.fn(),
|
||||
fetchReservationV4OrderTasks: vi.fn(),
|
||||
fetchReservationV4WorkbenchItems: vi.fn(),
|
||||
fetchSourceMessageConversation: vi.fn(),
|
||||
}
|
||||
})
|
||||
@@ -54,37 +60,47 @@ function createOrderListResult(
|
||||
}
|
||||
}
|
||||
|
||||
function createTaskListResult(
|
||||
displayOrderKey: string,
|
||||
sourceSubject: string,
|
||||
function createV4WorkbenchResult(
|
||||
displayOrderKey = 'GRP-001',
|
||||
sourceSubject = 'Booking Request',
|
||||
page: { page_num: number; page_size: number; total: number } = {
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
total: 1,
|
||||
},
|
||||
): ReservationTaskListResult {
|
||||
): ReservationV4WorkbenchListResult {
|
||||
return {
|
||||
items: [
|
||||
{
|
||||
task_id: displayOrderKey === 'GRP-NEW' ? '10002' : '10001',
|
||||
order_id: '20001',
|
||||
item_type: 'ORDER_TASK',
|
||||
target_id: displayOrderKey === 'GRP-NEW' ? '9002' : '9001',
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
display_order_key: displayOrderKey,
|
||||
temporary_order_no: null,
|
||||
task_type: 'NEW_BOOKING',
|
||||
task_subtype: 'NEW_BOOKING',
|
||||
task_status: 'PENDING_CONFIRM',
|
||||
card_name: 'New Booking',
|
||||
queue_sequence: 1,
|
||||
queue_participation: true,
|
||||
can_process: true,
|
||||
readonly_reason_code: null,
|
||||
source_message_id: '30001',
|
||||
source_subject: sourceSubject,
|
||||
source_sender_summary: null,
|
||||
source_received_at: null,
|
||||
external_conversation_id: null,
|
||||
conversation_message_count: null,
|
||||
source_message_summary: {
|
||||
source_message_id: '30001',
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
external_message_id: 'msg-30001',
|
||||
external_conversation_id: 'thread-30001',
|
||||
subject: sourceSubject,
|
||||
sender_summary: 'guest@example.test',
|
||||
received_at: '2026-07-08T03:00:00Z',
|
||||
source_sent_at: null,
|
||||
conversation_message_count: 2,
|
||||
},
|
||||
source_received_at: '2026-07-08T03:00:00Z',
|
||||
display_order_key: displayOrderKey,
|
||||
card_counts: {
|
||||
total_count: 3,
|
||||
readonly_count: 1,
|
||||
pending_confirm_count: 1,
|
||||
review_required_count: 1,
|
||||
confirmed_count: 0,
|
||||
},
|
||||
next_action_card_id: '9101',
|
||||
notification_status: null,
|
||||
order_task_status: 'OPEN',
|
||||
display_status: 'OPEN',
|
||||
readonly_reason_code: null,
|
||||
created_at: '2026-07-08T03:00:00Z',
|
||||
updated_at: '2026-07-08T03:10:00Z',
|
||||
},
|
||||
@@ -93,33 +109,33 @@ function createTaskListResult(
|
||||
}
|
||||
}
|
||||
|
||||
function createSourceMessageOnlyTaskListResult(taskSubtype: 'S10' | 'S99' = 'S10') {
|
||||
function createV4SourceNotificationWorkbenchResult(routeCode: 'S10' | 'S99' = 'S10'): ReservationV4WorkbenchListResult {
|
||||
return {
|
||||
items: [
|
||||
{
|
||||
task_id: taskSubtype === 'S10' ? '19990' : '19999',
|
||||
order_id: taskSubtype === 'S10' ? '29990' : '29999',
|
||||
item_type: 'SOURCE_NOTIFICATION',
|
||||
target_id: routeCode === 'S10' ? '7001' : '7002',
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
display_order_key: null,
|
||||
temporary_order_no: null,
|
||||
task_type: 'MESSAGE_NOTIFICATION',
|
||||
task_subtype: taskSubtype,
|
||||
result_type: 'source_message_review_notification',
|
||||
ai_task_type: 'Message Notification',
|
||||
route_code: taskSubtype,
|
||||
system_process_category: 'SOURCE_MESSAGE_NOTIFICATION',
|
||||
task_status: 'COMPLETED',
|
||||
card_name: 'Message Notification',
|
||||
queue_sequence: null,
|
||||
queue_participation: false,
|
||||
can_process: false,
|
||||
readonly_reason_code: 'QUEUE_EXCLUDED',
|
||||
source_message_id: taskSubtype === 'S10' ? '39990' : '39999',
|
||||
source_subject: taskSubtype === 'S10' ? 'FYI only' : 'Unclear email',
|
||||
source_sender_summary: 'guest@example.test',
|
||||
source_message_id: routeCode === 'S10' ? '39990' : '39999',
|
||||
source_message_summary: {
|
||||
source_message_id: routeCode === 'S10' ? '39990' : '39999',
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
external_message_id: `msg-${routeCode}`,
|
||||
external_conversation_id: 'thread-source-only',
|
||||
subject: routeCode === 'S10' ? 'FYI only' : 'Unclear email',
|
||||
sender_summary: 'guest@example.test',
|
||||
received_at: '2026-07-08T03:00:00Z',
|
||||
source_sent_at: null,
|
||||
conversation_message_count: 1,
|
||||
},
|
||||
source_received_at: '2026-07-08T03:00:00Z',
|
||||
external_conversation_id: 'thread-source-only',
|
||||
conversation_message_count: 1,
|
||||
display_order_key: null,
|
||||
card_counts: null,
|
||||
next_action_card_id: null,
|
||||
notification_status: 'ACK_REQUIRED',
|
||||
order_task_status: null,
|
||||
display_status: 'ACK_REQUIRED',
|
||||
readonly_reason_code: null,
|
||||
created_at: '2026-07-08T03:00:00Z',
|
||||
updated_at: '2026-07-08T03:10:00Z',
|
||||
},
|
||||
@@ -132,19 +148,73 @@ function createSourceMessageOnlyTaskListResult(taskSubtype: 'S10' | 'S99' = 'S10
|
||||
}
|
||||
}
|
||||
|
||||
function createParentGroupCancelAllotmentTaskListResult(): ReservationTaskListResult {
|
||||
const result = createTaskListResult('PARENT-2608-BLOCK', 'Parent split confirmation')
|
||||
result.items[0] = {
|
||||
...result.items[0]!,
|
||||
task_type: 'CANCEL_BOOKING',
|
||||
task_subtype: 'cancel_allotment_control_block',
|
||||
result_type: 'normal_task',
|
||||
ai_task_type: 'Cancel Allotment',
|
||||
route_code: 'R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_NORMAL',
|
||||
system_process_category: 'BUSINESS_TASK',
|
||||
card_name: 'CANCEL_ALLOTMENT',
|
||||
function createV4OrderTaskListResult(
|
||||
displayOrderKey = 'GRP-002',
|
||||
sourceSubject = 'Booking Update',
|
||||
page: { page_num: number; page_size: number; total: number } = {
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
total: 1,
|
||||
},
|
||||
): ReservationV4OrderTaskListResult {
|
||||
return {
|
||||
items: [
|
||||
{
|
||||
order_task: {
|
||||
order_task_id: displayOrderKey === 'GRP-NEW' ? '9002' : '9001',
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
source_message_id: '30002',
|
||||
ai_batch_id: '8001',
|
||||
order_ref: 'order-1',
|
||||
order_context_index: 1,
|
||||
order_id: '20001',
|
||||
target_booking_type: 'GROUP',
|
||||
target_locator_type: 'GROUP_CODE',
|
||||
target_locator_value: displayOrderKey,
|
||||
target_resolution_status: 'RESOLVED',
|
||||
order_task_status: 'OPEN',
|
||||
display_order_key: displayOrderKey,
|
||||
source_received_at: '2026-07-08T04:00:00Z',
|
||||
version: 0,
|
||||
created_at: '2026-07-08T04:00:00Z',
|
||||
updated_at: '2026-07-08T04:10:00Z',
|
||||
},
|
||||
source_message_summary: {
|
||||
source_message_id: '30002',
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
external_message_id: 'msg-30002',
|
||||
external_conversation_id: 'thread-30002',
|
||||
subject: sourceSubject,
|
||||
sender_summary: 'agent@example.test',
|
||||
received_at: '2026-07-08T04:00:00Z',
|
||||
source_sent_at: null,
|
||||
conversation_message_count: 3,
|
||||
},
|
||||
card_counts: {
|
||||
total_count: 2,
|
||||
readonly_count: 1,
|
||||
pending_confirm_count: 1,
|
||||
review_required_count: 0,
|
||||
confirmed_count: 0,
|
||||
},
|
||||
next_action_card_id: '9102',
|
||||
display_status: 'OPEN',
|
||||
availability: {
|
||||
blocked: false,
|
||||
read_only: false,
|
||||
editable: true,
|
||||
confirmable: true,
|
||||
reviewable: false,
|
||||
ackable: false,
|
||||
readonly_reason_code: null,
|
||||
blocked_by_order_task_id: null,
|
||||
blocked_by_card_id: null,
|
||||
blocked_reason: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
page,
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function createOrderDetailResult(displayName = 'GRP-001') {
|
||||
@@ -343,6 +413,8 @@ async function mountWithPlugins(component: object, initialPath = '/', stubs: Rec
|
||||
{ path: '/', component: { template: '<div />' } },
|
||||
{ path: '/reservation/orders/:orderId', component: { template: '<div />' } },
|
||||
{ path: '/reservation/tasks/:taskId', 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 />' } },
|
||||
],
|
||||
})
|
||||
@@ -369,6 +441,8 @@ describe('reservation P0 views', () => {
|
||||
vi.mocked(service.fetchReservationOrderDetail).mockReset()
|
||||
vi.mocked(service.fetchReservationOrders).mockReset()
|
||||
vi.mocked(service.fetchReservationTaskList).mockReset()
|
||||
vi.mocked(service.fetchReservationV4OrderTasks).mockReset()
|
||||
vi.mocked(service.fetchReservationV4WorkbenchItems).mockReset()
|
||||
vi.mocked(service.fetchSourceMessageConversation).mockReset()
|
||||
})
|
||||
|
||||
@@ -483,209 +557,127 @@ describe('reservation P0 views', () => {
|
||||
expect(wrapper.text()).toContain('GRP-002')
|
||||
})
|
||||
|
||||
it('renders task list items returned by the backend', async () => {
|
||||
vi.mocked(service.fetchReservationTaskList).mockResolvedValue(createTaskListResult('GRP-001', 'Booking Request'))
|
||||
it('renders V4 workbench items returned by the backend', async () => {
|
||||
vi.mocked(service.fetchReservationV4WorkbenchItems).mockResolvedValue(createV4WorkbenchResult('GRP-001', 'Booking Request'))
|
||||
|
||||
const wrapper = await mountWithPlugins(ReservationTaskListView)
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
expect(wrapper.text()).toContain('GRP-001')
|
||||
expect(wrapper.text()).toContain('Booking Request')
|
||||
expect(wrapper.text()).toContain('新预订')
|
||||
expect(wrapper.text()).toContain('订单任务')
|
||||
expect(wrapper.text()).toContain('待确认 1')
|
||||
expect(wrapper.text()).toContain('待复核 1')
|
||||
expect(wrapper.text()).toContain('查看邮件会话')
|
||||
expect(wrapper.text()).not.toContain('接口待补')
|
||||
expect(wrapper.text()).not.toContain('NEW_BOOKING / NEW_BOOKING')
|
||||
expect(service.fetchReservationV4WorkbenchItems).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
}),
|
||||
)
|
||||
expect(service.fetchReservationTaskList).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders task source received time in the source email column', async () => {
|
||||
const taskListResult = createTaskListResult('GRP-001', 'Booking Request')
|
||||
taskListResult.items[0] = {
|
||||
...taskListResult.items[0]!,
|
||||
source_sender_summary: 'guest@example.test',
|
||||
source_received_at: '2026-07-08T03:00:00Z',
|
||||
}
|
||||
vi.mocked(service.fetchReservationTaskList).mockResolvedValue(taskListResult)
|
||||
vi.mocked(service.fetchReservationV4WorkbenchItems).mockResolvedValue(createV4WorkbenchResult('GRP-001', 'Booking Request'))
|
||||
|
||||
const wrapper = await mountWithPlugins(ReservationTaskListView)
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
expect(wrapper.text()).toContain('来源时间')
|
||||
expect(wrapper.text()).toContain('2026-07-08 10:00:00')
|
||||
expect(wrapper.text()).toContain('guest@example.test')
|
||||
})
|
||||
|
||||
it('filters source-message-only tasks with S10 and S99 subtypes', async () => {
|
||||
vi.mocked(service.fetchReservationTaskList).mockResolvedValue(createSourceMessageOnlyTaskListResult('S10'))
|
||||
it('renders V4 S10/S99 source notification rows without a visible order action', async () => {
|
||||
vi.mocked(service.fetchReservationV4WorkbenchItems).mockResolvedValue(createV4SourceNotificationWorkbenchResult('S10'))
|
||||
|
||||
const wrapper = await mountWithPlugins(ReservationTaskListView)
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
const taskTypeOptions = wrapper.find('select[name="task_type"]').findAll('option')
|
||||
.map((option) => option.attributes('value'))
|
||||
const taskSubtypeOptions = wrapper.find('select[name="task_subtype"]').findAll('option')
|
||||
.map((option) => option.attributes('value'))
|
||||
expect(wrapper.text()).toContain('FYI only')
|
||||
expect(wrapper.text()).toContain('来源通知')
|
||||
expect(wrapper.text()).toContain('待确认已读')
|
||||
expect(wrapper.text()).toContain('查看通知')
|
||||
expect(wrapper.text()).toContain('查看邮件会话')
|
||||
expect(wrapper.text()).not.toContain('查看订单')
|
||||
})
|
||||
|
||||
expect(taskTypeOptions).toEqual([
|
||||
'',
|
||||
'NEW_BOOKING',
|
||||
'UPDATE_BOOKING',
|
||||
'CANCEL_BOOKING',
|
||||
'MANUAL_REVIEW',
|
||||
'SOURCE_MESSAGE_ONLY',
|
||||
])
|
||||
expect(taskTypeOptions).not.toContain('INFORMATIONAL_MESSAGE')
|
||||
expect(taskSubtypeOptions).toEqual([
|
||||
'',
|
||||
'NEW_BOOKING',
|
||||
'UPDATE_BOOKING',
|
||||
'CANCEL_BOOKING',
|
||||
'RATE_CHANGE',
|
||||
'UPDATE_STAY_DATES',
|
||||
'UPDATE_ROOM_TYPE',
|
||||
'UPDATE_FIX_CHARGE',
|
||||
'NEW_GROUP_BLOCK',
|
||||
'NEW_FIT_RESERVATION',
|
||||
'MANUAL_REVIEW',
|
||||
'S10',
|
||||
'S99',
|
||||
])
|
||||
expect(taskSubtypeOptions).not.toContain('S000')
|
||||
expect(taskSubtypeOptions).not.toContain('S999')
|
||||
expect(taskSubtypeOptions).not.toContain('linked_parent_release_after_child_split')
|
||||
expect(wrapper.find('select[name="task_type"]').text()).toContain('来源消息只读')
|
||||
expect(wrapper.find('select[name="task_subtype"]').text()).toContain('纯信息类邮件(S10)')
|
||||
expect(wrapper.find('select[name="task_subtype"]').text()).toContain('无法形成业务素材包(S99)')
|
||||
it('filters V4 workbench with supported backend fields only', async () => {
|
||||
vi.mocked(service.fetchReservationV4WorkbenchItems)
|
||||
.mockResolvedValueOnce(createV4WorkbenchResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 }))
|
||||
.mockResolvedValueOnce(createV4SourceNotificationWorkbenchResult('S10'))
|
||||
|
||||
await wrapper.find('select[name="task_type"]').setValue('SOURCE_MESSAGE_ONLY')
|
||||
await vi.dynamicImportSettled()
|
||||
await wrapper.find('select[name="task_subtype"]').setValue('S99')
|
||||
const wrapper = await mountWithPlugins(ReservationTaskListView)
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
expect(service.fetchReservationTaskList).toHaveBeenLastCalledWith(
|
||||
const itemTypeOptions = wrapper.find('select[name="item_type"]').findAll('option')
|
||||
.map((option) => option.attributes('value'))
|
||||
|
||||
expect(itemTypeOptions).toEqual(['', 'ORDER_TASK', 'SOURCE_NOTIFICATION'])
|
||||
expect(wrapper.find('select[name="item_type"]').text()).toContain('来源通知')
|
||||
expect(wrapper.find('select[name="item_type"]').text()).not.toContain('INFORMATIONAL_MESSAGE')
|
||||
|
||||
await wrapper.find('select[name="item_type"]').setValue('SOURCE_NOTIFICATION')
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
expect(service.fetchReservationV4WorkbenchItems).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
task_type: 'SOURCE_MESSAGE_ONLY',
|
||||
task_subtype: 'S99',
|
||||
item_type: 'SOURCE_NOTIFICATION',
|
||||
page_num: 1,
|
||||
}),
|
||||
)
|
||||
const lastTaskRequest = vi.mocked(service.fetchReservationTaskList).mock.calls[
|
||||
vi.mocked(service.fetchReservationTaskList).mock.calls.length - 1
|
||||
const lastTaskRequest = vi.mocked(service.fetchReservationV4WorkbenchItems).mock.calls[
|
||||
vi.mocked(service.fetchReservationV4WorkbenchItems).mock.calls.length - 1
|
||||
]?.[0] ?? {}
|
||||
expect(Object.keys(lastTaskRequest).sort()).toEqual([
|
||||
'keyword',
|
||||
'order_status',
|
||||
'page_num',
|
||||
'page_size',
|
||||
'queue_participation',
|
||||
'task_status',
|
||||
'task_subtype',
|
||||
'task_type',
|
||||
])
|
||||
expect(Object.keys(lastTaskRequest).sort()).toEqual(['item_type', 'keyword', 'page_num', 'page_size'])
|
||||
expect(lastTaskRequest).not.toHaveProperty('task_type')
|
||||
expect(lastTaskRequest).not.toHaveProperty('task_subtype')
|
||||
expect(lastTaskRequest).not.toHaveProperty('result_type')
|
||||
expect(lastTaskRequest).not.toHaveProperty('route_code')
|
||||
expect(lastTaskRequest).not.toHaveProperty('system_process_category')
|
||||
})
|
||||
|
||||
it('renders R08 parent group cancel allotment as a business card', async () => {
|
||||
vi.mocked(service.fetchReservationTaskList).mockResolvedValue(createParentGroupCancelAllotmentTaskListResult())
|
||||
it('filters and paginates V4 business order tasks with supported backend fields', async () => {
|
||||
vi.mocked(service.fetchReservationV4WorkbenchItems)
|
||||
.mockResolvedValueOnce(createV4WorkbenchResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 }))
|
||||
vi.mocked(service.fetchReservationV4OrderTasks)
|
||||
.mockResolvedValueOnce(createV4OrderTaskListResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 }))
|
||||
.mockResolvedValueOnce(createV4OrderTaskListResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 }))
|
||||
.mockResolvedValueOnce(createV4OrderTaskListResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 }))
|
||||
.mockResolvedValueOnce(createV4OrderTaskListResult('GRP-002', 'Booking Update', { page_num: 2, page_size: 20, total: 45 }))
|
||||
|
||||
const wrapper = await mountWithPlugins(ReservationTaskListView)
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
expect(wrapper.text()).toContain('PARENT-2608-BLOCK')
|
||||
expect(wrapper.text()).toContain('Parent Group')
|
||||
expect(wrapper.text()).toContain('取消 Parent Group 配额')
|
||||
expect(wrapper.text()).toContain('取消 Parent Group 整块配额')
|
||||
expect(wrapper.text()).toContain('查看订单')
|
||||
expect(wrapper.text()).not.toContain('适配契约异常')
|
||||
expect(wrapper.text()).not.toContain('linked_parent_release_after_child_split')
|
||||
})
|
||||
expect(wrapper.find('select[name="order_task_status"]').attributes('disabled')).toBeDefined()
|
||||
expect(wrapper.find('select[name="card_status"]').attributes('disabled')).toBeDefined()
|
||||
|
||||
it('renders source-message-only task rows without a visible order action', async () => {
|
||||
vi.mocked(service.fetchReservationTaskList).mockResolvedValue(createSourceMessageOnlyTaskListResult('S10'))
|
||||
await wrapper.find('select[name="item_type"]').setValue('ORDER_TASK')
|
||||
await vi.dynamicImportSettled()
|
||||
expect(wrapper.find('select[name="order_task_status"]').attributes('disabled')).toBeUndefined()
|
||||
expect(wrapper.find('select[name="card_status"]').attributes('disabled')).toBeUndefined()
|
||||
|
||||
const wrapper = await mountWithPlugins(ReservationTaskListView)
|
||||
await wrapper.find('select[name="order_task_status"]').setValue('OPEN')
|
||||
await vi.dynamicImportSettled()
|
||||
await wrapper.find('select[name="card_status"]').setValue('REVIEW_REQUIRED')
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
expect(wrapper.text()).toContain('来源消息只读')
|
||||
expect(wrapper.text()).toContain('纯信息类邮件(S10)')
|
||||
expect(wrapper.text()).toContain('来源消息通知')
|
||||
expect(wrapper.text()).toContain('FYI only')
|
||||
expect(wrapper.text()).toContain('查看任务')
|
||||
expect(wrapper.text()).toContain('查看邮件会话')
|
||||
expect(wrapper.text()).not.toContain('查看订单')
|
||||
expect(wrapper.text()).not.toContain('29990')
|
||||
})
|
||||
|
||||
it('does not treat a business task as source-message-only only because subtype is S10', async () => {
|
||||
const result = createTaskListResult('GRP-S10-BUSINESS', 'Business booking request')
|
||||
result.items[0] = {
|
||||
...result.items[0]!,
|
||||
task_type: 'NEW_BOOKING',
|
||||
task_subtype: 'S10',
|
||||
result_type: 'normal_task',
|
||||
ai_task_type: 'NEW_BOOKING',
|
||||
route_code: 'S10',
|
||||
system_process_category: 'BUSINESS_TASK',
|
||||
card_name: 'New Booking',
|
||||
}
|
||||
vi.mocked(service.fetchReservationTaskList).mockResolvedValue(result)
|
||||
|
||||
const wrapper = await mountWithPlugins(ReservationTaskListView)
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
const rowText = wrapper.find('tbody').text()
|
||||
expect(rowText).toContain('GRP-S10-BUSINESS')
|
||||
expect(rowText).toContain('20001')
|
||||
expect(rowText).toContain('查看订单')
|
||||
expect(rowText).not.toContain('来源消息只读')
|
||||
expect(rowText).not.toContain('不关联可见订单')
|
||||
})
|
||||
|
||||
it('filters and paginates task list with supported backend fields', async () => {
|
||||
vi.mocked(service.fetchReservationTaskList)
|
||||
.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-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()
|
||||
|
||||
expect(wrapper.text()).not.toContain('订单状态筛选待后端接入')
|
||||
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 wrapper.find('select[name="task_status"]').setValue('FAILED')
|
||||
await vi.dynamicImportSettled()
|
||||
await wrapper.find('select[name="task_subtype"]').setValue('RATE_CHANGE')
|
||||
await vi.dynamicImportSettled()
|
||||
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 taskListCalls = vi.mocked(service.fetchReservationV4OrderTasks).mock.calls
|
||||
const filteredTaskRequest = taskListCalls[taskListCalls.length - 1]?.[0] ?? {}
|
||||
expect(filteredTaskRequest).toMatchObject({
|
||||
task_status: 'FAILED',
|
||||
task_subtype: 'RATE_CHANGE',
|
||||
order_status: 'ENDED',
|
||||
queue_participation: true,
|
||||
order_task_status: 'OPEN',
|
||||
card_status: 'REVIEW_REQUIRED',
|
||||
page_num: 1,
|
||||
})
|
||||
|
||||
await wrapper.findAll('button').find((button) => button.text().includes('下一页'))?.trigger('click')
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
expect(service.fetchReservationTaskList).toHaveBeenLastCalledWith(
|
||||
expect(service.fetchReservationV4OrderTasks).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
task_status: 'FAILED',
|
||||
task_subtype: 'RATE_CHANGE',
|
||||
order_status: 'ENDED',
|
||||
queue_participation: true,
|
||||
order_task_status: 'OPEN',
|
||||
card_status: 'REVIEW_REQUIRED',
|
||||
page_num: 2,
|
||||
page_size: 20,
|
||||
}),
|
||||
@@ -695,8 +687,8 @@ describe('reservation P0 views', () => {
|
||||
})
|
||||
|
||||
it('renders backend-normalized task pagination metadata', async () => {
|
||||
vi.mocked(service.fetchReservationTaskList).mockResolvedValue(
|
||||
createTaskListResult('GRP-001', 'Booking Request', { page_num: 2, page_size: 10, total: 21 }),
|
||||
vi.mocked(service.fetchReservationV4WorkbenchItems).mockResolvedValue(
|
||||
createV4WorkbenchResult('GRP-001', 'Booking Request', { page_num: 2, page_size: 10, total: 21 }),
|
||||
)
|
||||
|
||||
const wrapper = await mountWithPlugins(ReservationTaskListView)
|
||||
@@ -706,9 +698,9 @@ describe('reservation P0 views', () => {
|
||||
})
|
||||
|
||||
it('reloads the task list when the selected hotel changes', async () => {
|
||||
vi.mocked(service.fetchReservationTaskList)
|
||||
.mockResolvedValueOnce(createTaskListResult('GRP-001', 'Booking Request'))
|
||||
.mockResolvedValueOnce(createTaskListResult('GRP-002', 'Booking Update'))
|
||||
vi.mocked(service.fetchReservationV4WorkbenchItems)
|
||||
.mockResolvedValueOnce(createV4WorkbenchResult('GRP-001', 'Booking Request'))
|
||||
.mockResolvedValueOnce(createV4WorkbenchResult('GRP-002', 'Booking Update'))
|
||||
|
||||
const wrapper = await mountWithPlugins(ReservationTaskListView)
|
||||
await vi.dynamicImportSettled()
|
||||
@@ -717,8 +709,8 @@ describe('reservation P0 views', () => {
|
||||
authStore.setSelectedHotelId('HOTEL-BKK')
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
expect(service.fetchReservationTaskList).toHaveBeenCalledTimes(2)
|
||||
expect(service.fetchReservationTaskList).toHaveBeenLastCalledWith(
|
||||
expect(service.fetchReservationV4WorkbenchItems).toHaveBeenCalledTimes(2)
|
||||
expect(service.fetchReservationV4WorkbenchItems).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
page_num: 1,
|
||||
}),
|
||||
@@ -748,21 +740,21 @@ describe('reservation P0 views', () => {
|
||||
})
|
||||
|
||||
it('keeps the latest task list response when filters change quickly', async () => {
|
||||
const firstRequest = createDeferred<Awaited<ReturnType<typeof service.fetchReservationTaskList>>>()
|
||||
const secondRequest = createDeferred<Awaited<ReturnType<typeof service.fetchReservationTaskList>>>()
|
||||
vi.mocked(service.fetchReservationTaskList)
|
||||
const firstRequest = createDeferred<Awaited<ReturnType<typeof service.fetchReservationV4WorkbenchItems>>>()
|
||||
const secondRequest = createDeferred<Awaited<ReturnType<typeof service.fetchReservationV4WorkbenchItems>>>()
|
||||
vi.mocked(service.fetchReservationV4WorkbenchItems)
|
||||
.mockReturnValueOnce(firstRequest.promise)
|
||||
.mockReturnValueOnce(secondRequest.promise)
|
||||
|
||||
const wrapper = await mountWithPlugins(ReservationTaskListView)
|
||||
await wrapper.find('input[type="search"]').setValue('fresh')
|
||||
expect(service.fetchReservationTaskList).toHaveBeenCalledTimes(2)
|
||||
expect(service.fetchReservationV4WorkbenchItems).toHaveBeenCalledTimes(2)
|
||||
|
||||
secondRequest.resolve(createTaskListResult('GRP-NEW', 'New Booking Request'))
|
||||
secondRequest.resolve(createV4WorkbenchResult('GRP-NEW', 'New Booking Request'))
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('GRP-NEW')
|
||||
|
||||
firstRequest.resolve(createTaskListResult('GRP-OLD', 'Old Booking Request'))
|
||||
firstRequest.resolve(createV4WorkbenchResult('GRP-OLD', 'Old Booking Request'))
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('GRP-NEW')
|
||||
expect(wrapper.text()).not.toContain('GRP-OLD')
|
||||
|
||||
Reference in new issue
Block a user