import { beforeEach, describe, expect, it, vi } from 'vitest' import { convertReservationManualReviewTask, ackReservationV4SourceNotification, confirmReservationV4OrderTaskCard, fetchReservationOrderDetail, fetchReservationOrders, fetchReservationTaskDetail, fetchReservationTaskList, fetchReservationV4AccountLookups, fetchReservationV4OrderTaskDetail, fetchReservationV4OrderTasks, fetchReservationV4RateCodeLookups, fetchReservationV4RoomTypeLookups, fetchReservationV4SourceNotificationDetail, fetchReservationV4WorkbenchItems, fetchSourceMessageConversation, resolveReservationManualReviewTask, resolveReservationV4OrderTaskCardReview, } from '@/services/reservationService' import { setReservationHotelIdProvider } from '@/config/reservationConfig' const jsonHeaders = { headers: { get: (name: string) => (name.toLowerCase() === 'content-type' ? 'application/json' : null), }, } function mockJsonResponse(payload: unknown): Response { return { ok: true, status: 200, json: async () => payload, text: async () => JSON.stringify(payload), ...jsonHeaders, } as Response } describe('reservationService real API mode', () => { beforeEach(() => { vi.restoreAllMocks() setReservationHotelIdProvider(null) }) it('fetches the task list from the backend by default', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue( mockJsonResponse({ items: [ { task_id: '10001', order_id: '20001', hotel_id: 'HOTEL-TEST', display_order_key: 'GRP-001', 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: 'Booking Request', created_at: '2026-07-08T03:00:00Z', updated_at: '2026-07-08T03:10:00Z', }, ], page: { page_num: 1, page_size: 20, total: 1, }, }), ) const result = await fetchReservationTaskList({ task_status: 'PENDING_CONFIRM', order_status: 'ACTIVE', page_num: 1, page_size: 20, }) expect(fetchMock).toHaveBeenCalledWith( '/api/reservation/tasks?task_status=PENDING_CONFIRM&order_status=ACTIVE&page_num=1&page_size=20', expect.objectContaining({ method: 'GET' }), ) expect(result.items).toHaveLength(1) expect(result.items[0]?.source_subject).toBe('Booking Request') }) it('passes task subtype and terminal task status filters to the backend', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue( mockJsonResponse({ items: [], page: { page_num: 1, page_size: 20, total: 0, }, }), ) await fetchReservationTaskList({ task_subtype: 'RATE_CHANGE', task_status: 'FAILED', page_num: 1, page_size: 20, }) expect(fetchMock).toHaveBeenCalledWith( '/api/reservation/tasks?task_subtype=RATE_CHANGE&task_status=FAILED&page_num=1&page_size=20', expect.objectContaining({ method: 'GET' }), ) }) it('uses the selected auth hotel when available', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue( mockJsonResponse({ items: [], page: { page_num: 1, page_size: 20, total: 0, }, }), ) setReservationHotelIdProvider(() => 'HOTEL-BKK') await fetchReservationTaskList({ page_num: 1, page_size: 20, }) expect(fetchMock).toHaveBeenCalledWith( '/api/reservation/tasks?hotel_id=HOTEL-BKK&page_num=1&page_size=20', expect.objectContaining({ method: 'GET' }), ) }) 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 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' } })) .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({ items: [ { order_id: '20001', hotel_id: 'HOTEL-TEST', order_status: 'ACTIVE', display_order_key: 'GRP-001', temporary_order_no: null, confirmation_number: null, group_code: 'GRP-001', display_name: 'GRP-001', open_task_count: 2, next_processable_task_id: '10002', next_v4_order_task_id: '9001', next_v4_action_card_id: '9101', next_v4_action_type: 'REVIEW', next_v4_action_status: 'REVIEW_REQUIRED', v4_open_order_task_count: 1, created_at: '2026-07-08T03:00:00Z', updated_at: '2026-07-08T03:10:00Z', }, ], page: { page_num: 1, page_size: 20, total: 1, }, }), ) const result = await fetchReservationOrders({ keyword: 'GRP-001', page_num: 1, page_size: 20, }) expect(fetchMock).toHaveBeenCalledWith( '/api/reservation/orders?keyword=GRP-001&page_num=1&page_size=20', expect.objectContaining({ method: 'GET' }), ) expect(result.items[0]?.display_order_key).toBe('GRP-001') expect(result.items[0]?.next_processable_task_id).toBe('10002') expect(result.items[0]?.next_v4_order_task_id).toBe('9001') expect(result.items[0]?.next_v4_action_type).toBe('REVIEW') expect(result.items[0]?.next_v4_action_status).toBe('REVIEW_REQUIRED') expect(result.items[0]?.v4_open_order_task_count).toBe(1) }) it('passes order status and business-key filters to the backend', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue( mockJsonResponse({ items: [], page: { page_num: 2, page_size: 10, total: 0, }, }), ) await fetchReservationOrders({ order_status: 'LOGIC_DELETED', group_code: 'GRP-001', confirmation_number: 'CNF-001', keyword: 'VIP', page_num: 2, page_size: 10, }) expect(fetchMock).toHaveBeenCalledWith( '/api/reservation/orders?order_status=LOGIC_DELETED&group_code=GRP-001&confirmation_number=CNF-001&keyword=VIP&page_num=2&page_size=10', expect.objectContaining({ method: 'GET' }), ) }) it('converts a manual review task through the backend endpoint', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue( mockJsonResponse({ task_id: '10001', order_id: '20002', original_order_id: '20001', system_task_type: 'UPDATE_BOOKING', task_card_type: 'UPDATE_BOOKING', task_status: 'PENDING_CONFIRM', original_order_logic_deleted: true, }), ) const result = await convertReservationManualReviewTask('10001', { target_task_type: 'UPDATE_BOOKING', target_order_id: '20002', reason: 'Matched by group code', }) expect(fetchMock).toHaveBeenCalledWith( '/api/reservation/tasks/10001/manual-review-conversions', expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ 'Content-Type': 'application/json', }), body: JSON.stringify({ target_task_type: 'UPDATE_BOOKING', target_order_id: '20002', reason: 'Matched by group code', }), }), ) expect(result.system_task_type).toBe('UPDATE_BOOKING') expect(result.original_order_logic_deleted).toBe(true) }) it('resolves a type-known manual review task on the original card', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue( mockJsonResponse({ 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: [], }), ) const result = await resolveReservationManualReviewTask('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', }, ], }) expect(fetchMock).toHaveBeenCalledWith( '/api/reservation/tasks/10001/manual-review-resolutions', expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ 'Content-Type': 'application/json', }), body: JSON.stringify({ 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', }, ], }), }), ) expect(result.task_status).toBe('READY') expect(result.review_status).toBe('RESOLVED') }) it('fetches order detail from the backend by default', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue( mockJsonResponse({ order: { order_id: '20001', hotel_id: 'HOTEL-TEST', order_status: 'ACTIVE', temporary_order_no: null, confirmation_number: 'CNF123456', group_code: 'GRP-001', block_code: null, allotment_code: null, display_name: 'GRP-001', created_at: '2026-07-08T03:00:00Z', updated_at: '2026-07-08T03:10:00Z', }, tasks: [], v4_order_tasks: [], warnings: [], }), ) const result = await fetchReservationOrderDetail('20001') expect(fetchMock).toHaveBeenCalledWith( '/api/reservation/orders/20001?include_tasks=true&include_source_summary=true', expect.objectContaining({ method: 'GET' }), ) expect(result.order.display_name).toBe('GRP-001') }) it('fetches task detail from the backend by default', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue( mockJsonResponse({ task_id: '10001', order_id: '20001', source_message_id: '30001', 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: [], }), ) const result = await fetchReservationTaskDetail('10001') expect(fetchMock).toHaveBeenCalledWith( '/api/reservation/tasks/10001', expect.objectContaining({ method: 'GET' }), ) expect(result.task_id).toBe('10001') }) it('fetches source message conversation from the backend by default', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue( mockJsonResponse({ conversation: { hotel_id: 'HOTEL-TEST', channel: 'EMAIL', external_conversation_id: 'thread-30001', subject: 'Booking Request', message_count: 2, first_received_at: '2026-07-08T02:00:00Z', last_received_at: '2026-07-08T03:00:00Z', }, messages: [ { id: '30001', external_message_id: 'msg-30001', external_conversation_id: 'thread-30001', sender_summary: 'guest@example.test', subject: 'Booking Request', received_at: '2026-07-08T02:00:00Z', source_sent_at: '2026-07-08T01:58:00Z', text_body: '完整邮件正文', html_body: '
完整邮件正文
', html_body_sanitized: '完整邮件正文
', html_sanitize_required: true, html_render_mode: 'SANITIZED_HTML', inline_images: [], attachments: [], related_orders: [ { order_id: '20001', display_order_key: 'GRP-001', order_status: 'ACTIVE', }, ], related_tasks: [ { task_id: '10001', order_id: '20001', task_type: 'NEW_BOOKING', task_subtype: 'NEW_BOOKING', task_status: 'PENDING_CONFIRM', card_name: 'New Booking', }, ], }, ], }), ) const result = await fetchSourceMessageConversation('30001') expect(fetchMock).toHaveBeenCalledWith( '/api/source-messages/30001/conversation', expect.objectContaining({ method: 'GET' }), ) expect(result.conversation.external_conversation_id).toBe('thread-30001') expect(result.messages[0]?.text_body).toBe('完整邮件正文') }) })