接入V4任务卡目录查询

This commit is contained in:
andy committed 2026-07-19 15:35:58 +07:00
1 parent 8b37232ac9
commit d808732107
16 files changed
+803 -38

No files matched your search

@@ -8,8 +8,11 @@ import {
fetchReservationOrders,
fetchReservationTaskDetail,
fetchReservationTaskList,
fetchReservationV4AccountLookups,
fetchReservationV4OrderTaskDetail,
fetchReservationV4OrderTasks,
fetchReservationV4RateCodeLookups,
fetchReservationV4RoomTypeLookups,
fetchReservationV4SourceNotificationDetail,
fetchReservationV4WorkbenchItems,
fetchSourceMessageConversation,
@@ -188,6 +191,85 @@ describe('reservationService real API mode', () => {
)
})
it('fetches V4 catalog lookups with selected hotel and query params', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch')
.mockResolvedValueOnce(mockJsonResponse({
hotel_id: 'HOTEL-BKK',
catalog_type: 'ACCOUNT',
catalog_source: 'SYSTEM_MANAGED',
catalog_version: 'v1',
stale: false,
items: [],
page: {
page_num: 1,
page_size: 20,
total: 0,
},
warnings: [],
}))
.mockResolvedValueOnce(mockJsonResponse({
hotel_id: 'HOTEL-BKK',
catalog_type: 'ROOM_TYPE',
catalog_source: 'PMS_SYNC',
catalog_version: 'v2',
stale: true,
items: [],
page: {
page_num: 1,
page_size: 20,
total: 0,
},
warnings: ['PMS sync stale'],
}))
.mockResolvedValueOnce(mockJsonResponse({
hotel_id: 'HOTEL-BKK',
catalog_type: 'RATE_CODE',
catalog_source: 'SYSTEM_MANAGED',
catalog_version: 'v3',
stale: false,
items: [],
page: {
page_num: 1,
page_size: 20,
total: 0,
},
warnings: [],
}))
setReservationHotelIdProvider(() => 'HOTEL-BKK')
await fetchReservationV4AccountLookups({
keyword: 'QBD',
page_num: 1,
page_size: 20,
})
await fetchReservationV4RoomTypeLookups({
keyword: 'RM',
page_num: 1,
page_size: 20,
})
await fetchReservationV4RateCodeLookups({
keyword: 'BAR',
page_num: 1,
page_size: 20,
})
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/reservation/lookups/accounts?hotel_id=HOTEL-BKK&keyword=QBD&page_num=1&page_size=20',
expect.objectContaining({ method: 'GET' }),
)
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/reservation/lookups/room-types?hotel_id=HOTEL-BKK&keyword=RM&page_num=1&page_size=20',
expect.objectContaining({ method: 'GET' }),
)
expect(fetchMock).toHaveBeenNthCalledWith(
3,
'/api/reservation/lookups/rate-codes?hotel_id=HOTEL-BKK&keyword=BAR&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' } }))
@@ -482,6 +564,7 @@ describe('reservationService real API mode', () => {
updated_at: '2026-07-08T03:10:00Z',
},
tasks: [],
v4_order_tasks: [],
warnings: [],
}),
)
@@ -7,6 +7,7 @@ import {
buildReservationV4ReviewOverrides,
isReservationV4ConfirmWritableField,
isReservationV4ReviewWritableField,
reservationV4LookupKindForOptionsSource,
stringifyReservationV4SafeDisplayValue,
validateReservationV4Fields,
} from '@/utils/reservationV4FieldRules'
@@ -188,4 +189,11 @@ describe('reservationV4FieldRules', () => {
'/basic_information/account_code': 'Account is required',
})
})
it('maps V4 options_source to lookup kinds without returning fixed catalog options', () => {
expect(reservationV4LookupKindForOptionsSource('reservation_v4_account_catalog')).toBe('ACCOUNT')
expect(reservationV4LookupKindForOptionsSource('RESERVATION_V4_ROOM_TYPE_CATALOG')).toBe('ROOM_TYPE')
expect(reservationV4LookupKindForOptionsSource('reservation_v4_rate_code_catalog')).toBe('RATE_CODE')
expect(reservationV4LookupKindForOptionsSource('static_enum')).toBeNull()
})
})
+166 -4
View File
@@ -20,7 +20,10 @@ vi.mock('@/services/reservationService', async (importOriginal) => {
...actual,
ackReservationV4SourceNotification: vi.fn(),
confirmReservationV4OrderTaskCard: vi.fn(),
fetchReservationV4AccountLookups: vi.fn(),
fetchReservationV4OrderTaskDetail: vi.fn(),
fetchReservationV4RateCodeLookups: vi.fn(),
fetchReservationV4RoomTypeLookups: vi.fn(),
fetchReservationV4SourceNotificationDetail: vi.fn(),
resolveReservationV4OrderTaskCardReview: vi.fn(),
}
@@ -33,12 +36,16 @@ describe('reservation V4 pages', () => {
sessionStorage.clear()
vi.mocked(service.ackReservationV4SourceNotification).mockReset()
vi.mocked(service.confirmReservationV4OrderTaskCard).mockReset()
vi.mocked(service.fetchReservationV4AccountLookups).mockReset()
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockReset()
vi.mocked(service.fetchReservationV4RateCodeLookups).mockReset()
vi.mocked(service.fetchReservationV4RoomTypeLookups).mockReset()
vi.mocked(service.fetchReservationV4SourceNotificationDetail).mockReset()
vi.mocked(service.resolveReservationV4OrderTaskCardReview).mockReset()
mockCatalogLookups()
})
it('renders V4 order task cards and confirms only editable basic information fields', async () => {
it('renders V4 order task cards, loads lookup options and confirms only selected codes', async () => {
const detail = createOrderTaskDetail()
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
vi.mocked(service.confirmReservationV4OrderTaskCard).mockResolvedValue({
@@ -60,8 +67,21 @@ describe('reservation V4 pages', () => {
expect(wrapper.text()).toContain('基础信息卡')
expect(wrapper.text()).toContain('业务任务卡')
expect(wrapper.text()).toContain('Backend contract issue')
expect(service.fetchReservationV4AccountLookups).toHaveBeenCalledWith({
hotel_id: 'HOTEL-TEST',
page_num: 1,
page_size: 100,
})
expect(service.fetchReservationV4RoomTypeLookups).toHaveBeenCalledWith({
hotel_id: 'HOTEL-TEST',
page_num: 1,
page_size: 100,
})
await wrapper.find('select').setValue('HANATOUR')
await wrapper.find('select').setValue('ACC-LIVE')
await flushPromises()
expect(wrapper.text()).toContain('LEISURE')
expect(wrapper.text()).toContain('TRAVEL_AGENT')
await wrapper.findAll('button').find((button) => button.text().includes('确认卡片'))?.trigger('click')
await flushPromises()
@@ -69,7 +89,7 @@ describe('reservation V4 pages', () => {
version: 7,
confirmed_payload: {
basic_information: {
account_code: 'HANATOUR',
account_code: 'ACC-LIVE',
},
},
})
@@ -153,6 +173,74 @@ describe('reservation V4 pages', () => {
})
})
it('shows non-blocking lookup empty, stale and warning states', async () => {
const detail = createOrderTaskDetail()
vi.mocked(service.fetchReservationV4AccountLookups).mockResolvedValue({
hotel_id: 'HOTEL-TEST',
catalog_type: 'ACCOUNT',
catalog_source: 'PMS_SYNC',
catalog_version: 'stale-v1',
stale: true,
items: [],
page: {
page_num: 1,
page_size: 100,
total: 0,
},
warnings: ['PMS sync is stale'],
})
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
const wrapper = await mountWithPlugins(
ReservationV4OrderTaskDetailView,
'/reservation/order-tasks/9001',
)
await flushPromises()
expect(wrapper.text()).toContain(zhCN.taskV4.lookup.empty)
expect(wrapper.text()).toContain(zhCN.taskV4.lookup.stale)
expect(wrapper.text()).toContain('PMS sync is stale')
expect(wrapper.text()).toContain('PMS_SYNC')
expect(wrapper.text()).toContain('stale-v1')
})
it('does not submit a catalog field value that is missing from the loaded lookup result', async () => {
const detail = createOrderTaskDetail({
basicAccountValue: 'UNKNOWN_ACCOUNT',
basicAccountRequired: true,
})
vi.mocked(service.fetchReservationV4AccountLookups).mockResolvedValue({
hotel_id: 'HOTEL-TEST',
catalog_type: 'ACCOUNT',
catalog_source: 'SYSTEM_MANAGED',
catalog_version: 'catalog-v1',
stale: false,
items: [],
page: {
page_num: 1,
page_size: 100,
total: 0,
},
warnings: [],
})
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
const wrapper = await mountWithPlugins(
ReservationV4OrderTaskDetailView,
'/reservation/order-tasks/9001',
)
await flushPromises()
expect(wrapper.text()).toContain('UNKNOWN_ACCOUNT')
expect(wrapper.text()).toContain(zhCN.taskV4.lookup.currentNotInCatalog.replace('{value}', 'UNKNOWN_ACCOUNT'))
await wrapper.findAll('button').find((button) => button.text().includes('确认卡片'))?.trigger('click')
await flushPromises()
expect(service.confirmReservationV4OrderTaskCard).not.toHaveBeenCalled()
expect(wrapper.text()).toContain(zhCN.taskV4.validationFailed)
})
it('requires confirmed order id before resolving an unresolved V4 review card', async () => {
const detail = createOrderTaskDetail({
businessCardStatus: 'REVIEW_REQUIRED',
@@ -277,10 +365,83 @@ async function mountWithPlugins(component: object, initialPath: string) {
})
}
function mockCatalogLookups() {
vi.mocked(service.fetchReservationV4AccountLookups).mockResolvedValue({
hotel_id: 'HOTEL-TEST',
catalog_type: 'ACCOUNT',
catalog_source: 'SYSTEM_MANAGED',
catalog_version: 'catalog-v1',
stale: false,
items: [
{
code: 'ACC-LIVE',
display_name: 'Live Account',
status: 'ACTIVE',
catalog_source: 'SYSTEM_MANAGED',
market_code: 'LEISURE',
market_name: 'Leisure',
source_code: 'TRAVEL_AGENT',
source_name: 'Travel Agent',
adult_capacity: null,
pricing_available: null,
},
],
page: {
page_num: 1,
page_size: 100,
total: 1,
},
warnings: [],
})
vi.mocked(service.fetchReservationV4RoomTypeLookups).mockResolvedValue({
hotel_id: 'HOTEL-TEST',
catalog_type: 'ROOM_TYPE',
catalog_source: 'SYSTEM_MANAGED',
catalog_version: 'catalog-v1',
stale: false,
items: [
{
code: 'TWN',
display_name: 'Twin',
status: 'ACTIVE',
catalog_source: 'SYSTEM_MANAGED',
market_code: null,
market_name: null,
source_code: null,
source_name: null,
adult_capacity: 2,
pricing_available: null,
},
],
page: {
page_num: 1,
page_size: 100,
total: 1,
},
warnings: [],
})
vi.mocked(service.fetchReservationV4RateCodeLookups).mockResolvedValue({
hotel_id: 'HOTEL-TEST',
catalog_type: 'RATE_CODE',
catalog_source: 'SYSTEM_MANAGED',
catalog_version: 'catalog-v1',
stale: false,
items: [],
page: {
page_num: 1,
page_size: 100,
total: 0,
},
warnings: [],
})
}
function createOrderTaskDetail(options: {
businessCardStatus?: string
businessCardAvailability?: Partial<ReservationV4TaskCardResult['availability']>
sourceDisplayPayload?: ReservationV4TaskCardResult['display_payload']
basicAccountValue?: string
basicAccountRequired?: boolean
} = {}): ReservationV4OrderTaskDetailResult {
const sourceCard = createCard('card-source', 'SOURCE_MESSAGE_DISPLAY', 'READONLY', {
fields: [],
@@ -301,7 +462,8 @@ function createOrderTaskDetail(options: {
fields: [
createField('/basic_information/account_code', {
display_name: 'Account',
value: '',
value: options.basicAccountValue ?? '',
required: options.basicAccountRequired ?? false,
options_source: 'RESERVATION_V4_ACCOUNT_CATALOG',
control_type: 'SELECT',
}),
+32
View File
@@ -270,6 +270,35 @@ function createOrderDetailResult(displayName = 'GRP-001') {
created_at: '2026-07-08T04:00:00Z',
},
],
v4_order_tasks: [
{
order_task_id: '9001',
order_ref: 'order-ref-1',
order_task_status: 'OPEN',
card_counts: {
total_count: 3,
readonly_count: 1,
pending_confirm_count: 1,
review_required_count: 1,
confirmed_count: 0,
},
source_message_summary: {
source_message_id: '30003',
hotel_id: 'HOTEL-TEST',
external_message_id: 'm-v4-1',
external_conversation_id: 'thread-v4-1',
subject: 'V4 Booking Package',
sender_summary: 'v4@example.test',
received_at: '2026-07-08T05:00:00Z',
source_sent_at: null,
conversation_message_count: 1,
},
source_received_at: '2026-07-08T05:00:00Z',
created_at: '2026-07-08T05:00:00Z',
updated_at: '2026-07-08T05:10:00Z',
latest_activity_at: '2026-07-08T05:10:00Z',
},
],
warnings: [],
}
}
@@ -461,6 +490,9 @@ describe('reservation P0 views', () => {
expect(wrapper.text()).toContain('2')
expect(wrapper.text()).toContain('前序任务未完成')
expect(wrapper.text()).toContain('查看邮件会话')
expect(wrapper.text()).toContain(zhCN.order.v4TaskTimeline)
expect(wrapper.text()).toContain('order-ref-1')
expect(wrapper.text()).toContain('V4 Booking Package')
expect(wrapper.text()).not.toContain('PREVIOUS_TASK_NOT_FINISHED')
expect(wrapper.text()).not.toContain('来源邮件摘要字段待后端补充')
})