import { describe, expect, it } from 'vitest' import type { ReservationTaskFieldResult } from '@/types/reservation' import { buildInitialFieldValues, buildEditableFieldValues, groupReservationFields, isReservationFieldEditable, isReservationManualReviewFieldWritable, isSelectField, validateReservationFieldValues, } from '@/utils/reservationFieldRules' function createField(overrides: Partial): ReservationTaskFieldResult { return { row_number: 1, card_name: 'New Booking', display_area: '基础信息', field_path: 'guest.name', display_name: '客人姓名', visible: 'Y', editable: 'Y', input_editable: 'Y', select_editable: 'N', date_picker: 'N', number_input: 'N', file_display: 'N', table_editable: 'N', enum_options: null, required_rule: null, display_condition: null, validation_rule: null, write_path: null, opera_write_participation: 'N', opera_parameter_mapping: null, notes: null, value: null, ...overrides, } } describe('validateReservationFieldValues', () => { it('validates required, enum, date, and number fields from backend metadata', () => { const errors = validateReservationFieldValues( [ createField({ field_path: 'guest.name', display_name: '客人姓名', required_rule: 'Y', }), createField({ field_path: 'room.type', display_name: '房型', select_editable: 'Y', enum_options: '豪华 Q1A, 行政套房', }), createField({ field_path: 'stay.arrival_date', display_name: '入住日期', date_picker: 'Y', }), createField({ field_path: 'room.count', display_name: '房间数', number_input: 'Y', }), ], { 'guest.name': '', 'room.type': '不存在的房型', 'stay.arrival_date': '2026/07/08', 'room.count': 'abc', }, ) expect(errors).toMatchObject({ 'guest.name': { code: 'required', field_name: '客人姓名', }, 'room.type': { code: 'enum', field_name: '房型', }, 'stay.arrival_date': { code: 'date', field_name: '入住日期', }, 'room.count': { code: 'number', field_name: '房间数', }, }) }) it('groups all backend-returned fields without rebuilding display-condition logic', () => { const groups = groupReservationFields( [ createField({ field_path: 'case_keys.group_code', display_name: 'Group Code', display_condition: '对象为Group Block或Allotment时展示', }), createField({ field_path: 'case_keys.confirmation_number', display_name: 'Confirmation No.', display_condition: '对象为FIT Reservation时展示', }), ], '基础信息', { 'extracted_fields.booking_object_type': 'Group Block', 'case_keys.group_code': 'GRP-001', }, ) expect(groups.flatMap((group) => group.fields.map((field) => field.field_path))).toEqual([ 'case_keys.group_code', 'case_keys.confirmation_number', ]) }) it('builds a backend-safe editable payload without readonly or omitted fields', () => { const payload = buildEditableFieldValues( [ createField({ field_path: 'visible_reason', display_name: '生成原因', editable: 'N', input_editable: 'N', required_rule: 'Y', }), createField({ field_path: 'case_keys.group_code', display_name: 'Group Code', display_condition: '对象为Group Block或Allotment时展示', }), ], { visible_reason: 'AI extracted this task.', 'case_keys.group_code': 'GRP-001', 'case_keys.confirmation_number': 'CNF-NOT-RETURNED', 'extracted_fields.booking_object_type': 'Group Block', }, false, ) expect(payload).toEqual({ 'case_keys.group_code': 'GRP-001', }) }) it('treats detail.fields as the authoritative backend-filtered field set', () => { const fields = [ createField({ field_path: 'extracted_fields.booking_object_type', display_name: '订单对象类型', select_editable: 'Y', input_editable: 'N', enum_options: 'Group Block, FIT Reservation', value: 'Group Block', }), createField({ field_path: 'case_keys.group_code', display_name: 'Group Code', value: 'GRP-001', }), ] const values = buildInitialFieldValues(fields, null, '20260711-p0') const groups = groupReservationFields(fields, '基础信息', { ...values, 'case_keys.confirmation_number': 'CNF-SHOULD-NOT-RENDER', attachments: [ { name: 'hidden-attachment.pdf', }, ], }) const payload = buildEditableFieldValues( fields, { ...values, 'case_keys.confirmation_number': 'CNF-SHOULD-NOT-SUBMIT', attachments: [ { name: 'hidden-attachment.pdf', }, ], }, false, ) expect(groups.flatMap((group) => group.fields.map((field) => field.field_path))).toEqual([ 'extracted_fields.booking_object_type', 'case_keys.group_code', ]) expect(payload).toEqual({ 'extracted_fields.booking_object_type': 'Group Block', 'case_keys.group_code': 'GRP-001', }) expect(payload).not.toHaveProperty('case_keys.confirmation_number') expect(payload).not.toHaveProperty('attachments') }) it('uses field control metadata before legacy editable flags when building payloads', () => { const payload = buildEditableFieldValues( [ createField({ field_path: 'case_keys.group_code', display_name: 'Group Code', value: 'GRP-001', control_type: 'text', edit_scope: 'normal_and_manual_review', write_target: 'field_values', raw_readonly: false, } as Partial), createField({ field_path: 'source_message.attachments', display_name: '来源附件', value: [{ name: 'rooming-list.pdf' }], editable: 'Y', input_editable: 'Y', control_type: 'file', edit_scope: 'never', write_target: 'none', raw_readonly: true, } as Partial), createField({ field_path: 'route_code', display_name: '路由代码', value: 'R01_NEW_BOOKING_NORMAL', editable: 'Y', input_editable: 'Y', control_type: 'workflow_state', edit_scope: 'system_only', write_target: 'none', raw_readonly: false, } as Partial), ], { 'case_keys.group_code': 'GRP-001', 'source_message.attachments': [{ name: 'rooming-list.pdf' }], route_code: 'R01_NEW_BOOKING_NORMAL', }, false, ) expect(payload).toEqual({ 'case_keys.group_code': 'GRP-001', }) }) it('submits dual draft and review fields in normal editable payloads', () => { const payload = buildEditableFieldValues( [ createField({ field_path: 'case_keys.group_code', display_name: 'Group Code', value: 'GRP-001', control_type: 'text', edit_scope: 'normal_and_manual_review', write_target: 'draft_payload_and_review_resolution', raw_readonly: false, } as Partial), ], { 'case_keys.group_code': 'GRP-001', }, false, ) expect(payload).toEqual({ 'case_keys.group_code': 'GRP-001', }) }) it('allows manual review overrides only for fields opened by backend metadata', () => { expect( isReservationManualReviewFieldWritable( createField({ field_path: 'extracted_fields.room_items.0.pms_room_type_code', field_pointer: '/extracted_fields/room_items/0/pms_room_type_code', control_type: 'select', edit_scope: 'manual_review_only', write_target: 'review_resolution.field_overrides', raw_readonly: false, }), ), ).toBe(true) expect( isReservationManualReviewFieldWritable( createField({ field_path: 'case_keys.group_code', field_pointer: '/case_keys/group_code', control_type: 'text', edit_scope: 'normal_and_manual_review', write_target: 'draft_payload_and_review_resolution', raw_readonly: false, }), ), ).toBe(true) expect( isReservationManualReviewFieldWritable( createField({ field_path: 'route_code', control_type: 'workflow_state', edit_scope: 'system_only', write_target: 'none', raw_readonly: false, }), ), ).toBe(false) expect( isReservationManualReviewFieldWritable( createField({ field_path: 'source_message.attachments', control_type: 'file', edit_scope: 'never', write_target: 'none', raw_readonly: true, }), ), ).toBe(false) }) it('does not render review-resolution-only fields as normal editable controls', () => { expect( isReservationFieldEditable( createField({ field_path: 'extracted_fields.room_items.0.pms_room_type_code', control_type: 'select', edit_scope: 'manual_review_only', write_target: 'review_resolution.field_overrides', enum_options: 'RM1, RM2', raw_readonly: false, }), false, ), ).toBe(false) expect( isReservationManualReviewFieldWritable( createField({ field_path: 'extracted_fields.room_items.0.pms_room_type_code', field_pointer: '/extracted_fields/room_items/0/pms_room_type_code', control_type: 'select', edit_scope: 'manual_review_only', write_target: 'review_resolution.field_overrides', enum_options: 'RM1, RM2', raw_readonly: false, }), ), ).toBe(true) }) it('does not treat catalog-backed select metadata as a static enum select', () => { const catalogField = createField({ field_path: 'extracted_fields.room_items.0.pms_room_type_code', control_type: 'select', edit_scope: 'manual_review_only', write_target: 'review_resolution.field_overrides', options_source: 'active_pms_room_type_catalog', enum_options: 'Q1A, DLX', raw_readonly: false, }) const staticField = createField({ field_path: 'extracted_fields.payment_method', control_type: 'select', edit_scope: 'normal_task', write_target: 'draft_payload.field_values', options_source: 'static_enum', enum_options: 'PREPAID, HOTEL_COLLECT', raw_readonly: false, }) expect(isSelectField(catalogField)).toBe(false) expect(isReservationManualReviewFieldWritable(catalogField)).toBe(true) expect(isSelectField(staticField)).toBe(true) }) it('honors edit scope white lists for normal payload and manual review writes', () => { const manualOnlyPayloadField = createField({ field_path: 'case_keys.confirmation_number', control_type: 'text', edit_scope: 'manual_review_only', write_target: 'field_values', raw_readonly: false, }) const workflowDualTargetField = createField({ field_path: 'review_status', control_type: 'text', edit_scope: 'workflow_only', write_target: 'draft_payload_and_review_resolution', raw_readonly: false, }) const normalOnlyReviewTargetField = createField({ field_path: 'case_keys.group_code', control_type: 'text', edit_scope: 'normal_task', write_target: 'review_resolution.field_overrides', raw_readonly: false, }) expect(isReservationFieldEditable(manualOnlyPayloadField, false)).toBe(false) expect(isReservationFieldEditable(workflowDualTargetField, false)).toBe(false) expect(isReservationManualReviewFieldWritable(manualOnlyPayloadField)).toBe(false) expect(isReservationManualReviewFieldWritable(workflowDualTargetField)).toBe(false) expect(isReservationManualReviewFieldWritable(normalOnlyReviewTargetField)).toBe(false) expect(buildEditableFieldValues( [manualOnlyPayloadField, workflowDualTargetField], { 'case_keys.confirmation_number': 'CNF-001', review_status: 'PENDING', }, false, )).toEqual({}) }) it('hydrates P0 room_items fields from field_values and submits main field_path keys', () => { const roomTypeField = createField({ field_path: 'extracted_fields.room_items.0.room_type_raw', field_pointer: '/extracted_fields/room_items/0/room_type_raw', legacy_field_path: 'extracted_fields.room_type', display_name: '房型原文', }) const pmsCodeField = createField({ field_path: 'extracted_fields.room_items.0.pms_room_type_code', field_pointer: '/extracted_fields/room_items/0/pms_room_type_code', legacy_field_path: 'extracted_fields.pms_room_type_code', display_name: 'PMS 房型代码', }) const values = buildInitialFieldValues( [roomTypeField, pmsCodeField], { field_values: { 'extracted_fields.room_items.0.room_type_raw': 'Deluxe Q1A', 'extracted_fields.room_items.0.pms_room_type_code': 'Q1A', }, }, '20260711-p0', ) const payload = buildEditableFieldValues([roomTypeField, pmsCodeField], values, false) expect(values).toMatchObject({ 'extracted_fields.room_items.0.room_type_raw': 'Deluxe Q1A', 'extracted_fields.room_items.0.pms_room_type_code': 'Q1A', }) expect(payload).toEqual({ 'extracted_fields.room_items.0.room_type_raw': 'Deluxe Q1A', 'extracted_fields.room_items.0.pms_room_type_code': 'Q1A', }) expect(payload).not.toHaveProperty('extracted_fields.room_type') expect(payload).not.toHaveProperty('extracted_fields.pms_room_type_code') }) it('hydrates P0 fields from nested field_values as a defensive fallback', () => { const values = buildInitialFieldValues( [ createField({ field_path: 'extracted_fields.room_items.0.room_quantity', field_pointer: '/extracted_fields/room_items/0/room_quantity', legacy_field_path: 'extracted_fields.room_quantity', display_name: '房间数量', }), ], { field_values: { extracted_fields: { room_items: [ { room_quantity: 2, }, ], }, }, }, '20260711-p0', ) expect(values).toEqual({ 'extracted_fields.room_items.0.room_quantity': 2, }) }) it('hydrates code-v1 historical fields from legacy_field_values into P0 main keys', () => { const values = buildInitialFieldValues( [ createField({ field_path: 'extracted_fields.room_items.0.pms_room_type_code', field_pointer: '/extracted_fields/room_items/0/pms_room_type_code', legacy_field_path: 'extracted_fields.pms_room_type_code', display_name: 'PMS 房型代码', }), ], { field_values: { 'extracted_fields.room_items.0.pms_room_type_code': 'NEW', }, legacy_field_values: { 'extracted_fields.pms_room_type_code': 'LEGACY', }, }, 'code-v1', ) expect(values).toEqual({ 'extracted_fields.room_items.0.pms_room_type_code': 'LEGACY', }) }) it('hydrates code-v1 historical fields from legacy effective_payload when legacy_field_values is absent', () => { const values = buildInitialFieldValues( [ createField({ field_path: 'extracted_fields.room_items.0.pms_room_type_code', field_pointer: '/extracted_fields/room_items/0/pms_room_type_code', legacy_field_path: 'extracted_fields.pms_room_type_code', display_name: 'PMS 房型代码', }), ], { effective_payload: { extracted_fields: { pms_room_type_code: 'LEGACY-NESTED', }, }, }, 'code-v1', ) expect(values).toEqual({ 'extracted_fields.room_items.0.pms_room_type_code': 'LEGACY-NESTED', }) }) it('does not create frontend required errors for readonly evidence fields missing in V3 payloads', () => { const errors = validateReservationFieldValues( [ createField({ field_path: 'visible_reason', display_name: '生成原因', editable: 'N', input_editable: 'N', required_rule: 'Y', value: null, }), createField({ field_path: 'extracted_fields.room_items.0.pms_room_type_code', display_name: 'PMS 房型代码', field_pointer: '/extracted_fields/room_items/0/pms_room_type_code', legacy_field_path: 'extracted_fields.pms_room_type_code', required_rule: 'Y', value: '', }), ], { relevant_message_excerpt: 'Please book a Deluxe Q1A room.', 'extracted_fields.room_items.0.pms_room_type_code': '', }, ) expect(errors).toEqual({ 'extracted_fields.room_items.0.pms_room_type_code': { code: 'required', field_name: 'PMS 房型代码', }, }) }) })