321 lines
13 KiB
JavaScript
321 lines
13 KiB
JavaScript
(function installLTJTPlanReconciliation(root, factory) {
|
|
const api = factory();
|
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
if (root) root.LTJTPlanReconciliation = api;
|
|
}(typeof self !== 'undefined' ? self : globalThis, () => {
|
|
'use strict';
|
|
|
|
function compact(value) {
|
|
return String(value || '')
|
|
.normalize('NFKC')
|
|
.toLowerCase()
|
|
.replace(/[^\p{L}\p{N}]+/gu, '');
|
|
}
|
|
|
|
function dateToken(value) {
|
|
const parts = String(value || '').trim().split(/[.\-/]/u).map((part) => Number(part));
|
|
if (parts.length !== 3 || parts.some((part) => !Number.isFinite(part))) return compact(value);
|
|
return `${parts[0]}-${parts[1]}-${parts[2]}`;
|
|
}
|
|
|
|
function numberValue(value) {
|
|
const number = Number(value || 0);
|
|
return Number.isFinite(number) ? Math.max(0, Math.trunc(number)) : 0;
|
|
}
|
|
|
|
function extractDdidFromAction(value) {
|
|
const source = String(value || '');
|
|
const queryMatch = source.match(/[?&]ddid=([^&#]+)/i);
|
|
if (queryMatch?.[1]) {
|
|
try {
|
|
return decodeURIComponent(queryMatch[1]);
|
|
} catch (_) {
|
|
return queryMatch[1];
|
|
}
|
|
}
|
|
const callMatch = source.match(/OPEN_update\s*\(([^)]*)\)/i);
|
|
if (!callMatch) return '';
|
|
const args = callMatch[1]
|
|
.split(',')
|
|
.map((part) => String(part || '').trim().replace(/^["']|["']$/g, ''));
|
|
// Independent orders use OPEN_update(ddid, 0, groupNo), while a
|
|
// concrete shared child uses OPEN_update(parentTid, childDdid).
|
|
if (args.length >= 3) return args[0] && args[0] !== '0' ? args[0] : '';
|
|
if (args.length === 2) return args[1] && args[1] !== '0' ? args[1] : '';
|
|
return args[0] && args[0] !== '0' ? args[0] : '';
|
|
}
|
|
|
|
function extractSharedChildRefs(links = [], parentTid = '') {
|
|
const expectedTid = String(parentTid || '').trim();
|
|
if (!expectedTid) return [];
|
|
const refs = new Map();
|
|
for (const link of links) {
|
|
const onclick = String(link?.onclick || '');
|
|
const href = String(link?.href || '');
|
|
const source = `${onclick} ${href}`;
|
|
const callMatch = source.match(/OPEN_update\s*\(\s*["']?(\d+)["']?\s*,\s*["']?(\d+)["']?/i);
|
|
const queryTid = source.match(/[?&](?:tdid|tid)=([^&#]+)/i)?.[1] || '';
|
|
const queryDdid = source.match(/[?&]ddid=([^&#]+)/i)?.[1] || '';
|
|
const tid = callMatch?.[1] || queryTid;
|
|
const ddid = callMatch?.[2] || queryDdid;
|
|
if (tid !== expectedTid || !/^\d+$/.test(ddid) || ddid === '0') continue;
|
|
refs.set(ddid, {
|
|
tid: expectedTid,
|
|
ddid,
|
|
child_order_no: `D${ddid}`,
|
|
link_text_length: String(link?.text || '').trim().length,
|
|
value_redacted: true
|
|
});
|
|
}
|
|
return [...refs.values()].sort((left, right) => Number(left.ddid) - Number(right.ddid));
|
|
}
|
|
|
|
function passengerMarkers(passengerCounts = {}) {
|
|
const markers = [];
|
|
const adult = numberValue(passengerCounts.adult);
|
|
const childBed = numberValue(passengerCounts.child_bed);
|
|
const childNoBed = numberValue(passengerCounts.child_no_bed);
|
|
const infant = numberValue(passengerCounts.infant);
|
|
const leader = numberValue(passengerCounts.leader);
|
|
if (adult) markers.push(`成人${adult}`);
|
|
if (childBed) markers.push(`小占${childBed}`);
|
|
if (childNoBed) markers.push(`小不占${childNoBed}`);
|
|
if (infant) markers.push(`婴儿${infant}`);
|
|
if (leader) markers.push(`领队${leader}`);
|
|
const total = adult + childBed + childNoBed + infant + leader;
|
|
if (total) markers.push(`合计${total}`);
|
|
return markers;
|
|
}
|
|
|
|
function matchRowText(rowText, expected = {}) {
|
|
const normalizedRow = compact(rowText);
|
|
const productCandidates = [expected.product_name, expected.product_label]
|
|
.map(compact)
|
|
.filter(Boolean);
|
|
const productMatch = productCandidates.length > 0
|
|
&& productCandidates.some((candidate) => normalizedRow.includes(candidate));
|
|
const dateMatch = Boolean(expected.date)
|
|
&& (normalizedRow.includes(compact(expected.date))
|
|
|| normalizedRow.includes(compact(dateToken(expected.date))));
|
|
const capacity = numberValue(expected.planned_capacity);
|
|
const capacityMatch = capacity > 0 && normalizedRow.includes(compact(`计划${capacity}`));
|
|
const markers = passengerMarkers(expected.passenger_counts);
|
|
const passengerCriteriaPresent = markers.length > 0;
|
|
const passengerMatch = !passengerCriteriaPresent || markers.every((marker) => normalizedRow.includes(compact(marker)));
|
|
const customerToken = compact(expected.customer_name);
|
|
const customerCriteriaPresent = Boolean(customerToken);
|
|
const customerMatch = Boolean(customerToken && normalizedRow.includes(customerToken));
|
|
const groupSuffixToken = compact(expected.group_suffix);
|
|
const groupSuffixMatch = !groupSuffixToken || normalizedRow.includes(groupSuffixToken);
|
|
const markerToken = compact(expected.marker);
|
|
const markerMatch = !markerToken || normalizedRow.includes(markerToken);
|
|
const ownerToken = compact(expected.owner_account);
|
|
const ownerAccountMatch = !ownerToken || normalizedRow.includes(ownerToken);
|
|
const missing = [];
|
|
if (!productMatch) missing.push('product');
|
|
if (!dateMatch) missing.push('date');
|
|
if (!capacityMatch) missing.push('planned_capacity');
|
|
if (passengerCriteriaPresent && !passengerMatch) missing.push('passenger_counts');
|
|
if (expected.require_customer_match === true && customerCriteriaPresent && !customerMatch) missing.push('customer');
|
|
if (!groupSuffixMatch) missing.push('group_suffix');
|
|
if (!markerMatch) missing.push('marker');
|
|
if (!ownerAccountMatch) missing.push('owner_account');
|
|
return {
|
|
matched: missing.length === 0,
|
|
customer_match: customerMatch,
|
|
customer_criteria_present: customerCriteriaPresent,
|
|
passenger_match: passengerMatch,
|
|
missing,
|
|
passenger_markers: markers,
|
|
passenger_criteria_present: passengerCriteriaPresent,
|
|
group_suffix_match: groupSuffixMatch,
|
|
marker_match: markerMatch,
|
|
owner_account_match: ownerAccountMatch
|
|
};
|
|
}
|
|
|
|
function splitOrderFact(row = {}, expected = {}) {
|
|
const match = matchRowText(row.text, expected);
|
|
const childRefs = Array.isArray(row.child_refs) ? row.child_refs : [];
|
|
const identityMissing = match.missing.filter((field) => !['customer', 'passenger_counts'].includes(field));
|
|
const parentIdentityDetermined = Boolean(row.group_no && /^\d+$/.test(String(row.tid || '')) && identityMissing.length === 0);
|
|
const childReferenceScanComplete = row.child_reference_scan_complete === true;
|
|
const requestedFactsPresent = match.customer_criteria_present || match.passenger_criteria_present;
|
|
const factsDetermined = parentIdentityDetermined && childReferenceScanComplete && requestedFactsPresent;
|
|
const customerSatisfied = !match.customer_criteria_present || match.customer_match;
|
|
const passengerSatisfied = !match.passenger_criteria_present || match.passenger_match;
|
|
let outcome = 'split_order_fact_indeterminate';
|
|
if (factsDetermined && childRefs.length > 0 && customerSatisfied && passengerSatisfied) {
|
|
outcome = 'concrete_shared_children_with_requested_facts';
|
|
} else if (factsDetermined && childRefs.length > 0) {
|
|
outcome = 'concrete_shared_children_with_fact_mismatch';
|
|
} else if (factsDetermined && customerSatisfied && passengerSatisfied) {
|
|
outcome = 'parent_row_facts_without_concrete_child';
|
|
} else if (factsDetermined) {
|
|
outcome = 'requested_split_facts_not_visible_and_no_concrete_child';
|
|
}
|
|
return {
|
|
facts_determined: factsDetermined,
|
|
outcome,
|
|
parent_identity_determined: parentIdentityDetermined,
|
|
parent_group_no: String(row.group_no || ''),
|
|
parent_tid: String(row.tid || ''),
|
|
customer_requested: match.customer_criteria_present,
|
|
customer_persisted_on_native_list: match.customer_criteria_present ? match.customer_match : null,
|
|
passenger_counts_requested: match.passenger_criteria_present,
|
|
passenger_counts_persisted_on_native_list: match.passenger_criteria_present ? match.passenger_match : null,
|
|
child_reference_scan_complete: childReferenceScanComplete,
|
|
child_count: childRefs.length,
|
|
child_refs: childRefs,
|
|
ownership: {
|
|
group_suffix_matched: match.group_suffix_match,
|
|
marker_matched: match.marker_match,
|
|
account_matched: match.owner_account_match
|
|
},
|
|
identity_missing: identityMissing,
|
|
value_redacted: true
|
|
};
|
|
}
|
|
|
|
function chooseUniqueRows(rows = [], expected = {}) {
|
|
const matches = rows
|
|
.filter((row) => row && row.group_no)
|
|
.map((row) => ({ ...row, match: matchRowText(row.text, expected) }))
|
|
.filter((row) => row.match.matched);
|
|
const customerMatches = matches.filter((row) => row.match.customer_match);
|
|
const preferred = customerMatches.length ? customerMatches : matches;
|
|
return {
|
|
matches,
|
|
preferred,
|
|
selected: preferred.length === 1 ? preferred[0] : null,
|
|
ambiguous: preferred.length > 1
|
|
};
|
|
}
|
|
|
|
function splitParentExactSearchValues(dateStart, dateEnd, groupNumber) {
|
|
return {
|
|
S_chufariqi: String(dateStart || ''),
|
|
S_chufarizhi: String(dateEnd || dateStart || ''),
|
|
S_tuanxuhao: String(groupNumber || ''),
|
|
S_kehuming: '',
|
|
S_chanpinming: '',
|
|
S_youkexinxi: '',
|
|
S_lingdui: '',
|
|
S_zhuangtai: ''
|
|
};
|
|
}
|
|
|
|
function splitParentDateSearchValues(date) {
|
|
return splitParentExactSearchValues(date, date, '');
|
|
}
|
|
|
|
async function verifyReturnedGroupNumbers(options = {}) {
|
|
const groupNumbers = [...new Set((Array.isArray(options.group_numbers) ? options.group_numbers : [])
|
|
.map((value) => String(value || '').trim())
|
|
.filter(Boolean))];
|
|
const retryDelays = (Array.isArray(options.retry_delays_ms) && options.retry_delays_ms.length
|
|
? options.retry_delays_ms
|
|
: [0])
|
|
.map((value) => Math.max(0, Number(value) || 0));
|
|
const searchGroup = options.search_group;
|
|
if (typeof searchGroup !== 'function') throw new TypeError('search_group must be a function');
|
|
const wait = typeof options.wait === 'function'
|
|
? options.wait
|
|
: (delay) => new Promise((resolve) => setTimeout(resolve, delay));
|
|
const attempts = [];
|
|
let rows = [];
|
|
for (let attemptIndex = 0; attemptIndex < retryDelays.length; attemptIndex += 1) {
|
|
const delayMs = retryDelays[attemptIndex];
|
|
if (delayMs > 0) await wait(delayMs);
|
|
const attemptRows = [];
|
|
for (const groupNumber of groupNumbers) {
|
|
let outcome = null;
|
|
let searchError = '';
|
|
try {
|
|
outcome = await searchGroup(groupNumber, {
|
|
attempt: attemptIndex + 1,
|
|
delay_ms: delayMs
|
|
});
|
|
} catch (error) {
|
|
searchError = String(error?.message || error || 'split_parent_requery_error').slice(0, 200);
|
|
}
|
|
const found = outcome?.found === true;
|
|
attemptRows.push({
|
|
group_number: groupNumber,
|
|
source: attemptIndex === 0 ? 'erp_response' : 'erp_response_retry',
|
|
status: found
|
|
? 'parent_group_found'
|
|
: searchError
|
|
? 'parent_group_requery_error'
|
|
: 'parent_group_not_found',
|
|
row_text_length: found ? Math.max(0, Number(outcome?.row_text_length || 0)) : 0,
|
|
attempt: attemptIndex + 1,
|
|
search_error: searchError || undefined
|
|
});
|
|
}
|
|
rows = attemptRows;
|
|
const missingGroupNumbers = rows
|
|
.filter((row) => row.status !== 'parent_group_found')
|
|
.map((row) => row.group_number);
|
|
attempts.push({
|
|
attempt: attemptIndex + 1,
|
|
delay_ms: delayMs,
|
|
found_count: rows.length - missingGroupNumbers.length,
|
|
missing_count: missingGroupNumbers.length,
|
|
missing_group_numbers: missingGroupNumbers
|
|
});
|
|
if (!missingGroupNumbers.length) break;
|
|
}
|
|
const complete = groupNumbers.length > 0
|
|
&& rows.length === groupNumbers.length
|
|
&& rows.every((row) => row.status === 'parent_group_found');
|
|
return {
|
|
complete,
|
|
rows,
|
|
attempts,
|
|
attempt_count: attempts.length,
|
|
retry_count: Math.max(0, attempts.length - 1),
|
|
group_numbers: groupNumbers
|
|
};
|
|
}
|
|
|
|
function shouldReconcileSplitParent(options = {}) {
|
|
if (options.write_attempted !== true) return false;
|
|
if (options.fact_probe_enabled === true) return true;
|
|
if (options.capture_success !== true) return true;
|
|
const groupNumbers = Array.isArray(options.group_numbers) ? options.group_numbers : [];
|
|
const requestedDateCount = Math.max(0, Number(options.requested_date_count || 0));
|
|
if (groupNumbers.length !== requestedDateCount) return true;
|
|
return options.verification_complete !== true;
|
|
}
|
|
|
|
function mergeVerificationRows(rows = []) {
|
|
const merged = new Map();
|
|
for (const row of rows) {
|
|
const groupNumber = String(row?.group_number || '').trim();
|
|
if (!groupNumber) continue;
|
|
const prior = merged.get(groupNumber);
|
|
if (!prior || (prior.status !== 'parent_group_found' && row.status === 'parent_group_found')) {
|
|
merged.set(groupNumber, row);
|
|
}
|
|
}
|
|
return [...merged.values()];
|
|
}
|
|
|
|
return {
|
|
compact,
|
|
dateToken,
|
|
extractDdidFromAction,
|
|
extractSharedChildRefs,
|
|
passengerMarkers,
|
|
matchRowText,
|
|
chooseUniqueRows,
|
|
splitOrderFact,
|
|
splitParentExactSearchValues,
|
|
splitParentDateSearchValues,
|
|
verifyReturnedGroupNumbers,
|
|
shouldReconcileSplitParent,
|
|
mergeVerificationRows
|
|
};
|
|
}));
|