594 lines
27 KiB
JavaScript
594 lines
27 KiB
JavaScript
(function installLTJTOperationPlans(root, factory) {
|
||
const api = factory();
|
||
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
||
if (root) root.LTJTOperationPlans = api;
|
||
}(typeof self !== 'undefined' ? self : globalThis, () => {
|
||
const OPERATION_DEFINITIONS = {
|
||
team_order_create: {
|
||
label: '团队-单个下单',
|
||
route: 'team_single',
|
||
execution: 'browser_team_single',
|
||
liveSubmit: 'test-only',
|
||
source: 'root-browser-validated'
|
||
},
|
||
team_order_batch_create: {
|
||
label: '团队-批量下单',
|
||
route: 'team_batch',
|
||
execution: 'browser_team_single_fallback',
|
||
nativeStatus: 'deferred',
|
||
fallback: 'team_single_per_date',
|
||
source: 'handoff-fallback-rule'
|
||
},
|
||
shared_plan_create: {
|
||
label: '散拼-创建母团计划',
|
||
route: 'split_parent',
|
||
execution: 'browser_live_split_parent',
|
||
liveSubmit: 'test-only',
|
||
source: 'handoff-browser-validated'
|
||
},
|
||
shared_child_order_create: {
|
||
label: '散拼-拼单/录入子单',
|
||
route: 'split_child',
|
||
execution: 'browser_live_split_child',
|
||
liveSubmit: 'test-only',
|
||
source: 'handoff-browser-validated'
|
||
},
|
||
order_update: {
|
||
label: '已有订单更新',
|
||
route: 'update_order',
|
||
execution: 'browser_dry_run',
|
||
dryRunAdapter: 'inspect_existing_order_update',
|
||
supportedTargets: ['rooms', 'remark', 'pax', 'prices'],
|
||
source: 'handoff-update-rule'
|
||
},
|
||
passenger_list_import: {
|
||
label: '旅客名单导入/补充',
|
||
route: 'update_order',
|
||
execution: 'browser_dry_run',
|
||
dryRunAdapter: 'inspect_existing_order_traveler',
|
||
supportedTargets: ['travelerList'],
|
||
source: 'handoff-traveler-rule'
|
||
},
|
||
confirmation_export: {
|
||
label: '确认件导出/恢复',
|
||
route: 'export_confirmation',
|
||
execution: 'browser_export_source',
|
||
dryRunAdapter: 'inspect_confirmation_sources',
|
||
source: 'handoff-browser-validated'
|
||
}
|
||
};
|
||
|
||
const ACTION_ALIASES = {
|
||
create_order: {
|
||
team_single: 'team_order_create',
|
||
team_batch: 'team_order_batch_create',
|
||
split_parent: 'shared_plan_create',
|
||
split_child: 'shared_child_order_create'
|
||
},
|
||
update_order: 'order_update',
|
||
export_confirmation: 'confirmation_export'
|
||
};
|
||
|
||
const EXPORT_TYPE_ALIASES = {
|
||
confirmation: 'xingyou-confirm',
|
||
confirm: 'xingyou-confirm',
|
||
xingyou: 'xingyou-confirm',
|
||
'xingyou-confirm': 'xingyou-confirm',
|
||
liantai: 'liantai-confirm',
|
||
'liantai-confirm': 'liantai-confirm',
|
||
job: 'job-order',
|
||
beian: 'job-order',
|
||
'job-order': 'job-order',
|
||
};
|
||
|
||
function toNumber(value) {
|
||
const normalized = String(value ?? '').replace(/[,,]/g, '').trim();
|
||
const number = Number(normalized);
|
||
return Number.isFinite(number) ? number : 0;
|
||
}
|
||
|
||
function dateYyyyMD(value) {
|
||
if (typeof value !== 'string') return String(value || '');
|
||
const match = value.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
|
||
if (!match) return value;
|
||
return `${match[1]}-${Number(match[2])}-${Number(match[3])}`;
|
||
}
|
||
|
||
function compactDate(value) {
|
||
const match = String(value || '').match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
|
||
if (!match) return String(value || '').replace(/-/g, '');
|
||
return `${match[1]}${match[2].padStart(2, '0')}${match[3].padStart(2, '0')}`;
|
||
}
|
||
|
||
function passengerTotal(counts = {}) {
|
||
return ['adult', 'child_bed', 'child_no_bed', 'infant', 'leader']
|
||
.reduce((sum, key) => sum + Math.max(0, Math.trunc(toNumber(counts[key]))), 0);
|
||
}
|
||
|
||
function actionForOperation(operation = {}) {
|
||
const direct = String(operation.action || '').trim();
|
||
if (OPERATION_DEFINITIONS[direct]) return direct;
|
||
const legacy = String(operation.operation || '').trim();
|
||
if (legacy === 'create_order') {
|
||
return ACTION_ALIASES.create_order[String(operation.route || '').trim()] || '';
|
||
}
|
||
return ACTION_ALIASES[legacy] || '';
|
||
}
|
||
|
||
function identifierForOperation(operation = {}, data = {}) {
|
||
if (operation.identifier) return String(operation.identifier).trim();
|
||
const refs = data.existing_refs || {};
|
||
return String(
|
||
refs.order_no
|
||
|| refs.group_no
|
||
|| refs.child_order_no
|
||
|| refs.parent_group_no
|
||
|| refs.identifier
|
||
|| ''
|
||
).trim();
|
||
}
|
||
|
||
function exportTypesForOperation(operation = {}, data = {}) {
|
||
const source = Array.isArray(operation.exportTypes) && operation.exportTypes.length
|
||
? operation.exportTypes
|
||
: (Array.isArray(data.exportTypes || data.export_types) && (data.exportTypes || data.export_types).length
|
||
? (data.exportTypes || data.export_types)
|
||
: [data.confirmation?.type || 'xingyou-confirm']);
|
||
return [...new Set(source.map((value) => {
|
||
const key = String(value || '').trim().toLowerCase();
|
||
return EXPORT_TYPE_ALIASES[key] || key;
|
||
}))];
|
||
}
|
||
|
||
function normalizePax(value = {}) {
|
||
if (typeof value === 'number' || typeof value === 'string') return { adult: toNumber(value) };
|
||
return {
|
||
adult: toNumber(value.adult),
|
||
child_bed: toNumber(value.child_bed ?? value.childBed),
|
||
child_no_bed: toNumber(value.child_no_bed ?? value.childNoBed),
|
||
infant: toNumber(value.infant),
|
||
leader: toNumber(value.leader),
|
||
expected_total: value.expected_total ?? value.expectedTotal,
|
||
};
|
||
}
|
||
|
||
function normalizeCreateFields(fields = {}) {
|
||
const departureDates = Array.isArray(fields.departureDates)
|
||
? fields.departureDates
|
||
: (fields.departureDate ? [fields.departureDate] : []);
|
||
const orderNature = fields.orderNature || fields.order_nature || '';
|
||
const parentGroupNo = fields.parentGroupNo || fields.parent_group_no || '';
|
||
const productName = fields.productName || fields.product || fields.productRoute || '';
|
||
const bookingCustomer = fields.bookingCustomer || fields.channelCustomer || fields.customer || '';
|
||
const pax = fields.pax || fields.passengerCounts || fields.defaultPax || {};
|
||
return {
|
||
order_mode: fields.orderMode || fields.order_mode || '',
|
||
product: {
|
||
name: productName,
|
||
route: fields.productRoute || fields.route || productName,
|
||
cpid: fields.cpid || ''
|
||
},
|
||
customer: {
|
||
name: bookingCustomer,
|
||
id: fields.customerId || fields.bookingCustomerId || '',
|
||
contact: fields.contact || fields.bookingContact || '',
|
||
origin: fields.customerOrigin || fields.origin || ''
|
||
},
|
||
route: { name: fields.route || fields.productRoute || productName },
|
||
departure_dates: departureDates,
|
||
passenger_counts: normalizePax(pax),
|
||
room_counts: fields.rooms || fields.roomCounts || fields.defaultRooms || {},
|
||
prices: fields.prices || {},
|
||
op_user: { name: fields.op || fields.opUser || fields.followOp || fields.follow_op || '' },
|
||
sales_user: { name: fields.salesperson || fields.salesUser || '' },
|
||
special_requests: fields.remark || fields.specialRequests || '',
|
||
test_marker: fields.testMarker || '',
|
||
order_nature: orderNature,
|
||
parent_group_no: parentGroupNo,
|
||
child_order_no: fields.childOrderNo || fields.child_order_no || '',
|
||
planned_capacity: fields.plannedGuests || fields.plannedPax || fields.planned_capacity || 0,
|
||
groups_per_date: fields.parentPlansPerDate || fields.groupsPerDate || fields.groups_per_date || 1,
|
||
cycle: fields.cycle || '',
|
||
date_adjustments: fields.dateAdjustments || fields.adjustments || {},
|
||
supplemental: fields.supplemental || {},
|
||
system_defaults: fields.system_defaults || fields.systemDefaults || {},
|
||
};
|
||
}
|
||
|
||
function updateActionsFromSource(source = {}) {
|
||
const plan = source.updatePlan || source.update_plan || source.data?.updatePlan || {};
|
||
if (Array.isArray(plan.actions)) return plan.actions;
|
||
if (Array.isArray(source.actions)) return source.actions;
|
||
if (Array.isArray(source.data?.updates?.actions)) return source.data.updates.actions;
|
||
return [];
|
||
}
|
||
|
||
function hasTravelerAction(actions = []) {
|
||
return actions.some((item) => /^travelerList(?:\.|$)/i.test(String(item?.target || ''))
|
||
|| String(item?.operation || '').toLowerCase() === 'traveler_upsert');
|
||
}
|
||
|
||
function normalizeOperation(input = {}) {
|
||
const source = input.task && typeof input.task === 'object' ? input.task : input;
|
||
const directAction = String(source.action || '').trim();
|
||
if (OPERATION_DEFINITIONS[directAction]) return source;
|
||
|
||
const operation = String(source.operation || '').trim();
|
||
if (operation === 'create_order') {
|
||
const data = normalizeCreateFields(source.fields || {});
|
||
return {
|
||
...source,
|
||
action: ACTION_ALIASES.create_order[String(source.route || '').trim()] || '',
|
||
order_nature: data.order_nature || source.orderNature || '',
|
||
submit_mode: source.submit_mode || 'dry_run',
|
||
data: {
|
||
...data,
|
||
original_text: source.originalText || source.original_text || '',
|
||
attachments: Array.isArray(source.attachments) ? source.attachments : [],
|
||
},
|
||
originalText: source.originalText || source.original_text || '',
|
||
};
|
||
}
|
||
if (operation === 'update_order') {
|
||
const actions = updateActionsFromSource(source);
|
||
const action = hasTravelerAction(actions) && actions.every((item) => /^travelerList(?:\.|$)/i.test(String(item?.target || ''))
|
||
|| String(item?.operation || '').toLowerCase() === 'traveler_upsert')
|
||
? 'passenger_list_import'
|
||
: 'order_update';
|
||
return {
|
||
...source,
|
||
action,
|
||
identifier: source.identifier || source.data?.existing_refs?.identifier || '',
|
||
data: {
|
||
...(source.data || {}),
|
||
existing_refs: {
|
||
...(source.data?.existing_refs || {}),
|
||
identifier: source.identifier || source.data?.existing_refs?.identifier || ''
|
||
},
|
||
updates: source.updatePlan || source.data?.updates || { actions },
|
||
updatePlan: source.updatePlan || source.data?.updatePlan || { actions },
|
||
attachments: Array.isArray(source.attachments) ? source.attachments : [],
|
||
},
|
||
};
|
||
}
|
||
if (operation === 'export_confirmation') {
|
||
return {
|
||
...source,
|
||
action: 'confirmation_export',
|
||
exportTypes: Array.isArray(source.exportTypes) && source.exportTypes.length
|
||
? source.exportTypes
|
||
: (Array.isArray(source.data?.exportTypes || source.data?.export_types)
|
||
? (source.data.exportTypes || source.data.export_types)
|
||
: undefined),
|
||
identifier: source.identifier || source.data?.existing_refs?.identifier || '',
|
||
data: {
|
||
...(source.data || {}),
|
||
existing_refs: {
|
||
...(source.data?.existing_refs || {}),
|
||
identifier: source.identifier || source.data?.existing_refs?.identifier || ''
|
||
},
|
||
confirmation: {
|
||
type: Array.isArray(source.exportTypes) && source.exportTypes.length ? source.exportTypes[0] : 'xingyou-confirm',
|
||
},
|
||
},
|
||
};
|
||
}
|
||
return source;
|
||
}
|
||
|
||
function addMissing(blockers, label, value) {
|
||
if (value === undefined || value === null || String(value).trim() === '') blockers.push(label);
|
||
}
|
||
|
||
function validateCreate(operation, action, data, blockers, warnings) {
|
||
const counts = data.passenger_counts || {};
|
||
if (!data.product?.name) blockers.push('缺少产品名称。');
|
||
if (!data.op_user?.name) blockers.push('缺少计调 OP。');
|
||
if (!data.sales_user?.name) blockers.push('缺少销售人。');
|
||
if (passengerTotal(counts) <= 0) blockers.push('人数合计必须大于 0。');
|
||
if (counts.expected_total != null && toNumber(counts.expected_total) !== passengerTotal(counts)) {
|
||
blockers.push('expected_total 与人数合计不一致。');
|
||
}
|
||
|
||
if (action === 'team_order_create') {
|
||
if (!Array.isArray(data.departure_dates) || data.departure_dates.length !== 1 || !data.departure_dates[0]) {
|
||
blockers.push('团队单必须有且只有一个出发日期。');
|
||
}
|
||
addMissing(blockers, '缺少 data.test_marker。', data.test_marker);
|
||
if (operation.submit_mode !== 'dry_run') {
|
||
blockers.push('submit_mode 必须保持 dry_run,真实保存由插件安全门单独控制。');
|
||
}
|
||
if (operation.order_nature !== 'test') {
|
||
blockers.push('当前插件自动保存只允许 order_nature=test 的测试订单。');
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (!Array.isArray(data.departure_dates) || data.departure_dates.length < 2) {
|
||
blockers.push('团队批量下单至少需要两个出发日期。');
|
||
}
|
||
warnings.push('原生 DoInfoJHs 尚未验证;当前业务规则要求按日期使用 team_single fallback。');
|
||
if (operation.order_nature !== 'test' || operation.submit_mode !== 'dry_run' || !data.test_marker) {
|
||
warnings.push('批量 fallback 当前仅允许带测试性质、dry_run 和测试标记的任务;本任务只生成计划,不写 ERP。');
|
||
}
|
||
}
|
||
|
||
function validateOperation(operation = {}) {
|
||
const action = actionForOperation(operation);
|
||
const definition = OPERATION_DEFINITIONS[action] || null;
|
||
const data = operation.data || {};
|
||
const blockers = [];
|
||
const warnings = [];
|
||
|
||
if (!definition) {
|
||
return {
|
||
ok: false,
|
||
action,
|
||
definition: null,
|
||
execution: 'blocked',
|
||
blockers: [`不支持的 ERP 业务类型:${operation.action || operation.operation || '未填写'}。`],
|
||
warnings,
|
||
};
|
||
}
|
||
|
||
if (action === 'team_order_create' || action === 'team_order_batch_create') {
|
||
validateCreate(operation, action, data, blockers, warnings);
|
||
} else if (action === 'shared_plan_create') {
|
||
addMissing(blockers, '缺少产品名称。', data.product?.name);
|
||
if (!Array.isArray(data.departure_dates) || !data.departure_dates.length) blockers.push('缺少母团出发日期。');
|
||
if (Array.isArray(data.departure_dates) && data.departure_dates.length !== 1) blockers.push('当前母团浏览器执行器一次只允许一个出发日期。');
|
||
if (toNumber(data.planned_capacity) <= 0) blockers.push('计划收客数必须大于 0。');
|
||
addMissing(blockers, '缺少跟团/接待 OP。', data.op_user?.name);
|
||
addMissing(blockers, '缺少预订客户。', data.customer?.name);
|
||
if (toNumber(data.groups_per_date) <= 0) blockers.push('每天母团计划数必须大于 0。');
|
||
if (toNumber(data.groups_per_date || 1) !== 1) blockers.push('当前母团浏览器执行器每天只允许一个计划。');
|
||
if (operation.order_nature !== 'test' || operation.submit_mode !== 'dry_run' || !data.test_marker) {
|
||
blockers.push('散拼母团真实保存只允许带测试性质、dry_run 和测试标记的任务。');
|
||
}
|
||
warnings.push('散拼母团当前使用已验证的浏览器提交路径,仅允许测试订单。');
|
||
} else if (action === 'shared_child_order_create') {
|
||
addMissing(blockers, '缺少渠道客户。', data.customer?.name);
|
||
if (passengerTotal(data.passenger_counts || {}) <= 0) blockers.push('人数合计必须大于 0。');
|
||
addMissing(blockers, '缺少计调 OP。', data.op_user?.name);
|
||
addMissing(blockers, '缺少销售人。', data.sales_user?.name);
|
||
const parentGroupNo = data.parent_group_no || data.existing_refs?.parent_group_no || '';
|
||
if (!parentGroupNo && !(data.product?.name && Array.isArray(data.departure_dates) && data.departure_dates.length)) {
|
||
blockers.push('缺少母团/子单已有编号,或产品与出发日期。');
|
||
}
|
||
if (!Array.isArray(data.departure_dates) || data.departure_dates.length !== 1) blockers.push('子单必须有且只有一个出发日期。');
|
||
if (operation.order_nature !== 'test' || operation.submit_mode !== 'dry_run' || !data.test_marker) {
|
||
blockers.push('散拼子单真实保存只允许带测试性质、dry_run 和测试标记的任务。');
|
||
}
|
||
warnings.push('散拼子单当前使用已验证的浏览器提交路径,仅允许测试订单。');
|
||
} else if (action === 'order_update') {
|
||
addMissing(blockers, '缺少已有订单编号。', identifierForOperation(operation, data));
|
||
const plan = operation.updatePlan || data.updatePlan || data.updates;
|
||
const actions = Array.isArray(plan?.actions) ? plan.actions : [];
|
||
if (!actions.length && !data.updates) blockers.push('缺少结构化修改内容。');
|
||
if (actions.some((item) => !item || !item.target || !item.operation)) blockers.push('修改动作必须包含 target 和 operation。');
|
||
const unsupportedTargets = actions.filter((item) => !isSupportedUpdateTarget(item?.target));
|
||
if (unsupportedTargets.length) blockers.push(`包含未接入的修改目标:${unsupportedTargets.map((item) => item.target).join('、')}。`);
|
||
if (actions.some((item) => /^prices\.|^pax\./.test(String(item?.target || '')))) {
|
||
warnings.push('价格/人数跨路线更新仍属于 optional/unverified,本次只记录计划,不承诺等价执行。');
|
||
}
|
||
warnings.push('当前只做订单定位和修改预览,实际编辑页提交仍需独立预检与显式授权。');
|
||
} else if (action === 'passenger_list_import') {
|
||
addMissing(blockers, '缺少已有订单编号。', identifierForOperation(operation, data));
|
||
const attachments = Array.isArray(data.attachments) ? data.attachments : [];
|
||
const passengerList = data.passenger_list || data.travelerList;
|
||
const plan = operation.updatePlan || data.updatePlan || data.updates || {};
|
||
const actions = Array.isArray(plan?.actions) ? plan.actions : [];
|
||
if (!attachments.length && !passengerList && !hasTravelerAction(actions)) blockers.push('缺少旅客名单附件或 passenger_list 描述。');
|
||
if (actions.length && actions.some((item) => !/^travelerList(?:\.|$)/i.test(String(item?.target || ''))
|
||
&& String(item?.operation || '').toLowerCase() !== 'traveler_upsert')) {
|
||
blockers.push('旅客名单任务不能混入未声明的非旅客修改动作。');
|
||
}
|
||
warnings.push('当前只定位订单并校验旅客导入计划;文件解析、粘贴导入和行数回查仍需独立执行器。');
|
||
} else if (action === 'confirmation_export') {
|
||
addMissing(blockers, '缺少已有订单编号。', identifierForOperation(operation, data));
|
||
if (data.route === 'split_parent' || operation.route === 'split_parent') blockers.push('母团计划不能直接导出客户确认件。');
|
||
const exportTypes = exportTypesForOperation(operation, data);
|
||
const unsupported = exportTypes.filter((type) => !['xingyou-confirm', 'liantai-confirm', 'job-order'].includes(type));
|
||
if (unsupported.length) blockers.push(`包含不支持的确认件类型:${unsupported.join('、')}。`);
|
||
if (operation.recovery?.exportOnly && operation.recovery?.neverResave !== true) {
|
||
blockers.push('恢复导出任务必须同时声明 neverResave=true。');
|
||
}
|
||
warnings.push('导出路径只读取 ERP 源文件并回报响应摘要;插件不会重保存订单,也不会把未落盘文件误报为已交付。');
|
||
}
|
||
|
||
let execution = definition.execution;
|
||
if (
|
||
action === 'team_order_batch_create'
|
||
&& (operation.order_nature !== 'test' || operation.submit_mode !== 'dry_run' || !data.test_marker)
|
||
) {
|
||
execution = 'planned_only';
|
||
}
|
||
|
||
return {
|
||
ok: blockers.length === 0,
|
||
action,
|
||
definition,
|
||
execution: blockers.length ? 'blocked' : execution,
|
||
blockers,
|
||
warnings,
|
||
identifier: identifierForOperation(operation, data),
|
||
exportTypes: action === 'confirmation_export' ? exportTypesForOperation(operation, data) : [],
|
||
noErpWrite: !['browser_team_single', 'browser_team_single_fallback', 'browser_live_split_parent', 'browser_live_split_child'].includes(execution),
|
||
};
|
||
}
|
||
|
||
function expandTeamBatch(operation = {}) {
|
||
const data = operation.data || {};
|
||
const dates = Array.isArray(data.departure_dates) ? data.departure_dates : [];
|
||
const marker = String(data.test_marker || 'TEAM-BATCH-TEST').trim();
|
||
return dates.map((date, index) => ({
|
||
...operation,
|
||
action: 'team_order_create',
|
||
data: {
|
||
...data,
|
||
departure_dates: [dateYyyyMD(date)],
|
||
test_marker: `${marker}-D${compactDate(date)}`,
|
||
},
|
||
source: {
|
||
...(operation.source || {}),
|
||
batch_index: index + 1,
|
||
batch_total: dates.length,
|
||
},
|
||
}));
|
||
}
|
||
|
||
function publicPlan(result = {}) {
|
||
return {
|
||
action: result.action || '',
|
||
label: result.definition?.label || '',
|
||
route: result.definition?.route || '',
|
||
execution: result.execution || 'blocked',
|
||
native_status: result.definition?.nativeStatus || '',
|
||
fallback: result.definition?.fallback || '',
|
||
dry_run_adapter: result.definition?.dryRunAdapter || '',
|
||
identifier: result.identifier || '',
|
||
export_types: Array.isArray(result.exportTypes) ? result.exportTypes : [],
|
||
no_erp_write: result.noErpWrite !== false,
|
||
blockers: Array.isArray(result.blockers) ? result.blockers : [],
|
||
warnings: Array.isArray(result.warnings) ? result.warnings : [],
|
||
};
|
||
}
|
||
|
||
function operationSummary(operation = {}) {
|
||
const action = actionForOperation(operation);
|
||
const definition = OPERATION_DEFINITIONS[action] || {};
|
||
const data = operation.data || {};
|
||
return {
|
||
action,
|
||
label: definition.label || action || '未知 ERP 业务',
|
||
route: definition.route || operation.route || '',
|
||
product: data.product?.name || data.productName || '',
|
||
departure_dates: Array.isArray(data.departure_dates) ? data.departure_dates : [],
|
||
identifier: identifierForOperation(operation, data),
|
||
test_marker: data.test_marker || '',
|
||
};
|
||
}
|
||
|
||
function isSupportedUpdateTarget(target = '') {
|
||
const value = String(target || '').trim();
|
||
return value === 'remark'
|
||
|| value === 'xiadanbeizhu'
|
||
|| /^rooms\.(SGL|TWN|TRP|DBL|HNM|TL)$/i.test(value)
|
||
|| /^pax\.(adult|child_bed|childBed|child_no_bed|childNoBed|infant|leader)$/i.test(value)
|
||
|| /^prices\.(adult|child_bed|childBed|child_no_bed|childNoBed|infant|leader)$/i.test(value)
|
||
|| /^travelerList(?:\.|$)/i.test(value);
|
||
}
|
||
|
||
function canonicalUpdateTarget(target = '') {
|
||
return String(target || '').replace(/\.(childBed|childNoBed)$/i, (match, key) => `.${key.toLowerCase() === 'childbed' ? 'child_bed' : 'child_no_bed'}`);
|
||
}
|
||
|
||
function clone(value) {
|
||
if (value === undefined) return undefined;
|
||
return JSON.parse(JSON.stringify(value));
|
||
}
|
||
|
||
function snapshotValue(snapshot = {}, target = '') {
|
||
if (target === 'remark' || target === 'xiadanbeizhu') return snapshot.remark || '';
|
||
const match = String(target).match(/^(rooms|pax|prices)\.(.+)$/);
|
||
if (!match) return undefined;
|
||
return snapshot[match[1]]?.[match[2]];
|
||
}
|
||
|
||
function setSnapshotValue(snapshot, target, value) {
|
||
const match = String(target).match(/^(rooms|pax|prices)\.(.+)$/);
|
||
if (target === 'remark' || target === 'xiadanbeizhu') {
|
||
snapshot.remark = String(value ?? '');
|
||
return;
|
||
}
|
||
if (!match) return;
|
||
if (!snapshot[match[1]]) snapshot[match[1]] = {};
|
||
snapshot[match[1]][match[2]] = value;
|
||
}
|
||
|
||
function applyUpdatePlanToSnapshot(updatePlan = {}, currentSnapshot = {}) {
|
||
const snapshot = clone(currentSnapshot) || {};
|
||
const actions = Array.isArray(updatePlan.actions) ? updatePlan.actions : [];
|
||
const changes = [];
|
||
for (const action of actions) {
|
||
const target = canonicalUpdateTarget(String(action?.target || '').trim());
|
||
const operation = String(action?.operation || '').trim().toLowerCase();
|
||
if (!isSupportedUpdateTarget(target) || /^travelerList(?:\.|$)/i.test(target)) {
|
||
return { status: 'blocked', reason: `unsupported_update_target:${target}` };
|
||
}
|
||
const before = snapshotValue(snapshot, target);
|
||
let after;
|
||
if (operation === 'set') {
|
||
after = target === 'remark' || target === 'xiadanbeizhu' ? String(action.value ?? '') : toNumber(action.value);
|
||
} else if (operation === 'delta') {
|
||
if (before === undefined || before === null || before === '') return { status: 'blocked', reason: `delta_baseline_missing:${target}` };
|
||
after = toNumber(before) + toNumber(action.value);
|
||
} else if (operation === 'append') {
|
||
if (target !== 'remark' && target !== 'xiadanbeizhu') return { status: 'blocked', reason: `append_target_invalid:${target}` };
|
||
after = [String(before || '').trim(), String(action.value || '').trim()].filter(Boolean).join('\n');
|
||
} else {
|
||
return { status: 'blocked', reason: `unsupported_update_operation:${operation}` };
|
||
}
|
||
if (['rooms', 'pax', 'prices'].includes(String(target).split('.')[0]) && after < 0) {
|
||
return { status: 'blocked', reason: `negative_update_result:${target}` };
|
||
}
|
||
setSnapshotValue(snapshot, target, after);
|
||
changes.push({ target, operation, before: clone(before), after: clone(after) });
|
||
}
|
||
return { status: 'ready', snapshot, changes };
|
||
}
|
||
|
||
function buildUpdatePreview(operation = {}, currentSnapshot = {}) {
|
||
const data = operation.data || {};
|
||
const updatePlan = operation.updatePlan || data.updatePlan || data.updates || {};
|
||
const applied = applyUpdatePlanToSnapshot(updatePlan, currentSnapshot);
|
||
return {
|
||
status: applied.status,
|
||
identifier: identifierForOperation(operation, data),
|
||
changes: applied.changes || [],
|
||
snapshot: applied.snapshot,
|
||
write_targets: (applied.changes || []).map((item) => item.target),
|
||
no_erp_write: true,
|
||
reason: applied.reason || ''
|
||
};
|
||
}
|
||
|
||
function buildConfirmationExportPlan(operation = {}, record = {}, basePath = '/System/Business') {
|
||
const data = operation.data || {};
|
||
const types = exportTypesForOperation(operation, data);
|
||
const ddid = String(record.ddid || record.did || data.existing_refs?.ddid || '').trim();
|
||
const tid = String(record.tid || data.existing_refs?.tid || '').trim();
|
||
const identifier = identifierForOperation(operation, data);
|
||
const artifacts = types.map((type) => {
|
||
if (type === 'xingyou-confirm') return { type, path: `${basePath}/orders_confirm_news.asp`, params: { did: ddid, tid } };
|
||
if (type === 'liantai-confirm') return { type, path: `${basePath}/orders_confirm_new.asp`, params: { did: ddid, tid } };
|
||
if (type === 'job-order') return { type, path: `${basePath}/teams_beian1.asp`, params: { did: ddid } };
|
||
return { type, path: '', params: {} };
|
||
});
|
||
return {
|
||
identifier,
|
||
export_only: true,
|
||
never_resave: true,
|
||
types,
|
||
artifacts,
|
||
ready_for_browser_lookup: Boolean(identifier),
|
||
no_erp_write: true
|
||
};
|
||
}
|
||
|
||
return {
|
||
OPERATION_DEFINITIONS,
|
||
actionForOperation,
|
||
dateYyyyMD,
|
||
expandTeamBatch,
|
||
applyUpdatePlanToSnapshot,
|
||
buildConfirmationExportPlan,
|
||
buildUpdatePreview,
|
||
isSupportedUpdateTarget,
|
||
normalizeOperation,
|
||
operationSummary,
|
||
publicPlan,
|
||
validateOperation,
|
||
};
|
||
}));
|