(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 }; }));