PageSourceSearch

https://www.galls.com/assets/fulfillment/delivery/est-delivery-date.js?v=0.0.120

js galls.com collected 2026-09-24 19:40:01 UTC 142,015 bytes, 3,178 lines download raw bytes

1/**
2 * FedEx Ground Transit Time - Estimated Delivery Dates
3 * Calculates delivery estimates based on:
4 * - ESD (Est Ship Date) from backend token #SIESDTd
5 * - FedEx transit times from CSV data by destination zip
6 * - Range: earliestDate + 1 business day for upper bound
7 * 
8 * Supports: PDP, Quickview, Cart, Minicart
9 */
10
11class EDDHandler {
12  constructor() {
13    this.config = {
14      origin: {
15        zip: '40505', // FedEx origin zip (Lexington warehouse)
16      },
17
18      cache: {
19        duration: 1000 * 60 * 60 * 24 * 7, // 7 days (CSV is static, updated annually)
20      },
21
22      carrierData: {
23        fedex: {
24          csvPath: '/assets/fulfillment/delivery/edd-csv-imports/fedex-transit-times-11-26-25.csv',
25          key: 'fedexTransitData',
26        },
27      },
28
29      holidayConfig: {
30        csvPath: '/assets/fulfillment/delivery/edd-csv-imports/edd-2026_edd-holidays.csv',
31        key: 'holidaysData',
32        enabled: false, // Set to true when ready to load holidays
33      },
34
35      deliveryRules: {
36        bufferBusinessDays: 1,
37        cutoffHourEST: 14
38      },
39      checkoutDecisionTable: {
40        scenarios: [
41          { condition: 'GQ ESD > 1', shipDateUsed: 'GQ ESD', standardDelivery: 'GQ ESD + Ground', secondDayDelivery: 'GQ ESD + 2', nextDayDelivery: 'GQ ESD + 1' },
42          { condition: 'GQ ESD = 1, before 2PM weekday', shipDateUsed: 'Today', standardDelivery: 'Today + Ground', secondDayDelivery: 'Today + 2', nextDayDelivery: 'Tomorrow' },
43          { condition: 'GQ ESD = 1, after 2PM', shipDateUsed: 'Tomorrow', standardDelivery: 'Tomorrow + Ground', secondDayDelivery: 'Tomorrow + 2', nextDayDelivery: 'Tomorrow + 1' },
44          { condition: 'Friday before 2PM', shipDateUsed: 'Friday', standardDelivery: 'Friday + Ground', secondDayDelivery: 'Tuesday', nextDayDelivery: 'Monday' },
45          { condition: 'Friday after 2PM', shipDateUsed: 'Monday', standardDelivery: 'Monday + Ground', secondDayDelivery: 'Wednesday', nextDayDelivery: 'Tuesday' },
46          { condition: 'Weekend', shipDateUsed: 'Monday', standardDelivery: 'Monday + Ground', secondDayDelivery: 'Wednesday', nextDayDelivery: 'Tuesday' }
47        ],
48        shipDateRules: {
49          weekendShipDay: 'monday',
50          fridayAfterCutoffShipDay: 'monday',
51          afterCutoffDelayBusinessDays: 1
52        },
53        deliveryBusinessDays: {
54          secondDay: 2,
55          nextDay: 1
56        }
57      },
58      stockMsgDisplay: {
59        defaultTooltip: 'Customized items may take longer to ship.',
60        stockMsgText: {
61          block: {
62            esd: {
63              inStock: 'In Stock',
64              partnerFulfilled: 'Partner Fulfilled',
65              backordered: 'Backordered',
66              backorderDateOnly: '',
67              preOrder: 'Pre-Order'
68            },
69            edd: {
70              inStock: 'In Stock',
71              partnerFulfilled: 'Partner Fulfilled',
72              backordered: 'Backordered',
73              backorderDateOnly: '',
74              preOrder: 'Pre-Order'
75            }
76          },
77          inline: {
78            esd: {
79              inStock: 'In Stock',
80              partnerFulfilled: 'Partner Fulfilled',
81              backordered: 'Backordered',
82              backorderDateOnly: '',
83              preOrder: 'Pre-Order'
84            },
85            edd: {
86              inStock: 'In Stock',
87              partnerFulfilled: 'Partner Fulfilled',
88              backordered: 'Backordered',
89              backorderDateOnly: '',
90              preOrder: 'Pre-Order'
91            }
92          }
93        },
94        // Drawer messages displayed when user clicks the stock message (per status type per variation mode)
95        // Placeholders: {startDate} = "Wednesday, February 12", {endDate} = "Thursday, February 13"
96        drawerMessages: {
97          esd: {
98            inStock: 'This item is currently in stock and estimated to ship between {startDate} and {endDate} with standard shipping. Ship timing is based on your delivery ZIP code. Customized items may require additional processing time.',
99            partnerFulfilled: 'This item ships from a partner facility and is estimated to ship between {startDate} and {endDate}. Processing and transit times may differ from warehouse items.',
100            backordered: 'Due to high demand, this item is currently backordered and estimated to ship between {startDate} and {endDate} once inventory becomes available. Ship timing may adjust based on availability. We will notify you of any changes.',
101            backorderDateOnly: 'Estimated to ship between {startDate} and {endDate}. Ship timing may adjust based on availability.',
102            preOrder: 'This item is available for pre-order and estimated to ship between {startDate} and {endDate}
102. Ship timing is based on expected inventory arrival and may change. You will be notified of any updates.'
103          },
104          edd: {
105            inStock: 'This item is in stock and estimated to arrive between {startDate} and {endDate} with standard shipping. Delivery timing is based on your ZIP code and carrier transit times. Customized items may require additional processing time.',
106            partnerFulfilled: 'This item ships from a partner facility and is estimated to arrive between {startDate} and {endDate}. Delivery timing may vary from warehouse-shipped items.',
107            backordered: 'Due to high demand, this item is currently backordered and estimated to arrive between {startDate} and {endDate} once inventory is available. Delivery timing may adjust if availability changes.',
108            backorderDateOnly: 'Estimated to arrive between {startDate} and {endDate}. Delivery timing may adjust based on availability.',
109            preOrder: 'This item is available for pre-order and estimated to arrive between {startDate} and {endDate}. Delivery timing is based on expected inventory arrival and standard transit times. Updates will be provided if dates change.'
110          },
111          sameDay: {
112            esd: {
113              inStock: 'This item is currently in stock and estimated to ship on {date} with standard shipping. Ship timing is based on your delivery ZIP code. Customized items may require additional processing time.',
114              partnerFulfilled: 'This item ships from a partner facility and is estimated to ship on {date}. Processing and transit times may differ from warehouse items.',
115              backordered: 'Due to high demand, this item is currently backordered and estimated to ship on {date} once inventory becomes available. Ship timing may adjust based on availability. We will notify you of any changes.',
116              backorderDateOnly: 'Estimated to ship on {date}. Ship timing may adjust based on availability.',
117              preOrder: 'This item is available for pre-order and estimated to ship on {date}. Ship timing is based on expected inventory arrival and may change. You will be notified of any updates.'
118            },
119            edd: {
120              inStock: 'This item is in stock and estimated to arrive on {date} with standard shipping. Delivery timing is based on your ZIP code and carrier transit times. Customized items may require additional processing time.',
121              partnerFulfilled: 'This item ships from a partner facility and is estimated to arrive on {date}. Delivery timing may vary from warehouse-shipped items.',
122              backordered: 'Due to high demand, this item is currently backordered and estimated to arrive on {date} once inventory is available. Delivery timing may adjust if availability changes.',
123              backorderDateOnly: 'Estimated to arrive on {date}. Delivery timing may adjust based on availability.',
124              preOrder: 'This item is available for pre-order and estimated to arrive on {date}. Delivery timing is based on expected inventory arrival and standard transit times. Updates will be provided if dates change.'
125            }
126          }
127        }
128      },
129      abTestVariations: {
130        galls: {
131          stage: {
132            experimentId: 29279,
133            eddVariationId: 43901,
134            esdVariationId: 48269
135          },
136          prod: {
137            experimentId: 33283,
138            eddVariationId: 49866,
139            esdVariationId: 666
140          }
141        },
142        usp: {
143          stage: {
144            experimentId: 32405,
145            eddVariationId: 48565,
146            esdVariationId: 48566
147          },
148          prod: {
149            experimentId: 32406,
150            eddVariationId: 48567,
151            esdVariationId: 48568
152          }
153        }
154      }
155    };
156    // Loaded from CSV files
157    this.fedexTransitData = null;
158    this.holidaysData = new Set(); // Set of date strings "YYYY-MM-DD" for O(1) lookup
159    this.userZip = null;
160  }
161
162  async init() {
163    // Load FedEx transit data
164    await this.loadTransitData();
165    
166    // Load holiday data if enabled
167    if (this.config.holidayConfig.enabled) {
168      await this.loadHolidayData();
169    }
170    
171    // Get and store user's zip code
172    this.userZip = this.getUserZipCode();
173    
174    this.displayPDPDeliveryEstimate();
175    
176    // Listen for location changes and recalculate
177    this.setupLocationChangeListener();
178  }
179
180  /** Set up listener for user location changes. Recalculates delivery estimate when user updates their zip code */
181  setupLocationChangeListener() {
182    // Listen for storage events (works across tabs)
183    window.addEventListener('storage', (e) => {
184      if (e.key === 'userLocationData') {
185        this.userZip = this.getUserZipCode();
186        this.displayPDPDeliveryEstimate();
187      }
188    });
189    
190    // Also listen for custom event (same-tab updates)
191    window.addEventListener('userLocationUpdated', (e) => {
192      this.userZip = this.getUserZipCode();
193
194      // ESD mode: ship dates don't depend on user zip — skip date recalculation
195      const variationMode = window.edd?.getABVariationMode?.() || 'control';
196      if (variationMode === 'esd') return;
197
198      this.displayPDPDeliveryEstimate();
199      
200      // Also update the dynamically injected EDD UI if it exists
201      this.updateDynamicEDDUI();
202    });
203  }
204
205  /** Update dynamically injected EDD UI (from stock-status.js) when location changes. Skipped in ESD mode since ship dates don't depend on user zip. */
206  updateDynamicEDDUI() {
207    // ESD mode: ship dates don't depend on user zip — skip recalculation
208    const variationMode = window.edd?.getABVariationMode?.() || 'control';
209    if (variationMode === 'esd') return;
210
211    const eddDateEl = document.querySelector('.pdp-edd [data-target="eddDeliveryDate"]');
212    const eddZipEl = document.querySelector('.pdp-edd [data-target="locationTrackerText"]');
213    
214    if (!eddDateEl) {
215      return;
216    }
217    
218    // Get new user zip
219    const userZip = this.getUserZipCode();
220    
221    if (!userZip) return;
222    
223    // Get ESD from cached delivery estimate (not DOM - SIESDT field doesn't exist on page)
224    let esdDate = null;
225    try {
226      const cached = localStorage.getItem('currentDeliveryEstimate');
227      if (cached) {
228        const parsedCache = JSON.parse(cached);
229        if (parsedCache.data && parsedCache.data.esdDate) {
230          // esdDate was stored as ISO string, convert back to Date
231          esdDate = new Date(parsedCache.data.esdDate);
232        }
233      }
234    } catch (e) {}
235    
236    if (!esdDate || isNaN(esdDate.getTime())) {
237      return;
238    }
239    
240    const transitDays = this.getTransitDays(userZip);
241    
242    if (transitDays === null) return;
243    
244    const deliveryInfo = this.calculateDeliveryDates(esdDate, transitDays);
245    
246    // Update the date display (preserve <strong> wrapper for bold styling)
247    eddDateEl.innerHTML = `<strong>${deliveryInfo.rangeFormatted}</strong>`;
248    
249    // Update the zip display
250    if (eddZipEl) {
251      eddZipEl.textContent = userZip;
252    }
253    
254    // Also update cache
255    this.cacheDeliveryEstimate(deliveryInfo);
256  }
257
258  /** @returns {string|null} Product SKU */
259  getCurrentProductSku() {
260    const skuElement = document.querySelector('#style[name="SISTYL"]');
261    return skuElement ? skuElement.value : null;
262  }
263
264  /** @returns {string|null} User's zip code */
265  getUserZipCode() {
266    try {
267      const locationData = localStorage.getItem('userLocationData');
268      if (locationData) {
269        const location = JSON.parse(locationData);
270        return location.zip || null;
271      }
272    } catch (error) {
273      // Silent fail
274    }
275    return null;
276  }
277
278  /** @returns {string} User's country code (defaults to USA) */
279  getUserCountry() {
280    try {
281      const locationData = localStorage.getItem('userLocationData');
282      if (locationData) {
283        const location = JSON.parse(locationData);
284        return location.country === 'US' ? 'USA' : location.country;
285      }
286    } catch (error) {
287      // Silent fail
288    }
289    return 'USA';
290  }
291
292  /** Get ESD (Est Ship Date) from backend hidden field @returns {Date|null} ESD as Date object or null if not available */
293  getESDDate() {
294    // Look for hidden field - try both IDs (backend may use either)
295    // <span id="SIESDT"><input type="hidden" name="SIESDT" value="02/16/2026" id="SIESDTd"></span>
296    const esdInput = document.querySelector('#SIESDTd') || document.querySelector('#SIESDT input[name="SIESDT"]') || document.querySelector('#SIESDT');
297    
298    if (!esdInput || !esdInput.value) {
299      return null;
300    }
301    
302    const esdValue = esdInput.value.trim();
303    if (!esdValue) return null;
304    
305    // Parse MM/DD/YYYY format
306    const parts = esdValue.split('/');
307    if (parts.length !== 3) return null;
308    
309    const month = parseInt(parts[0], 10) - 1; // JS months are 0-indexed
310    const day = parseInt(parts[1], 10);
311    const year = parseInt(parts[2], 10);
312    
313    if (isNaN(month) || isNaN(day) || isNaN(year)) return null;
314    
315    return new Date(year, month, day);
316  }
317
318  /** Load FedEx transit data. Delegates to window.edd.ensureTransitDataLoaded() (single source of truth) and syncs the parsed data back to this.fedexTransitData for EDDHandler methods. */
319  async loadTransitData() {
320    if (window.edd && typeof window.edd.ensureTransitDataLoaded === 'function') {
321      await window.edd.ensureTransitDataLoaded();
322      // Sync parsed data from the shared cache into EDDHandler memory
323      this.fedexTransitData = window.edd._transitDataCache || {};
324    }
325  }
326
327  /** Check if a date is a weekend (Saturday or Sunday) @param {Date} date @returns {boolean} */
328  isWeekend(date) {
329    const day = date.getDay();
330    return day === 0 || day === 6;
331  }
332
333  /** Check if a date is a FedEx holiday @param {Date} date @returns {boolean} */
334  isHoliday(date) {
335    // Only check if holiday logic is enabled
336    if (!this.config.holidayConfig.enabled) {
337      return false;
338    }
339    // Convert date to YYYY-MM-DD string for Set lookup
340    return this.holidaysData.has(this.formatDateKey(date));
341  }
342
343  /** Format a Date to YYYY-MM-DD string @param {Date} date @returns {string} */
344  formatDateKey(date) {
345    const year = date.getFullYear();
346    const month = String(date.getMonth() + 1).padStart(2, '0');
347    const day = String(date.getDate()).padStart(2, '0');
348    return `${year}-${month}-${day}`;
349  }
350
351  /** Load holiday data from CSV. Fetches and parses CSV, stores in memory and localStorage */
352  async loadHolidayData() {
353    const cacheKey = this.config.holidayConfig.key;
354    
355    // Check localStorage cache first
356    const cached = localStorage.getItem(cacheKey);
357    if (cached) {
358      try {
359        const parsedCache = JSON.parse(cached);
360        const now = Date.now();
361        
362        if (parsedCache.data && 
363            parsedCache.timestamp && 
364            (now - parsedCache.timestamp) < this.config.cache.duration) {
365          this.holidaysData = new Set(parsedCache.data);
366          return;
367        }
368      } catch (error) {
369        // Invalid cache, continue to fetch
370      }
371    }
372
373    // Fetch CSV file
374    try {
375      const response = await fetch(this.config.holidayConfig.csvPath);
376      
377      if (!response.ok) {
378        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
379      }
380      
381      const csvText = await response.text();
382      
383      // Parse CSV into Set of date strings
384      this.holidaysData = this.parseHolidayCSV(csvText);
385      
386      // Cache the parsed data (convert Set to Array for JSON)
387      localStorage.setItem(cacheKey, JSON.stringify({
388        data: Array.from(this.holidaysData),
389        timestamp: Date.now()
390      }));
391    } catch (error) {
392      this.holidaysData = new Set();
393    }
394  }
395
396  /** Parse holiday CSV into Set of date strings @param {string} csvText @returns {Set<string>} Set of "YYYY-MM-DD" date strings */
397  parseHolidayCSV(csvText) {
398    const lines = csvText.split(/\r?\n/);
399    const holidays = new Set();
400    
401    // Skip header row (index 0)
402    for (let i = 1; i < lines.length; i++) {
403      const line = lines[i].trim();
404      if (!line) continue;
405      
406      const columns = line.split(',');
407      if (columns.length < 3) continue;
408      
409      // Column indices: 0: Date (MM/DD/YYYY), 1: Holiday name, 2: FedEx (Y/N)
410      const dateStr = columns[0]?.trim();
411      const isFedExHoliday = columns[2]?.trim().toUpperCase() === 'Y';
412      
413      // Only include FedEx holidays
414      if (!isFedExHoliday) continue;
415      
416      // Parse MM/DD/YYYY and convert to YYYY-MM-DD for consistent lookup
417      const parts = dateStr.split('/');
418      if (parts.length !== 3) continue;
419      
420      const month = parts[0].padStart(2, '0');
421      const day = parts[1].padStart(2, '0');
422      const year = parts[2];
423      
424      if (year && month && day) {
425        const dateKey = `${year}-${month}-${day}`;
426        holidays.add(dateKey);
427        
428        // Federal observed date logic: if holiday falls on weekend, add observed weekday
429        const holidayDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
430        const dayOfWeek = holidayDate.getDay();
431        
432        if (dayOfWeek === 6) {
433          // Saturday → observed on preceding Friday
434          const observedDate = new Date(holidayDate);
435          observedDate.setDate(observedDate.getDate() - 1);
436          holidays.add(this.formatDateKey(observedDate));
437        } else if (dayOfWeek === 0) {
438          // Sunday → observed on following Monday
439          const observedDate = new Date(holidayDate);
440          observedDate.setDate(observedDate.getDate() + 1);
441          holidays.add(this.formatDateKey(observedDate));
442        }
443      }
444    }
445    
446    return holidays;
447  }
448
449  /** Get transit days for a destination zip code from FedEx CSV @param {string} zipCode @returns {number|null} Transit days or null if not found */
450  getTransitDays(zipCode) {
451    if (!zipCode) return null;
452    
453    // Clean zip code (remove hyphens, take first 5 digits)
454    const cleanZip = zipCode.replace(/\D/g, '').slice(0, 5);
455    
456    const transitDays = this.fedexTransitData?.[cleanZip] ?? null;
457    return transitDays;
458  }
459
460  /** Add business days to a date (excludes weekends and holidays) @param {Date} startDate @param {number} businessDays @returns {Date} */
461  addBusinessDays(startDate, businessDays) {
462    const result = new Date(startDate);
463    let daysAdded = 0;
464    
465    while (daysAdded < businessDays) {
466      result.setDate(result.getDate() + 1);
467      
468      // Skip weekends
469      if (!this.isWeekend(result) && !this.isHoliday(result)) {
470        daysAdded++;
471      }
472    }
473    
474    return result;
475  }
476
477  /** Calculate delivery date range based on ESD + FedEx transit @param {Date} esdDate - Est Ship Date from backend @param {number} transitDays - FedEx transit days @returns {object} Delivery date information with range */
478  calculateDeliveryDates(esdDate, transitDays) {
479    // Apply ship date cutoff (2PM EST, weekends, holidays)
480    const shipDate = window.edd?.calculateShipDate ? window.edd.calculateShipDate(esdDate) : esdDate;
481    // earliestDate = adjusted ship date + transit days
482    const earliestDelivery = this.addBusinessDays(shipDate, transitDays);
483    const bufferBusinessDays = this.config?.deliveryRules?.bufferBusinessDays || 1;
484    
485    // latestDate = earliestDate + 1 business day
486    const latestDelivery = this.addBusinessDays(earliestDelivery, bufferBusinessDays);
487    
488    // Format dates
489    const shortOptionsWithDay = { month: 'short', day: 'numeric' };
490    const longOptions = { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' };
491    
492    const earliestFormatted = earliestDelivery.toLocaleDateString('en-US', shortOptionsWithDay);
493    const latestFormatted = latestDelivery.toLocaleDateString('en-US', shortOptionsWithDay);
494    
495    // Range format: "Nov 23 - 24" (same month) or "Nov 30 - Dec 1" (different month)
496    let rangeFormatted;
497    if (earliestDelivery.getFullYear() === latestDelivery.getFullYear() && earliestDelivery.getMonth() === latestDelivery.getMonth()) {
498      const month = earliestDelivery.toLocaleDateString('en-US', { month: 'short' });
499      rangeFormatted = `${month} ${earliestDelivery.getDate()} - ${latestDelivery.getDate()}`;
500    } else {
501      rangeFormatted = `${earliestFormatted} - ${latestFormatted}`;
502    }
503    
504    return {
505      transitDays: transitDays,
506      esdDate: esdDate,
507      earliestDelivery: earliestDelivery,
508      latestDelivery: latestDelivery,
509      earliestFormatted: earliestDelivery.toLocaleDateString('en-US', longOptions),
510      latestFormatted: latestDelivery.toLocaleDateString('en-US', longOptions),
511      rangeFormatted: rangeFormatted,
512      orderDate: new Date(),
513      orderDateFormatted: new Date().toLocaleDateString('en-US', longOptions)
514    };
515  }
516
517  /** Main function to calculate and display delivery estimate (PDP ONLY) */
518  async displayPDPDeliveryEstimate() {
519    const zipCode = this.userZip;
520
521    // Need user zip code
522    if (!zipCode) {
523      return;
524    }
525
526    // Get ESD from backend token
527    const esdDate = this.getESDDate();
528    if (!esdDate) {
529      // No ESD available - display nothing
530      return;
531    }
532
533    // Get transit days from FedEx CSV
534    const transitDays = this.getTransitDays(zipCode);
535    if (transitDays === null) {
536      return;
537    }
538
539    // Calculate delivery dates
540    const deliveryInfo = this.calculateDeliveryDates(esdDate, transitDays);
541
542    // Update UI elements with delivery info
543    this.updateDeliveryUI(deliveryInfo);
544  }
545
546  /** Recalculate delivery estimate. Call this when location changes */
547  recalculateDeliveryEstimate() {
548    this.displayPDPDeliveryEstimate();
549  }
550
551  /** Cache delivery estimate data for use by other modules @param {object} deliveryInfo */
552  cacheDeliveryEstimate(deliveryInfo) {
553    try {
554      const cacheObject = {
555        data: deliveryInfo,
556        timestamp: Date.now()
557      };
558      localStorage.setItem('currentDeliveryEstimate', JSON.stringify(cacheObject));
559    } catch (error) {
560      // Silent fail
561    }
562  }
563
564  /** Update UI with delivery information. Dispatches custom event for other modules @param {object} deliveryInfo */
565  updateDeliveryUI(deliveryInfo) {
566    this.cacheDeliveryEstimate(deliveryInfo);
567    
568    // Dispatch custom event for modules that need immediate notification
569    window.dispatchEvent(new CustomEvent('deliveryEstimateReady', {
570      detail: deliveryInfo
571    }));
572    
573    // Update PDP EDD UI if it exists on page
574    this.updatePDPDeliveryDate(deliveryInfo);
575  }
576  
577  /** Update the delivery date in the PDP EDD UI @param {object} deliveryInfo */
578  updatePDPDeliveryDate(deliveryInfo) {
579    const eddStatusText = document.querySelector('[data-target="eddDeliveryDate"]');
580    if (eddStatusText) {
581      eddStatusText.innerHTML = `<strong>${deliveryInfo.rangeFormatted}</strong>`;
582    }
583  }
584
585  /** Detect if cart item is in stock @param {string} stockMsg @returns {boolean} */
586  detectItemInStock(stockMsg) {
587    return stockMsg && stockMsg.toLowerCase().includes('in stock');
588  }
589
590  /** Calculate delivery estimate for a cart item @param {object} baseDelivery - Base delivery data @param {boolean} isInStock @returns {object|null} */
591  calculateItemDelivery(baseDelivery, isInStock) {
592    if (!baseDelivery || !isInStock) {
593      return null;
594    }
595
596    const earliestDeliveryDate = baseDelivery.earliestDelivery instanceof Date
597      ? baseDelivery.earliestDelivery
598      : new Date(baseDelivery.earliestDelivery);
599    const latestDeliveryDate = baseDelivery.latestDelivery instanceof Date
600      ? baseDelivery.latestDelivery
601      : new Date(baseDelivery.latestDelivery);
602
603    if (isNaN(earliestDeliveryDate.getTime()) || isNaN(latestDeliveryDate.getTime())) {
604      return null;
605    }
606    
607    return {
608      rangeFormatted: baseDelivery.rangeFormatted,
609      earliestDate: earliestDeliveryDate.toISOString().split('T')[0],
610      latestDate: latestDeliveryDate.toISOString().split('T')[0]
611    };
612  }
613}
614
615/**
616 * EDD (Estimated Delivery Date) - Namespace for delivery estimate functionality
617 * Exposed as window.edd
618 * Supports: PDP, Quickview, Cart, Minicart
619 */
620
621/**
622 * Centralized stock status configuration - shared between est-delivery-date.js and stock-status.js
623 * @type {{inStock: {label: string, color: string}, partnerFulfilled: {label: string, color: string}, backordered: {label: string, color: string}, preOrder: {label: string, color: string}}}
624 */
625const eddStockStatusConfig = (new EDDHandler()).config || {};
626const eddStockMsgDisplayConfig = eddStockStatusConfig.stockMsgDisplay || {};
627const eddStockStatusLabels = (eddStockMsgDisplayConfig.stockMsgText || {})?.block?.edd || {};
628
629/** Read a CSS custom property from :root. @param {string} varName @param {string} [fallback] @returns {string} */
630const getEDDCSSColor = (varName, fallback = '') => {
631  try { return getComputedStyle(document.documentElement).getPropertyValue(varName).trim() || fallback; }
632  catch (e) { return fallback; }
633};
634
635window.stockStatusConfig = {
636  inStock: { label: eddStockStatusLabels.inStock, color: getEDDCSSColor('--edd-status-in-stock', '#2E7D32') },
637  partnerFulfilled: { label: eddStockStatusLabels.partnerFulfilled, color: getEDDCSSColor('--edd-status-partner-fulfilled', '#2E7D32') },
638  backordered: { label: eddStockStatusLabels.backordered, color: getEDDCSSColor('--edd-status-backordered', '#F4B400') },
639  backorderDateOnly: { label: '', color: getEDDCSSColor('--edd-status-in-stock', '#2E7D32') },
640  preOrder: { label: eddStockStatusLabels.preOrder, color: getEDDCSSColor('--edd-status-pre-order', '#5DADE2') }
641};
642
643// Hide Backorder -- apply body class at script parse time (before DOM renders)
644// A/B test concluded on both brands. Galls: was exp 35803 / variant 53625. USP: was exp 36663 / variant 54905.
645(function() {
646      document.documentElement.classList.add('hide-backorder-msg');
647})();
648
649// Integration contract (stock-status.js):
650// 1) This file loads first and defines window.edd before DOMContentLoaded.
651// 2) Checkout/cart wrappers emitted here must use: data-stock-info-trigger + data-tip + data-original-stock-text.
652// 3) Avoid relying on parent inline color; stock-status.js and CSS handle style normalization.
653window.edd = {
654  config: {},
655  
656  // ===== CART API WRAPPER =====
657  
658  /** Fetch cart items from API @returns {Promise<Array>} */
659  fetchCartItems: async function() {
660    try {
661      const response = await fetch('/CGBCCART?CARTDRAWER=Y', {
662        method: 'GET',
663        headers: { 'Content-Type': 'text/html' }
664      });
665      
666      if (!response.ok) {
667        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
668      }
669      
670      const data = await response.text();
671      const parsedData = JSON.parse(data);
672      if (Array.isArray(parsedData) && typeof this.setCartPayloadContext === 'function') {
673        this.setCartPayloadContext(parsedData);
674      }
675      return parsedData;
676    } catch (error) {
677      return [];
678    }
679  },
680  
681  // ===== STATE MANAGEMENT =====
682  
683  /** Generate hash from cart items to detect changes @param {Array} cartItems @returns {string} */
684  generateCartHash: function(cartItems) {
685    if (!cartItems || !Array.isArray(cartItems)) return '';
686    
687    const itemKeys = this.getCartPayloadItems(cartItems)
688      .map(item => this.getCartPayloadCdno(item))
689      .filter(key => key !== '')
690      .sort()
691      .join('|');
692    
693    return itemKeys;
694  },
695
696  /** Get cdno/product key from a payload row. @param {object|Array} item @returns {string} */
697  getCartPayloadCdno: function(item) {
698    if (!item) return '';
699    if (Array.isArray(item)) {
700      return (item[14] || item[7] || '').toString().trim();
701    }
702    return (item.cdno || '').toString().trim();
703  },
704
705  /** Normalize payload and remove trailing TO_QUALIFY row when present. @param {Array} cartItems @returns {Array} */
706  getCartPayloadItems: function(cartItems) {
707    if (!Array.isArray(cartItems) || !cartItems.length) return [];
708
709    const items = cartItems.slice();
710    const lastItem = items[items.length - 1];
711    const lastCdno = this.getCartPayloadCdno(lastItem);
712
713    if (lastCdno === 'TO_QUALIFY') {
714      items.pop();
715    }
716
717    return items;
718  },
719
720  /** Extract ESD date token from stock message text. @param {string} stockMsg @returns {string} */
721  extractESDFromStockMessage: function(stockMsg) {
722    if (!stockMsg) return '';
723    const match = stockMsg.match(/ESD:\s*(\d{1,2}\/\d{1,2}\/\d{4})/i);
724    return match && match[1] ? match[1] : '';
725  },
726
727  /** Extract tooltip text from stock message markup (data-tip first, then title). @param {string} stockMsg @param {string} [fallback] @returns {string} */
728  extractTooltipTextFromMarkup: function(stockMsg, fallback) {
729    if (!fallback) fallback = this.config?.stockMsgDisplay?.defaultTooltip || 'Customized items may take longer to ship.';
730    if (!stockMsg || typeof stockMsg !== 'string') return fallback;
731    const dataTipMatch = stockMsg.match(/data-tip="([^"]*)"/i);
732    if (dataTipMatch && dataTipMatch[1]) return dataTipMatch[1];
733    const titleMatch = stockMsg.match(/title="([^"]*)"/i);
734    if (titleMatch && titleMatch[1]) return titleMatch[1];
735    return fallback;
736  },
737
738  /** Parse MM/DD/YYYY into Date. @param {string} esdValue @returns {Date|null} */
739  parseESDDateValue: function(esdValue) {
740    if (!esdValue) return null;
741    const parts = esdValue.split('/');
742    if (parts.length !== 3) return null;
743
744    const month = parseInt(parts[0], 10) - 1;
745    const day = parseInt(parts[1], 10);
746    const year = parseInt(parts[2], 10);
747
748    if (isNaN(month) || isNaN(day) || isNaN(year)) return null;
749    return new Date(year, month, day);
750  },
751
752  /** Get ship range info from ESD value. @param {string} esdValue @returns {{rangeEndDate: Date}|null} */
753  getShipRangeInfoFromESD: function(esdValue) {
754    const esdDate = this.parseESDDateValue(esdValue);
755    if (!esdDate) return null;
756    const shipDate = this.calculateShipDate(esdDate);
757    const bufferBusinessDays = this.config?.deliveryRules?.bufferBusinessDays || 1;
758    const rangeEndDate = this.addBusinessDaysSimple(shipDate, bufferBusinessDays);
759    return {
760      rangeEndDate: rangeEndDate
761    };
762  },
763
764  /** Eligible original in-stock rule: only pure in-stock items aggregate. @param {string} stockMsg @returns {boolean} */
765  isEligibleOriginalInStockStatus: function(stockMsg) {
766    const statusType = this.detectStockStatus(stockMsg);
767    return statusType === 'inStock';
768  },
769
770  /** Build and cache aggregate in-stock dates from API payload. @param {Array} cartItems @returns {object|null} */
771  setCartPayloadContext: function(cartItems) {
772    if (!Array.isArray(cartItems)) return null;
773
774    const parsedItems = this.getCartPayloadItems(cartItems)
775      .map((item) => {
776        if (!item) return null;
777
778        if (Array.isArray(item)) {
779          return {
780            stockMsg: (item[3] || '').toString(),
781            esdValue: (item[17] || item[16] || item[18] || '').toString().trim()
782          };
783        }
784
785        return {
786          stockMsg: (item.stock_msg || '').toString(),
787          esdValue: (item.esd || item.esdt || item.siesdt || '').toString().trim()
788        };
789      })
790      .filter(Boolean);
791
792    let latestESDAggregate = null;
793    let latestEDDAggregate = null;
794
795    parsedItems.forEach((item) => {
796      if (!item.stockMsg) return;
797      if (!this.isEligibleOriginalInStockStatus(item.stockMsg)) return;
798
799      const esdValue = item.esdValue || this.extractESDFromStockMessage(item.stockMsg);
800      if (!esdValue) return;
801
802      const shipRangeInfo = this.getShipRangeInfoFromESD(esdValue);
803      if (shipRangeInfo && (!latestESDAggregate || shipRangeInfo.rangeEndDate > latestESDAggregate.sortDate)) {
804        latestESDAggregate = {
805          esdValue: esdValue,
806          sortDate: shipRangeInfo.rangeEndDate
807        };
808      }
809
810      const deliveryInfo = this.calculateEDDFromESD(esdValue);
811      if (deliveryInfo && deliveryInfo.latestDelivery && (!latestEDDAggregate || deliveryInfo.latestDelivery > latestEDDAggregate.sortDate)) {
812        latestEDDAggregate = {
813          rangeFormatted: deliveryInfo.rangeFormatted,
814          sortDate: deliveryInfo.latestDelivery
815        };
816      }
817    });
818
819    this._cartPayloadContext = {
820      timestamp: Date.now(),
821      esdAggregate: latestESDAggregate,
822      eddAggregate: latestEDDAggregate
823    };
824
825    return this._cartPayloadContext;
826  },
827
828  /** Get aggregated in-stock date info from latest payload context. @param {'esd'|'edd'} mode @returns {object|null} */
829  getCartPayloadAggregate: function(mode) {
830    if (!this._cartPayloadContext) return null;
831    if (mode === 'esd') return this._cartPayloadContext.esdAggregate || null;
832    if (mode === 'edd') return this._cartPayloadContext.eddAggregate || null;
833    return null;
834  },
835  
836  /** Get cart state from sessionStorage @returns {object|null} */
837  getCartState: function() {
838    try {
839      const stateData = sessionStorage.getItem('eddCartState');
840      if (!stateData) return null;
841      
842      const state = JSON.parse(stateData);
843      
844      // Check if state is too old (1 hour max)
845      const now = Date.now();
846      if (state.timestamp && (now - state.timestamp) > 3600000) {
847        sessionStorage.removeItem('eddCartState');
848        return null;
849      }
850      
851      return state;
852    } catch (error) {
853      sessionStorage.removeItem('eddCartState');
854      return null;
855    }
856  },
857  
858  /** Save cart state to sessionStorage @param {object} state */
859  saveCartState: function(state) {
860    try {
861      state.timestamp = Date.now();
862      sessionStorage.setItem('eddCartState', JSON.stringify(state));
863    } catch (error) {
864      // Silent fail
865    }
866  },
867  
868  /** Check if cart has changed @param {Array} currentCartItems @returns {boolean} */
869  validateCartState: function(currentCartItems) {
870    const currentHash = this.generateCartHash(currentCartItems);
871    const savedState = this.getCartState();
872    
873    if (!savedState || savedState.cartHash !== currentHash) {
874      sessionStorage.removeItem('eddCartState');
875      return true;
876    }
877    
878    return false;
879  },
880  
881  /** Initialize cart state from cart items @param {Array} cartItems @returns {object} */
882  initializeCartState: function(cartItems) {
883    const state = {
884      sessionId: Date.now().toString(36) + Math.random().toString(36),
885      timestamp: Date.now(),
886      userZip: this.getUserZipCode(),
887      items: {},
888      cartHash: this.generateCartHash(cartItems)
889    };
890    
891    const baseDelivery = this.getBaseDeliveryData();
892    if (!baseDelivery) {
893      return state;
894    }
895    
896    // Ensure EDDHandler instance exists
897    if (!window.eddHandler) {
898      window.eddHandler = new EDDHandler();
899    }
900    
901    // Process each valid cart item
902    this.getCartPayloadItems(cartItems)
903      .forEach((item) => {
904      const cdno = this.getCartPayloadCdno(item);
905      if (!cdno) return;
906
907      const stockMsg = Array.isArray(item)
908        ? (item[3] || '').toString()
909        : (item?.stock_msg || '').toString();
910      
911      const isInStock = window.eddHandler.detectItemInStock(stockMsg);
912      const deliveryEstimate = window.eddHandler.calculateItemDelivery(baseDelivery, isInStock);
913      
914      state.items[cdno] = {
915        isInStock: isInStock,
916        deliveryEstimate: deliveryEstimate
917      };
918    });
919    
920    return state;
921  },
922  
923  // ===== STOCK STATUS HELPERS =====
924
925  /** Detect stock status type from message @param {string} stockMsg @returns {'inStock'|'partnerFulfilled'|'backordered'|'preOrder'} */
926  detectStockStatus: function(stockMsg) {
927    if (!stockMsg) return 'inStock';
928    const lower = stockMsg.toLowerCase();
929    
930    if (lower.includes('pre-order') || lower.includes('preorder') || lower.includes('available for pre-order')) {
931      return 'preOrder';
932    }
933    if (lower.includes('backorder') || lower.includes('due to high demand') || lower.includes('extended')) {
934      if (this.shouldShowBackorderDateOnly()) return 'backorderDateOnly';
935      return 'backordered';
936    }
937    if (lower.includes('partner')) {
938      return 'partnerFulfilled';
939    }
940    return 'inStock';
941  },
942
943  /** Check if user is in any "Hide Backorder" A/B test variation @returns {boolean} */
944  isHideBackorderVariation: function() {
945    var cl = document.documentElement.classList;
946    return cl.contains('hide-backorder-msg') || cl.contains('hide-backorder-date-only');
947  },
948
949  shouldShowBackorderDateOnly: function() {
950    return document.documentElement.classList.contains('hide-backorder-date-only');
951  },
952
953  shouldHideBackorderEntirely: function() {
954    return document.documentElement.classList.contains('hide-backorder-msg');
955  },
956
957  isGiftCard: function() {
958    const styleInput = document.querySelector('input#style[value="GIFT"]');
959    return !!styleInput;
960  },
961
962  isGiftCardItem: function(sku, title) {
963    if (sku && (sku.toUpperCase() === 'GIFT' || sku.toUpperCase().startsWith('GIFT '))) return true;
964    if (title && title.toLowerCase().includes('gift card')) return true;
965    return false;
966  },
967
968  /** Check if stock message should be enhanced by EDD/ESD UI. @param {string} stockMsg @returns {boolean} */
969  isEnhanceableStockMessage: function(stockMsg) {
970    if (!stockMsg || typeof stockMsg !== 'string') return false;
971    const statusType = this.detectStockStatus(stockMsg);
972    const statusConfig = window.stockStatusConfig || {};
973
974    if (statusType !== 'inStock') {
975      return !!statusConfig[statusType];
976    }
977
978    return !!statusConfig.inStock && stockMsg.toLowerCase().includes('in stock');
979  },
980
981  /** Extract displayed action/date from existing stock message markup. @param {string} stockMsg @returns {{actionText: string, dateText: string}|null} */
982  extractDisplayedActionDate: function(stockMsg) {
983    if (!stockMsg || typeof stockMsg !== 'string') return null;
984    const plainText = stockMsg
985      .replace(/<[^>]*>/g, ' ')
986      .replace(/\s+/g, ' ')
987      .trim();
988
989    const actionDateMatch = plainText.match(/\b(?:Est\.\s*to\s*(arrive|ship)|(Arrives|Ships))\s+([A-Za-z]{3}\s+\d{1,2}(?:\s*-\s*(?:[A-Za-z]{3}\s+)?\d{1,2})?)/i);
990    if (!actionDateMatch) return null;
991
992    const actionToken = (actionDateMatch[1] || actionDateMatch[2] || '').toLowerCase();
993
994    return {
995      actionText: actionToken.includes('arrive') ? 'Arrives' : 'Ships',
996      dateText: actionDateMatch[3].replace(/\s*-\s*/g, ' - ').trim()
997    };
998  },
999
1000  /** Normalize date to midnight for date-only comparison. @param {Date} date @returns {Date} */
1001  normalizeToDateOnlyMidnight: function(date) {
1002    const normalized = new Date(date);
1003    normalized.setHours(0, 0, 0, 0);
1004    return normalized;
1005  },
1006
1007  /** Check whether a date is before today in EST calendar time. @param {Date|null} date @returns {boolean} */
1008  isDatePastToday: function(date) {
1009    if (!(date instanceof Date) || isNaN(date.getTime())) return false;
1010    const target = this.normalizeToDateOnlyMidnight(date);
1011    const todayEST = this.getTodayEST();
1012    const today = this.normalizeToDateOnlyMidnight(new Date(
1013      todayEST.getFullYear(),
1014      todayEST.getMonth(),
1015      todayEST.getDate()
1016    ));
1017    return target < today;
1018  },
1019
1020  /** Parse display date text into start and end Date objects. Handles single dates ("Feb 12") and ranges ("Feb 12 - 13", "Feb 12 - Mar 1"). @param {string} dateText @returns {{ start: Date|null, end: Date|null }} */
1021  parseDisplayDateRange: function(dateText) {
1022    if (!dateText || typeof dateText !== 'string') return { start: null, end: null };
1023
1024    const normalizedText = dateText.replace(/[–—]/g, '-').replace(/\s+/g, ' ').trim();
1025    if (!normalizedText) return { start: null, end: null };
1026
1027    const monthMap = {
1028      jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5,
1029      jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11
1030    };
1031
1032    const currentYear = this.getTodayEST().getFullYear();
1033
1034    const parseMonthDay = (input, fallbackYear = currentYear) => {
1035      const match = input.match(/([A-Za-z]{3,9})\s+(\d{1,2})(?:,\s*(\d{4}))?/);
1036      if (!match) return null;
1037      const month = monthMap[match[1].slice(0, 3).toLowerCase()];
1038      const day = parseInt(match[2], 10);
1039      const year = match[3] ? parseInt(match[3], 10) : fallbackYear;
1040      if (month === undefined || isNaN(day) || isNaN(year)) return null;
1041      return new Date(year, month, day);
1042    };
1043
1044    if (!normalizedText.includes('-')) {
1045      const single = parseMonthDay(normalizedText, currentYear);
1046      return { start: single, end: single };
1047    }
1048
1049    const parts = normalizedText.split('-');
1050    const leftText = (parts[0] || '').trim();
1051    const rightText = (parts.slice(1).join('-') || '').trim();
1052
1053    const leftDate = parseMonthDay(leftText, currentYear);
1054    if (!leftDate) return { start: null, end: null };
1055
1056    let rightDate = parseMonthDay(rightText, leftDate.getFullYear());
1057
1058    if (!rightDate) {
1059      const rightDayMatch = rightText.match(/^(\d{1,2})(?:,\s*(\d{4}))?$/);
1060      if (!rightDayMatch) return { start: leftDate, end: leftDate };
1061      const day = parseInt(rightDayMatch[1], 10);
1062      const year = rightDayMatch[2] ? parseInt(rightDayMatch[2], 10) : leftDate.getFullYear();
1063      if (isNaN(day) || isNaN(year)) return { start: leftDate, end: leftDate };
1064      rightDate = new Date(year, leftDate.getMonth(), day);
1065    }
1066
1067    if (rightDate < leftDate && !/,\s*\d{4}\s*$/.test(rightText)) {
1068      rightDate.setFullYear(rightDate.getFullYear() + 1);
1069    }
1070
1071    return { start: leftDate, end: rightDate };
1072  },
1073
1074  /** Parse display date text and return the ending date. Thin wrapper over parseDisplayDateRange. @param {string} dateText @returns {Date|null} */
1075  parseDisplayDateTextEndDate: function(dateText) {
1076    return this.parseDisplayDateRange(dateText).end;
1077  },
1078
1079  /** Check whether a display date text resolves to a past date. @param {string} dateText @returns {boolean} */
1080  isDisplayDateTextPastToday: function(dateText) {
1081    const endDate = this.parseDisplayDateTextEndDate(dateText);
1082    return this.isDatePastToday(endDate);
1083  },
1084
1085  /** Check whether an ESD value (MM/DD/YYYY) is in the past. @param {string} esdValue @returns {boolean} */
1086  isESDValuePastToday: function(esdValue) {
1087    const esdDate = this.parseESDDateValue(esdValue);
1088    return this.isDatePastToday(esdDate);
1089  },
1090
1091  /** Get configured label text for stock row. @param {'inStock'|'partnerFulfilled'|'backordered'|'preOrder'} statusType @param {'inline'|'block'} displayMode @param {'esd'|'edd'} variationMode @returns {string} */
1092  getEnhancedStatusLabel: function(statusType, displayMode = 'inline', variationMode = 'edd') {
1093    const stockMsgText = this.config?.stockMsgDisplay?.stockMsgText || {};
1094    const labelsByMode = stockMsgText[displayMode] || {};
1095    const labels = labelsByMode[variationMode] || labelsByMode.edd || labelsByMode.esd || {};
1096    return labels[statusType] || window.stockStatusConfig?.[statusType]?.label || window.stockStatusConfig?.inStock?.label || '';
1097  },
1098
1099  /** CSS variable map: statusType → pill color CSS var name */
1100  _pillColorVars: { inStock: '--edd-pill-in-stock', partnerFulfilled: '--edd-pill-partner-fulfilled', backordered: '--edd-pill-backordered', backorderDateOnly: '--edd-pill-in-stock', preOrder: '--edd-pill-pre-order' },
1101  /** CSS variable map: statusType → info icon color CSS var name */
1102  _iconColorVars: { inStock: '--edd-icon-in-stock', partnerFulfilled: '--edd-icon-partner-fulfilled', backordered: '--edd-icon-backordered', backorderDateOnly: '--edd-icon-in-stock', preOrder: '--edd-icon-pre-order' },
1103
1104  /** Get EDD/ESD pill color by status type (reads from CSS custom property). @param {'inStock'|'partnerFulfilled'|'backordered'|'preOrder'} statusType @returns {string} */
1105  getVariationPillColor: function(statusType) {
1106    const varName = this._pillColorVars[statusType] || this._pillColorVars.inStock;
1107    return getEDDCSSColor(varName);
1108  },
1109
1110  /** Get EDD/ESD info icon color by status type (reads from CSS custom property). @param {'inStock'|'partnerFulfilled'|'backordered'|'preOrder'} statusType @returns {string} */
1111  getVariationInfoIconColor: function(statusType) {
1112    const varName = this._iconColorVars[statusType] || this._iconColorVars.inStock;
1113    return getEDDCSSColor(varName);
1114  },
1115
1116  /** Convert action verb into A/B display copy. @param {string} actionText @returns {string} */
1117  getEstimatedActionText: function(actionText) {
1118    if (!actionText) return '';
1119    const lower = actionText.toLowerCase();
1120    if (lower.includes('est. to')) return actionText;
1121    if (lower.includes('arrive')) return 'Est. to arrive';
1122    if (lower.includes('ship')) return 'Est. to ship';
1123    return actionText;
1124  },
1125
1126  /** Build enhanced stock message row. @param {object} stockInfo @param {string} statusLabel @param {string} actionText @param {string} dateText @param {string} tooltipText @param {string} [wrapperClass] @param {Date|null} [drawerStartDate] - Optional start date for config-driven drawer message @param {Date|null} [drawerEndDate] - Optional end date for config-driven drawer message @returns {string} */
1127  buildEnhancedStockMessage: function(stockInfo, statusLabel, actionText, dateText, tooltipText, wrapperClass = 'stock-message__wrapper stock-message__wrapper--edd', drawerStartDate = null, drawerEndDate = null) {
1128    const hasDate = !!(actionText && dateText);
1129    // Drawer message: EDD/ESD always use config templates, control uses backend tooltip
1130    const variationMode = this.getABVariationMode();
1131    const resolvedTip = (variationMode === 'edd' || variationMode === 'esd')
1132      ? this.getDrawerMessage(stockInfo.type, variationMode, drawerStartDate, drawerEndDate)
1133      : tooltipText;
1134    return `<span class="${wrapperClass}" data-stock-type="${stockInfo.type}" data-stock-info-trigger data-tip="${resolvedTip}" tabindex="0" role="button" aria-label="Stock information">
1135      <span class="stock-message__dot">${this.getColoredDotSVG()}</span>
1136      <span class="stock-message__status">${statusLabel}</span>
1137      ${hasDate ? `<span class="stock-message__sep">\u2022</span>
1138      <span class="stock-message__text">${this.getEstimatedActionText(actionText)} <strong>${dateText}</strong> <span class="stock-message__info-icon" aria-hidden="true">${this.getInfoIconSVG('inline')}</span></span>` :
1139      `<span class="stock-message__info-icon" aria-hidden="true">${this.getInfoIconSVG('inline')}</span>`}
1140    </span>`;
1141  },
1142
1143  /** Build variation-styled pill without dates (used when dates are past or unavailable). @param {string} stockMsg - Original stock message text for status detection @param {string} [wrapperClass] @returns {string} */
1144  buildDatelessVariationPill: function(stockMsg, wrapperClass = 'stock-message__wrapper stock-message__wrapper--edd') {
1145    const stockInfo = this.getStockStatusInfo(stockMsg);
1146    if (stockInfo.type === 'backorderDateOnly') return '';
1147    const variationMode = this.getABVariationMode();
1148    const statusLabel = this.getEnhancedStatusLabel(stockInfo.type, 'inline', variationMode);
1149    return this.buildEnhancedStockMessage(stockInfo, statusLabel, '', '', '', wrapperClass);
1150  },
1151
1152  /** Shared PDP block-format pill. Omits date row when actionText/dateText are empty. Includes location tracker for EDD variation. @param {string} originalStockMsg @param {string} [actionText] @param {string} [dateText] @param {Date} [drawerStartDate] @param {Date} [drawerEndDate] @returns {string} */
1153  buildPDPBlockPill: function(originalStockMsg, actionText = '', dateText = '', drawerStartDate = null, drawerEndDate = null) {
1154    const hasDate = !!(actionText && dateText);
1155    const variationMode = this.getABVariationMode();
1156    const statusType = this.detectStockStatus(originalStockMsg);
1157    if (statusType === 'backorderDateOnly' && !hasDate) return '<div style="display:none"></div>';
1158    const stockInfo = this.getStockStatusInfo(originalStockMsg);
1159    const statusLabel = this.getEnhancedStatusLabel(statusType, 'block', variationMode);
1160    const drawerTip = this.getDrawerMessage(statusType, variationMode, drawerStartDate, drawerEndDate);
1161    // EDD variation: include location tracker shipping section (arrival dates depend on user zip)
1162    let shippingSectionHTML = '';
1163    if (variationMode === 'edd') {
1164      const { zip: userZip, city: userCity, state: userState } = this.getUserLocationData();
1165      const cityStateMsg = (userCity && userState)
1166        ? `${userCity}, ${userState}`
1167        : 'Enter a zip to set your location';
1168      shippingSectionHTML = this.getEDDShippingSectionHTML(userZip, cityStateMsg);
1169    }
1170    return `
1171    <div class="b5-active pdp-edd" role="status" aria-live="polite">
1172      <div class="pdp__status-pill" data-stock-type="${stockInfo.type}" data-stock-info-trigger data-tip="${drawerTip}" tabindex="0" role="button" aria-label="Stock information: ${statusLabel}">
1173        <span class="pdp-edd__dot" aria-hidden="true">${this.getColoredDotSVG()}</span>
1174        <div class="pdp-edd__content">
1175          <div class="pdp-edd__row">
1176            <span class="pdp-edd__label">${statusLabel}</span>
1177            ${hasDate ? `<span class="pdp-edd__separator">\u2022</span>
1178            <span class="pdp-edd__action">${this.getEstimatedActionText(actionText)}</span>
1179            <span class="pdp-edd__date" data-target="eddDeliveryDate"><strong>${dateText}</strong></span>` : ''}
1180          </div>
1181          ${shippingSectionHTML}
1182        </div>
1183        <span class="pdp__status-pill-icon pdp-edd__info-icon" aria-hidden="true">${this.getInfoIconSVG('block')}</span>
1184      </div>
1185    </div>`;
1186  },
1187
1188  /** Get stock info icon SVG. @param {'block'|'inline'} sizeMode @returns {string} */
1189  getInfoIconSVG: function(sizeMode = 'inline') {
1190    const iconSize = sizeMode === 'block' ? 16 : 14;
1191    return `<svg xmlns="http://www.w3.org/2000/svg" width="${iconSize}" height="${iconSize}" viewBox="0 0 16 16" fill="none" focusable="false" aria-hidden="true">
1192      <path d="M8.00065 14.6667C11.6825 14.6667 14.6673 11.6819 14.6673 8.00004C14.6673 4.31814 11.6825 1.33337 8.00065 1.33337C4.31875 1.33337 1.33398 4.31814 1.33398 8.00004C1.33398 11.6819 4.3187
11925 14.6667 8.00065 14.6667Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
1193      <path d="M8 10.6667V8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
1194      <path d="M8 5.33337H8.00583" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
1195    </svg>`;
1196  },
1197
1198  /** Generate colored dot SVG. Uses currentColor so CSS controls the fill via parent color property. @param {string} [color] - Optional hex override (for non-variation contexts) @returns {string} SVG HTML */
1199  getColoredDotSVG: function(color) {
1200    const fill = color || 'currentColor';
1201    return `<svg class="pdp__status-pill-dot" width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
1202      <circle cx="4" cy="4" r="4" fill="${fill}"/>
1203    </svg>`;
1204  },
1205
1206  /** Get stock status label and color from centralized config @param {string} stockMsg @returns {{label: string, color: string, dotSVG: string}} */
1207  getStockStatusInfo: function(stockMsg) {
1208    const statusType = this.detectStockStatus(stockMsg);
1209    const config = window.stockStatusConfig[statusType];
1210    return {
1211      type: statusType,
1212      label: config.label,
1213      color: config.color,
1214      dotSVG: this.getColoredDotSVG(config.color)
1215    };
1216  },
1217
1218  // ===== PDP SHARED TEMPLATE HELPERS =====
1219
1220  /** Build the EDD shipping section HTML with location tracker popup. @param {string} userZip @param {string} cityStateMsg @returns {string} */
1221  getEDDShippingSectionHTML: function(userZip, cityStateMsg) {
1222    return `
1223      <div class="pdp-edd__shipping">
1224        <span class="pdp-edd__shipping-label">Standard shipping</span>
1225        <span class="pdp-edd__shipping-separator">\u2022</span>
1226        <div class="pdp-edd__location" data-target="locationTrackerTrigger">
1227          <span class="pdp-edd__shipping-zip" data-target="locationTrackerText" role="button" tabindex="0" aria-haspopup="dialog" aria-expanded="false" aria-label="Change delivery zip code">${userZip || '00000'}</span>
1228          <div class="location-tracker-popup position-absolute p-12 rounded-md shadow bg-light border" data-target="locationTrackerPopup" role="dialog" aria-modal="true" style="display: none;">
1229            <div class="d-flex flex-column gap-12">
1230              <div class="d-flex justify-content-between gap-6">
1231                <div class="d-flex flex-column">
1232                  <span class="fs-6 fw-bold text-normal roboto">Deliver to</span>
1233                  <p class="location-tracker-state-message roboto text-muted" data-target="locationTrackerLocationMsg">${cityStateMsg}</p>
1234                </div>
1235                <div class="location-tracker-close pointer" data-target="locationTrackerClosePopup" role="button" tabindex="0" aria-label="Close location tracker">
1236                  <svg class="feather-icon" style="width: 20px;height: 20px;" aria-hidden="true">
1237                    <use href="/images/icons/feather-sprite.svg#x"></use>
1238                  </svg>
1239                </div>
1240              </div>
1241              <div>
1242                <div class="location-tracker-input-container d-flex rounded overflow-hidden align-items-center">
1243                  <div class="position-relative w-100">
1244                    <input type="text" class="form-control border-0 shadow-none w-100" placeholder="${userZip || 'Enter zip code'}" aria-label="Enter zip code">
1245                    <div class="location-tracker-err-icon position-absolute" style="display:none" data-target="locationTrackerErrorIcon">
1246                      <svg class="feather-icon" style="width: 20px; height: 18px;" aria-hidden="true">
1247                        <use href="/images/icons/feather-sprite.svg#alert-triangle"></use>
1248                      </svg>
1249                    </div>
1250                  </div>
1251                  <button class="btn btn-primary" type="button" aria-label="Save zip code">
1252                    <p class="fs-6 text-capitalize fw-regular">Save</p>
1253                  </button>
1254                </div>
1255                <p class="location-tracker-error-message roboto pt-2" style="visibility: hidden;" data-target="locationTrackerErrorMsg" aria-live="polite"></p>
1256              </div>
1257            </div>
1258          </div>
1259        </div>
1260      </div>`;
1261  },
1262
1263  // ===== TRANSIT DATA LOADING =====
1264
1265  /** Ensure transit data is loaded — single source of truth for CSV fetch + parse + cache. Reads config from EDDHandler when available. Stores parsed {zip: transitDays} map in memory + localStorage. @returns {Promise<boolean>} */
1266  ensureTransitDataLoaded: async function() {
1267    const cfg = window.eddHandler?.config;
1268    const cacheKey = cfg?.carrierData?.fedex?.key || 'fedexTransitData';
1269    const cacheDuration = cfg?.cache?.duration || (1000 * 60 * 60 * 24 * 7);
1270    const csvPath = cfg?.carrierData?.fedex?.csvPath || '/assets/fulfillment/delivery/edd-csv-imports/fedex-transit-times-11-26-25.csv';
1271
1272    try {
1273      const cached = localStorage.getItem(cacheKey);
1274      if (cached) {
1275        const parsedCache = JSON.parse(cached);
1276        if (parsedCache.timestamp && (Date.now() - parsedCache.timestamp) < cacheDuration) {
1277          if (parsedCache.data) this._transitDataCache = parsedCache.data;
1278          return true;
1279        }
1280      }
1281
1282      const response = await fetch(csvPath);
1283      if (!response.ok) throw new Error('Failed to fetch CSV');
1284
1285      const csvText = await response.text();
1286      const lines = csvText.split(/\r?\n/);
1287      const data = {};
1288
1289      // Parse CSV (skip header row)
1290      // Columns: 0=Origin Zip, 1=Destination Zip, 2=State, 3=Zone, 4
1290=Ground Transit, 5=Population
1291      for (let i = 1; i < lines.length; i++) {
1292        const line = lines[i].trim();
1293        if (!line) continue;
1294        const columns = line.split(',');
1295        if (columns.length < 5) continue;
1296
1297        const destinationZip = columns[1]?.trim();
1298        const transitValue = columns[4]?.trim();
1299        const groundTransit = parseInt(transitValue, 10);
1300
1301        if (destinationZip && /^\d{5}$/.test(destinationZip) && !isNaN(groundTransit) && groundTransit >= 0) {
1302          data[destinationZip] = groundTransit;
1303        }
1304      }
1305
1306      this._transitDataCache = data;
1307      localStorage.setItem(cacheKey, JSON.stringify({ data, timestamp: Date.now() }));
1308      return true;
1309    } catch (error) {
1310      return false;
1311    }
1312  },
1313
1314  /** Get base delivery data from PDP calculation @returns {object|null} */
1315  getBaseDeliveryData: function() {
1316    try {
1317      const cached = localStorage.getItem('currentDeliveryEstimate');
1318      if (!cached) return null;
1319      
1320      const parsedCache = JSON.parse(cached);
1321      return parsedCache.data;
1322    } catch (error) {
1323      return null;
1324    }
1325  },
1326  
1327  /** Get user's zip code. On checkout, reads from SISZIP field; otherwise delegates to eddHandler. @returns {string|null} */
1328  getUserZipCode: function() {
1329    // On checkout page, read directly from delivery zip field
1330    const checkoutZipInput = document.getElementById('SISZIP');
1331    if (checkoutZipInput && checkoutZipInput.value) {
1332      const checkoutZip = checkoutZipInput.value.trim();
1333      if (/^\d{5}$/.test(checkoutZip)) {
1334        return checkoutZip;
1335      }
1336    }
1337    
1338    // Delegate to eddHandler for localStorage lookup
1339    if (window.eddHandler && window.eddHandler.getUserZipCode) {
1340      return window.eddHandler.getUserZipCode();
1341    }
1342    
1343    // Fallback if eddHandler not initialized
1344    try {
1345      const locationData = localStorage.getItem('userLocationData');
1346      if (locationData) {
1347        const location = JSON.parse(locationData);
1348        return location.zip || null;
1349      }
1350    } catch (error) {
1351      // Silent fail
1352    }
1353    return null;
1354  },
1355
1356  /** Get user location data (zip, city, state) from localStorage. @returns {{zip: string, city: string, state: string}} */
1357  getUserLocationData: function() {
1358    const result = { zip: '', city: '', state: '' };
1359    try {
1360      const locationData = localStorage.getItem('userLocationData');
1361      if (locationData) {
1362        const location = JSON.parse(locationData);
1363        result.zip = location.zip || '';
1364        result.city = location.city || '';
1365        result.state = location.state || '';
1366      }
1367    } catch (e) { /* silent */ }
1368    return result;
1369  },
1370
1371  /** Get FedEx transit days for a zip code. Tries eddHandler → localStorage cache in order. @param {string} zip - 5-digit zip code @param {boolean} [triggerLoad=true] - If true, triggers async CSV load when data unavailable @returns {number|null} Transit days or null if unavailable */
1372  getTransitDaysForZip: function(zip, triggerLoad = true) {
1373    if (!zip) return null;
1374    const cleanZip = zip.replace(/\D/g, '').slice(0, 5);
1375    if (!cleanZip) return null;
1376
1377    // Try eddHandler in-memory data first
1378    if (window.eddHandler && window.eddHandler.fedexTransitData) {
1379      const transit = window.eddHandler.fedexTransitData[cleanZip];
1380      if (transit !== null && transit !== undefined) return transit;
1381    }
1382
1383    // Try parsed cache (avoids re-parsing localStorage JSON on every call)
1384    if (this._transitDataCache) {
1385      const transit = this._transitDataCache[cleanZip];
1386      if (transit !== null && transit !== undefined) {
1387        return transit;
1388      }
1389    }
1390
1391    // Try localStorage cache — parse once, store in memory for subsequent lookups
1392    try {
1393      const cached = localStorage.getItem('fedexTransitData');
1394      if (cached) {
1395        const parsedCache = JSON.parse(cached);
1396        if (parsedCache.data) {
1397          this._transitDataCache = parsedCache.data;
1398        }
1399        const transit = parsedCache.data?.[cleanZip];
1400        if (transit !== null && transit !== undefined) return transit;
1401      }
1402    } catch (e) { /* silent */ }
1403
1404    // Trigger async load if requested
1405    if (triggerLoad) {
1406      this.ensureTransitDataLoaded();
1407    }
1408
1409    return null;
1410  },
1411  
1412  /** Get slowest delivery date across all cart items @param {object} cartState @returns {object|null} */
1413  getSlowestDelivery: function(cartState) {
1414    if (!cartState || !cartState.items) return null;
1415    
1416    let slowest = null;
1417    let slowestDate = null;
1418    
1419    Object.keys(cartState.items).forEach(cdno => {
1420      const item = cartState.items[cdno];
1421      if (!item.deliveryEstimate || !item.isInStock) return;
1422      
1423      const latestDate = new Date(item.deliveryEstimate.latestDate);
1424      
1425      if (!slowestDate || latestDate > slowestDate) {
1426        slowestDate = latestDate;
1427        slowest = item.deliveryEstimate;
1428      }
1429    });
1430    
1431    return slowest;
1432  },
1433  
1434  /** Get delivery estimate for specific cart item @param {string} cdno @returns {object|null} */
1435  getItemDelivery: function(cdno) {
1436    const cartState = this.getCartState();
1437    if (!cartState || !cartState.items || !cartState.items[cdno]) {
1438      return null;
1439    }
1440    
1441    return cartState.items[cdno].deliveryEstimate;
1442  },
1443  
1444  /** Process cart data and update state @param {Array} cartItems @returns {object} */
1445  processCartData: function(cartItems) {
1446    const cartChanged = this.validateCartState(cartItems);
1447    
1448    if (cartChanged) {
1449      const newState = this.initializeCartState(cartItems);
1450      this.saveCartState(newState);
1451      return newState;
1452    }
1453    
1454    return this.getCartState();
1455  },
1456  
1457  /** Add business days (excludes weekends) @param {Date} startDate @param {number} businessDays @returns {Date} */
1458  addBusinessDays: function(startDate, businessDays) {
1459    if (window.eddHandler) {
1460      return window.eddHandler.addBusinessDays(startDate, businessDays);
1461    }
1462    
1463    // Fallback without holiday checking
1464    const result = new Date(startDate);
1465    let daysAdded = 0;
1466    while (daysAdded < businessDays) {
1467      result.setDate(result.getDate() + 1);
1468      if (result.getDay() !== 0 && result.getDay() !== 6) {
1469        daysAdded++;
1470      }
1471    }
1472    return result;
1473  },
1474  
1475  // ===== PDP UI RENDERING =====
1476
1477  /** Render EDD/ESD UI for PDP @param {object} options @param {string} options.mode - 'edd' or 'esd' @param {string} [options.esdValue] - ESD date string (MM/DD/YYYY) @returns {HTMLElement|null} */
1478  renderPDPDeliveryUI: function(options = { mode: 'esd' }) {
1479    if (this.isGiftCard()) return null;
1480
1481    const isESD = options.mode === 'esd';
1482    const stockInfo = this.getStockStatusInfo('In Stock');
1483    const statusLabel = this.getEnhancedStatusLabel('inStock', 'block', isESD ? 'esd' : 'edd');
1484    
1485    let dateText = '';
1486    let actionLabel = '';
1487    let drawerStartDate = null;
1488    let drawerEndDate = null;
1489    
1490    if (isESD) {
1491      let esdValue = options.esdValue || '';
1492      
1493      if (!esdValue) {
1494        const esdInput = document.querySelector('#SIESDTq') || document.querySelector('#SIESDTd');
1495        if (esdInput && esdInput.value) {
1496          esdValue = esdInput.value.trim();
1497        }
1498      }
1499
1500      if (!esdValue) {
1501        return null;
1502      }
1503      
1504      const esdDate = this.parseESDDateValue(esdValue);
1505      if (!esdDate) return null;
1506
1507      // Apply cutoff logic for ship date (weekends, holidays, 2PM EST cutoff)
1508      const shipDate = this.calculateShipDate(esdDate);
1509      dateText = this.formatShipRange(shipDate);
1510      actionLabel = 'Ships';
1511      // Track dates for drawer message
1512      drawerStartDate = shipDate;
1513      const bufferBusinessDays = this.config?.deliveryRules?.bufferBusinessDays || 1;
1514      drawerEndDate = this.addBusinessDaysSimple(shipDate, bufferBusinessDays);
1515    } else {
1516      const deliveryInfo = this.getBaseDeliveryData();
1517      if (!deliveryInfo) {
1518        return null;
1519      }
1520      dateText = deliveryInfo.rangeFormatted;
1521      actionLabel = 'Arrives';
1522      // Track dates for drawer message
1523      drawerStartDate = deliveryInfo.earliestDelivery instanceof Date ? deliveryInfo.earliestDelivery : (deliveryInfo.earliestDelivery ? new Date(deliveryInfo.earliestDelivery) : null);
1524      drawerEndDate = deliveryInfo.latestDelivery instanceof Date ? deliveryInfo.latestDelivery : (deliveryInfo.latestDelivery ? new Date(deliveryInfo.latestDelivery) : null);
1525    }
1526
1527    actionLabel = this.getEstimatedActionText(actionLabel);
1528
1529    const container = document.createElement('div');
1530    container.className = 'b5-active pdp-edd';
1531    container.setAttribute('role', 'status');
1532    container.setAttribute('aria-live', 'polite');
1533    
1534    // Build shipping section HTML only for EDD mode
1535    const { zip: userZip, city: userCity, state: userState } = this.getUserLocationData();
1536
1537    const cityStateMsg = (userCity && userState)
1538      ? `${userCity}, ${userState}`
1539      : 'Enter a zip to set your location';
1540
1541    const shippingSection = isESD ? '' : this.getEDDShippingSectionHTML(userZip, cityStateMsg);
1542
1543    // New unified design: [dot] Status • Ships/Arrives DateRange
1544    // EDD only: second line is Standard shipping • zip
1545    // Wrapped in pdp__status-pill for border styling, data-stock-info-trigger opens drawer
1546    const drawerMessage = this.getDrawerMessage('inStock', isESD ? 'esd' : 'edd', drawerStartDate, drawerEndDate);
1547    container.innerHTML = `
1548      <div class="pdp__status-pill" data-stock-type="${stockInfo.type}" data-stock-info-trigger data-tip="${drawerMessage}" tabindex="0" role="button" aria-label="Stock information">
1549        <div class="pdp-edd__content">
1550          <div class="pdp-edd__row">
1551            <span class="pdp-edd__dot" aria-hidden="true">${this.getColoredDotSVG()}</span>
1552            <span class="pdp-edd__label">${statusLabel}</span>
1553            <span class="pdp-edd__separator">\u2022</span>
1554            <span class="pdp-edd__action">${actionLabel}</span>
1555            <span class="pdp-edd__date" data-target="eddDeliveryDate"><strong>${dateText}</strong></span>
1556          </div>
1557          ${shippingSection}
1558        </div>
1559        <span class="pdp__status-pill-icon pdp-edd__info-icon" aria-hidden="true">${this.getInfoIconSVG('block')}</span>
1560      </div>
1561    `;
1562
1563    return container;
1564  },
1565
1566  // ===== A/B TEST HELPERS =====
1567
1568  /** Resolve active Varify experiment config for current brand+environment. @returns {{experimentId:number,eddVariationId:number,esdVariationId:number}|null} */
1569  getABExperimentConfig: function() {
1570    const configuredABTests = this.config?.abTestVariations || eddStockStatusConfig?.abTestVariations || {};
1571    const brandName = typeof getBrandName === 'function' ? getBrandName() : '';
1572    const brandKey = brandName === 'U.S. Patriot' ? 'usp' : 'galls';
1573
1574    const baseUrl = typeof getBaseUrl === 'function' ? getBaseUrl() : window.location.origin;
1575    const envKey = baseUrl.includes('stage') ? 'stage' : 'prod';
1576
1577    return configuredABTests?.[brandKey]?.[envKey] || configuredABTests?.galls?.stage || null;
1578  },
1579
1580  /** Resolve active delivery variation mode from Varify experiment. @returns {'edd'|'esd'|'control'} */
1581  getABVariationMode: function() {
1582    return 'edd'; // A/B test concluded — EDD always active
1583  },
1584
1585  /** Check if user is in EDD A/B test variation @returns {boolean} */
1586  isEDDABTestVariation: function() {
1587    return this.getABVariationMode() === 'edd';
1588  },
1589
1590  /** Check if user is in ESD A/B test variation @returns {boolean} */
1591  isESDABTestVariation: function() {
1592    return this.getABVariationMode() === 'esd';
1593  },
1594
1595  /** Calculate delivery estimate on-demand using ESD from page and cached transit data @returns {object|null} Delivery info with rangeFormatted */
1596  calculateDeliveryOnDemand: function() {
1597    // Get user zip
1598    const userZip = this.getUserZipCode();
1599    if (!userZip) return null;
1600    
1601    // Get ESD from page hidden field (check QV field first for quickview context)
1602    const esdInput = document.querySelector('#SIESDTq') || document.querySelector('#SIESDTd');
1603    if (!esdInput || !esdInput.value) return null;
1604    
1605    const esdValue = esdInput.value.trim();
1606    if (!esdValue) return null;
1607    
1608    const esdDate = this.parseESDDateValue(esdValue);
1609    if (!esdDate || this.isDatePastToday(esdDate)) return null;
1610    
1611    // Get transit days via shared helper
1612    const transitDays = this.getTransitDaysForZip(userZip);
1613    if (transitDays === null) return null;
1614    
1615    // Apply ship date cutoff (2PM EST, weekends, holidays)
1616    const shipDate = this.calculateShipDate(esdDate);
1617    // Calculate delivery dates from adjusted ship date
1618    const earliestDelivery = this.addBusinessDays(shipDate, transitDays);
1619    const bufferBusinessDays = this.config?.deliveryRules?.bufferBusinessDays || 1;
1620    const latestDelivery = this.addBusinessDays(earliestDelivery, bufferBusinessDays);
1621    
1622    const rangeFormatted = this.formatCompactRange(earliestDelivery, latestDelivery);
1623    
1624    return {
1625      rangeFormatted: rangeFormatted,
1626      earliestDelivery: earliestDelivery,
1627      latestDelivery: latestDelivery,
1628      transitDays: transitDays,
1629      esdDate: esdDate
1630    };
1631  },
1632
1633  /** Get PDP stock message HTML for A/B test variation (called by stock-status.js) @param {string} [esdValue] - Optional ESD date string (MM/DD/YYYY) from Ajax response @returns {string|null} HTML string or null if no data */
1634  getPDPStockMessageHTML: function(esdValue) {
1635    if (!this.isEDDABTestVariation()) {
1636      return null;
1637    }
1638    
1639    let deliveryInfo = null;
1640    
1641    // If ESD provided directly, calculate from it
1642    if (esdValue) {
1643      if (this.isESDValuePastToday(esdValue)) {
1644        return this.buildPDPBlockPill('In Stock');
1645      }
1646      const userZip = this.getUserZipCode();
1647      if (userZip) {
1648        deliveryInfo = this.calculateEDDFromESD(esdValue, userZip);
1649      }
1650    }
1651    
1652    // Only use cache/on-demand fallbacks when ESD token exists (no-options product or options already selected).
1653    // Without esdValue, fallbacks could return stale data from a previously viewed product.
1654    if (esdValue) {
1655      if (!deliveryInfo || !deliveryInfo.rangeFormatted) {
1656        deliveryInfo = this.getBaseDeliveryData();
1657      }
1658      if (!deliveryInfo || !deliveryInfo.rangeFormatted) {
1659        deliveryInfo = this.calculateDeliveryOnDemand();
1660      }
1661    }
1662    
1663    if (!deliveryInfo || !deliveryInfo.rangeFormatted) {
1664      // No delivery data available (no zip or transit data) — show dateless variation pill
1665      // with location tracker so user can set zip and trigger recalculation
1666      if (esdValue) {
1667        return this.buildPDPBlockPill('In Stock');
1668      }
1669      return null;
1670    }
1671
1672    if (this.isDisplayDateTextPastToday(deliveryInfo.rangeFormatted)) {
1673      return this.buildPDPBlockPill('In Stock');
1674    }
1675    
1676    // Cache the delivery info so updateDynamicEDDUI can access ESD when user changes zip
1677    try {
1678      const cacheObject = {
1679        data: {
1680          rangeFormatted: deliveryInfo.rangeFormatted,
1681          earliestDelivery: deliveryInfo.earliestDelivery ? deliveryInfo.earliestDelivery.toISOString() : null,
1682          latestDelivery: deliveryInfo.latestDelivery ? deliveryInfo.latestDelivery.toISOString() : null,
1683          transitDays: deliveryInfo.transitDays,
1684          esdDate: deliveryInfo.esdDate ? deliveryInfo.esdDate.toISOString() : null
1685        },
1686        timestamp: Date.now()
1687      };
1688      localStorage.setItem('currentDeliveryEstimate', JSON.stringify(cacheObject));
1689    } catch (e) {
1690      // Silent fail
1691    }
1692
1693    const { zip: userZip, city: userCity, state: userState } = this.getUserLocationData();
1694
1695    const cityStateMsg = (userCity && userState)
1696      ? `${userCity}, ${userState}`
1697      : 'Enter a zip to set your location';
1698
1699    // Get stock status info for colored dot
1700    const stockInfo = this.getStockStatusInfo('In Stock');
1701    const statusLabel = this.getEnhancedStatusLabel('inStock', 'block', 'edd');
1702
1703    // Resolve drawer message from config with delivery dates
1704    const drawerStartDate = deliveryInfo.earliestDelivery instanceof Date ? deliveryInfo.earliestDelivery : (deliveryInfo.earliestDelivery ? new Date(deliveryInfo.earliestDelivery) : null);
1705    const drawerEndDate = deliveryInfo.latestDelivery instanceof Date ? deliveryInfo.latestDelivery : (deliveryInfo.latestDelivery ? new Date(deliveryInfo.latestDelivery) : null);
1706    const drawerMessage = this.getDrawerMessage('inStock', 'edd', drawerStartDate, drawerEndDate);
1707
1708    // Return HTML string - New unified design with shipping inside border
1709    const shippingSectionHTML = this.getEDDShippingSectionHTML(userZip, cityStateMsg);
1710    return `
1711      <div class="b5-active pdp-edd" role="status" aria-live="polite">
1712        <div class="pdp__status-pill" data-stock-type="${stockInfo.type}" data-stock-info-trigger data-tip="${drawerMessage}" tabindex="0" role="button" aria-label="Stock information">
1713          <div class="pdp-edd__content">
1714            <div class="pdp-edd__row">
1715              <span class="pdp-edd__dot" aria-hidden="true">${this.getColoredDotSVG()}</span>
1716              <span class="pdp-edd__label">${statusLabel}</span>
1717              <span class="pdp-edd__separator">\u2022</span>
1718              <span class="pdp-edd__action">${this.getEstimatedActionText('Arrives')}</span>
1719              <span class="pdp-edd__date" data-target="eddDeliveryDate"><strong>${deliveryInfo.rangeFormatted}</strong></span>
1720            </div>
1721            ${shippingSectionHTML}
1722          </div>
1723          <span class="pdp__status-pill-icon pdp-edd__info-icon" aria-hidden="true">${this.getInfoIconSVG('block')}</span>
1724        </div>
1725      </div>
1726    `;
1727  },
1728
1729  /** Get PDP stock message HTML for non-in-stock statuses in active variation. @param {string} originalStockMsg @param {string} [esdValue] @returns {string|null} */
1730  getPDPNonInStockMessageHTML: function(originalStockMsg, esdValue = null) {
1731    if (this.isGiftCard()) return '';
1732
1733    const variationMode = this.getABVariationMode();
1734    if (variationMode !== 'edd' && variationMode !== 'esd') return null;
1735
1736    const statusType = this.detectStockStatus(originalStockMsg);
1737    if (statusType === 'inStock') return null;
1738
1739    if (statusType === 'backordered' && this.isHideBackorderVariation()) {
1740      return '<div style="display:none"></div>';
1741    }
1742
1743    const displayedActionDate = this.extractDisplayedActionDate(originalStockMsg);
1744    if (displayedActionDate) {
1745      if (this.isDisplayDateTextPastToday(displayedActionDate.dateText)) {
1746        return this.buildPDPBlockPill(originalStockMsg);
1747      }
1748      // EDD variation + extracted "Ships" action: skip extracted date and fall through
1749      // to ESD-based path which properly computes arrival dates for EDD mode
1750      const isExtractedShipAction = displayedActionDate.actionText === 'Ships';
1751      if (!(variationMode === 'edd' && isExtractedShipAction)) {
1752        const { start, end } = this.parseDisplayDateRange(displayedActionDate.dateText);
1753        return this.buildPDPBlockPill(originalStockMsg, displayedActionDate.actionText, displayedActionDate.dateText, start, end);
1754      }
1755    }
1756
1757    const effectiveESD = esdValue || this.extractESDFromStockMessage(originalStockMsg);
1758    if (this.isESDValuePastToday(effectiveESD)) return this.buildPDPBlockPill(originalStockMsg);
1759    const esdDate = this.parseESDDateValue(effectiveESD);
1760    if (!esdDate) return this.buildPDPBlockPill(originalStockMsg);
1761
1762    // Apply cutoff logic for ship date (weekends, holidays, 2PM EST cutoff)
1763    const shipDate = this.calculateShipDate(esdDate);
1764
1765    let actionText = 'Ships';
1766    let dateText = this.formatShipRange(shipDate);
1767    const bufferBusinessDays = this.config?.deliveryRules?.bufferBusinessDays || 1;
1768    let drawerStartDate = shipDate;
1769    let drawerEndDate = this.addBusinessDaysSimple(shipDate, bufferBusinessDays);
1770
1771    if (variationMode === 'edd') {
1772      const deliveryInfo = this.calculateEDDFromESD(effectiveESD, this.getUserZipCode());
1773      if (deliveryInfo && deliveryInfo.rangeFormatted) {
1774        actionText = 'Arrives';
1775        dateText = deliveryInfo.rangeFormatted;
1776        drawerStartDate = deliveryInfo.earliestDelivery;
1777        drawerEndDate = deliveryInfo.latestDelivery;
1778      }
1779    }
1780
1781    return this.buildPDPBlockPill(originalStockMsg, actionText, dateText, drawerStartDate, drawerEndDate);
1782  },
1783
1784  /** Calculate EDD from a specific ESD value (for cart/minicart items) @param {string} esdValue - ESD in MM/DD/YYYY format @param {string} [userZip] - Optional user zip (defaults to cached) @returns {object|null} Delivery info with rangeFormatted */
1785  calculateEDDFromESD: function(esdValue, userZip) {
1786    if (!esdValue) return null;
1787    
1788    const esdDate = this.parseESDDateValue(esdValue);
1789    if (!esdDate || this.isDatePastToday(esdDate)) return null;
1790    
1791    // Get user zip
1792    const zip = userZip || this.getUserZipCode();
1793    if (!zip) return null;
1794    
1795    // Get transit days via shared helper
1796    const transitDays = this.getTransitDaysForZip(zip);
1797    if (!transitDays) return null;
1798
1799    // Apply cutoff logic: adjusts ship date for weekends, holidays, 2PM EST cutoff
1800    const shipDate = this.calculateShipDate(esdDate);
1801    
1802    // Calculate delivery dates from adjusted ship date
1803    const earliestDelivery = this.addBusinessDays(shipDate, transitDays);
1804    const bufferBusinessDays = this.config?.deliveryRules?.bufferBusinessDays || 1;
1805    const latestDelivery = this.addBusinessDays(earliestDelivery, bufferBusinessDays);
1806    
1807    const rangeFormatted = this.formatCompactRange(earliestDelivery, latestDelivery);
1808    
1809    return {
1810      rangeFormatted: rangeFormatted,
1811      earliestDelivery: earliestDelivery,
1812      latestDelivery: latestDelivery,
1813      shipDate: shipDate,
1814      transitDays: transitDays,
1815      esdDate: esdDate
1816    };
1817  },
1818
1819  /** Get delivery estimate message for minicart/cart @param {string} originalStockMsg @param {boolean} includeTooltip @param {string} [esdValue] - Optional ESD value for per-item calculation @param {string} [sku] - Product SKU for per-item checks @param {string} [title] - Product title for per-item checks @returns {string} */
1820  getMinicartStockMessage: function(originalStockMsg, includeTooltip = true, esdValue = null, sku = null, title = null) {
1821    if (this.isGiftCardItem(sku, title)) return '';
1822
1823    if (this.detectStockStatus(originalStockMsg) === 'backordered' && this.isHideBackorderVariation()) {
1824      return '';
1825    }
1826
1827    const variationMode = this.getABVariationMode();
1828    const isEDDVariation = variationMode === 'edd';
1829    const isESDVariation = variationMode === 'esd';
1830    
1831    if (!isEDDVariation && !isESDVariation) {
1832      return originalStockMsg;
1833    }
1834    
1835    const tooltipText = this.extractTooltipTextFromMarkup(originalStockMsg);
1836
1837    const isEnhanceableStatus = this.isEnhanceableStockMessage(originalStockMsg);
1838
1839    if (!isEnhanceableStatus) {
1840      return originalStockMsg;
1841    }
1842
1843    const statusType = this.detectStockStatus(originalStockMsg);
1844    const statusLabel = this.getEnhancedStatusLabel(statusType, 'inline', isESDVariation ? 'esd' : 'edd');
1845    const isEligibleInStock = this.isEligibleOriginalInStockStatus(originalStockMsg);
1846    const aggregatedESD = this.getCartPayloadAggregate('esd');
1847    const aggregatedEDD = this.getCartPayloadAggregate('edd');
1848
1849    // ESD variation: show ship date
1850    if (isESDVariation) {
1851      const effectiveESD = isEligibleInStock && aggregatedESD && aggregatedESD.esdValue ? aggregatedESD.esdValue : esdValue;
1852      if (this.isESDValuePastToday(effectiveESD)) {
1853        return this.buildDatelessVariationPill(originalStockMsg);
1854      }
1855      return this.getESDStockMessage(originalStockMsg, effectiveESD, tooltipText, true);
1856    }
1857    
1858    try {
1859      let rangeFormatted = null;
1860      
1861      // In-stock rule: use latest eligible in-stock range from API payload if available
1862      if (isEligibleInStock && aggregatedEDD && aggregatedEDD.rangeFormatted) {
1863        rangeFormatted = aggregatedEDD.rangeFormatted;
1864      }
1865
1866      // Non-in-stock rule: prefer currently displayed DOM date first
1867      if (!rangeFormatted && !isEligibleInStock) {
1868        const displayedActionDate = this.extractDisplayedActionDate(originalStockMsg);
1869        if (displayedActionDate) {
1870          if (this.isDisplayDateTextPastToday(displayedActionDate.dateText)) {
1871            return this.buildDatelessVariationPill(originalStockMsg);
1872          }
1873          // EDD variation + extracted "Ships" action: skip extracted date and fall through
1874          // to ESD-based path which properly computes arrival dates for EDD mode
1875          const isExtractedShipAction = displayedActionDate.actionText === 'Ships';
1876          if (!(isEDDVariation && isExtractedShipAction)) {
1877            const stockInfo = this.getStockStatusInfo(originalStockMsg);
1878            const { start: dStartDate, end: dEndDate } = this.parseDisplayDateRange(displayedActionDate.dateText);
1879            return this.buildEnhancedStockMessage(stockInfo, statusLabel, displayedActionDate.actionText, displayedActionDate.dateText, tooltipText, undefined, dStartDate, dEndDate);
1880          }
1881        }
1882      }
1883
1884      // If ESD value provided, calculate per-item EDD
1885      if (!rangeFormatted && esdValue) {
1886        const deliveryInfo = this.calculateEDDFromESD(esdValue);
1887        if (deliveryInfo && deliveryInfo.rangeFormatted) {
1888          rangeFormatted = deliveryInfo.rangeFormatted;
1889        }
1890      }
1891
1892      // Non-in-stock fallback: if no EDD range could be calculated, still show ship range from ESD
1893      if (!rangeFormatted && !isEligibleInStock && esdValue) {
1894        if (this.isESDValuePastToday(esdValue)) {
1895          return this.buildDatelessVariationPill(originalStockMsg);
1896        }
1897        const esdDate = this.parseESDDateValue(esdValue);
1898        if (esdDate) {
1899          const stockInfo = this.getStockStatusInfo(originalStockMsg);
1900          const shipDate = this.calculateShipDate(esdDate);
1901          const bufferDays = this.config?.deliveryRules?.bufferBusinessDays || 1;
1902          return this.buildEnhancedStockMessage(stockInfo, statusLabel, 'Ships', this.formatShipRange(shipDate), tooltipText, undefined, shipDate, this.addBusinessDaysSimple(shipDate, bufferDays));
1903        }
1904      }
1905      
1906      // Fallback to cart state (slowest delivery across all items) - in-stock only
1907      if (!rangeFormatted && isEligibleInStock) {
1908        const cartState = this.getCartState();
1909        if (cartState && cartState.items) {
1910          const slowestDelivery = this.getSlowestDelivery(cartState);
1911          if (slowestDelivery && slowestDelivery.rangeFormatted) {
1912            rangeFormatted = slowestDelivery.rangeFormatted;
1913          }
1914        }
1915      }
1916      
1917      // Final fallback - base delivery data (in-stock only)
1918      if (!rangeFormatted && isEligibleInStock) {
1919        const baseDelivery = this.getBaseDeliveryData();
1920        if (baseDelivery && baseDelivery.rangeFormatted) {
1921          rangeFormatted = baseDelivery.rangeFormatted;
1922        }
1923      }
1924
1925      if (!rangeFormatted) {
1926        return this.buildDatelessVariationPill(originalStockMsg);
1927      }
1928      if (this.isDisplayDateTextPastToday(rangeFormatted)) {
1929        return this.buildDatelessVariationPill(originalStockMsg);
1930      }
1931      
1932      // Get stock status info for colored dot
1933      const stockInfo = this.getStockStatusInfo(originalStockMsg);
1934      const { start: arrStartDate, end: arrEndDate } = this.parseDisplayDateRange(rangeFormatted);
1935      return this.buildEnhancedStockMessage(stockInfo, statusLabel, 'Arrives', rangeFormatted, tooltipText, undefined, arrStartDate, arrEndDate);
1936    } catch (e) {
1937      return this.buildDatelessVariationPill(originalStockMsg);
1938    }
1939  },
1940
1941  /** Get ESD (ship date) message for cart/minicart @param {string} originalStockMsg @param {string} [esdValue] @returns {string} */
1942  getESDStockMessage: function(originalStockMsg, esdValue = null, tooltipText = null, skipPageFallback = false) {
1943    try {
1944      // Try to use provided esdValue or get from page
1945      let esdDateStr = esdValue;
1946      if (!esdDateStr && !skipPageFallback) {
1947        const esdInput = document.querySelector('#SIESDTd') || document.querySelector('#SIESDT input[name="SIESDT"]');
1948        if (esdInput && esdInput.value) {
1949          esdDateStr = esdInput.value.trim();
1950        }
1951      }
1952      
1953      if (!esdDateStr) {
1954        return this.buildDatelessVariationPill(originalStockMsg);
1955      }
1956      if (this.isESDValuePastToday(esdDateStr)) {
1957        return this.buildDatelessVariationPill(originalStockMsg);
1958      }
1959
1960      const esdDate = this.parseESDDateValue(esdDateStr);
1961      if (!esdDate) return this.buildDatelessVariationPill(originalStockMsg);
1962
1963      // Apply cutoff logic for ship date (weekends, holidays, 2PM EST cutoff)
1964      const shipDate = this.calculateShipDate(esdDate);
1965      const formattedDate = this.formatShipRange(shipDate);
1966      const bufferDays = this.config?.deliveryRules?.bufferBusinessDays || 1;
1967      const esdEndDate = this.addBusinessDaysSimple(shipDate, bufferDays);
1968      
1969      // Get stock status info for colored dot
1970      const stockInfo = this.getStockStatusInfo(originalStockMsg);
1971      const statusType = this.detectStockStatus(originalStockMsg);
1972      const statusLabel = this.getEnhancedStatusLabel(statusType, 'inline', 'esd');
1973      const tip = tooltipText || this.config?.stockMsgDisplay?.defaultTooltip || '';
1974      return this.buildEnhancedStockMessage(stockInfo, statusLabel, 'Ships', formattedDate, tip, undefined, shipDate, esdEndDate);
1975    } catch (e) {
1976      return this.buildDatelessVariationPill(originalStockMsg);
1977    }
1978  },
1979
1980  // ===== CHECKOUT DELIVERY CALCULATIONS =====
1981
1982  /** Checkout shipping method values */
1983  // Note: Shipping method values are backend tokens - we detect by radio button ID instead
1984  checkoutShippingMethods: {
1985    standard: 'StandardChecked',
1986    secondDay: '2ndDayAirChecked',
1987    nextDay: 'NextDayChecked',
1988    saturday: 'SaturdayChecked'
1989  },
1990
1991  /** Get currently selected shipping method on checkout @returns {'standard'|'2day'|'nextday'|'saturday'|'none'|'other'} */
1992  getSelectedShippingMethod: function() {
1993    const checkedInput = document.querySelector('input[name="SISHMT"]:checked');
1994    if (!checkedInput) return 'none';
1995    
1996    const id = checkedInput.id || '';
1997    const lowerId = id.toLowerCase();
1998    let result = 'other';
1999    if (id === this.checkoutShippingMethods.standard) result = 'standard';
2000    else if (id === this.checkoutShippingMethods.secondDay) result = '2day';
2001    else if (id === this.checkoutShippingMethods.nextDay) result = 'nextday';
2002    else if (id === this.checkoutShippingMethods.saturday || lowerId.includes('saturday') || lowerId.includes('sat')) result = 'saturday';
2003    else if (lowerId.includes('nextbusiness') || (lowerId.includes('next') && lowerId.includes('day')) || lowerId.includes('expedited')) result = 'nextday';
2004    return result;
2005  },
2006
2007  /** Check if current time is before 2PM EST @returns {boolean} */
2008  isBeforeCutoff: function() {
2009    const now = new Date();
2010    // Convert to EST (UTC-5) or EDT (UTC-4)
2011    const estOffset = this.isDST(now) ? -4 : -5;
2012    const utc = now.getTime() + (now.getTimezoneOffset() * 60000);
2013    const estDate = new Date(utc + (3600000 * estOffset));
2014    const cutoffHourEST = this.config?.deliveryRules?.cutoffHourEST ?? 14;
2015    return estDate.getHours() < cutoffHourEST;
2016  },
2017
2018  /** Get current date in EST timezone (warehouse timezone) @returns {Date} Today's date at midnight EST */
2019  getTodayEST: function() {
2020    const now = new Date();
2021    // Convert to EST (UTC-5) or EDT (UTC-4)
2022    const estOffset = this.isDST(now) ? -4 : -5;
2023    const utc = now.getTime() + (now.getTimezoneOffset() * 60000);
2024    const estDate = new Date(utc + (3600000 * estOffset));
2025    // Return date at midnight (date only, no time component)
2026    estDate.setHours(0, 0, 0, 0);
2027    return estDate;
2028  },
2029
2030  /** Check if date is during Daylight Saving Time @param {Date} date @returns {boolean} */
2031  isDST: function(date) {
2032    const jan = new Date(date.getFullYear(), 0, 1);
2033    const jul = new Date(date.getFullYear(), 6, 1);
2034    const stdOffset = Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
2035    return date.getTimezoneOffset() < stdOffset;
2036  },
2037
2038  /** Check if date is a weekend (Saturday=6, Sunday=0) @param {Date} date @returns {boolean} */
2039  isWeekend: function(date) {
2040    const day = date.getDay();
2041    return day === 0 || day === 6;
2042  },
2043
2044  /** Check if date is Friday @param {Date} date @returns {boolean} */
2045  isFriday: function(date) {
2046    return date.getDay() === 5;
2047  },
2048
2049  /** Get next Monday from a date @param {Date} date @returns {Date} */
2050  getNextMonday: function(date) {
2051    const result = new Date(date);
2052    const day = result.getDay();
2053    const daysUntilMonday = day === 0 ? 1 : (8 - day);
2054    result.setDate(result.getDate() + daysUntilMonday);
2055    return result;
2056  },
2057
2058  /** Format date as EST calendar date (timezone-agnostic for display). These are calendar dates, not timestamps - user sees same date regardless of their timezone. @param {Date} date @param {object} options - Intl.DateTimeFormat options @returns {string} Formatted date string */
2059  formatDateEST: function(date, options) {
2060    const defaultOptions = { weekday: 'short', month: 'short', day: 'numeric' };
2061    const formatOptions = { ...defaultOptions, ...options, timeZone: 'America/Ne
2061w_York' };
2062    return date.toLocaleDateString('en-US', formatOptions);
2063  },
2064
2065  /** Format date range as "Nov 23 - 24" or "Nov 30 - Dec 1" @param {Date} startDate @param {Date} endDate @returns {string} */
2066  formatCompactRange: function(startDate, endDate) {
2067    const startFormatted = startDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
2068    const endFormatted = endDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
2069
2070    if (startDate.getFullYear() === endDate.getFullYear() && startDate.getMonth() === endDate.getMonth()) {
2071      const month = startDate.toLocaleDateString('en-US', { month: 'short' });
2072      return `${month} ${startDate.getDate()} - ${endDate.getDate()}`;
2073    }
2074
2075    return `${startFormatted} - ${endFormatted}`;
2076  },
2077
2078  /** Format ship range as ESD to ESD+1 business day (e.g., "Feb 19 - 20") @param {Date} shipDate @returns {string} */
2079  formatShipRange: function(shipDate) {
2080    if (!(shipDate instanceof Date) || isNaN(shipDate.getTime())) return '';
2081    const bufferBusinessDays = this.config?.deliveryRules?.bufferBusinessDays || 1;
2082    const bufferDate = this.addBusinessDaysSimple(shipDate, bufferBusinessDays);
2083    return this.formatCompactRange(shipDate, bufferDate);
2084  },
2085
2086  /** Format date as long form for drawer messages (e.g., "Wednesday, February 12") @param {Date} date @returns {string} */
2087  formatLongDate: function(date) {
2088    if (!(date instanceof Date) || isNaN(date.getTime())) return '';
2089    return date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' });
2090  },
2091
2092  /** Get drawer message for a given status type and variation mode with date placeholders resolved. @param {'inStock'|'partnerFulfilled'|'backordered'|'preOrder'} statusType @param {'esd'|'edd'} variationMode @param {Date|null} [startDate] - Range start date @param {Date|null} [endDate] - Range end date @returns {string} Resolved drawer message */
2093  getDrawerMessage: function(statusType, variationMode, startDate = null, endDate = null) {
2094    const drawerConfig = this.config?.stockMsgDisplay?.drawerMessages || {};
2095    const hasDates = startDate && endDate && startDate instanceof Date && endDate instanceof Date && !isNaN(startDate.getTime()) && !isNaN(endDate.getTime());
2096    const isSameDay = hasDates && startDate.toDateString() === endDate.toDateString();
2097
2098    // Pick same-day or date-range template
2099    let template = '';
2100    if (isSameDay) {
2101      const sameDayTemplates = drawerConfig.sameDay?.[variationMode] || drawerConfig.sameDay?.esd || {};
2102      template = sameDayTemplates[statusType] || sameDayTemplates.inStock || '';
2103    } else {
2104      const modeTemplates = drawerConfig[variationMode] || drawerConfig.esd || {};
2105      template = modeTemplates[statusType] || modeTemplates.inStock || '';
2106    }
2107
2108    if (!template) return '';
2109
2110    // Resolve date placeholders when available, otherwise strip date phrases
2111    if (hasDates) {
2112      if (isSameDay) {
2113        template = template.replace(/\{date\}/g, this.formatLongDate(startDate));
2114      } else {
2115        template = template.replace(/\{startDate\}/g, this.formatLongDate(startDate));
2116        template = template.replace(/\{endDate\}/g, this.formatLongDate(endDate));
2117      }
2118    } else {
2119      template = template
2120        .replace(/\s*between\s*\{startDate\}\s*and\s*\{endDate\}/gi, '')
2121        .replace(/\s*on\s*\{date\}/gi, '')
2122        .replace(/\s{2,}/g, ' ')
2123        .trim();
2124    }
2125
2126    return template;
2127  },
2128
2129  /** Add business days (skips weekends only - holidays checked separately) @param {Date} startDate @param {number} days @returns {Date} */
2130  addBusinessDaysSimple: function(startDate, days) {
2131    const result = new Date(startDate);
2132    let added = 0;
2133    while (added < days) {
2134      result.setDate(result.getDate() + 1);
2135      if (!this.isWeekend(result)) {
2136        added++;
2137      }
2138    }
2139    return result;
2140  },
2141
2142  /** Calculate ship date based on ESD and current conditions (uses EST timezone) @param {Date} esdDate @returns {Date} Ship date to use for calculations */
2143  calculateShipDate: function(esdDate) {
2144    // Use EST timezone for "today" to align with warehouse
2145    const today = this.getTodayEST();
2146    
2147    const esdNormalized = new Date(esdDate);
2148    esdNormalized.setHours(0, 0, 0, 0);
2149    
2150    // GQ ESD > 1 (ESD is in the future relative to EST)
2151    if (esdNormalized > today) {
2152      return esdNormalized;
2153    }
2154    
2155    // GQ ESD = 1 (ESD is today in EST)
2156    const isWeekendNow = this.isWeekend(today);
2157    const isFridayNow = this.isFriday(today);
2158    const beforeCutoff = this.isBeforeCutoff();
2159    const afterCutoffDelayBusinessDays = this.config?.checkoutDecisionTable?.shipDateRules?.afterCutoffDelayBusinessDays ?? 1;
2160    
2161    // Weekend: ship Monday
2162    if (isWeekendNow) {
2163      return this.getNextMonday(today);
2164    }
2165    
2166    // Friday after 2PM: ship Monday
2167    if (isFridayNow && !beforeCutoff) {
2168      return this.getNextMonday(today);
2169    }
2170    
2171    // Weekday after 2PM: ship next business day
2172    if (!beforeCutoff) {
2173      return this.addBusinessDaysSimple(today, afterCutoffDelayBusinessDays);
2174    }
2175    
2176    // Weekday before 2PM: ship today
2177    return today;
2178  },
2179
2180  /** Calculate checkout delivery date based on shipping method @param {string} esdValue - ESD in MM/DD/YYYY format @param {'standard'|'2day'|'nextday'|'saturday'|'none'|'other'} shippingMethod @returns {object|null} {shipDate, arrivalDate, arrivalFormatted, rangeFormatted, shippingMethod} */
2181  calculateCheckoutDelivery: function(esdValue, shippingMethod) {
2182    if (!esdValue || shippingMethod === 'other' || shippingMethod === 'none' || shippingMethod === 'saturday') {
2183      return null;
2184    }
2185    
2186    const esdDate = this.parseESDDateValue(esdValue);
2187    if (!esdDate) return null;
2188    
2189    // Calculate ship date
2190    const shipDate = this.calculateShipDate(esdDate);
2191    
2192    // Calculate arrival date based on shipping method
2193    let arrivalDate;
2194    const shortOptions = { month: 'short', day: 'numeric' };
2195    
2196    if (shippingMethod === 'standard') {
2197      // Standard: Ship date + Ground transit days (from FedEx CSV)
2198      const userZip = this.getUserZipCode();
2199      const transitDays = this.getTransitDaysForZip(userZip, false);
2200      if (!transitDays) return null;
2201      
2202      const earliestArrival = this.addBusinessDays(shipDate, transitDays);
2203      const bufferBusinessDays = this.config?.deliveryRules?.bufferBusinessDays || 1;
2204      const latestArrival = this.addBusinessDays(earliestArrival, bufferBusinessDays);
2205      
2206      return {
2207        shipDate: shipDate,
2208        arrivalDate: earliestArrival,
2209        latestArrivalDate: latestArrival,
2210        shipRangeEndDate: this.addBusinessDays(shipDate, bufferBusinessDays),
2211        shipFormatted: shipDate.toLocaleDateString('en-US', shortOptions),
2212        shipRangeFormatted: this.formatShipRange(shipDate),
2213        arrivalFormatted: earliestArrival.toLocaleDateString('en-US', shortOptions),
2214        rangeFormatted: this.formatCompactRange(earliestArrival, latestArrival),
2215        shippingMethod: 'standard'
2216      };
2217    }
2218    
2219    if (shippingMethod === '2day') {
2220      // 2nd Day: Ship date + 2 business days
2221      const secondDayBusinessDays = this.config?.checkoutDecisionTable?.deliveryBusinessDays?.secondDay ?? 2;
2222      arrivalDate = this.addBusinessDays(shipDate, secondDayBusinessDays);
2223      const bufferBusinessDays = this.config?.deliveryRules?.bufferBusinessDays || 1;
2224      return {
2225        shipDate: shipDate,
2226        arrivalDate: arrivalDate,
2227        latestArrivalDate: arrivalDate,
2228        shipRangeEndDate: this.addBusinessDays(shipDate, bufferBusinessDays),
2229        shipFormatted: shipDate.toLocaleDateString('en-US', shortOptions),
2230        shipRangeFormatted: this.formatShipRange(shipDate),
2231        arrivalFormatted: arrivalDate.toLocaleDateString('en-US', shortOptions),
2232        shippingMethod: '2day'
2233      };
2234    }
2235    
2236    if (shippingMethod === 'nextday') {
2237      // Next Day: Ship date + 1 business day
2238      const nextDayBusinessDays = this.config?.checkoutDecisionTable?.deliveryBusinessDays?.nextDay ?? 1;
2239      arrivalDate = this.addBusinessDays(shipDate, nextDayBusinessDays);
2240      const bufferBusinessDays = this.config?.deliveryRules?.bufferBusinessDays || 1;
2241      return {
2242        shipDate: shipDate,
2243        arrivalDate: arrivalDate,
2244        latestArrivalDate: arrivalDate,
2245        shipRangeEndDate: this.addBusinessDays(shipDate, bufferBusinessDays),
2246        shipFormatted: shipDate.toLocaleDateString('en-US', shortOptions),
2247        shipRangeFormatted: this.formatShipRange(shipDate),
2248        arrivalFormatted: arrivalDate.toLocaleDateString('en-US', shortOptions),
2249        shippingMethod: 'nextday'
2250      };
2251    }
2252    
2253    return null;
2254  },
2255
2256  /** Get cached variation display for checkout when no shipping option is selected yet. @param {'edd'|'esd'} variationMode @param {'inStock'|'partnerFulfilled'|'backordered'|'preOrder'} statusType @param {string} esdValue @param {object|null} inStockAggregate @returns {{actionText:string,dateText:string}|null} */
2257  getCheckoutCachedDisplayForNoSelection: function(variationMode, statusType, esdValue, inStockAggregate = null) {
2258    if (statusType !== 'inStock') return null;
2259
2260    if (variationMode === 'esd') {
2261      if (inStockAggregate && inStockAggregate.deliveryInfo && inStockAggregate.deliveryInfo.shipRangeFormatted) {
2262        return { actionText: 'Ships', dateText: inStockAggregate.deliveryInfo.shipRangeFormatted };
2263      }
2264
2265      const aggregateESD = this.getCartPayloadAggregate('esd');
2266      const cachedESDValue = aggregateESD && aggregateESD.esdValue ? aggregateESD.esdValue : esdValue;
2267      const esdDate = this.parseESDDateValue(cachedESDValue);
2268      if (!esdDate) return null;
2269      const shipDate = this.calculateShipDate(esdDate);
2270      return { actionText: 'Ships', dateText: this.formatShipRange(shipDate) };
2271    }
2272
2273    if (variationMode === 'edd') {
2274      if (inStockAggregate && inStockAggregate.deliveryInfo && inStockAggregate.deliveryInfo.rangeFormatted) {
2275        return { actionText: 'Arrives', dateText: inStockAggregate.deliveryInfo.rangeFormatted };
2276      }
2277
2278      // Always recalculate fresh from ESD + ZIP (same path as cart/minicart)
2279      // to ensure dates match across surfaces
2280      if (esdValue) {
2281        const freshZip = this.getUserZipCode();
2282        const directDelivery = this.calculateEDDFromESD(esdValue, freshZip);
2283        if (directDelivery && directDelivery.rangeFormatted) {
2284          return { actionText: 'Arrives', dateText: directDelivery.rangeFormatted };
2285        }
2286      }
2287    }
2288
2289    return null;
2290  },
2291
2292  /** Check whether checkout shipping address is currently valid. @returns {boolean} */
2293  isAddressValid: function() {
2294    const zipInput = document.getElementById('SISZIP');
2295    if (!zipInput) return true;
2296    return !(zipInput.classList && zipInput.classList.contains('samOPC_error'));
2297  },
2298
2299  /** Get checkout stock message HTML based on shipping method and A/B test @param {string} originalStockMsg - raw status text like "In Stock" @param {string} esdValue - ESD in MM/DD/YYYY format @param {string} [tooltipText] - Original tooltip text from page load @returns {string} Formatted stock message HTML */
2300  getCheckoutStockMessage: function(originalStockMsg, esdValue, tooltipText, shippingMethodOverride = null, inStockAggregate = null, statusTypeOverride = null) {
2301    const statusType = statusTypeOverride || this.detectStockStatus(originalStockMsg);
2302
2303    if (statusType === 'backordered' && this.isHideBackorderVariation()) {
2304      return '';
2305    }
2306
2307    // Get stock info based on status text (includes color from stockStatusConfig)
2308    const stockInfo = this.getStockStatusInfo(originalStockMsg);
2309    const fallbackStatusText = (typeof originalStockMsg === 'string' && originalStockMsg.trim())
2310      ? originalStockMsg.trim().replace(/\s+/g, ' ')
2311      : (statusType === 'partnerFulfilled' ? 'Ships from a Trusted Partner' : stockInfo.label);
2312    const tip = tooltipText || this.extractTooltipTextFromMarkup(originalStockMsg);
2313    
2314    // Helper to return stock message with FA icon and drawer trigger (non-EDD format).
2315    // Drawer message: EDD/ESD always use config templates (dates stripped if unavailable), control uses backend tooltip.
2316    const getOriginalStockHTML = () => {
2317      const variationMode = this.getABVariationMode();
2318      const resolvedTip = (variationMode === 'edd' || variationMode === 'esd')
2319        ? this.getDrawerMessage(statusType, variationMode)
2320        : tip;
2321      return `<span class="stock-message__wrapper" data-stock-info-trigger data-tip="${resolvedTip}" tabindex="0" role="button" aria-label="Stock information: ${fallbackStatusText}">
2322        <span class="stock-message__row">
2323          <span class="stock-message__text">${fallbackStatusText}</span>
2324          <i class="fa-solid fa-circle-info stock-message__icon"></i>
2325        </span>
2326      </span>`;
2327    };
2328
2329    const isEnhanceableStatus = this.isEnhanceableStockMessage(originalStockMsg);
2330
2331    // Rule 1: keep original prod status message for non-enhanceable states
2332    if (!isEnhanceableStatus) {
2333      return getOriginalStockHTML();
2334    }
2335    
2336    const variationMode = this.getABVariationMode();
2337    const isEDDVariation = variationMode === 'edd';
2338    const isESDVariation = variationMode === 'esd';
2339    const enhancedStatusLabel = this.getEnhancedStatusLabel(statusType, 'inline', variationMode);
2340    
2341    if (!isEDDVariation && !isESDVariation) {
2342      return getOriginalStockHTML();
2343    }
2344
2345    if (this.isESDValuePastToday(esdValue)) {
2346      return this.buildDatelessVariationPill(originalStockMsg, 'stock-message__wrapper stock-message__wrapper--edd stock-message__wrapper--checkout');
2347    }
2348    
2349    const shippingMethod = shippingMethodOverride || this.getSelectedShippingMethod();
2350
2351    // Non-in-stock: dates come from BE text or ESD, independent of shipping method / zip selection
2352    if (statusType !== 'inStock') {
2353      const displayedActionDate = this.extractDisplayedActionDate(originalStockMsg);
2354      if (displayedActionDate) {
2355        if (this.isDisplayDateTextPastToday(displayedActionDate.dateText)) {
2356          return this.buildDatelessVariationPill(originalStockMsg, 'stock-message__wrapper stock-message__wrapper--edd stock-message__wrapper--checkout');
2357        }
2358        // EDD variation + extracted "Ships" action: skip extracted date and fall through
2359        // to ESD-based path which properly computes arrival dates for EDD mode
2360        const isExtractedShipAction = displayedActionDate.actionText === 'Ships';
2361        if (!(isEDDVariation && isExtractedShipAction)) {
2362          const { start: chkStartDate, end: chkEndDate } = this.parseDisplayDateRange(displayedActionDate.dateText);
2363          return this.buildEnhancedStockMessage(
2364            stockInfo,
2365            enhancedStatusLabel,
2366            displayedActionDate.actionText,
2367            displayedActionDate.dateText,
2368            tip,
2369            'stock-message__wrapper stock-message__wrapper--edd stock-message__wrapper--checkout',
2370            chkStartDate,
2371            chkEndDate
2372          );
2373        }
2374      }
2375
2376      const esdDate = this.parseESDDateValue(esdValue);
2377      if (!esdDate) {
2378        return this.buildDatelessVariationPill(originalStockMsg, 'stock-message__wrapper stock-message__wrapper--edd stock-message__wrapper--checkout');
2379      }
2380
2381      // Apply cutoff logic for ship date (weekends, holidays, 2PM EST cutoff)
2382      const shipDate = this.calculateShipDate(esdDate);
2383
2384      if (isESDVariation) {
2385        const bufferDays = this.config?.deliveryRules?.bufferBusinessDays || 1;
2386        return this.buildEnhancedStockMessage(
2387          stockInfo,
2388          enhancedStatusLabel,
2389          'Ships',
2390          this.formatShipRange(shipDate),
2391          tip,
2392          'stock-message__wrapper stock-message__wrapper--edd stock-message__wrapper--checkout',
2393          shipDate,
2394          this.addBusinessDaysSimple(shipDate, bufferDays)
2395        );
2396      }
2397
2398      // EDD variation: resolve arrival date once, then render
2399      // Priority: expedited method → standard/ground transit → fallback to ship date
2400      let nonInStockActionText = 'Ships';
2401      let nonInStockDateText = this.formatShipRange(shipDate);
2402      const bufferDays = this.config?.deliveryRules?.bufferBusinessDays || 1;
2403      let nonInStockStart = shipDate;
2404      let nonInStockEnd = this.addBusinessDaysSimple(shipDate, bufferDays);
2405
2406      if (shippingMethod === '2day' || shippingMethod === 'nextday') {
2407        const secondDayBusinessDays = this.config?.checkoutDecisionTable?.deliveryBusinessDays?.secondDay ?? 2;
2408        const nextDayBusinessDays = this.config?.checkoutDecisionTable?.deliveryBusinessDays?.nextDay ?? 1;
2409        const arrivalDate = this.addBusinessDays(shipDate, shippingMethod === '2day' ? secondDayBusinessDays : nextDayBusinessDays);
2410        nonInStockActionText = 'Arrives';
2411        nonInStockDateText = arrivalDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
2412        nonInStockStart = arrivalDate;
2413        nonInStockEnd = arrivalDate;
2414      } else {
2415        // Standard, none, or unsupported: use ground transit calculation (same as cart/minicart)
2416        const directDelivery = this.calculateEDDFromESD(esdValue, this.getUserZipCode());
2417        if (directDelivery && directDelivery.rangeFormatted) {
2418          nonInStockActionText = 'Arrives';
2419          nonInStockDateText = directDelivery.rangeFormatted;
2420          nonInStockStart = directDelivery.earliestDelivery;
2421          nonInStockEnd = directDelivery.latestDelivery;
2422        }
2423      }
2424
2425      if (this.isDisplayDateTextPastToday(nonInStockDateText)) {
2426        return this.buildDatelessVariationPill(originalStockMsg, 'stock-message__wrapper stock-message__wrapper--edd stock-message__wrapper--checkout');
2427      }
2428      return this.buildEnhancedStockMessage(
2429        stockInfo,
2430        enhancedStatusLabel,
2431        nonInStockActionText,
2432        nonInStockDateText,
2433        tip,
2434        'stock-message__wrapper stock-message__wrapper--edd stock-message__wrapper--checkout',
2435        nonInStockStart,
2436        nonInStockEnd
2437      );
2438    }
2439
2440    const zipInput = document.getElementById('SISZIP');
2441    const hasZipValue = !!(zipInput && zipInput.value && zipInput.value.trim());
2442    if (!hasZipValue && shippingMethod !== 'none') {
2443      return this.buildDatelessVariationPill(originalStockMsg, 'stock-message__wrapper stock-message__wrapper--edd stock-message__wrapper--checkout');
2444    }
2445
2446    if (statusType === 'inStock' && hasZipValue && !this.isAddressValid()) {
2447      const cachedDisplay = this.getCheckoutCachedDisplayForNoSelection(variationMode, statusType, esdValue, inStockAggregate);
2448      if (cachedDisplay) {
2449        if (this.isDisplayDateTextPastToday(cachedDisplay.dateText)) {
2450          return this.buildDatelessVariationPill(originalStockMsg, 'stock-message__wrapper stock-message__wrapper--edd stock-message__wrapper--checkout');
2451        }
2452        const { start: cStart, end: cEnd } = this.parseDisplayDateRange(cachedDisplay.dateText);
2453        return this.buildEnhancedStockMessage(
2454          stockInfo,
2455          enhancedStatusLabel,
2456          cachedDisplay.actionText,
2457          cachedDisplay.dateText,
2458          tip,
2459          'stock-message__wrapper stock-message__wrapper--edd stock-message__wrapper--checkout',
2460          cStart,
2461          cEnd
2462        );
2463      }
2464    }
2465
2466    if (shippingMethod === 'none') {
2467      const cachedDisplay = this.getCheckoutCachedDisplayForNoSelection(variationMode, statusType, esdValue, inStockAggregate);
2468      if (cachedDisplay) {
2469        if (this.isDisplayDateTextPastToday(cachedDisplay.dateText)) {
2470          return this.buildDatelessVariationPill(originalStockMsg, 'stock-message__wrapper stock-message__wrapper--edd stock-message__wrapper--checkout');
2471        }
2472        const { start: cStart2, end: cEnd2 } = this.parseDisplayDateRange(cachedDisplay.dateText);
2473        return this.buildEnhancedStockMessage(
2474          stockInfo,
2475          enhancedStatusLabel,
2476          cachedDisplay.actionText,
2477          cachedDisplay.dateText,
2478          tip,
2479          'stock-message__wrapper stock-message__wrapper--edd stock-message__wrapper--checkout',
2480          cStart2,
2481          cEnd2
2482        );
2483      }
2484      return this.buildDatelessVariationPill(originalStockMsg, 'stock-message__wrapper stock-message__wrapper--edd stock-message__wrapper--checkout');
2485    }
2486
2487    // Unsupported shipping methods: show dateless variation pill
2488    if (!this.isSupportedCheckoutShippingMethod(shippingMethod)) {
2489      return this.buildDatelessVariationPill(originalStockMsg, 'stock-message__wrapper stock-message__wrapper--edd stock-message__wrapper--checkout');
2490    }
2491
2492    const deliveryInfo = this.calculateCheckoutDelivery(esdValue, shippingMethod);
2493    
2494    // If delivery calculation failed and no aggregate available, show dateless variation pill
2495    if (!deliveryInfo && !(statusType === 'inStock' && inStockAggregate && inStockAggregate.deliveryInfo)) {
2496      return this.buildDatelessVariationPill(originalStockMsg, 'stock-message__wrapper stock-message__wrapper--edd stock-message__wrapper--checkout');
2497    }
2498
2499    const effectiveDeliveryInfo = (statusType === 'inStock' && inStockAggregate && inStockAggregate.deliveryInfo)
2500      ? inStockAggregate.deliveryInfo
2501      : deliveryInfo;
2502    
2503    let actionText, dateText;
2504    
2505    if (shippingMethod === 'standard') {
2506      // Standard shipping: use A/B test variation phrasing
2507      if (isESDVariation) {
2508        // ESD variation: "Ships [range]"
2509        actionText = 'Ships';
2510        dateText = effectiveDeliveryInfo.shipRangeFormatted || this.formatShipRange(effectiveDeliveryInfo.shipDate);
2511      } else {
2512        // EDD variation: "Arrives [range]"
2513        actionText = 'Arrives';
2514        dateText = effectiveDeliveryInfo.rangeFormatted;
2515      }
2516    } else {
2517      // 2nd Day / Next Day: always use "Arrives [single date]"
2518      actionText = 'Arrives';
2519      dateText = effectiveDeliveryInfo.arrivalFormatted;
2520    }
2521
2522    if (this.isDisplayDateTextPastToday(dateText)) {
2523      return this.buildDatelessVariationPill(originalStockMsg, 'stock-message__wrapper stock-message__wrapper--edd stock-message__wrapper--checkout');
2524    }
2525
2526    const formattedActionText = this.getEstimatedActionText(actionText);
2527    
2528    // Resolve drawer message for checkout inline template
2529    let checkoutDrawerStartDate = null;
2530    let checkoutDrawerEndDate = null;
2531    if (shippingMethod === 'standard') {
2532      if (isESDVariation && effectiveDeliveryInfo.shipDate) {
2533        checkoutDrawerStartDate = effectiveDeliveryInfo.shipDate;
2534        checkoutDrawerEndDate = effectiveDeliveryInfo.shipRangeEndDate || this.addBusinessDaysSimple(effectiveDeliveryInfo.shipDate, this.config?.deliveryRules?.bufferBusinessDays || 1);
2535      } else {
2536        checkoutDrawerStartDate = effectiveDeliveryInfo.earliestDelivery || effectiveDeliveryInfo.earliestArrivalDate || null;
2537        checkoutDrawerEndDate = effectiveDeliveryInfo.latestDelivery || effectiveDeliveryInfo.latestArrivalDate || null;
2538      }
2539    } else {
2540      checkoutDrawerStartDate = effectiveDeliveryInfo.arrivalDate || null;
2541      checkoutDrawerEndDate = effectiveDeliveryInfo.arrivalDate || null;
2542    }
2543    const drawerMessage = this.getDrawerMessage(stockInfo.type, variationMode, checkoutDrawerStartDate, checkoutDrawerEndDate);
2544    
2545    // Format: [dot] Status • Ships/Arrives DateRange
2546    // data-stock-info-trigger opens drawer on click
2547    return `<span class="stock-message__wrapper stock-message__wrapper--edd stock-message__wrapper--checkout" data-stock-type="${stockInfo.type}" data-stock-info-trigger data-tip="${drawerMessage}" tabindex="0" role="button" aria-label="Stock information">
2548      <span class="stock-message__row">
2549        <span class="stock-message__dot">${this.getColoredDotSVG()}</span>
2550        <span class="stock-message__status">${enhancedStatusLabel}</span>
2551        <span class="stock-message__sep">\u2022</span>
2552        <span class="stock-message__text">${formattedActionText} <strong>${dateText}</strong> <span class="stock-message__info-icon" aria-hidden="true">${this.getInfoIconSVG('inline')}</span></span>
2553      </span>
2554    </span>`;
2555  },
2556
2557  /** Check if shipping method should display delivery dates @param {'standard'|'2day'|'nextday'|'saturday'|'none'|'other'} shippingMethod @returns {boolean} */
2558  isSupportedCheckoutShippingMethod: function(shippingMethod) {
2559    return shippingMethod === 'standard' || shippingMethod === '2day' || shippingMethod === 'nextday';
2560  },
2561
2562  /** Pick comparison date for checkout aggregate selection. @param {object} deliveryInfo @param {string} shippingMethod @param {boolean} isESDVariation @returns {Date|null} */
2563  getCheckoutDeliverySortDate: function(deliveryInfo, shippingMethod, isESDVariation) {
2564    if (!deliveryInfo) return null;
2565
2566    if (shippingMethod === 'standard') {
2567      if (isESDVariation) {
2568        return deliveryInfo.shipRangeEndDate || null;
2569      }
2570      return deliveryInfo.latestArrivalDate || deliveryInfo.arrivalDate || null;
2571    }
2572
2573    return deliveryInfo.arrivalDate || null;
2574  },
2575
2576  /** Build latest eligible in-stock checkout delivery aggregate. @param {Array} lineItems @param {string} shippingMethod @param {boolean} isESDVariation @returns {{deliveryInfo: object, sortDate: Date}|null} */
2577  getCheckoutInStockAggregate: function(lineItems, shippingMethod, isESDVariation) {
2578    if (!Array.isArray(lineItems) || !lineItems.length) return null;
2579
2580    let latestAggregate = null;
2581
2582    lineItems.forEach((item) => {
2583      if (!item.isEligibleInStock || !item.deliveryInfoForAggregate) return;
2584      const sortDate = this.getCheckoutDeliverySortDate(item.deliveryInfoForAggregate, shippingMethod, isESDVariation);
2585      if (!sortDate) return;
2586
2587      if (!latestAggregate || sortDate > latestAggregate.sortDate) {
2588        latestAggregate = {
2589          deliveryInfo: item.deliveryInfoForAggregate,
2590          sortDate: sortDate
2591        };
2592      }
2593    });
2594
2595    return latestAggregate;
2596  },
2597
2598  /** Debounce checkout stock message updates to avoid repeated render bursts during init. */
2599  scheduleCheckoutStockMessagesUpdate: function() {
2600    if (this._checkoutUpdateTimer) {
2601      clearTimeout(this._checkoutUpdateTimer);
2602    }
2603
2604    this._checkoutUpdateTimer = setTimeout(() => {
2605      this._checkoutUpdateTimer = null;
2606      this.updateCheckoutStockMessages();
2607    }, 60);
2608  },
2609
2610  /** Called by checkout flow after dynamic data updates. @param {string} actn @param {string} fieldId */
2611  onCheckoutFormUpdated: function(actn, fieldId) {
2612    const normalizedActn = (actn || '').toUpperCase();
2613    const normalizedFieldId = (fieldId || '').toUpperCase();
2614    const isInitialLoad = normalizedActn === 'INIT';
2615    const isZipChange = normalizedActn === 'SHIPPING' && normalizedFieldId === 'SISZIP';
2616    const isShippingMethodChange = normalizedActn === 'SHMT' || normalizedFieldId === 'SISHMT';
2617
2618    if (isZipChange) {
2619      const zipInput = document.getElementById('SISZIP');
2620      const hasZipValidationError = !!(zipInput && zipInput.classList && zipInput.classList.contains('samOPC_error'));
2621      if (hasZipValidationError) {
2622        return;
2623      }
2624    }
2625
2626    if (!isInitialLoad && !isZipChange && !isShippingMethodChange) {
2627      return;
2628    }
2629
2630    this.scheduleCheckoutStockMessagesUpdate();
2631  },
2632
2633  /** Backward-compatible wrapper used by template calls. */
2634  onShippingMethodChange: function() {
2635    this.onCheckoutFormUpdated('SHMT', 'SISHMT');
2636  },
2637
2638  /** Called by saveCheckoutForm() when address validation fails. @param {string} fieldId - Field ID that failed validation (e.g., 'SISZIP') */
2639  onAddressValidationError: function(fieldId) {
2640    // Only handle shipping zip errors for now
2641    if (fieldId !== 'SISZIP') return;
2642
2643    const zipInput = document.getElementById('SISZIP');
2644    const zipValue = zipInput && zipInput.value ? zipInput.value.trim() : '';
2645    // Empty ZIP: nothing to recalculate — INIT render already handled this state
2646    if (!zipValue) return;
2647    
2648    // Non-empty zip with backend validation error: remove EDD/ESD dates
2649    const stockMessages = document.querySelectorAll('.newStockMessage');
2650    stockMessages.forEach(stockMsgEl => {
2651      const cartRow = stockMsgEl.closest('.samOPC_row_flex');
2652      if (cartRow) {
2653        const titleEl = cartRow.querySelector('.samOPC_row_flex_desc b');
2654        const itemTitle = titleEl?.textContent?.trim() || null;
2655        if (this.isGiftCardItem(null, itemTitle)) {
2656          stockMsgEl.style.display = 'none';
2657          return;
2658        }
2659      }
2660      const originalText = stockMsgEl.getAttribute('data-original-stock-text');
2661      const originalTip = stockMsgEl.getAttribute('data-original-tip') || this.config?.stockMsgDisplay?.defaultTooltip || 'Customized items may take longer to ship.';
2662      if (originalText) {
2663        const stockInfo = this.getStockStatusInfo(originalText);
2664        const displayText = originalText.trim();
2665        stockMsgEl.innerHTML = `<span class="stock-message__wrapper" data-stock-info-trigger data-tip="${originalTip}" tabindex="0" role="button" aria-label="Stock information: ${displayText}">
2666          <span class="stock-message__row">
2667            <span class="stock-message__dot">${stockInfo.dotSVG}</span>
2668            <span class="stock-message__status">${displayText}</span>
2669          </span>
2670        </span>`;
2671        stockMsgEl.removeAttribute('style');
2672        var p = stockMsgEl.parentElement;
2673        while (p) {
2674          if (p.style.display === 'none') { p.style.display = ''; break; }
2675          p = p.parentElement;
2676        }
2677      }
2678    });
2679  },
2680
2681  /** Update checkout stock messages when shipping method changes */
2682  updateCheckoutStockMessages: function() {
2683    const variationMode = this.getABVariationMode();
2684    const isEDDVariation = variationMode === 'edd';
2685    const isESDVariation = variationMode === 'esd';
2686    
2687    const pageType = document.getElementById("checkoutForm");
2688    if (!pageType) {
2689      return;
2690    }
2691    
2692    const stockMessages = document.querySelectorAll('.newStockMessage');
2693    if (!stockMessages.length) return;
2694
2695    const selectedShippingMethod = this.getSelectedShippingMethod();
2696
2697    const lineItems = [];
2698
2699    stockMessages.forEach((stockMsgEl) => {
2700      // Find per-item ESD from hidden field
2701      let esdValue = null;
2702      const cartRow = stockMsgEl.closest('.samOPC_row_flex');
2703      if (cartRow) {
2704        const esdInput = cartRow.querySelector('input[name^="SIESDT_"]');
2705        if (esdInput && esdInput.value) {
2706          esdValue = esdInput.value.trim();
2707        }
2708        const titleEl = cartRow.querySelector('.samOPC_row_flex_desc b');
2709        const itemTitle = titleEl?.textContent?.trim() || null;
2710        if (this.isGiftCardItem(null, itemTitle)) {
2711          stockMsgEl.style.display = 'none';
2712          return;
2713        }
2714      }
2715
2716      const existingOriginalText = stockMsgEl.getAttribute('data-original-stock-text');
2717      const existingOriginalTip = stockMsgEl.getAttribute('data-original-tip');
2718      const existingOriginalStyle = stockMsgEl.getAttribute('data-original-style');
2719
2720      const textContent = (stockMsgEl.textContent || '').replace(/\s+/g, ' ').trim();
2721      const stockTextEl = stockMsgEl.querySelector('.stock-message__text');
2722      const stockStatusEl = stockMsgEl.querySelector('.stock-message__status');
2723      const extractedOriginalText = (stockTextEl && stockTextEl.textContent ? stockTextEl.textContent : (stockStatusEl && stockStatusEl.textContent ? stockStatusEl.textContent : textContent)).replace(/\s+/g, ' ').trim();
2724      const tipElement = stockMsgEl.querySelector('[data-tip]');
2725      const extractedOriginalTip = tipElement
2726        ? (tipElement.getAttribute('data-tip') || '')
2727        : this.extractTooltipTextFromMarkup(stockMsgEl.innerHTML || '', '');
2728
2729      const originalText = (existingOriginalText || extractedOriginalText || (window.stockStatusConfig?.inStock?.label || '')).replace(/\s+/g, ' ').trim();
2730      const originalTip = (existingOriginalTip !== null ? existingOriginalTip : extractedOriginalTip) || this.config?.stockMsgDisplay?.defaultTooltip || 'Customized items may take longer to ship.';
2731      const statusType = this.detectStockStatus(originalText);
2732      const isEligibleInStock = statusType === 'inStock';
2733      const deliveryInfoForAggregate = isEligibleInStock
2734        ? this.calculateCheckoutDelivery(esdValue, selectedShippingMethod)
2735        : null;
2736
2737      lineItems.push({
2738        stockMsgEl,
2739        setOriginalText: !existingOriginalText,
2740        originalText,
2741        setOriginalTip: existingOriginalTip === null,
2742        originalTip,
2743        setOriginalStyle: existingOriginalStyle === null,
2744        originalStyle: existingOriginalStyle !== null ? existingOriginalStyle : (stockMsgEl.getAttribute('style') || ''),
2745        statusType,
2746        isEligibleInStock,
2747        esdValue,
2748        deliveryInfoForAggregate
2749      });
2750    });
2751
2752    let inStockAggregate = this.getCheckoutInStockAggregate(lineItems, selectedShippingMethod, isESDVariation);
2753
2754    // If primary aggregate is empty (no shipping method selected, or transit data unavailable),
2755    // build a fallback aggregate using calculateEDDFromESD / getShipRangeInfoFromESD
2756    // (same logic cart/minicart use) so all in-stock items show the same slowest date.
2757    if (!inStockAggregate) {
2758      let latestFallback = null;
2759      lineItems.forEach((item) => {
2760        if (!item.isEligibleInStock || !item.esdValue) return;
2761        if (isESDVariation) {
2762          const shipRangeInfo = this.getShipRangeInfoFromESD(item.esdValue);
2763          if (shipRangeInfo && (!latestFallback || shipRangeInfo.rangeEndDate > latestFallback.sortDate)) {
2764            const shipDate = this.calculateShipDate(this.parseESDDateValue(item.esdValue));
2765            latestFallback = {
2766              deliveryInfo: { shipRangeFormatted: this.formatShipRange(shipDate), shipDate: shipDate },
2767              sortDate: shipRangeInfo.rangeEndDate
2768            };
2769          }
2770        } else {
2771          const deliveryInfo = this.calculateEDDFromESD(item.esdValue, this.getUserZipCode());
2772          if (deliveryInfo && deliveryInfo.latestDelivery && (!latestFallback || deliveryInfo.latestDelivery > latestFallback.sortDate)) {
2773            latestFallback = {
2774              deliveryInfo: deliveryInfo,
2775              sortDate: deliveryInfo.latestDelivery
2776            };
2777          }
2778        }
2779      });
2780      if (latestFallback) {
2781        inStockAggregate = latestFallback;
2782      }
2783    }
2784
2785    const pendingUpdates = [];
2786
2787    lineItems.forEach((entry) => {
2788      const updatedMsg = this.getCheckoutStockMessage(
2789        entry.originalText,
2790        entry.esdValue,
2791        entry.originalTip,
2792        selectedShippingMethod,
2793        inStockAggregate,
2794        entry.statusType
2795      );
2796      const currentMarkup = (entry.stockMsgEl.innerHTML || '').replace(/\s+/g, ' ').trim();
2797      const nextMarkup = (updatedMsg || '').replace(/\s+/g, ' ').trim();
2798
2799      pendingUpdates.push({
2800        stockMsgEl: entry.stockMsgEl,
2801        nextMarkup,
2802        updatedMsg,
2803        shouldWriteMarkup: currentMarkup !== nextMarkup,
2804        setOriginalText: entry.setOriginalText,
2805        originalText: entry.originalText,
2806        setOriginalTip: entry.setOriginalTip,
2807        originalTip: entry.originalTip,
2808        setOriginalStyle: entry.setOriginalStyle,
2809        originalStyle: entry.originalStyle
2810      });
2811    });
2812
2813    pendingUpdates.forEach((entry) => {
2814      if (entry.setOriginalText) {
2815        entry.stockMsgEl.setAttribute('data-original-stock-text', entry.originalText);
2816      }
2817
2818      if (entry.setOriginalTip) {
2819        entry.stockMsgEl.setAttribute('data-original-tip', entry.originalTip);
2820      }
2821
2822      if (entry.setOriginalStyle) {
2823        entry.stockMsgEl.setAttribute('data-original-style', entry.originalStyle);
2824      }
2825
2826      if (entry.shouldWriteMarkup) {
2827        entry.stockMsgEl.innerHTML = entry.updatedMsg;
2828        const isEDDMarkup = entry.updatedMsg && entry.updatedMsg.includes('stock-message__wrapper--edd');
2829        if (isEDDMarkup) {
2830          // EDD variation controls its own colors via CSS variables — remove backend inline style
2831          entry.stockMsgEl.removeAttribute('style');
2832        } else if (entry.originalStyle) {
2833          // Non-EDD (original format): restore backend inline style (e.g., color:GREEN)
2834          entry.stockMsgEl.setAttribute('style', entry.originalStyle);
2835        }
2836      }
2837
2838      // Hide Backorder A/B: explicit display overrides CSS .hide-backorder-msg rule
2839      if (entry.updatedMsg) {
2840        entry.stockMsgEl.style.display = this.isHideBackorderVariation() ? 'block' : '';
2841        var p = entry.stockMsgEl.parentElement;
2842        while (p) {
2843          if (p.style.display === 'none') { p.style.display = ''; break; }
2844          p = p.parentElement;
2845        }
2846      }
2847    });
2848
2849    // Note: updateCheckoutESDFields is intentionally NOT called here.
2850    // It overwrites SIESDT_* hidden fields with HTML, corrupting ESD date values
2851    // needed by subsequent renders. It runs only on submitOrder() in CGBCCART2.html.
2852  },
2853
2854  /** Update cart stock messages with EDD/ESD (reads from #SIESDT_X hidden fields) */
2855  updateCartStockMessages: function() {
2856    const variationMode = this.getABVariationMode();
2857    const isEDDVariation = variationMode === 'edd';
2858    const isESDVariation = variationMode === 'esd';
2859    
2860    if (!isEDDVariation && !isESDVariation) {
2861      return;
2862    }
2863
2864    const cartTable = document.querySelector('.shopping-cart__table');
2865    if (!cartTable) {
2866      return;
2867    }
2868
2869    const stockMessages = document.getElementsByClassName('newStockMessage');
2870    if (!stockMessages.length) {
2871      return;
2872    }
2873
2874    let latestESDAggregate = null;
2875    let latestEDDAggregate = null;
2876
2877    for (let i = 0; i < stockMessages.length; i++) {
2878      const stockMsgEl = stockMessages[i];
2879      const sourceStockMsg = stockMsgEl.getAttribute('data-control-stock-msg') || stockMsgEl.innerHTML || '';
2880
2881      if (!this.isEligibleOriginalInStockStatus(sourceStockMsg)) {
2882        continue;
2883      }
2884
2885      let esdValue = null;
2886      const cartRow = stockMsgEl.closest('.shopping-cart__table--row');
2887      if (cartRow) {
2888        const esdInput = cartRow.querySelector('input[name^="SIESDT_"]');
2889        if (esdInput && esdInput.value) {
2890          esdValue = esdInput.value.trim();
2891        }
2892      }
2893
2894      if (!esdValue) {
2895        continue;
2896      }
2897
2898      const shipRangeInfo = this.getShipRangeInfoFromESD(esdValue);
2899      if (shipRangeInfo && (!latestESDAggregate || shipRangeInfo.rangeEndDate > latestESDAggregate.sortDate)) {
2900        latestESDAggregate = {
2901          esdValue: esdValue,
2902          sortDate: shipRangeInfo.rangeEndDate
2903        };
2904      }
2905
2906      const deliveryInfo = this.calculateEDDFromESD(esdValue);
2907      if (deliveryInfo && deliveryInfo.latestDelivery && (!latestEDDAggregate || deliveryInfo.latestDelivery > latestEDDAggregate.sortDate)) {
2908        latestEDDAggregate = {
2909          rangeFormatted: deliveryInfo.rangeFormatted,
2910          sortDate: deliveryInfo.latestDelivery
2911        };
2912      }
2913    }
2914
2915    this._cartPayloadContext = {
2916      ...(this._cartPayloadContext || {}),
2917      timestamp: Date.now(),
2918      esdAggregate: latestESDAggregate,
2919      eddAggregate: latestEDDAggregate
2920    };
2921
2922    for (let i = 0; i < stockMessages.length; i++) {
2923      const stockMsgEl = stockMessages[i];
2924      const controlStockMsg = stockMsgEl.getAttribute('data-control-stock-msg');
2925      const currentStockMsg = controlStockMsg || stockMsgEl.innerHTML;
2926      
2927      // Find per-item ESD and SKU from cart row
2928      let esdValue = null;
2929      let itemSku = null;
2930      const cartRow = stockMsgEl.closest('.shopping-cart__table--row');
2931      if (cartRow) {
2932        const esdInput = cartRow.querySelector('input[name^="SIESDT_"]');
2933        if (esdInput && esdInput.value) {
2934          esdValue = esdInput.value.trim();
2935        }
2936        const itemInfoLIs = cartRow.querySelectorAll('.shopping-cart__item--info li');
2937        itemInfoLIs.forEach(li => {
2938          if (li.textContent.includes('Item#:')) {
2939            itemSku = li.textContent.replace('Item#:', '').trim();
2940          }
2941        });
2942      }
2943
2944      if (this.isGiftCardItem(itemSku, null)) {
2945        stockMsgEl.style.display = 'none';
2946        continue;
2947      }
2948      
2949      const eddStockMsg = this.getMinicartStockMessage(currentStockMsg, true, esdValue);
2950      const currentMarkup = (stockMsgEl.innerHTML || '').replace(/\s+/g, ' ').trim();
2951      const nextMarkup = (eddStockMsg || '').replace(/\s+/g, ' ').trim();
2952      if (currentMarkup !== nextMarkup) {
2953        stockMsgEl.innerHTML = eddStockMsg;
2954        if (eddStockMsg && eddStockMsg.includes('stock-message__wrapper--edd')) {
2955          stockMsgEl.removeAttribute('style');
2956        }
2957      }
2958      // Hide Backorder A/B: explicit display overrides CSS .hide-backorder-msg rule
2959      if (eddStockMsg && this.isHideBackorderVariation()) {
2960        stockMsgEl.style.display = 'block';
2961      }
2962      var p = stockMsgEl.parentElement;
2963      while (p) {
2964        if (p.style.display === 'none') { p.style.display = ''; break; }
2965        p = p.parentElement;
2966      }
2967    }
2968  },
2969
2970  /** Update checkout SIESDT hidden fields for BE round-trip to OC. Called by submitOrder() in CGBCCART2.html before form submission. Only processes mobile section (inside <form>) — desktop section is outside <form> and not submitted. @returns {void} */
2971  updateCheckoutESDFields: function() {
2972    const checkoutForm = document.querySelector('form[name="f"]') || document.getElementById('checkoutForm');
2973    if (!checkoutForm) return;
2974
2975    const stockMessages = checkoutForm.querySelectorAll('.newStockMessage');
2976    if (!stockMessages.length) return;
2977
2978    const temp = document.createElement('textarea');
2979
2980    stockMessages.forEach((stockMsgEl) => {
2981      const cartRow = stockMsgEl.closest('.samOPC_row_flex');
2982      if (!cartRow) return;
2983
2984      const esdInput = cartRow.querySelector('input[name^="SIESDT_"]');
2985      if (!esdInput) return;
2986
2987      const renderedHTML = stockMsgEl.innerHTML || '';
2988      if (!renderedHTML.trim()) return;
2989
2990      // Strip SVGs before encoding — they break BE parsing. OC re-injects them.
2991      const htmlToEncode = renderedHTML.replace(/<svg[\s\S]*?<\/svg>/gi, '');
2992
2993      // HTML-encode so angle brackets survive BE storage (BE strips raw < >)
2994      temp.textContent = htmlToEncode;
2995      esdInput.value = temp.innerHTML;
2996    });
2997  },
2998
2999  // ===== ORDER CONFIRMATION =====
3000
3001  /** Decode HTML-encoded stock messages on OC, re-inject SVGs stripped during checkout, and unhide. BE is the sole data source — FE only decodes entities, restores icons, and unhides. @returns {void} */
3002  processOrderConfirmation: function() {
3003    const stockMsgElements = document.querySelectorAll('[data-target="orderConfirmStockMsg"]');
3004    if (!stockMsgElements.length) return;
3005
3006    /** Decode HTML entities (handles multiple encoding layers from BE). @param {string} html @returns {string} */
3007    function decodeHTMLEntities(html) {
3008      if (!html) return html;
3009      const temp = document.createElement('textarea');
3010      let decoded = html;
3011      let prev = '';
3012      for (let i = 0; i < 5 && decoded !== prev; i++) {
3013        prev = decoded;
3014        temp.innerHTML = decoded;
3015        decoded = temp.value;
3016      }
3017      return decoded;
3018    }
3019
3020    stockMsgElements.forEach((el) => {
3021      try {
3022        const rawContent = el.innerHTML.trim();
3023        if (!rawContent) {
3024          el.classList.remove('order-confirm__stock-msg--hidden');
3025          return;
3026        }
3027
3028        // Decode BE HTML-encoded content back to markup and display
3029        const decoded = decodeHTMLEntities(rawContent);
3030        if (decoded !== rawContent) el.innerHTML = decoded;
3031
3032        // Hide gift card stock messages on order confirmation
3033        const confirmRow = el.closest('.samFlexRow');
3034        if (confirmRow) {
3035          const titleEl = confirmRow.querySelector('.review__item--name b');
3036          const itemTitle = titleEl?.textContent?.trim() || null;
3037          if (this.isGiftCardItem(null, itemTitle)) return;
3038        }
3039
3040        // Set stock type on container so CSS can target it (e.g. green for in-stock)
3041        const stockType = window.edd.detectStockStatus(el.textContent || '');
3042        el.dataset.stockType = stockType;
3043
3044        if (stockType === 'backordered' && this.isHideBackorderVariation()) {
3045          return;
3046        }
3047
3048        // Re-inject SVGs stripped during checkout to prevent BE parsing issues
3049        el.querySelectorAll('.stock-message__dot').forEach(dot => {
3050          if (!dot.querySelector('svg')) dot.innerHTML = window.edd.getColoredDotSVG();
3051        });
3052        el.querySelectorAll('.stock-message__info-icon').forEach(icon => {
3053          if (!icon.querySelector('svg')) icon.innerHTML = window.edd.getInfoIconSVG('inline');
3054        });
3055      } catch (e) {
3056        // Silent fail — show whatever BE rendered
3057      }
3058      el.classList.remove('order-confirm__stock-msg--hidden');
3059    });
3060  },
3061
3062};
3063
3064// Initialize on DOM ready
3065(function() {
3066
3067  /** Get or create shared EDDHandler instance without auto-initializing network work. @returns {EDDHandler} */
3068  function ensureEDDHandler() {
3069    if (!window.eddHandler) {
3070      window.eddHandler = new EDDHandler();
3071    }
3072    return window.eddHandler;
3073  }
3074  
3075  /** Sync window.edd config from constructor-defined EDDHandler config. @param {EDDHandler|null} handlerInstance */
3076  function syncEDDConfigFromConstructor(handlerInstance = null) {
3077    if (!window.edd) return;
3078
3079    const handler = handlerInstance || ensureEDDHandler();
3080    const stockMsgDisplayConfig = handler.config?.stockMsgDisplay || {};
3081    const configSlice = {
3082      deliveryRules: handler.config?.deliveryRules || {},
3083      checkoutDecisionTable: handler.config?.checkoutDecisionTable || {},
3084      stockMsgDisplay: {
3085        defaultTooltip: stockMsgDisplayConfig.defaultTooltip || '',
3086        stockMsgText: stockMsgDisplayConfig.stockMsgText || {},
3087        drawerMessages: stockMsgDisplayConfig.drawerMessages || {}
3088      },
3089      abTestVariations: handler.config?.abTestVariations || {}
3090    };
3091
3092    window.edd.config = JSON.parse(JSON.stringify(configSlice));
3093  }
3094
3095  function init() {
3096    const handler = ensureEDDHandler();
3097    syncEDDConfigFromConstructor(handler);
3098
3099    window.recalculateEDD = () => handler.recalculateDeliveryEstimate();
3100    
3101    const pageTypeInput = document.querySelector('input[name="PAGETYPE"]');
3102    const isPDP = pageTypeInput && pageTypeInput.value === 'PDP';
3103
3104    // Async: refresh transit CSV if expired (non-blocking)
3105    window.edd.ensureTransitDataLoaded();
3106
3107    if (!isPDP) {
3108      return;
3109    }
3110
3111    if (!handler._isEDDInitialized) {
3112      handler._isEDDInitialized = true;
3113      handler.init();
3114    }
3115  }
3116
3117  function initCartPageEDD() {
3118    const checkoutForm = document.getElementById("checkoutForm");
3119    const cartTable = document.querySelector('.shopping-cart__table');
3120    if (cartTable && window.edd && typeof window.edd.updateCartStockMessages === 'function') {
3121      window.edd.updateCartStockMessages();
3122      return;
3123    }
3124
3125    // Checkout: skip direct updateCheckoutStockMessages() call here.
3126    // CGBCCART2.html already calls onCheckoutFormUpdated('INIT') on $(document).ready,
3127    // which triggers the debounced update. Running both causes a duplicate render pass.
3128    if (checkoutForm) {
3129      return;
3130    }
3131  }
3132
3133  /** Re-render cart/minicart EDD when user updates location in the header tracker */
3134  function initLocationChangeListener() {
3135    window.addEventListener('userLocationUpdated', () => {
3136      if (!window.edd) return;
3137
3138      // Cart page: recalculate aggregates and re-render all row stock messages
3139      const cartTable = document.querySelector('.shopping-cart__table');
3140      const checkoutForm = document.getElementById('checkoutForm');
3141      if (cartTable && !checkoutForm && typeof window.edd.updateCartStockMessages === 'function') {
3142        window.edd.updateCartStockMessages();
3143      }
3144    });
3145  }
3146  
3147  function initOrderConfirmation() {
3148    if (document.getElementById('confirm__container_top') && window.edd && typeof window.edd.processOrderConfirmation === 'function') {
3149      window.edd.processOrderConfirmation();
3150    }
3151  }
3152  
3153  // Populate transit data cache synchronously at script-eval time so
3154  // getTransitDaysForZip never re-parses localStorage during renders.
3155  // Must run BEFORE DOMContentLoaded — jQuery's $(document).ready in
3156  // CGBCCART2.html may fire onCheckoutFormUpdated('INIT') before init().
3157  try {
3158    const transitCached = localStorage.getItem('fedexTransitData');
3159    if (transitCached) {
3160      const parsed = JSON.parse(transitCached);
3161      if (parsed.data) window.edd._transitDataCache = parsed.data;
3162    }
3163  } catch (e) { /* silent */ }
3164
3165  if (document.readyState === 'loading') {
3166    document.addEventListener('DOMContentLoaded', function() {
3167      init();
3168      initCartPageEDD();
3169      initLocationChangeListener();
3170      initOrderConfirmation();
3171    });
3172  } else {
3173    init();
3174    initCartPageEDD();
3175    initLocationChangeListener();
3176    initOrderConfirmation();
3177  }
3178})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.