570 lines
17 KiB
TypeScript
570 lines
17 KiB
TypeScript
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>): 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<ReservationTaskFieldResult>),
|
|
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<ReservationTaskFieldResult>),
|
|
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<ReservationTaskFieldResult>),
|
|
],
|
|
{
|
|
'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<ReservationTaskFieldResult>),
|
|
],
|
|
{
|
|
'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 房型代码',
|
|
},
|
|
})
|
|
})
|
|
})
|