import { beforeEach, describe, expect, it, vi } from 'vitest' import { convertReservationManualReviewTask, fetchReservationOrderDetail, fetchReservationOrders, fetchReservationTaskDetail, fetchReservationTaskList, fetchSourceMessageConversation, resolveReservationManualReviewTask, } 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 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', 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') }) 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: [], 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('完整邮件正文') }) })