Setting the file. One moment.
Shipping Option Stage · Rp Target Wix · wix/skills · Skills Docs
ContentsBack to the top of the page 41.10
Multilingual Restore
This file
Number 41.162
Position 162 of 174
Type JavaScript
Size 25 KB
Lines 475 lib/ shipping-option-stage.js
JavaScript · 475 lines · 25 KB
14 // 4. creates happen only on a complete per-region snapshot, advanced only with known ids;
15 // 5. the completion fragment is built from the CURRENT state, so each run replaces the last.
16
17 const fs = require ( 'node:fs/promises' );
18 const path = require ( 'node:path' );
19 const crypto = require ( 'node:crypto' );
20 const s = require ( './shipping-build.js' );
21 const w = require ( './wix-writers.js' );
22 const local = require ( '../../../lib/local-state.js' );
23 const recovery = require ( '../../../lib/import-recovery.js' );
24 const wixAuth = require ( '../../../lib/wix-auth.js' );
25 const { getDecisionValue , isNewSiteStrategy } = require ( '../../../lib/orchestration-decisions.js' );
26
27 const EXTRACT_FILE = 'data/source-extract/shipping-zone.ndjson' ;
28 const EXTRACT_MANIFEST = 'data/source-extract/shipping-zone.manifest.json' ;
29 const DECISIONS_FILE = 'orchestration/decisions.json' ;
30 const COMPLETION_REPORT = 'execution/completion-report.json' ;
31 const COMPLETION_SUMMARY = 'execution/completion-summary.md' ;
32 const PASS_MODES = [ 'unfiltered' , 'missing-only' , 'unresolved-only' , 'failed-only' ];
33
34 async function readJsonIfExists ( filePath ) {
35 try {
36 return JSON . parse ( await fs. readFile (filePath, 'utf8' ));
37 } catch (error) {
38 if (error && error.code === 'ENOENT' ) return null ;
39 throw error;
40 }
41 }
42
43 async function exists ( filePath ) {
44 try {
45 await fs. access (filePath);
46 return true ;
47 } catch {
48 return false ;
49 }
50 }
51
52 async function writeJsonAtomic ( filePath , value ) {
53 await fs. mkdir (path. dirname (filePath), { recursive: true });
54 const temp = `${ filePath }.${ process . pid }.${ Date . now () }.tmp` ;
55 await fs. writeFile (temp, `${ JSON . stringify ( value , null , 2 ) } \n ` , 'utf8' );
56 await fs. rename (temp, filePath);
57 }
58
59 // The shipping stage refuses to run on an in-progress or unmanifested extract.
60 async function readShippingZoneExtract ( projectDir , { required = true } = {}) {
61 const file = path. join (projectDir, EXTRACT_FILE );
62 const manifest = path. join (projectDir, EXTRACT_MANIFEST );
63 const [ hasFile , hasManifest ] = await Promise . all ([ exists (file), exists (manifest)]);
64 if ( ! hasFile || ! hasManifest) {
65 if ( ! required) return null ;
66 throw new Error ( `shipping stage: ${ EXTRACT_FILE } and ${ EXTRACT_MANIFEST } must both exist (a .tmp extract is still in progress)` );
67 }
68 const text = await fs. readFile (file, 'utf8' );
69 return text. split ( / \r ? \n / ). filter (( line ) => line. trim ()). map (( line , index ) => {
70 try {
71 return JSON . parse (line);
72 } catch (error) {
73 throw new Error ( `${ EXTRACT_FILE }:${ index + 1 } invalid NDJSON: ${ error . message }` );
74 }
75 });
76 }
77
78 async function readDecisions ( projectDir ) {
79 const decisions = await readJsonIfExists (path. join (projectDir, DECISIONS_FILE ));
80 return decisions && typeof decisions === 'object' && ! Array. isArray (decisions) ? decisions : {};
81 }
82
83 async function readSiteLanguage ( wix ) {
84 try {
85 return s. siteLanguageFromProperties ( await w. getSiteProperties (wix));
86 } catch (error) {
87 if (wixAuth. isRunInterruption (error)) throw error;
88 return null ; // a failed read is treated as an unsupported language: no invented text
89 }
90 }
91
92 function shippingAttemptRows ( rows ) {
93 return (rows || []). filter (( row ) => row && row.sourceEntityType === 'shipping-option' );
94 }
95
96 function latestShippingAttempts ( rows ) {
97 return recovery. foldAttemptsBySource ( shippingAttemptRows (rows));
98 }
99
100 // Zone → region id: the importer's own map wins; a finished project falls back to the zones'
101 // crosswalk rows (ecom/delivery-profile, keyed like "shipping-zone:1" with sourceId "1").
102 function regionIdsByZone ( regionIdByZone , crosswalkBySource ) {
103 const out = {};
104 for ( const row of Object. values (crosswalkBySource || {})) {
105 if ( ! row || row.targetEntityType !== 'ecom/delivery-profile' || ! row.targetId) continue ;
106 const zoneId = row.sourceId ? String (row.sourceId) : String (row.sourceStableKey || '' ). split ( ':' ). pop ();
107 if (zoneId) out[zoneId] = row.targetId;
108 }
109 return { ... out, ... (regionIdByZone || {}) };
110 }
111
112 async function readPlaceholderLedger ( projectDir ) {
113 const ledger = await readJsonIfExists (path. join (projectDir, s. SHIPPING_PLACEHOLDER_LEDGER ));
114 return ledger && Array. isArray (ledger.deletions) ? ledger : { schemaVersion: 1 , deletions: [] };
115 }
116
117 // What was on each region before the migration's first create there (the earliest `started`
118 // shipping-option row's reconcile.preWriteTargetIds), and the regions where a create's outcome is
119 // still unknown. Only an option in the first set can be Wix's; while a region is in the second, an
120 // uncrosswalked option there may be the migration's own, so nothing there is deleted.
121 function regionWriteHistory ( journalRows ) {
122 const rows = shippingAttemptRows (journalRows);
123 const before = new Map ();
124 for ( const row of rows) {
125 const reconcile = row.reconcile || {};
126 if (row.status === 'started' && reconcile.deliveryRegionId && Array. isArray (reconcile.preWriteTargetIds) && ! before. has (reconcile.deliveryRegionId)) {
127 before. set (reconcile.deliveryRegionId, new Set (reconcile.preWriteTargetIds));
128 }
129 }
130 const unresolved = new Set (Object. values ( latestShippingAttempts (rows))
131 . filter (( row ) => row.status === 'started' || row.status === 'succeeded_unverified' )
132 . map (( row ) => row.reconcile && row.reconcile.deliveryRegionId). filter (Boolean));
133 return { before, unresolved };
134 }
135
136 // The live delivery-region state for the completion fragment, and -- in the import stage only, on
137 // a NEW site (Elad KC, 2026-10-29) -- the deletion of Wix's placeholder "Free shipping" option from
138 // a zone's region once the migration's own options are on it. Only an option that was on the region
139 // before the migration first wrote there is a candidate; the source store's own "Free shipping",
140 // even one whose create outcome is unknown, never is. The full option is written to the
141 // ledger BEFORE the DELETE, so it can be re-created; a row left `deleting` by a crash is retried
142 // (a 404 means it is already gone). An existing site keeps every option and only reports it.
143 // `snapshots` are the stage's own per-region scans (advanced with the ids it created), reused so a
144 // region is still read once per run; a region without one is read here.
145 async function regionState ( wix , { projectDir , zonePlans = [], regionIdByZone = {}, crosswalkBySource = {}, decisions = {}, mayDelete = false , snapshots = null , journalRows = [] } = {}) {
146 const byZone = regionIdsByZone (regionIdByZone, crosswalkBySource);
147 let regions = null ;
148 try {
149 regions = ( await w. queryDeliveryProfiles (wix)). flatMap (( profile ) => profile.deliveryRegions || profile.regions || []);
150 } catch (error) {
151 if (wixAuth. isRunInterruption (error)) throw error;
152 regions = null ; // coverage is then not reported this run; nothing is guessed
153 }
154 const coverage = regions ? s. regionCoverage ({ zonePlans, regionIdByZone: byZone, regions }) : [];
155 const nameOf = ( regionId ) => (regions && (regions. find (( region ) => region.id === regionId) || {}).name) || null ;
156 const zonesOf = ( regionId ) => zonePlans. filter (( plan ) => byZone[ String (plan.zone.id)] === regionId). map (( plan ) => `zone ${ plan . zone . name ? `"${ plan . zone . name }"` : plan . zone . id }` );
157 const ours = new Set (Object. values ( shippingCrosswalk (crosswalkBySource)). map (( row ) => row.targetId). filter (Boolean));
158 const newSite = isNewSiteStrategy ( getDecisionValue (decisions, 'targetSiteStrategy' ));
159 const ledger = await readPlaceholderLedger (projectDir);
160 const saveLedger = () => writeJsonAtomic (path. join (projectDir, s. SHIPPING_PLACEHOLDER_LEDGER ), ledger);
161 const history = regionWriteHistory (journalRows);
162 const preexisting = [];
163 for ( const regionId of new Set (zonePlans. map (( plan ) => byZone[ String (plan.zone.id)]). filter (Boolean))) {
164 const snapshot = snapshots && snapshots. get (regionId);
165 const scan = snapshot && snapshot.complete && ! snapshot.unprovable
166 ? { complete: true , options: [ ... snapshot.options, ... [ ... snapshot.ids]. filter (( id ) => ! snapshot.options. some (( option ) => option.id === id)). map (( id ) => ({ id }))] }
167 : await w. queryAllShippingOptions (wix, { deliveryRegionId: regionId });
168 let foreign = scan.options. filter (( option ) => ! ours. has (option.id));
169 const replaced = scan.complete && scan.options. some (( option ) => ours. has (option.id));
170 const settled = ! history.unresolved. has (regionId) && ! (snapshot && snapshot.unprovable);
171 if (mayDelete && newSite && replaced && settled && history.before. has (regionId)) {
172 const wixOwned = history.before. get (regionId);
173 const pending = ledger.deletions. filter (( entry ) => entry.regionId === regionId && entry.status === 'deleting' ). map (( entry ) => entry.option);
174 const targets = [ ... pending, ... foreign. filter (( option ) => wixOwned. has (option.id) && s. isWixPlaceholderShippingOption (option) && ! pending. some (( p ) => p.id === option.id))];
175 for ( const option of targets) {
176 let entry = ledger.deletions. find (( row ) => row.option && row.option.id === option.id);
177 if ( ! entry) {
178 entry = { optionId: option.id, regionId, regionName: nameOf (regionId), zones: zonesOf (regionId), option, status: 'deleting' , at: new Date (). toISOString () };
179 ledger.deletions. push (entry);
180 await saveLedger ();
181 }
182 try {
183 const result = await w. deleteShippingOption (wix, option.id);
184 Object. assign (entry, { status: result.deleted ? 'deleted' : 'already-gone' , at: new Date (). toISOString () });
185 } catch (error) {
186 if (wixAuth. isRunInterruption (error)) throw error;
187 Object. assign (entry, { status: 'failed' , error: String ((error && error.message) || error). slice ( 0 , 300 ), at: new Date (). toISOString () });
188 }
189 await saveLedger ();
190 if (entry.status !== 'failed' ) foreign = foreign. filter (( other ) => other.id !== option.id);
191 }
192 }
193 preexisting. push ({ regionId, regionName: nameOf (regionId), zones: zonesOf (regionId), complete: scan.complete,
194 options: foreign. map (( option ) => ({ id: option.id, title: option.title || null })) });
195 }
196 return { coverage, preexisting, deletions: ledger.deletions };
197 }
198
199 function shippingCrosswalk ( bySource ) {
200 const out = {};
201 for ( const [ key , row ] of Object. entries (bySource || {})) if (row && row.targetEntityType === s. SHIPPING_OPTION_TARGET_TYPE ) out[key] = row;
202 return out;
203 }
204
205 async function writeEstimates ( projectDir , resolution , fragment , audit ) {
206 const document = {
207 ... (resolution || { schemaVersion: 1 , siteLanguage: null , options: [], ignoredDecisions: [] }),
208 legacy: (audit && audit.legacyBlank) || [],
209 items: (fragment && fragment.items) || [],
210 };
211 await writeJsonAtomic (path. join (projectDir, s. SHIPPING_ESTIMATES_FILE ), document);
212 return document;
213 }
214
215 async function persistUpserts ( projectDir , audit ) {
216 for ( const row of (audit && audit.crosswalkUpserts) || []) await local. upsertCrosswalkRow (projectDir, row);
217 }
218
219 function passFilters ( passMode ) {
220 if ( ! PASS_MODES . includes (passMode)) throw new Error ( `shipping stage: passMode must be one of ${ PASS_MODES . join ( ', ' ) }` );
221 return {
222 missingOnly: passMode === 'missing-only' ,
223 unresolvedOnly: passMode === 'unresolved-only' ,
224 failedOnly: passMode === 'failed-only' ,
225 };
226 }
227
228 // Routing for a record with NO crosswalk row. `--missing-only` routes like
229 // an unfiltered pass here: it differs only in skipping crosswalked records, which never reach this
230 // function. Never creates an option whose latest row is unresolved or failed_terminal.
231 function routeFor ( latest , passMode ) {
232 const normal = passMode === 'unfiltered' || passMode === 'missing-only' ;
233 if ( ! latest) return normal ? 'create' : 'skip' ;
234 if (latest.status === 'imported' ) return 'skip' ;
235 if (latest.status === 'started' || latest.status === 'succeeded_unverified' ) return passMode === 'failed-only' ? 'skip' : 'recover' ;
236 if (latest.status === 'failed_retryable' ) return normal || passMode === 'failed-only' ? 'create' : 'skip' ;
237 return 'skip' ; // failed_terminal (including NOT_FOUND) and anything else: the operator decides
238 }
239
240 // Only a zone with destinations can have a delivery region, so only those can carry options.
241 function writableZonePlans ( zones ) {
242 return s. planShippingZones (zones). filter (( plan ) => plan.destinations. length > 0 && plan.shippingOptions. length > 0 );
243 }
244
245 async function runShippingOptionStage ( wix , {
246 projectDir ,
247 regionIdByZone = {},
248 sourceSystem = 'wordpress' ,
249 runId ,
250 phase = 'import' ,
251 passMode = 'unfiltered' ,
252 writeSpecId = 'ecom/shipping-option' ,
253 newAttemptId = () => crypto. randomUUID (),
254 } = {}) {
255 if ( ! projectDir) throw new Error ( 'runShippingOptionStage: projectDir is required' );
256 if ( ! runId) throw new Error ( 'runShippingOptionStage: runId is required' );
257 const filters = passFilters (passMode);
258 const zones = await readShippingZoneExtract (projectDir);
259 const zonePlans = writableZonePlans (zones);
260 const decisions = await readDecisions (projectDir);
261 const siteLanguage = await readSiteLanguage (wix);
262 const resolution = s. planShippingEstimates ({ zonePlans, decisions, siteLanguage });
263 await writeEstimates (projectDir, resolution, null , null );
264 const resolutionByKey = new Map (resolution.options. map (( option ) => [option.optionKey, option]));
265 // A zone already written on an earlier run keeps its region through its crosswalk row.
266 const regionByZone = regionIdsByZone (regionIdByZone, ( await local. loadCrosswalk (projectDir)).bySource);
267
268 // Planned payloads, keyed by the raw option key.
269 const planned = [];
270 for ( const plan of zonePlans) {
271 const zoneId = String (plan.zone.id);
272 for ( const option of plan.shippingOptions) {
273 const optionKey = s. shippingOptionKey (zoneId, option);
274 // A zone whose region could not be written (e.g. an unresolved destination collision) keeps
275 // its options planned with no input: they get a retryable not-attempted row below, and the
276 // rest of the stage runs. One zone never stops the import.
277 const deliveryRegionId = regionByZone[zoneId] || null ;
278 const estimate = resolutionByKey. get (optionKey);
279 planned. push ({
280 id: optionKey,
281 optionKey,
282 zoneId,
283 deliveryRegionId,
284 input: deliveryRegionId ? s. buildShippingOptionInput (option.classified, { deliveryRegionId, estimatedDeliveryTime: estimate.text, rates: option.rates }) : null ,
285 });
286 }
287 }
288
289 // Crosswalk first, for EVERY linked option and every pass mode: read-only, never a POST.
290 const preRunCrosswalk = await local. loadCrosswalk (projectDir);
291 let journal = await local. loadAttemptJournal (projectDir);
292 const audit = await s. auditShippingEstimates (wix, {
293 crosswalkBySource: shippingCrosswalk (preRunCrosswalk.bySource),
294 attemptRows: journal.rows,
295 resolution,
296 decisions,
297 });
298 await persistUpserts (projectDir, audit);
299
300 const selection = recovery. selectImportRecords (planned, {
301 sourceSystem,
302 filters: { ... filters, entity: 'shipping-option' },
303 crosswalkBySource: preRunCrosswalk.bySource,
304 attemptRows: journal.rows,
305 });
306
307 const readBack = { ... audit.readBack };
308 const results = [];
309 const regions = new Map ();
310 const ctxFor = ( record , attemptId , reconcile ) => ({
311 attemptId,
312 sourceSystem,
313 sourceId: record.optionKey,
314 sourceStableKey: recovery. sourceStableKey (sourceSystem, 'shipping-option' , record.optionKey),
315 writeSpecId,
316 runId,
317 phase,
318 ... (reconcile ? { reconcile } : {}),
319 });
320
321 const routed = selection.selected
322 . filter (( entry ) => ! entry.existing)
323 . map (( entry ) => ({ ... entry, route: routeFor (entry.latestAttempt, passMode) }));
324
325 // 1. Recover unresolved rows first: read back, never re-send.
326 for ( const entry of routed. filter (( item ) => item.route === 'recover' )) {
327 const record = entry.record;
328 const latest = entry.latestAttempt;
329 if ( ! record.input) {
330 results. push ({ optionKey: record.optionKey, route: 'recover' , outcome: 'shipping-option-recovery-incomplete' , errorCode: 'DELIVERY_REGION_MISSING' });
331 continue ; // nothing to compare against until the zone has a region; the row stays as it was
332 }
333 const result = await w. recoverShippingOption (wix, latest, record.input, { decisions });
334 if (result.outcome === 'shipping-option-recovery-incomplete' ) {
335 results. push ({ optionKey: record.optionKey, route: 'recover' , outcome: result.outcome });
336 continue ; // the row stays exactly as it was
337 }
338 const { attempt , crosswalk } = s. shippingOptionWriteRecord (result, ctxFor (record, latest.attemptId, latest.reconcile));
339 await local. markAttempt (projectDir, latest.attemptId, { ... attempt, failure: attempt.failure });
340 if (crosswalk) await local. appendCrosswalkRow (projectDir, crosswalk);
341 if (result.outcome === 'shipping-option-created' && result.stored) readBack[record.optionKey] = result.stored;
342 results. push ({ optionKey: record.optionKey, route: 'recover' , outcome: result.outcome, status: attempt.status });
343 }
344
345 // 2. Creates, on a complete per-region snapshot advanced only with known ids.
346 for ( const entry of routed. filter (( item ) => item.route === 'create' )) {
347 const record = entry.record;
348 const regionId = record.deliveryRegionId;
349 if ( ! regionId) {
350 const errorCode = 'DELIVERY_REGION_MISSING' ;
351 const { attempt } = s. shippingOptionWriteRecord ({ outcome: 'shipping-option-not-attempted' , id: null , errorCode, reason: `zone ${ record . zoneId } has no delivery region yet; created on the next run once it has one` }, ctxFor (record, newAttemptId ()));
352 await local. appendAttempt (projectDir, attempt);
353 results. push ({ optionKey: record.optionKey, route: 'create' , outcome: 'not-attempted' , errorCode });
354 continue ;
355 }
356 if ( ! regions. has (regionId)) {
357 const scan = await w. queryAllShippingOptions (wix, { deliveryRegionId: regionId });
358 regions. set (regionId, { complete: scan.complete, ids: new Set (scan.options. map (( option ) => option.id)), unprovable: false , options: scan.options });
359 }
360 const region = regions. get (regionId);
361 if ( ! region.complete || region.unprovable) {
362 const errorCode = region.complete ? 'REGION_SNAPSHOT_UNPROVABLE' : 'SNAPSHOT_INCOMPLETE' ;
363 const reason = region.complete
364 ? 'an earlier create in this region had an unknown outcome; the region is rescanned next run'
365 : 'the region snapshot scan was incomplete; nothing is created in this region this run' ;
366 const { attempt } = s. shippingOptionWriteRecord ({ outcome: 'shipping-option-not-attempted' , id: null , errorCode, reason }, ctxFor (record, newAttemptId (), { deliveryRegionId: regionId }));
367 await local. appendAttempt (projectDir, attempt);
368 results. push ({ optionKey: record.optionKey, route: 'create' , outcome: 'not-attempted' , errorCode });
369 continue ;
370 }
371 const attemptId = newAttemptId ();
372 const reconcile = { deliveryRegionId: regionId, preWriteTargetIds: [ ... region.ids] };
373 // The start half of the shared start/terminal pair: written BEFORE the POST, so a crash
374 // mid-write leaves a `started` row carrying the snapshot recovery needs.
375 const ctx = ctxFor (record, attemptId, reconcile);
376 await local. appendAttempt (projectDir, {
377 schemaVersion: 1 ,
378 attemptId,
379 sourceSystem,
380 sourceEntityType: 'shipping-option' ,
381 sourceId: ctx.sourceId,
382 sourceStableKey: ctx.sourceStableKey,
383 writeSpecId,
384 runId,
385 phase,
386 operation: s. SHIPPING_OPTION_OPERATION ,
387 targetEntityType: s. SHIPPING_OPTION_TARGET_TYPE ,
388 status: 'started' ,
389 reconcile,
390 updatedAt: new Date (). toISOString (),
391 });
392 const result = await w. createShippingOptionVerified (wix, record.input);
393 const { attempt , crosswalk } = s. shippingOptionWriteRecord (result, ctx);
394 await local. markAttempt (projectDir, attemptId, { ... attempt, failure: attempt.failure });
395 if (crosswalk) await local. appendCrosswalkRow (projectDir, crosswalk);
396 if (result.id) region.ids. add (result.id);
397 if (result.outcome === 'shipping-option-unknown' ) region.unprovable = true ;
398 if (result.outcome === 'shipping-option-created' && result.stored) readBack[record.optionKey] = result.stored;
399 results. push ({ optionKey: record.optionKey, route: 'create' , outcome: result.outcome, status: attempt.status });
400 }
401
402 journal = await local. loadAttemptJournal (projectDir);
403 const latestAttempts = latestShippingAttempts (journal.rows);
404 const regionReport = await regionState (wix, { projectDir, zonePlans, regionIdByZone: regionByZone, crosswalkBySource: ( await local. loadCrosswalk (projectDir)).bySource, decisions, mayDelete: true , snapshots: regions, journalRows: journal.rows });
405 const fragment = s. shippingEstimateCompletionFragment ({ resolution, readBack, latestAttempts, audit, projectDir, regions: regionReport });
406 const estimates = await writeEstimates (projectDir, resolution, fragment, audit);
407 return { resolution, estimates, fragment, audit, results, selection: selection.summary };
408 }
409
410 // --- Finished projects -----------------------------------------------------------
411 // Replaces the shipping rows of an already-written completion report and regenerates the
412 // post-execution handoff. Every other part of the report is kept as written: the report's own
413 // rows are already the final record of that run, and re-deriving them could double rows the
414 // writer joined (source coverage) or derived (voucher/coverage-check actions). The merged rows go
415 // through createCompletionReport's own operator-action normalization before they are written.
416 async function rebuildCompletionWithFragment ( projectDir , fragment ) {
417 const { createCompletionReport , renderCompletionSummary } = require ( '../../../lib/completion-report.js' );
418 const { generatePostExecutionHandoff } = require ( '../../../lib/website-handoff.js' );
419 const reportPath = path. join (projectDir, COMPLETION_REPORT );
420 const report = await readJsonIfExists (reportPath);
421 if ( ! report) return { completionReport: null , handoff: { ok: false , reason: 'missing_completion_report' } };
422 const merged = s. mergeShippingEstimateCompletion (report, fragment);
423 const normalized = createCompletionReport ({ operatorActions: merged.operatorActions, lastWriteAt: report.lastWriteAt || null }).operatorActions;
424 const rebuilt = { ... report, operatorActions: normalized };
425 await writeJsonAtomic (reportPath, rebuilt);
426 await fs. writeFile (path. join (projectDir, COMPLETION_SUMMARY ), renderCompletionSummary (rebuilt), 'utf8' );
427 // Rewriting import outcomes also refreshes the discovered-set join before its handoff.
428 if (rebuilt.managementImportMode !== 'quick' ) {
429 await require ( '../../../lib/discovered-set-status.js' ). buildDiscoveredSetStatus (projectDir);
430 }
431 const handoff = await generatePostExecutionHandoff (projectDir, { runId: report.runId || null });
432 return { completionReport: rebuilt, handoff };
433 }
434
435 async function runShippingEstimateAudit ( wix , { projectDir } = {}) {
436 if ( ! projectDir) throw new Error ( 'runShippingEstimateAudit: projectDir is required' );
437 const decisions = await readDecisions (projectDir);
438 const zones = await readShippingZoneExtract (projectDir, { required: false });
439 const siteLanguage = await readSiteLanguage (wix);
440 const resolution = zones ? s. planShippingEstimates ({ zonePlans: writableZonePlans (zones), decisions, siteLanguage }) : null ;
441 const crosswalk = await local. loadCrosswalk (projectDir);
442 const journal = await local. loadAttemptJournal (projectDir);
443 const audit = await s. auditShippingEstimates (wix, {
444 crosswalkBySource: shippingCrosswalk (crosswalk.bySource),
445 attemptRows: journal.rows,
446 resolution,
447 decisions,
448 });
449 await persistUpserts (projectDir, audit);
450 const latestAttempts = latestShippingAttempts (journal.rows);
451 // The audit only reads: it reports the region state and never deletes.
452 const regionReport = await regionState (wix, { projectDir, zonePlans: zones ? writableZonePlans (zones) : [], crosswalkBySource: crosswalk.bySource, decisions });
453 const fragment = s. shippingEstimateCompletionFragment ({ resolution, readBack: audit.readBack, latestAttempts, audit, projectDir, regions: regionReport });
454 const previous = await readJsonIfExists (path. join (projectDir, s. SHIPPING_ESTIMATES_FILE ));
455 const base = resolution || (previous ? { ... previous } : null );
456 const estimates = await writeEstimates (projectDir, base, fragment, audit);
457 const rebuilt = await rebuildCompletionWithFragment (projectDir, fragment);
458 return { resolution, audit, fragment, estimates, ... rebuilt };
459 }
460
461 async function listShippingEstimateItems ( projectDir , code ) {
462 const document = await readJsonIfExists (path. join (projectDir, s. SHIPPING_ESTIMATES_FILE ));
463 return ((document && document.items) || []). filter (( item ) => ! code || item.code === code);
464 }
465
466 module . exports = {
467 EXTRACT_FILE,
468 EXTRACT_MANIFEST,
469 PASS_MODES,
470 readShippingZoneExtract,
471 runShippingOptionStage,
472 runShippingEstimateAudit,
473 rebuildCompletionWithFragment,
474 listShippingEstimateItems,
475 };