接入M002V4订单任务多卡前端
This commit is contained in:
397
client/src/utils/reservationV4FieldRules.ts
Normal file
397
client/src/utils/reservationV4FieldRules.ts
Normal file
@@ -0,0 +1,397 @@
|
||||
import type {
|
||||
ReservationRecord,
|
||||
ReservationV4ReviewFieldOverrideRequest,
|
||||
ReservationV4TaskCardFieldResult,
|
||||
} from '@/types/reservation'
|
||||
|
||||
const confirmEditScopes = new Set(['NORMAL_TASK', 'NORMAL_AND_MANUAL_REVIEW', 'CONFIRM'])
|
||||
const reviewEditScopes = new Set(['MANUAL_REVIEW_ONLY', 'NORMAL_AND_MANUAL_REVIEW', 'REVIEW'])
|
||||
const confirmWriteTargets = new Set([
|
||||
'CONFIRMED_PAYLOAD_JSON',
|
||||
'CONFIRMED_PAYLOAD',
|
||||
'CONFIRMED_PAYLOAD.FIELD_VALUES',
|
||||
'FIELD_VALUES',
|
||||
])
|
||||
const reviewWriteTargets = new Set([
|
||||
'REVIEW_RESOLUTION_FIELD_OVERRIDES',
|
||||
'REVIEW_RESOLUTION.FIELD_OVERRIDES',
|
||||
'FIELD_OVERRIDES',
|
||||
])
|
||||
const readonlyControlTypes = new Set(['READONLY', 'FILE', 'WORKFLOW_STATE', 'STRUCTURED_TABLE'])
|
||||
const reviewWritablePointerPrefixes = ['/basic_information/', '/business_fields/']
|
||||
const hiddenDisplayValue = Symbol('reservation-v4-hidden-display-value')
|
||||
|
||||
export type ReservationV4FieldErrorMap = Record<string, string>
|
||||
|
||||
export function reservationV4FieldKey(field: ReservationV4TaskCardFieldResult): string {
|
||||
return field.field_pointer || field.field_path
|
||||
}
|
||||
|
||||
export function buildReservationV4InitialFieldValues(
|
||||
fields: ReservationV4TaskCardFieldResult[],
|
||||
): ReservationRecord {
|
||||
return fields.reduce<ReservationRecord>((values, field) => {
|
||||
values[reservationV4FieldKey(field)] = field.value ?? ''
|
||||
return values
|
||||
}, {})
|
||||
}
|
||||
|
||||
export function buildReservationV4ConfirmedPayload(
|
||||
fields: ReservationV4TaskCardFieldResult[],
|
||||
values: ReservationRecord,
|
||||
): ReservationRecord {
|
||||
return fields
|
||||
.filter(isReservationV4ConfirmWritableField)
|
||||
.reduce<ReservationRecord>((payload, field) => {
|
||||
assignByField(payload, field, readV4FieldValue(field, values))
|
||||
return payload
|
||||
}, {})
|
||||
}
|
||||
|
||||
export function buildReservationV4ReviewOverrides(
|
||||
fields: ReservationV4TaskCardFieldResult[],
|
||||
values: ReservationRecord,
|
||||
): ReservationV4ReviewFieldOverrideRequest[] {
|
||||
return fields
|
||||
.filter(isReservationV4ReviewWritableField)
|
||||
.map((field) => ({
|
||||
field_pointer: field.field_pointer || fieldPathToPointer(field.field_path),
|
||||
value: readV4FieldValue(field, values),
|
||||
}))
|
||||
}
|
||||
|
||||
export function validateReservationV4Fields(
|
||||
fields: ReservationV4TaskCardFieldResult[],
|
||||
values: ReservationRecord,
|
||||
): ReservationV4FieldErrorMap {
|
||||
return fields.filter(isReservationV4EditableField).reduce<ReservationV4FieldErrorMap>((errors, field) => {
|
||||
const key = reservationV4FieldKey(field)
|
||||
if (field.required && isEmptyV4Value(readV4FieldValue(field, values))) {
|
||||
errors[key] = `${field.display_name} is required`
|
||||
}
|
||||
return errors
|
||||
}, {})
|
||||
}
|
||||
|
||||
export function isReservationV4ConfirmWritableField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
if (!isReservationV4EditableField(field)) {
|
||||
return false
|
||||
}
|
||||
if (isUnsafeV4Field(field)) {
|
||||
return false
|
||||
}
|
||||
const editScope = normalizeV4RuleCode(field.edit_scope)
|
||||
if (!confirmEditScopes.has(editScope)) {
|
||||
return false
|
||||
}
|
||||
return confirmWriteTargets.has(normalizeV4RuleCode(field.write_target))
|
||||
}
|
||||
|
||||
export function isReservationV4ReviewWritableField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
if (!isReservationV4EditableField(field)) {
|
||||
return false
|
||||
}
|
||||
const pointer = field.field_pointer || fieldPathToPointer(field.field_path)
|
||||
if (!reviewWritablePointerPrefixes.some((prefix) => pointer.startsWith(prefix))) {
|
||||
return false
|
||||
}
|
||||
if (isUnsafeV4Field(field)) {
|
||||
return false
|
||||
}
|
||||
const editScope = normalizeV4RuleCode(field.edit_scope)
|
||||
if (!reviewEditScopes.has(editScope)) {
|
||||
return false
|
||||
}
|
||||
return reviewWriteTargets.has(normalizeV4RuleCode(field.write_target))
|
||||
}
|
||||
|
||||
export function isReservationV4EditableField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
if (field.editable !== true || field.raw_readonly === true) {
|
||||
return false
|
||||
}
|
||||
if (normalizeV4RuleCode(field.write_target) === 'NONE') {
|
||||
return false
|
||||
}
|
||||
return !readonlyControlTypes.has(normalizeV4RuleCode(field.control_type))
|
||||
}
|
||||
|
||||
export function readV4FieldValue(
|
||||
field: ReservationV4TaskCardFieldResult,
|
||||
values: ReservationRecord,
|
||||
): unknown {
|
||||
const pointer = field.field_pointer ? values[field.field_pointer] : undefined
|
||||
if (pointer !== undefined) {
|
||||
return pointer
|
||||
}
|
||||
const path = field.field_path ? values[field.field_path] : undefined
|
||||
return path !== undefined ? path : field.value ?? ''
|
||||
}
|
||||
|
||||
export function fieldPathToPointer(fieldPath: string): string {
|
||||
if (!fieldPath) {
|
||||
return ''
|
||||
}
|
||||
return `/${fieldPath.split('.').map(encodePointerSegment).join('/')}`
|
||||
}
|
||||
|
||||
export function mapReservationV4BackendDetailsToFields(
|
||||
details: unknown,
|
||||
fields: ReservationV4TaskCardFieldResult[],
|
||||
): {
|
||||
fieldErrors: ReservationV4FieldErrorMap
|
||||
globalErrors: string[]
|
||||
} {
|
||||
const messages = normalizeBackendDetailMessages(details)
|
||||
const fieldErrors: ReservationV4FieldErrorMap = {}
|
||||
const globalErrors: string[] = []
|
||||
|
||||
for (const message of messages) {
|
||||
const matchedField = fields.find((field) =>
|
||||
message.includes(field.field_pointer) ||
|
||||
Boolean(field.field_path && message.includes(field.field_path)),
|
||||
)
|
||||
if (matchedField) {
|
||||
fieldErrors[reservationV4FieldKey(matchedField)] = message
|
||||
} else {
|
||||
globalErrors.push(message)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
fieldErrors,
|
||||
globalErrors,
|
||||
}
|
||||
}
|
||||
|
||||
export function staticReservationV4Options(optionsSource: string | null): Array<{ value: string; label: string }> {
|
||||
switch (normalizeV4RuleCode(optionsSource)) {
|
||||
case 'RESERVATION_V4_ACCOUNT_CATALOG':
|
||||
case 'ACCOUNT_CATALOG':
|
||||
return [
|
||||
{ value: 'HANATOUR', label: 'HANATOUR' },
|
||||
{ value: 'LIAN_TAI', label: 'LIAN TAI' },
|
||||
{ value: 'QBD_TRAVEL', label: 'QBD Travel' },
|
||||
]
|
||||
case 'RESERVATION_V4_ROOM_TYPE_CATALOG':
|
||||
case 'ROOM_TYPE_CATALOG':
|
||||
return [
|
||||
{ value: 'TWN', label: 'TWN' },
|
||||
{ value: 'DBL', label: 'DBL' },
|
||||
{ value: 'KING', label: 'KING' },
|
||||
{ value: 'SGL', label: 'SGL' },
|
||||
{ value: 'TRP', label: 'TRP' },
|
||||
]
|
||||
case 'RESERVATION_V4_RATE_CODE_CATALOG':
|
||||
case 'RATE_CODE_CATALOG':
|
||||
return [
|
||||
{ value: 'BAR', label: 'BAR' },
|
||||
{ value: 'RACK', label: 'RACK' },
|
||||
{ value: 'GROUP', label: 'GROUP' },
|
||||
{ value: 'FIT', label: 'FIT' },
|
||||
]
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function isReservationV4UnsafeDisplayKey(key: string): boolean {
|
||||
const normalizedKey = key.toLowerCase()
|
||||
return [
|
||||
'raw',
|
||||
'html_body',
|
||||
'body',
|
||||
'agentbus',
|
||||
'payload',
|
||||
'url',
|
||||
'oss',
|
||||
'evidence',
|
||||
'attachment',
|
||||
'file_reference',
|
||||
].some((fragment) => normalizedKey.includes(fragment))
|
||||
}
|
||||
|
||||
export function stringifyReservationV4SafeDisplayValue(
|
||||
value: unknown,
|
||||
emptyLabel: string,
|
||||
hiddenLabel: string,
|
||||
): string {
|
||||
const sanitizedValue = sanitizeReservationV4DisplayValue(value)
|
||||
if (sanitizedValue === hiddenDisplayValue) {
|
||||
return hiddenLabel
|
||||
}
|
||||
if (isEmptyV4Value(sanitizedValue)) {
|
||||
return emptyLabel
|
||||
}
|
||||
if (Array.isArray(sanitizedValue)) {
|
||||
return sanitizedValue.length ? JSON.stringify(sanitizedValue) : emptyLabel
|
||||
}
|
||||
if (isRecord(sanitizedValue)) {
|
||||
return Object.keys(sanitizedValue).length ? JSON.stringify(sanitizedValue) : hiddenLabel
|
||||
}
|
||||
return String(sanitizedValue)
|
||||
}
|
||||
|
||||
function assignByField(
|
||||
payload: ReservationRecord,
|
||||
field: ReservationV4TaskCardFieldResult,
|
||||
value: unknown,
|
||||
): void {
|
||||
const pointer = field.field_pointer || fieldPathToPointer(field.field_path)
|
||||
assignByPointer(payload, pointer, value)
|
||||
}
|
||||
|
||||
function assignByPointer(target: ReservationRecord, pointer: string, value: unknown): void {
|
||||
const segments = decodePointer(pointer)
|
||||
if (!segments.length) {
|
||||
return
|
||||
}
|
||||
|
||||
let current: ReservationRecord | unknown[] = target
|
||||
segments.forEach((segment, index) => {
|
||||
const isLast = index === segments.length - 1
|
||||
if (isLast) {
|
||||
setContainerValue(current, segment, value)
|
||||
return
|
||||
}
|
||||
|
||||
const nextSegment = segments[index + 1] ?? ''
|
||||
const nextContainer = readContainerValue(current, segment)
|
||||
if (isRecord(nextContainer) || Array.isArray(nextContainer)) {
|
||||
current = nextContainer
|
||||
return
|
||||
}
|
||||
|
||||
const createdContainer: ReservationRecord | unknown[] = isNumericSegment(nextSegment) ? [] : {}
|
||||
setContainerValue(current, segment, createdContainer)
|
||||
current = createdContainer
|
||||
})
|
||||
}
|
||||
|
||||
function readContainerValue(container: ReservationRecord | unknown[], segment: string): unknown {
|
||||
if (Array.isArray(container)) {
|
||||
return container[Number(segment)]
|
||||
}
|
||||
return container[segment]
|
||||
}
|
||||
|
||||
function setContainerValue(container: ReservationRecord | unknown[], segment: string, value: unknown): void {
|
||||
if (Array.isArray(container)) {
|
||||
container[Number(segment)] = value
|
||||
return
|
||||
}
|
||||
container[segment] = value
|
||||
}
|
||||
|
||||
function decodePointer(pointer: string): string[] {
|
||||
if (!pointer || pointer === '/') {
|
||||
return []
|
||||
}
|
||||
return pointer
|
||||
.replace(/^\//, '')
|
||||
.split('/')
|
||||
.filter(Boolean)
|
||||
.map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~'))
|
||||
}
|
||||
|
||||
function encodePointerSegment(segment: string): string {
|
||||
return segment.replace(/~/g, '~0').replace(/\//g, '~1')
|
||||
}
|
||||
|
||||
function normalizeBackendDetailMessages(details: unknown): string[] {
|
||||
if (details === null || details === undefined || details === '') {
|
||||
return []
|
||||
}
|
||||
if (Array.isArray(details)) {
|
||||
return details.flatMap((item) => normalizeBackendDetailMessages(item))
|
||||
}
|
||||
if (typeof details === 'string') {
|
||||
return [details]
|
||||
}
|
||||
if (typeof details === 'object') {
|
||||
const record = details as ReservationRecord
|
||||
const path = readFirstDetailString(record, ['field_path', 'field_pointer', 'field', 'path', 'json_pointer'])
|
||||
const message = typeof record.message === 'string'
|
||||
? record.message
|
||||
: typeof record.error_message === 'string'
|
||||
? record.error_message
|
||||
: JSON.stringify(record)
|
||||
return [`${path ? `${path}: ` : ''}${message}`]
|
||||
}
|
||||
return [String(details)]
|
||||
}
|
||||
|
||||
function readFirstDetailString(record: ReservationRecord, keys: string[]): string {
|
||||
for (const key of keys) {
|
||||
const value = record[key]
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
return value.trim()
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function sanitizeReservationV4DisplayValue(value: unknown): unknown {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return value
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return isUnsafeUrlLikeValue(value) ? hiddenDisplayValue : value
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
if (!value.length) {
|
||||
return value
|
||||
}
|
||||
const sanitizedItems = value
|
||||
.map((item) => sanitizeReservationV4DisplayValue(item))
|
||||
.filter((item) => item !== hiddenDisplayValue)
|
||||
return sanitizedItems.length ? sanitizedItems : hiddenDisplayValue
|
||||
}
|
||||
if (isRecord(value)) {
|
||||
const sanitizedRecord = Object.entries(value).reduce<ReservationRecord>((record, [key, nestedValue]) => {
|
||||
if (isReservationV4UnsafeDisplayKey(key)) {
|
||||
return record
|
||||
}
|
||||
const sanitizedValue = sanitizeReservationV4DisplayValue(nestedValue)
|
||||
if (sanitizedValue !== hiddenDisplayValue) {
|
||||
record[key] = sanitizedValue
|
||||
}
|
||||
return record
|
||||
}, {})
|
||||
return Object.keys(sanitizedRecord).length ? sanitizedRecord : hiddenDisplayValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function isUnsafeV4Field(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return isReservationV4UnsafeDisplayKey(field.field_path) || isReservationV4UnsafeDisplayKey(field.field_pointer)
|
||||
}
|
||||
|
||||
function isUnsafeUrlLikeValue(value: string): boolean {
|
||||
const trimmedValue = value.trim()
|
||||
return /^(https?:\/\/|oss:\/\/|s3:\/\/)/i.test(trimmedValue)
|
||||
}
|
||||
|
||||
function normalizeV4RuleCode(value: string | null | undefined): string {
|
||||
return value?.trim().replace(/[.\s-]+/g, '_').toUpperCase() ?? ''
|
||||
}
|
||||
|
||||
function isEmptyV4Value(value: unknown): boolean {
|
||||
if (value === null || value === undefined) {
|
||||
return true
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value.trim() === ''
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.length === 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function isNumericSegment(segment: string): boolean {
|
||||
return /^\d+$/.test(segment)
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is ReservationRecord {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value))
|
||||
}
|
||||
Reference in New Issue
Block a user