PageSourceSearch

https://www.alliedschools.com/wp-content/plugins/wp-marketing-api/…/colibri-product-loader.js?ver=1789733782

js alliedschools.com collected 2026-09-29 07:55:31 UTC 106,786 bytes, 3,137 lines download raw bytes

1/******/ (() => { // webpackBootstrap
2/******/ 	"use strict";
3
4// UNUSED EXPORTS: applyProductPropertiesToMarketingProduct, fetchOffering, fetchProductPropertiesRow, getOfferingFromProduct
5
6;// ./src/Rendering/scripts/marketingApiAuth.ts
7/**
8 * Browser Authorization parity with PHP {@see \Colibri\MarketingApi\Service\BaseService::authorization_header_value}.
9 * Apply only when calling the upstream Marketing API host (`/marketing/...`),
10 * not the WordPress REST proxy (`/wp-json/.../wp-marketing-api/v1/...`).
11 */
12
13/**
14 * True when fetch targets upstream Marketing paths; WordPress proxies do not get this header.
15 * @param url
16 */
17function marketingApiUrlUsesDirectAuth(url) {
18  const s = String(url || '');
19  if (!/\/?marketing(\/|$)/i.test(s)) {
20    return false;
21  }
22  return !/\/wp-json\/.*?wp-marketing-api\b/i.test(s);
23}
24
25/**
26 * Raw cookie lookup (readable, non–HttpOnly names only).
27 * @param name
28 */
29function cookieValue(name) {
30  if (typeof document === 'undefined') {
31    return '';
32  }
33  const needle = `${name}=`;
34  const parts = document.cookie ? document.cookie.split(';') : [];
35  for (let i = 0; i < parts.length; i++) {
36    const part = parts[i].trim();
37    if (!part.startsWith(needle)) {
38      continue;
39    }
40    const raw = part.slice(needle.length);
41    try {
42      return decodeURIComponent(raw).trim();
43    } catch {
44      return raw.trim();
45    }
46  }
47  return '';
48}
49
50/**
51 * Same scheme as PHP + {@see CartTrafficSplit::insert_frontend_cart_experience_object}.
52 *
53 * Core cart (`cartExperience === 'core'`): upstream Marketing fetches use the readable
54 * `access_token` cookie (Bearer) only — not STYXKEY student cookies. Legacy uses
55 * `X-Credential` when Core token is absent.
56 */
57function getMarketingAuthorizationHeaders() {
58  const accessToken = cookieValue('access_token');
59  const xCredential = cookieValue('X-Credential');
60  const ce = typeof window !== 'undefined' ? window.colibri?.cartExperience : undefined;
61  if (ce === 'core') {
62    return accessToken ? {
63      Authorization: `Bearer ${accessToken}`
64    } : {};
65  }
66  if (ce === 'legacy') {
67    return xCredential ? {
68      Authorization: `xcred ${xCredential}`
69    } : {};
70  }
71  if (accessToken) {
72    return {
73      Authorization: `Bearer ${accessToken}`
74    };
75  }
76  if (xCredential) {
77    return {
78      Authorization: `xcred ${xCredential}`
79    };
80  }
81  return {};
82}
83
84/**
85 * Attach Marketing API Authorization when URL is upstream; merges with optional RequestInit.headers.
86 * @param url
87 * @param init
88 */
89function mergeMarketingAuthHeadersForFetchUrl(url, init) {
90  if (!marketingApiUrlUsesDirectAuth(url)) {
91    return init ? {
92      ...init
93    } : {};
94  }
95  const auth = getMarketingAuthorizationHeaders();
96  if (Object.keys(auth).length === 0) {
97    return init ? {
98      ...init
99    } : {};
100  }
101  const headers = new Headers(init?.headers ?? undefined);
102  for (const [k, v] of Object.entries(auth)) {
103    headers.set(k, v);
104  }
105  const out = init ? {
106    ...init
107  } : {};
108  out.headers = headers;
109  return out;
110}
111;// ./src/Rendering/scripts/marketingProductQueryDefaults.ts
112/**
113 * Default query params for Marketing product endpoints that support promoted discount codes.
114 * Keep in sync with ProductsService::apply_default_discount_code_query_params().
115 */
116
117const MARKETING_PROMOTED_DISCOUNT_CODES_PARAM = 'includeMarketingPromotedDiscountCodes';
118
119/**
120 * Append the default promoted-discount flag when not already set on the params object.
121 */
122function applyMarketingPromotedDiscountCodesDefault(params) {
123  if (!params.has(MARKETING_PROMOTED_DISCOUNT_CODES_PARAM)) {
124    params.set(MARKETING_PROMOTED_DISCOUNT_CODES_PARAM, 'true');
125  }
126  return params;
127}
128
129/**
130 * Merge the default promoted-discount flag into a plain query params record.
131 */
132function withMarketingPromotedDiscountCodesDefault(params) {
133  if (!Object.prototype.hasOwnProperty.call(params, MARKETING_PROMOTED_DISCOUNT_CODES_PARAM)) {
134    return {
135      ...params,
136      [MARKETING_PROMOTED_DISCOUNT_CODES_PARAM]: true
137    };
138  }
139  return params;
140}
141
142/**
143 * True when a URL targets a Marketing product endpoint that accepts the flag.
144 */
145function urlShouldIncludeMarketingPromotedDiscountCodes(url) {
146  const path = String(url || '').split('?')[0] || '';
147  return /\/marketing\/products(?:\/|$)/i.test(path) || /\/products(?:\/|$|-list|-get-by-code|-filters|-filters-and-counts|-with-filters|-related-offerings|-swap-options)/i.test(path);
148}
149
150/**
151 * Append `?includeMarketingPromotedDiscountCodes=true` to product detail URLs when missing.
152 */
153function appendMarketingPromotedDiscountCodesToUrl(url) {
154  if (!urlShouldIncludeMarketingPromotedDiscountCodes(url)) {
155    return url;
156  }
157  const [base, query = ''] = String(url).split('?');
158  const params = new URLSearchParams(query);
159  applyMarketingPromotedDiscountCodesDefault(params);
160  const qs = params.toString();
161  return qs ? `${base}?${qs}` : `${base}?${MARKETING_PROMOTED_DISCOUNT_CODES_PARAM}=true`;
162}
163;// ./src/Gutenberg/Store/queryKey.ts
164/* unused harmony import specifier */ var DEFAULT_PRODUCT_LIST_SORT;
165
166const queryKey_MARKETING_PROMOTED_DISCOUNT_CODES_PARAM = 'includeMarketingPromotedDiscountCodes';
167
168/** Sort filter bracket segments lexicographically for stable cache keys. */
169function canonicalFilterSegments(filterParams) {
170  return (filterParams || []).map(segment =>
170 String(segment || '').trim()).filter(Boolean).sort((a, b) => a.localeCompare(b));
171}
172
173/** Numeric license type ids, sorted ascending. */
174function canonicalLicenseTypeIds(value) {
175  if (Array.isArray(value)) {
176    return [...value].map(n => Number(n)).filter(n => Number.isFinite(n)).sort((a, b) => a - b);
177  }
178  if (typeof value === 'string' && value.trim() !== '') {
179    return value.split(',').map(part => Number(String(part || '').trim())).filter(n => Number.isFinite(n)).sort((a, b) => a - b);
180  }
181  return [];
182}
183
184/** State abbreviations, sorted lexicographically. */
185function canonicalStateAbbvs(value) {
186  if (Array.isArray(value)) {
187    return [...value].map(abbv => String(abbv || '').trim()).filter(Boolean).sort((a, b) => a.localeCompare(b));
188  }
189  if (typeof value === 'string' && value.trim() !== '') {
190    return value.split(',').map(part => String(part || '').trim()).filter(Boolean).sort((a, b) => a.localeCompare(b));
191  }
192  return [];
193}
194function canonicalLicenseTypeIdsCsv(value) {
195  return canonicalLicenseTypeIds(value).join(',');
196}
197function canonicalStateAbbvsCsv(value) {
198  return canonicalStateAbbvs(value).join(',');
199}
200function normalizeSortCompare(a, b) {
201  if (typeof a === 'number' && typeof b === 'number') {
202    return a - b;
203  }
204  if (typeof a === 'string' && typeof b === 'string') {
205    return a.localeCompare(b);
206  }
207  if (typeof a === 'boolean' && typeof b === 'boolean') {
208    if (a === b) {
209      return 0;
210    }
211    return a ? 1 : -1;
212  }
213  try {
214    return JSON.stringify(a).localeCompare(JSON.stringify(b));
215  } catch {
216    return 0;
217  }
218}
219function normalize(value) {
220  if (Array.isArray(value)) {
221    return value.map(normalize).sort(normalizeSortCompare);
222  }
223  if (value && typeof value === 'object') {
224    const keys = Object.keys(value).sort();
225    const out = {};
226    for (const k of keys) {
227      out[k] = normalize(value[k]);
228    }
229    return out;
230  }
231  return value;
232}
233function buildQueryKey(path, params) {
234  if (!params) return path;
235  const entries = Object.entries(params).filter(([_, v]) => v !== undefined && v !== null);
236  if (entries.length === 0) return path;
237  const norm = normalize(Object.fromEntries(entries));
238  return `${path}?${JSON.stringify(norm)}`;
239}
240function applyDefaultProductListSort(params) {
241  const sort = typeof params.sort === 'string' && String(params.sort).trim() !== '' ? params.sort : DEFAULT_PRODUCT_LIST_SORT;
242  return {
243    ...params,
244    sort
245  };
246}
247function applyDefaultPromotedDiscountCodes(params) {
248  if (Object.prototype.hasOwnProperty.call(params, queryKey_MARKETING_PROMOTED_DISCOUNT_CODES_PARAM)) {
249    return params;
250  }
251  return {
252    ...params,
253    [queryKey_MARKETING_PROMOTED_DISCOUNT_CODES_PARAM]: true
254  };
255}
256
257/**
258 * Canonicalize products-list params so reads/writes use the same cache key.
259 * Sorts filter segments (tags, ranges, etc.), license types, and states.
260 */
261function normalizeProductListQueryParams(params) {
262  if (!params) {
263    return undefined;
264  }
265  let q = {
266    ...params
267  };
268  if (Array.isArray(q.licenseTypeIds)) {
269    q.licenseTypeIds = canonicalLicenseTypeIds(q.licenseTypeIds);
270  }
271  if (Array.isArray(q.stateAbbvs)) {
272    q.stateAbbvs = canonicalStateAbbvs(q.stateAbbvs);
273  }
274  if (Array.isArray(q.filter)) {
275    q.filter = canonicalFilterSegments(q.filter);
276  }
277  q = applyDefaultProductListSort(q);
278  q = applyDefaultPromotedDiscountCodes(q);
279  return q;
280}
281
282/**
283 * Canonicalize products-with-filters / filters-and-counts query params.
284 */
285function normalizeProductsFacetQueryParams(params) {
286  if (!params) {
287    return undefined;
288  }
289  let q = {
290    ...params
291  };
292  if (Array.isArray(q.licenseTypeIds)) {
293    q.licenseTypeIds = canonicalLicenseTypeIds(q.licenseTypeIds);
294  }
295  if (Array.isArray(q.stateAbbvs)) {
296    q.stateAbbvs = canonicalStateAbbvs(q.stateAbbvs);
297  }
298  if (Array.isArray(q.filter)) {
299    q.filter = canonicalFilterSegments(q.filter);
300  }
301  q = applyDefaultProductListSort(q);
302  q = applyDefaultPromotedDiscountCodes(q);
303  return q;
304}
305function buildProductsListQueryKey(params) {
306  return buildQueryKey('products-list', normalizeProductListQueryParams(params));
307}
308function buildProductsWithFiltersQueryKey(params) {
309  return buildQueryKey('products-with-filters', normalizeProductsFacetQueryParams(params));
310}
311function buildProductsFiltersAndCountsQueryKey(params) {
312  return buildQueryKey('products-filters-and-counts', normalizeProductsFacetQueryParams(params));
313}
314;// ./src/Gutenberg/constants/productListIncludedOrder.ts
315/**
316 * Preserve ProductQuery / catalogue list order implied by {@link includedItems}
317 * (`itemId` pin & `[itemId==…]` / `[itemId==(…)]`), matching PHP
318 * `ProductQueryContext::order_products_by_included_item_ids` and filter parsing helpers.
319 */
320
321function unescapeFilterValue(raw) {
322  return raw.replace(/\\(.)/g, '$1');
323}
324
325/**
326 * Split a comma-separated tuple body where commas can be escaped with backslash,
327 * aligned with `ProductQueryContext::split_escaped_list`.
328 */
329function splitEscapedCommaList(value) {
330  const out = [];
331  let buffer = '';
332  let escaped = false;
333  for (let i = 0; i < value.length; i++) {
334    const ch = value.charAt(i);
335    if (escaped) {
336      buffer += ch;
337      escaped = false;
338      continue;
339    }
340    if (ch === '\\') {
341      escaped = true;
342      continue;
343    }
344    if (ch === ',') {
345      out.push(unescapeFilterValue(buffer));
346      buffer = '';
347      continue;
348    }
349    buffer += ch;
350  }
351  if (buffer !== '' || out.length === 0) {
352    out.push(unescapeFilterValue(buffer));
353  }
354  return out.map(s => s.trim()).filter(s => s !== '');
355}
356
357/**
358 * Parse ordered itemIds from a multi-value `[itemId==(…)]` or single `[itemId==…]` segment,
359 * aligned with PHP `ordered_included_item_ids_from_filter_segments`.
360 */
361function orderedIncludedItemIdsFromFilterSegments(filterSegments) {
362  for (const rawUntyped of filterSegments) {
363    const raw = String(rawUntyped || '').trim();
364    if (!raw || !raw.startsWith('[itemId==')) {
365      continue;
366    }
367    if (!raw.endsWith(']')) {
368      continue;
369    }
370    const core = raw.slice('[itemId=='.length, -1);
371    if (!core) {
372      continue;
373    }
374    if (core.startsWith('(') && core.endsWith(')')) {
375      const inner = core.slice(1, -1);
376      return splitEscapedCommaList(inner);
377    }
378    return [unescapeFilterValue(core).trim()].filter(id => id !== '');
379  }
380  return [];
381}
382
383/**
384 * Deduped trimmed string ids preserving first-seen positions (aligned with PHP).
385 */
386function normalizeIncludedItemIdsForOrder(source) {
387  if (!Array.isArray(source)) {
388    return [];
389  }
390  const ranks = new Map();
391  let r = 0;
392  for (const item of source) {
393    if (typeof item !== 'string' && typeof item !== 'number') {
394      continue;
395    }
396    const key = String(item).trim();
397    if (!key || ranks.has(key)) {
398      continue;
399    }
400    ranks.set(key, r++);
401  }
402  return Array.from(ranks.keys());
403}
404
405/**
406 * Prefer explicit {@link includedItems}; otherwise read order from `[itemId==…]` in `filter[]`.
407 */
408function resolveIncludedItemsOrderFromQueryParts(parts) {
409  const fromExplicit = normalizeIncludedItemIdsForOrder(parts.includedItems);
410  if (fromExplicit.length) {
411    return fromExplicit;
412  }
413  const segs = Array.isArray(parts.filter) ? parts.filter.map(s => String(s || '').trim()) : [];
414  return orderedIncludedItemIdsFromFilterSegments(segs);
415}
416
417/**
418 * `Record` payload (marketing product JSON): read top-level {@link itemId} / snake_case fallback.
419 */
420function readMarketingProductItemId(product) {
421  if (!product || typeof product !== 'object') {
422    return '';
423  }
424  const id = product.itemId ?? product.item_id ?? '';
425  return typeof id === 'string' || typeof id === 'number' ? String(id).trim() : '';
426}
427
428/** @see UnknownRecord variant for browser loader */
429function orderUnknownRecordsByIncludedItemIds(products, orderedItemIds) {
430  if (!orderedItemIds.length) {
431    return products.slice();
432  }
433  const ranks = new Map();
434  let r = 0;
435  for (const id of orderedItemIds) {
436    const key = String(id || '').trim();
437    if (!key || ranks.has(key)) {
438      continue;
439    }
440    ranks.set(key, r++);
441  }
442  if (ranks.size === 0) {
443    return products.slice();
444  }
445  const max = Number.MAX_SAFE_INTEGER;
446  return products.slice().sort((a, b) => {
447    const ia = readMarketingProductItemId(a);
448    const ib = readMarketingProductItemId(b);
449    const ra = ia !== '' ? ranks.get(ia) ?? max : max;
450    const rb = ib !== '' ? ranks.get(ib) ?? max : max;
451    return ra === rb ? 0 : ra < rb ? -1 : 1;
452  });
453}
454
455/**
456 * OpenAPI-aligned product shape (editor store).
457 */
458function orderMarketingApiProductsByIncludedItemIds(products, orderedItemIds) {
459  if (!orderedItemIds.length) {
460    return products.slice();
461  }
462  const ranks = new Map();
463  let r = 0;
464  for (const id of orderedItemIds) {
465    const key = String(id || '').trim();
466    if (!key || ranks.has(key)) {
467      continue;
468    }
469    ranks.set(key, r++);
470  }
471  if (ranks.size === 0) {
472    return products.slice();
473  }
474  const max = Number.MAX_SAFE_INTEGER;
475  return products.slice().sort((a, b) => {
476    const ia = String((a.itemId ?? '') || '').trim();
477    const ib = String((b.itemId ?? '') || '').trim();
478    const ra = ia !== '' ? ranks.get(ia) ?? max : max;
479    const rb = ib !== '' ? ranks.get(ib) ?? max : max;
480    return ra === rb ? 0 : ra < rb ? -1 : 1;
481  });
482}
483;// ./src/Gutenberg/constants/productListPackageSortOrder.ts
484/**
485 * Sort catalogue products by `offerings[0].properties.packageSortOrder`
486 * (mirrors PHP `ProductQueryContext::order_products_by_first_offering_package_sort_order`
487 * and `ProductRendererMethods::select_offering` using the first offering).
488 *
489 * Pinned roster order via `includedItems` takes precedence; see `productListIncludedOrder.ts`.
490 */
491
492/**
493 * Read packageSortOrder from the first offering on a marketing product payload.
494 */
495function readFirstOfferingPackageSortOrder(product) {
496  if (!product || typeof product !== 'object') {
497    return '';
498  }
499  const offerings = Array.isArray(product.offerings) ? product.offerings : [];
500  const first = offerings[0];
501  if (!first || typeof first !== 'object') {
502    return '';
503  }
504  const offering = first;
505  const props = offering.properties && typeof offering.properties === 'object' ? offering.properties : null;
506  const fromProps = props?.packageSortOrder;
507  if (typeof fromProps === 'string' || typeof fromProps === 'number') {
508    return String(fromProps).trim();
509  }
510  const direct = offering.packageSortOrder;
511  if (typeof direct === 'string' || typeof direct === 'number') {
512    return String(direct).trim();
513  }
514  return '';
515}
516function comparePackageSortOrder(a, b) {
517  if (a === b) {
518    return 0;
519  }
520  if (a === '') {
521    return 1;
522  }
523  if (b === '') {
524    return -1;
525  }
526  const aNum = Number(a);
527  const bNum = Number(b);
528  if (!Number.isNaN(aNum) && !Number.isNaN(bNum)) {
529    return aNum === bNum ? 0 : aNum < bNum ? -1 : 1;
530  }
531  return a.localeCompare(b, undefined, {
532    numeric: true,
533    sensitivity: 'base'
534  });
535}
536
537/** @see readFirstOfferingPackageSortOrder */
538function orderUnknownRecordsByFirstOfferingPackageSortOrder(products) {
539  return products.slice().sort((a, b) => comparePackageSortOrder(readFirstOfferingPackageSortOrder(a), readFirstOfferingPackageSortOrder(b)));
540}
541;// ./src/Gutenberg/Blocks/AddToCart/inc/normalizeProductPropertiesResponse.ts
542function normalizeProductCode(code) {
543  if (code == null || code === '') {
544    return '';
545  }
546  return String(code).trim();
547}
548function unwrapProductPropertiesPayloadArray(data) {
549  if (Array.isArray(data)) {
550    return data;
551  }
552  if (data && typeof data === 'object') {
553    const keys = ['Products', 'products', 'data', 'Data', 'Items', 'items', 'rows', 'Rows'];
554    const o = data;
555    for (let i = 0; i < keys.length; i++) {
556      const v = o[keys[i]];
557      if (Array.isArray(v)) {
558        return v;
559      }
560    }
561  }
562  return [];
563}
564
565/**
566 * Normalize casing/shape so merge + cacheCovers always see `ProductCode`.
567 */
568function normalizeProductPropertiesApiRows(data) {
569  const raw = unwrapProductPropertiesPayloadArray(data);
570  const out = [];
571  for (let i = 0; i < raw.length; i++) {
572    const row = raw[i];
573    if (!row || typeof row !== 'object') {
574      continue;
575    }
576    const r = row;
577    const pcRaw = r.ProductCode ?? r.productCode ?? r.Product_Code ?? r.product_code ?? '';
578    const code = normalizeProductCode(pcRaw);
579    if (!code) {
580      continue;
581    }
582    let props = {};
583    const pascal = r.Properties;
584    const camel = r.properties;
585    if (pascal && typeof pascal === 'object') {
586      props = {
587        ...pascal
588      };
589    } else if (camel && typeof camel === 'object') {
590      props = {
591        ...camel
592      };
593    }
594    out.push({
595      ...r,
596      ProductCode: code,
597      Properties: props,
598      EnrollmentId: r.EnrollmentId ?? r.enrollmentId
599    });
600  }
601  return out;
602}
603
604/**
605 * Marks every requested SKU as satisfied for cacheCovers even when the API omits a row.
606 */
607function ensureProductPropsBucketTracksCodes(requestedCodes) {
608  const codes = [...new Set((requestedCodes || []).map(c => normalizeProductCode(c)).filter(Boolean))];
609  if (!codes.length) {
610    return;
611  }
612  const w = window;
613  if (!w.__colibriProductProps) {
614    w.__colibriProductProps = {
615      response: [],
616      codes: []
617    };
618  }
619  const bucket = w.__colibriProductProps;
620  if (!Array.isArray(bucket.codes)) {
621    bucket.codes = [];
622  }
623  const set = new Set(bucket.codes.map(c => normalizeProductCode(c)).filter(Boolean));
624  for (let i = 0; i < codes.length; i++) {
625    set.add(codes[i]);
626  }
627  bucket.codes = [...set];
628}
629;// ./src/Gutenberg/Blocks/AddToCart/inc/productPropsCache.js
630/**
631 * Shared cache for ProductProperties (prefetch + mediator status checks).
632 *
633 * Shape matches AddToCart.php inline prefetch: { response, codes }.
634 */
635
636/**
637 * @param {unknown} code
638 * @return {string}
639 */
640function productPropsCache_normalizeProductCode(code) {
641  if (code == null || code === '') {
642    return '';
643  }
644  return String(code).trim();
645}
646
647/**
648 * Canonical key for matching DOM / API / prefetch product codes (trim + stable casing).
649 * SKUs are compared case-insensitively so `cacheCovers` stays aligned with PDP buttons.
650 *
651 * @param {unknown} code
652 * @return {string}
653 */
654function normalizeProductCodeForCacheMatch(code) {
655  const t = productPropsCache_normalizeProductCode(code);
656  return t ? t.toLowerCase() : '';
657}
658
659/**
660 * Returns true when the cache bucket has either a response row or an
661 * acknowledged "no row" entry (via bucket.codes) for every requested product
662 * code. Replaces the old cacheBucketSatisfiesProductCodes / responseRowsCoverProductCodes
663 * pair with a single pass.
664 *
665 * @param {{ response?: unknown, codes?: unknown }|null|undefined} bucket
666 * @param {string[]}
666                                               productCodes
667 * @return {boolean}
668 */
669function cacheCovers(bucket, productCodes) {
670  if (!bucket || !Array.isArray(productCodes) || !productCodes.length) {
671    return false;
672  }
673  const requested = [];
674  for (let i = 0; i < productCodes.length; i++) {
675    const norm = normalizeProductCodeForCacheMatch(productCodes[i]);
676    if (norm) {
677      requested.push(norm);
678    }
679  }
680  if (!requested.length) {
681    return false;
682  }
683  const known = new Set();
684  if (Array.isArray(bucket.codes)) {
685    for (const code of bucket.codes) {
686      const norm = normalizeProductCodeForCacheMatch(code);
687      if (norm) {
688        known.add(norm);
689      }
690    }
691  }
692  if (Array.isArray(bucket.response)) {
693    for (const row of bucket.response) {
694      const norm = normalizeProductCodeForCacheMatch(row?.ProductCode);
695      if (norm) {
696        known.add(norm);
697      }
698    }
699  }
700  for (const code of requested) {
701    if (!known.has(code)) {
702      return false;
703    }
704  }
705  return true;
706}
707
708/**
709 * Merge API ProductProperties rows into window.__colibriProductProps in place
710 * (mutating the existing bucket and preserving any prev.codes "requested but
711 * no row" entries). Avoids a full deep clone of the cached response on every
712 * mediator notify.
713 *
714 * @param {Array<{ ProductCode: string, Properties?: object, EnrollmentId?: * }>|null|undefined} response
715 * @return void
716 */
717function mergeColibriProductPropsFromApiResponse(response) {
718  if (!Array.isArray(response) || !response.length) {
719    return;
720  }
721  if (!window.__colibriProductProps) {
722    window.__colibriProductProps = {
723      response: [],
724      codes: []
725    };
726  }
727  const bucket = window.__colibriProductProps;
728  if (!Array.isArray(bucket.response)) {
729    bucket.response = [];
730  }
731  if (!Array.isArray(bucket.codes)) {
732    bucket.codes = [];
733  }
734  const codeIndex = new Map();
735  for (let i = 0; i < bucket.response.length; i++) {
736    const row = bucket.response[i];
737    if (row?.ProductCode != null) {
738      codeIndex.set(row.ProductCode, i);
739    }
740  }
741  const codeSet = new Set(bucket.codes);
742  for (const row of response) {
743    if (!row?.ProductCode) {
744      continue;
745    }
746    const existingIdx = codeIndex.get(row.ProductCode);
747    if (existingIdx !== undefined) {
748      const existing = bucket.response[existingIdx];
749      bucket.response[existingIdx] = {
750        ...existing,
751        ...row,
752        Properties: {
753          ...(existing.Properties || {}),
754          ...(row.Properties || {})
755        },
756        EnrollmentId: row.EnrollmentId ?? existing.EnrollmentId
757      };
758    } else {
759      bucket.response.push({
760        ProductCode: row.ProductCode,
761        ...row,
762        Properties: {
763          ...(row.Properties || {})
764        }
765      });
766      codeIndex.set(row.ProductCode, bucket.response.length - 1);
767    }
768    codeSet.add(row.ProductCode);
769  }
770  bucket.codes = [...codeSet];
771}
772
773/**
774 * @param {string}                           productCode
775 * @param {Record<string, *>|null|undefined} productStatus
776 * @return void
777 */
778function patchColibriProductPropsFromMediatorStatus(productCode, productStatus) {
779  if (!productCode || !productStatus || typeof productStatus !== 'object') {
780    return;
781  }
782  const bool = v => v ? 'True' : 'False';
783  const props = {};
784  if (productStatus.inMembership !== undefined) {
785    props.InMembership = bool(productStatus.inMembership);
786  }
787  if (productStatus.isEnrolled !== undefined) {
788    props.IsEnrolled = bool(productStatus.isEnrolled);
789  }
790  if (productStatus.inCart !== undefined) {
791    props.InCart = bool(productStatus.inCart);
792  }
793  if (productStatus.isInvalid !== undefined) {
794    props.Invalid = bool(productStatus.isInvalid);
795  }
796  if (productStatus.soldOutFromProductProps) {
797    if (props.SeatsRemaining === undefined) {
798      props.SeatsRemaining = '0';
799    }
800  }
801  if (productStatus.seatsRemaining !== undefined) {
802    props.SeatsRemaining = productStatus.seatsRemaining === false ? '' : String(productStatus.seatsRemaining);
803  }
804  if (productStatus.upsell?.name) {
805    props.UpsellMembershipName = productStatus.upsell.name;
806    if (productStatus.upsell.productCode != null) {
807      props.UpsellMembershipProductCode = productStatus.upsell.productCode;
808    }
809    if (productStatus.upsell.monthlyPrice != null) {
810      props.UpsellMembershipMonthlyPrice = productStatus.upsell.monthlyPrice;
811    }
812    if (productStatus.upsell.months != null) {
813      props.UpsellMembershipMonths = productStatus.upsell.months;
814    }
815  }
816  if (!Object.keys(props).length && !productStatus.enrollmentId) {
817    return;
818  }
819  const row = {
820    ProductCode: productCode,
821    Properties: props
822  };
823  if (productStatus.enrollmentId) {
824    row.EnrollmentId = productStatus.enrollmentId;
825  }
826  mergeColibriProductPropsFromApiResponse([row]);
827}
828function clearColibriProductPropsLoading() {
829  delete window.__colibriProductPropsLoading;
830}
831
832/**
833 * Drop the prefetch/merge bucket so a new ProductProperties round-trip (same URL/body
834 * as today) cannot satisfy cacheCovers with cart rows from a prior session.
835 */
836function resetColibriProductPropsForSessionRecheck() {
837  if (window.__colibriProductProps) {
838    delete window.__colibriProductProps;
839  }
840  clearColibriProductPropsLoading();
841}
842;// ./src/Gutenberg/Blocks/AddToCart/inc/utils.js
843/* unused harmony import specifier */ var parseGa4Price;
844/* unused harmony import specifier */ var pushGa4ViewItemListEvent;
845/* unused harmony import specifier */ var pushViewItemListAnalyticsCore;
846
847
848
849/**
850 * Read a cookie by name.
851 * @param {string} name - The name of the cookie.
852 * @return {string|null} - The value of the cookie or null if not found.
853 */
854function getCookie(name) {
855  const prefix = `${name}=`;
856  const parts = document.cookie.split('; ');
857  for (let i = 0; i < parts.length; i += 1) {
858    const cookie = parts[i].trim();
859    if (cookie.startsWith(prefix)) {
860      return cookie.slice(prefix.length);
861    }
862  }
863  return null;
864}
865
866/**
867 * Set a cookie with the given name, value, and expiration days.
868 * @param {string} name  - The name of the cookie.
869 * @param {string} value - The value of the cookie.
870 * @param {number} days  - The number of days until expiration.
871 */
872function setCookie(name, value = '', days) {
873  const expires = days ? `; expires=${new Date(Date.now() + days * 864e5).toUTCString()}` : '';
874  const domainParts = location.host.split('.').reverse();
875  const domain = domainParts.length > 1 ? `; domain=.${domainParts[1]}.${domainParts[0]}` : '';
876  document.cookie = `${name}=${value}${expires}${domain}; path=/`;
877}
878
879/**
880 * Delete a cookie by name.
881 * @param {string} name - The name of the cookie to delete.
882 */
883function deleteCookie(name) {
884  document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
885}
886let cachedUser = null;
887let cachedUserCookieString = null;
888
889/**
890 * Cached snapshot of the user's status derived from cookies. The cache is
891 * invalidated automatically whenever document.cookie changes (cheap string
892 * compare instead of re-parsing every cookie variable on every call).
893 */
894function freshUserStatus() {
895  if (cachedUser !== null && cachedUserCookieString === document.cookie) {
896    return cachedUser;
897  }
898  cachedUserCookieString = document.cookie;
899  const studentId = getCookie('STYXKEY-McKissock-UserId');
900  const cartId = getCookie('STYXKEY-shoppingcartid');
901  const role = getCookie('STYXKEY-McKissock-Role');
902  let isLoggedIn;
903  if (studentId && role && role !== 'Guest') {
904    isLoggedIn = true;
905  } else if (role === 'Guest') {
906    isLoggedIn = 'Guest';
907  } else {
908    isLoggedIn = false;
909  }
910  cachedUser = {
911    studentId: studentId || '',
912    cartId: cartId || '',
913    role: role || '',
914    isLoggedIn
915  };
916  return cachedUser;
917}
918const DATA_STORE_KEY = '__colibriProductButtonData';
919function normalizeElements(value) {
920  if (!value) {
921    return [];
922  }
923  if (value instanceof Element || value === window || value === document) {
924    return [value];
925  }
926  if (Array.isArray(value)) {
927    return value.filter(Boolean);
928  }
929  if (value instanceof NodeList || value instanceof HTMLCollection) {
930    return Array.from(value);
931  }
932  return [value];
933}
934function getElementData(element, key) {
935  if (!element) {
936    return undefined;
937  }
938  const localData = element[DATA_STORE_KEY];
939  if (localData && Object.prototype.hasOwnProperty.call(localData, key)) {
940    return localData[key];
941  }
942  return undefined;
943}
944function setElementData(element, key, value) {
945  if (!element) {
946    return value;
947  }
948  const store = element[DATA_STORE_KEY] || (element[DATA_STORE_KEY] = {});
949  store[key] = value;
950  return value;
951}
952function removeElementData(element, key) {
953  if (!element || !element[DATA_STORE_KEY]) {
954    return;
955  }
956  delete element[DATA_STORE_KEY][key];
957}
958function parseJSONAttribute(element, attributeName, fallback = {}) {
959  const rawValue = element?.getAttribute(`data-${attributeName}`);
960  if (!rawValue) {
961    return fallback;
962  }
963  try {
964    return JSON.parse(rawValue);
965  } catch (error) {
966    console.error(`Unable to parse data-${attributeName}`, error);
967    return fallback;
968  }
969}
970function emitEvent(target, name, detail = null) {
971  normalizeElements(target).forEach(currentTarget => {
972    if (currentTarget?.dispatchEvent) {
973      currentTarget.dispatchEvent(new CustomEvent(name, {
974        detail,
975        bubbles: true
976      }));
977    }
978  });
979}
980
981/**
982 * Marketing API origin for learner endpoints (no trailing slash).
983 * Prefers `window.colibri.marketingApi.baseUrl`, then `settings.baseUri`.
984 *
985 * @return {string}
986 */
987function getMarketingApiBaseUrl() {
988  const m = window.colibri?.marketingApi;
989  const raw = m && (m.baseUrl || m.settings?.baseUri) || '';
990  return String(raw || '').replace(/\/+$/, '');
991}
992
993/**
994 * Headers for direct browser calls to Marketing / learner APIs: X-Host + Authorization
995 * (Bearer from access_token cookie, else xcred from X-Credential), aligned with
996 * server-side Marketing API config.
997 *
998 * @return {Record<string, string>}
999 */
1000function buildMarketingLearnerRequestHeaders() {
1001  const settings = window.colibri?.marketingApi?.settings || {};
1002  const xHost = settings.xHost || '';
1003  const headers = {
1004    Accept: 'application/json',
1005    'Content-Type': 'application/json'
1006  };
1007  if (xHost) {
1008    headers['X-Host'] = xHost;
1009  }
1010  const access = getCookie('access_token') || getCookie('STYXKEY-access_token');
1011  const xcred = getCookie('X-Credential') || getCookie('STYXKEY-X-Credential');
1012  if (access) {
1013    headers.Authorization = `Bearer ${access}`;
1014  } else if (xcred) {
1015    headers.Authorization = `xcred ${xcred}`;
1016  }
1017  return headers;
1018}
1019
1020/**
1021 * Cart CTAs must read {@see price}, {@see subscriptionId}, etc. from an **offering** row,
1022 * not a Marketing *product* wrapper. Mirrors `getOfferingFromProduct()` in theme
1023 * {@see cl2-pdp-offering.js}: match `offerings[].productCode` to `preferredProductCode`,
1024 * otherwise the first offering.
1025 *
1026 * @param {unknown} value
1027 * @param {string}  [preferredProductCode]
1028 * @return {object|null}
1029 */
1030function resolveMarketingProductOfferingRow(value, preferredProductCode = '') {
1031  if (!value || typeof value !== 'object') {
1032    return null;
1033  }
1034  const hint = String(preferredProductCode || '').trim();
1035  const rowProductCode = o => {
1036    if (!o || typeof o !== 'object') {
1037      return '';
1038    }
1039    const props = o.properties;
1040    const fromProps = props && typeof props === 'object' ? String(props.productCode ?? '').trim() : '';
1041    return fromProps || String(o.productCode ?? '').trim();
1042  };
1043  if (Array.isArray(value.offerings) && value.offerings.length) {
1044    if (hint) {
1045      const match = value.offerings.find(o => rowProductCode(o) === hint);
1046      if (match) {
1047        return match;
1048      }
1049    }
1050    return /** @type {object} */value.offerings[0];
1051  }
1052  return /** @type {object} */value;
1053}
1054
1055/**
1056 * Whether an offering payload matches the sku string against `sku`, `offeringId`, or `id`.
1057 *
1058 * @param {unknown} offering
1059 * @param {string}  skuTrimmed Already normalized with {@see String.trim()}.
1060 * @return {boolean}
1061 */
1062function marketingOfferingMatchesSkuKey(offering, skuTrimmed) {
1063  if (!offering || typeof offering !== 'object' || !skuTrimmed) {
1064    return false;
1065  }
1066  const keys = ['sku', 'offeringId', 'id'];
1067  for (const k of keys) {
1068    if (Object.prototype.hasOwnProperty.call(offering, k) && String(offering[k] ?? '').trim() === skuTrimmed) {
1069      return true;
1070    }
1071  }
1072  const props = offering.properties;
1073  if (props && typeof props === 'object') {
1074    for (const k of ['sku', 'offeringId']) {
1075      if (Object.prototype.hasOwnProperty.call(props, k) && String(props[k] ?? '').trim() === skuTrimmed) {
1076        return true;
1077      }
1078    }
1079  }
1080  return false;
1081}
1082
1083/**
1084 * Pick one offering row for CTA logic: honor {@see sku} when present,
1085 * otherwise match {@see preferredProductCode} / first listing (Marketing product wrapper).
1086 *
1087 * @param {unknown} wrapper                 Product `{ offerings[] }`, a single offering, or PDP map row.
1088 * @param {string}  [sku]
1089 * @param {string}  [preferredProductCode]
1090 * @return {object|null}
1091 */
1092function resolveMarketingOfferingForCta(wrapper, sku = '', preferredProductCode = '') {
1093  if (!wrapper || typeof wrapper !== 'object') {
1094    return null;
1095  }
1096  const skuClean = String(sku || '').trim();
1097  const hints = Array.isArray(wrapper.offerings) ? wrapper.offerings : [];
1098  if (hints.length) {
1099    if (skuClean) {
1100      const hit = hints.find(row => row && typeof row === 'object' && marketingOfferingMatchesSkuKey(row, skuClean));
1101      if (hit) {
1102        return hit;
1103      }
1104      return null;
1105    }
1106    return resolveMarketingProductOfferingRow(wrapper, preferredProductCode);
1107  }
1108  if (skuClean) {
1109    return marketingOfferingMatchesSkuKey(wrapper, skuClean) ? (/** @type {object} */wrapper) : null;
1110  }
1111  return resolveMarketingProductOfferingRow(wrapper, preferredProductCode);
1112}
1113
1114/**
1115 * True when ProductProperties exposes a truthy "covered by plan" hint — supplementary only.
1116 * Absence or falseness here does **not** prove the SKU is outside a subscription catalog row:
1117 * {@see isMarketingOfferingIncludedInSubscriptionPlan} on Marketing offerings is authoritative for that case.
1118 *
1119 * @param {Record<string, *>|null|undefined} props
1120 * @return {boolean}
1121 */
1122function isSubscriptionCoverageFromProductProps(props) {
1123  if (!props || typeof props !== 'object') {
1124    return false;
1125  }
1126  const truthyScalar = v => {
1127    if (v === true) {
1128      return true;
1129    }
1130    if (v === false || v == null || v === '') {
1131      return false;
1132    }
1133    if (typeof v === 'string') {
1134      const t = v.trim().toLowerCase();
1135      return t === 'true' || t === '1' || t === 'yes' || t === 'y';
1136    }
1137    return false;
1138  };
1139  const keys = ['inMembership', 'includedInSubscription'];
1140  for (const key of keys) {
1141    if (Object.prototype.hasOwnProperty.call(props, key)) {
1142      if (truthyScalar(props[key])) {
1143        return true;
1144      }
1145    }
1146  }
1147  return false;
1148}
1149
1150/**
1151 * Subscription-catalog / included-in-plan SKU per Marketing API offering payload:
1152 * price === 0 and subscriptionId !== 0.
1153 *
1154 * @param {object|null|undefined} offering Resolved Marketing offering row (camelCase).
1155 * @return {boolean}
1156 */
1157function isMarketingOfferingIncludedInSubscriptionPlan(offering) {
1158  if (!offering || typeof offering !== 'object') {
1159    return false;
1160  }
1161  const rawPrice = offering.price ?? offering.unitPrice;
1162  const rawSub = offering.subscriptionId;
1163  const price = rawPrice === '' || rawPrice == null ? NaN : Number.parseFloat(String(rawPrice).replace(/[^0-9.-]/g, ''), 10);
1164  const subscriptionId = Number(rawSub ?? 0);
1165  return Number.isFinite(price) && price === 0 && Number.isFinite(subscriptionId) && subscriptionId !== 0;
1166}
1167function requestJSON({
1168  url,
1169  method = 'POST',
1170  data = null,
1171  withCredentials = false,
1172  headers = {
1173    'Content-Type': 'application/json'
1174  }
1175}) {
1176  return fetch(url, {
1177    method,
1178    headers,
1179    credentials: withCredentials ? 'include' : 'same-origin',
1180    body: data != null ? JSON.stringify(data) : null
1181  }).then(async response => {
1182    const xhr = {
1183      status: response.status,
1184      statusText: response.statusText
1185    };
1186    if (!response.ok) {
1187      return Promise.reject({
1188        statusText: 'error',
1189        xhr
1190      });
1191    }
1192    const text = await response.text();
1193    let body = null;
1194    if (text) {
1195      try {
1196        body = JSON.parse(text);
1197      } catch (error) {
1198        return Promise.reject({
1199          statusText: 'error',
1200          xhr,
1201          error
1202        });
1203      }
1204    }
1205    return {
1206      data: body,
1207      statusText: 'success',
1208      xhr
1209    };
1210  }, error => Promise.reject({
1211    statusText: 'error',
1212    xhr: null,
1213    error
1214  }));
1215}
1216function isVisible(element) {
1217  return !!(element && (element.offsetWidth || element.offsetHeight || element.getClientRects().length));
1218}
1219function escapeSelectorValue(value) {
1220  if (window.CSS?.escape) {
1221    return window.CSS.escape(value);
1222  }
1223  return String(value).replace(/["\\]/g, '\\$&');
1224}
1225function logDataLayerSkip(reason, detail = null) {
1226  console.warn('[dataLayer] skipped push', reason, detail);
1227}
1228
1229/**
1230 * Push a standardized GA4 `add_to_cart` event after a successful c
1230art action.
1231 * Payload fields are read directly from the Marketing offering (and product display name).
1232 *
1233 * @param {{
1234 *   productCode?: string,
1235 *   offering?: object|null,
1236 *   productName?: string,
1237 *   quantity?: number,
1238 * }} [opts]
1239 * @return {boolean} Whether a dataLayer push was attempted.
1240 */
1241function pushGa4AddToCartEvent(opts = {}) {
1242  const offering = opts.offering && typeof opts.offering === 'object' ? opts.offering : null;
1243  if (!offering) {
1244    logDataLayerSkip('add_to_cart: missing offering', opts);
1245    return false;
1246  }
1247  const item_id = String(offering.productCode ?? opts.productCode ?? '').trim();
1248  const item_name = String(opts.productName ?? '').trim();
1249  const price = parseGa4Price(offering.price ?? offering.originalPrice);
1250  const quantity = Number.isFinite(opts.quantity) && opts.quantity > 0 ? opts.quantity : 1;
1251  if (!item_id || !item_name || !Number.isFinite(price)) {
1252    logDataLayerSkip('add_to_cart: invalid GA4 item payload', {
1253      item_id,
1254      item_name,
1255      price,
1256      quantity,
1257      productCode: opts.productCode
1258    });
1259    return false;
1260  }
1261  window.dataLayer = window.dataLayer || [];
1262  const value = Math.round(price * quantity * 100) / 100;
1263  window.dataLayer.push({
1264    ecommerce: null
1265  });
1266  window.dataLayer.push({
1267    event: 'add_to_cart',
1268    ecommerce: {
1269      currency: 'USD',
1270      value,
1271      items: [{
1272        item_id,
1273        item_name,
1274        price,
1275        quantity
1276      }]
1277    }
1278  });
1279  return true;
1280}
1281
1282/**
1283 * Push GA4 `add_to_cart` from Marketing offering + product display name.
1284 *
1285 * @param {{
1286 *   productCode?: string,
1287 *   offering?: object|null,
1288 *   productName?: string,
1289 *   quantity?: number,
1290 * }} [opts]
1291 * @return {boolean} Whether a dataLayer push was attempted.
1292 */
1293function pushAddToCartAnalytics(opts = {}) {
1294  return pushGa4AddToCartEvent(opts);
1295}
1296const GA4_VIEW_ITEM_TYPES = new Set(['detail', 'quick', 'option']);
1297
1298/**
1299 * Push a standardized GA4 `view_item` event when a product is viewed.
1300 *
1301 * @param {{
1302 *   productCode?: string,
1303 *   offering?: object|null,
1304 *   productName?: string,
1305 *   viewType?: 'detail'|'quick'|'option',
1306 * }} [opts]
1307 * @return {boolean} Whether a dataLayer push was attempted.
1308 */
1309function pushGa4ViewItemEvent(opts = {}) {
1310  const offering = opts.offering && typeof opts.offering === 'object' ? opts.offering : null;
1311  if (!offering) {
1312    logDataLayerSkip('view_item: missing offering', opts);
1313    return false;
1314  }
1315  const viewType = GA4_VIEW_ITEM_TYPES.has(opts.viewType) ? opts.viewType : 'detail';
1316  const item_id = String(offering.productCode ?? opts.productCode ?? '').trim();
1317  const item_name = String(opts.productName ?? '').trim();
1318  const price = parseGa4Price(offering.price ?? offering.originalPrice);
1319  if (!item_id || !item_name || !Number.isFinite(price)) {
1320    logDataLayerSkip('view_item: invalid GA4 item payload', {
1321      item_id,
1322      item_name,
1323      price,
1324      viewType,
1325      productCode: opts.productCode
1326    });
1327    return false;
1328  }
1329  window.dataLayer = window.dataLayer || [];
1330  window.dataLayer.push({
1331    ecommerce: null
1332  });
1333  window.dataLayer.push({
1334    event: 'view_item',
1335    view_type: viewType,
1336    ecommerce: {
1337      currency: 'USD',
1338      items: [{
1339        item_id,
1340        item_name,
1341        price
1342      }]
1343    }
1344  });
1345  return true;
1346}
1347
1348/**
1349 * Push GA4 `view_item` from Marketing offering + product display name.
1350 *
1351 * @param {{
1352 *   productCode?: string,
1353 *   offering?: object|null,
1354 *   product?: object|null,
1355 *   productName?: string,
1356 *   viewType?: 'detail'|'quick'|'option',
1357 * }} [opts]
1358 * @return {boolean} Whether a dataLayer push was attempted.
1359 */
1360function pushViewItemAnalytics(opts = {}) {
1361  const productCode = String(opts.productCode || '').trim();
1362  let offering = opts.offering && typeof opts.offering === 'object' ? opts.offering : null;
1363  let productName = String(opts.productName ?? '').trim();
1364  const product = opts.product && typeof opts.product === 'object' ? opts.product : null;
1365  if (product && (!offering || !productName)) {
1366    offering = offering || resolveMarketingOfferingForCta(product, '', productCode);
1367    if (!productName) {
1368      productName = String(product.name ?? '').trim();
1369    }
1370  }
1371  return pushGa4ViewItemEvent({
1372    productCode,
1373    offering,
1374    productName,
1375    viewType: opts.viewType
1376  });
1377}
1378
1379/**
1380 * Push GA4 `view_item_list` from Marketing products (listing impressions).
1381 *
1382 * @param {{
1383 *   products?: Array<object>,
1384 *   items?: Array<object>,
1385 *   itemListName?: string,
1386 *   resolveOffering?: function(object, string): object|null,
1387 * }} [opts]
1388 * @return {boolean}
1389 */
1390function pushViewItemListAnalytics(opts = {}) {
1391  if (Array.isArray(opts.items) && opts.items.length) {
1392    return pushGa4ViewItemListEvent(opts);
1393  }
1394  return pushViewItemListAnalyticsCore({
1395    ...opts,
1396    resolveOffering: opts.resolveOffering || ((product, code) => resolveMarketingOfferingForCta(product, '', code))
1397  });
1398}
1399function pushAnalyticsData(productCode, action, productsInList, options) {
1400  if (action === 'add_to_cart' && productCode && productCode !== 'list') {
1401    pushAddToCartAnalytics({
1402      productCode,
1403      offering: options?.offering ?? null,
1404      productName: options?.productName ?? '',
1405      quantity: options?.quantity ?? 1
1406    });
1407    return;
1408  }
1409  if (action === 'view_item' && productCode && productCode !== 'list') {
1410    pushViewItemAnalytics({
1411      productCode,
1412      offering: options?.offering ?? null,
1413      product: options?.product ?? null,
1414      productName: options?.productName ?? '',
1415      viewType: options?.viewType ?? 'detail'
1416    });
1417    return;
1418  }
1419  if (action === 'view_item_list' || productCode === 'list') {
1420    pushViewItemListAnalytics({
1421      products: Array.isArray(productsInList) ? productsInList : [],
1422      items: options?.items,
1423      itemListName: options?.itemListName ?? options?.item_list_name ?? '',
1424      resolveOffering: options?.resolveOffering
1425    });
1426    return;
1427  }
1428  window.colibri?.utils?.pushAnalyticsData?.(productCode, action, productsInList);
1429}
1430
1431/**
1432 * Student ProductProperties and marketing SKU prefetch must use the helpers in this
1433 * section (`shouldFetchMarketingApisForAddToCartDomScope`, `buildCartMarketingStudentPrefetchGateSets`,
1434 * and related exports) so `select.select-state` stays aligned with `button[data-product-code]`.
1435 * Batched prefetch in `product-loader.ts` builds a document snapshot via `buildCartMarketingStudentPrefetchGateSets`;
1436 * `ProductButtonMediator` applies the same gate per registered `[data-legacy-add-to-cart]` control
1437 * (`productCodesEligibleForMarketingStatusFetch`).
1438 */
1439
1440/**
1441 * When the closest PDP cart wrapper contains `select.select-state`, defer Marketing
1442 * ProductProperties + product-by-code hydration until the select value matches the
1443 * cart button `data-product-code` (avoids fetching for a stale SSR code).
1444 *
1445 * @param {Element|null|undefined} scopeEl Element inside `[data-legacy-add-to-cart]`, `[data-core-add-to-cart]`, etc.
1446 * @param {Element|null|undefined} [buttonEl] Optional `button[data-product-code]` (Core passes the concrete button).
1447 * @return {boolean} `false` → omit ProductProperties / `products` GET-by-code for this scope.
1448 */
1449function shouldFetchMarketingApisForAddToCartDomScope(scopeEl, buttonEl = null) {
1450  if (!(scopeEl instanceof Element)) {
1451    return true;
1452  }
1453  if (scopeEl.closest('.upsell-block') || buttonEl instanceof Element && buttonEl.closest('.upsell-block')) {
1454    return false;
1455  }
1456  const wrap = scopeEl.closest('.add-to-cart-container, .individual-add-to-cart-container');
1457  if (!wrap) {
1458    return true;
1459  }
1460  if (wrap.closest('.upsell-block')) {
1461    return false;
1462  }
1463  const stateSelect = wrap.querySelector('select.select-state');
1464  if (!stateSelect) {
1465    return true;
1466  }
1467  const btn = buttonEl instanceof Element ? buttonEl : wrap.querySelector('button[data-product-code]');
1468  const buttonCode = String(btn?.getAttribute?.('data-product-code') ?? '').trim();
1469  const selected = String(/** @type {HTMLSelectElement} */stateSelect.value ?? '').trim();
1470  if (wrap.querySelector('select.select-seminar-offering, select.select-session, select.select-parent-seminar')) {
1471    return !!selected;
1472  }
1473  return selected === buttonCode;
1474}
1475
1476/**
1477 * Locate legacy `[data-legacy-add-to-cart]` or Core `[data-core-add-to-cart]` controls whose
1478 * primary SKU matches `productCode`, optionally scoped to a PDP subtree (`scopeRoot`).
1479 *
1480 * @param {string} productCode
1481 * @param {Element|null} scopeRoot e.g. `[data-colibri-pdp]`
1482 * @return {{ scope: Element, button: Element } | null}
1483 */
1484function findPrimaryCartGateTargetsForProductCode(productCode, scopeRoot = null) {
1485  const code = String(productCode || '').trim();
1486  if (!code) {
1487    return null;
1488  }
1489  const scanRoots = scopeRoot instanceof Element ? [scopeRoot] : [];
1490  if (!scanRoots.length) {
1491    return null;
1492  }
1493  for (let r = 0; r < scanRoots.length; r++) {
1494    const root = scanRoots[r];
1495    const legacyRoots = root.querySelectorAll('[data-legacy-add-to-cart]');
1496    for (let i = 0; i < legacyRoots.length; i++) {
1497      const lr = legacyRoots[i];
1498      if (lr.closest('.upsell-block')) {
1499        continue;
1500      }
1501      const btn = lr.querySelector('button[data-product-code]');
1502      const primary = String(btn && btn.getAttribute('data-product-code') || lr.getAttribute('data-product-code') || '').trim();
1503      if (primary !== code) {
1504        continue;
1505      }
1506      if (btn instanceof Element) {
1507        return {
1508          scope: lr,
1509          button: btn
1510        };
1511      }
1512    }
1513    const coreBtns = root.querySelectorAll('.individual-add-to-cart-container[data-cart-experience="core"] [data-core-add-to-cart] button[data-product-code]');
1514    for (let i = 0; i < coreBtns.length; i++) {
1515      const btn = coreBtns[i];
1516      if (!(btn instanceof Element)) {
1517        continue;
1518      }
1519      const host = btn.closest('.individual-add-to-cart-container');
1520      if (!host || host.closest('.upsell-block')) {
1521        continue;
1522      }
1523      const primary = String(btn.getAttribute('data-product-code') || '').trim();
1524      if (primary !== code) {
1525        continue;
1526      }
1527      const wrap = btn.closest('[data-core-add-to-cart]');
1528      if (wrap instanceof Element) {
1529        return {
1530          scope: wrap,
1531          button: btn
1532        };
1533      }
1534    }
1535  }
1536  return null;
1537}
1538
1539/**
1540 * One DOM pass for Student ProductProperties / prefetch eligibility:
1541 * SKUs tied to cart UI vs SKUs allowed while {@see shouldFetchMarketingApisForAddToCartDomScope} passes.
1542 *
1543 * Core rows use `getCoreButtonProductCode(btn)` so orchestrated PDP can follow the equivalent-offering
1544 * `<select>` (see `effectiveCorePdpButtonProductCodeForPrefetch` in product-loader).
1545 *
1546 * @param {(btn: Element) => string} [getCoreButtonProductCode]
1547 * @return {{ trackedCodes: Set<string>, eligibleCodes: Set<string> }}
1548 */
1549function buildCartMarketingStudentPrefetchGateSets(getCoreButtonProductCode) {
1550  const trackedCodes = new Set();
1551  const eligibleCodes = new Set();
1552  const resolveCoreCode = typeof getCoreButtonProductCode === 'function' ? getCoreButtonProductCode : btn => String(btn.getAttribute?.('data-product-code') ?? '').trim();
1553  const legacyRoots = typeof document !== 'undefined' ? document.querySelectorAll('[data-legacy-add-to-cart]') : [];
1554  for (let i = 0; i < legacyRoots.length; i++) {
1555    const root = legacyRoots[i];
1556    const btn = root.querySelector?.('button[data-product-code]');
1557    const alt = String(root.getAttribute?.('data-alt-product-code') || '').trim();
1558    const primary = String(btn && btn.getAttribute('data-product-code') || root.getAttribute?.('data-product-code') || '').trim();
1559    const passes = shouldFetchMarketingApisForAddToCartDomScope(root, btn instanceof Element ? btn : null);
1560    for (const code of [alt, primary].filter(Boolean)) {
1561      trackedCodes.add(code);
1562      if (passes) {
1563        eligibleCodes.add(code);
1564      }
1565    }
1566  }
1567  const upsellCoreBtns = typeof document !== 'undefined' ? document.querySelectorAll('.upsell-block [data-core-add-to-cart] button[data-product-code]') : [];
1568  for (let i = 0; i < upsellCoreBtns.length; i++) {
1569    const btn = upsellCoreBtns[i];
1570    if (!(btn instanceof Element)) {
1571      continue;
1572    }
1573    const code = String(btn.getAttribute('data-product-code') || '').trim();
1574    if (code) {
1575      trackedCodes.add(code);
1576    }
1577  }
1578  const coreBtns = typeof document !== 'undefined' ? document.querySelectorAll('.individual-add-to-cart-container[data-cart-experience
1578="core"] [data-core-add-to-cart] button[data-product-code]') : [];
1579  for (let i = 0; i < coreBtns.length; i++) {
1580    const btn = coreBtns[i];
1581    if (!(btn instanceof Element)) {
1582      continue;
1583    }
1584    const host = btn.closest('.individual-add-to-cart-container');
1585    if (!host || host.closest('.upsell-block')) {
1586      continue;
1587    }
1588    const effective = String(resolveCoreCode(btn) || '').trim();
1589    if (!effective) {
1590      continue;
1591    }
1592    trackedCodes.add(effective);
1593    /* Core commerce CTAs resolve from Marketing product detail + Core cart — never Student ProductProperties. */
1594  }
1595  return {
1596    trackedCodes,
1597    eligibleCodes
1598  };
1599}
1600
1601/**
1602 * @param {string} code
1603 * @param {{ trackedCodes: Set<string>, eligibleCodes: Set<string> }} sets
1604 * @return {boolean}
1605 */
1606function shouldStudentPrefetchProductPropertiesForProductCode(code, sets) {
1607  const c = String(code || '').trim();
1608  if (!c) {
1609    return false;
1610  }
1611  if (!sets || !sets.trackedCodes || !sets.eligibleCodes) {
1612    return true;
1613  }
1614  if (!sets.trackedCodes.has(c)) {
1615    return true;
1616  }
1617  return sets.eligibleCodes.has(c);
1618}
1619
1620/**
1621 * @param {(btn: Element) => string} [getCoreButtonProductCode]
1622 * @return {string[]}
1623 */
1624function collectEligibleStudentPrefetchCartProductCodes(getCoreButtonProductCode) {
1625  const {
1626    eligibleCodes
1627  } = buildCartMarketingStudentPrefetchGateSets(getCoreButtonProductCode);
1628  return [...eligibleCodes];
1629}
1630
1631/**
1632 * Primary core add-to-cart control on an orchestrated PDP (`[data-colibri-pdp]`).
1633 * Mirrors the cart-host checks in `core-add-to-cart-view.js` / {@see maybeDispatchPdpProductResolvedFromLoader}:
1634 * used when `select.select-state` lags localStorage / SSR `data-product-code` so we still allow
1635 * Marketing loader + Student ProductProperties for the button SKU.
1636 *
1637 * @param {Element|null|undefined} buttonEl
1638 * @return {boolean}
1639 */
1640function isPrimaryColibriPdpCoreCartButton(buttonEl) {
1641  if (!(buttonEl instanceof Element)) {
1642    return false;
1643  }
1644  if (!document.querySelector('[data-colibri-pdp]')) {
1645    return false;
1646  }
1647  const cart = buttonEl.closest('.individual-add-to-cart-container');
1648  if (!cart || cart.closest('.upsell-block')) {
1649    return false;
1650  }
1651  if (String(cart.dataset.cartExperience || '').toLowerCase() !== 'core') {
1652    return false;
1653  }
1654  return !!cart.closest('[data-colibri-pdp]');
1655}
1656
1657/**
1658 * Comparison grid SSR already resolves product + offering; enable Core CTAs when
1659 * `data-product-code` and `data-sku` are present without waiting on client catalogue hydration.
1660 *
1661 * @param {Element|null|undefined} buttonEl
1662 * @return {boolean}
1663 */
1664function isComparisonGridListingCoreCartButton(buttonEl) {
1665  if (!(buttonEl instanceof Element)) {
1666    return false;
1667  }
1668  if (isPrimaryColibriPdpCoreCartButton(buttonEl)) {
1669    return false;
1670  }
1671  if (!buttonEl.closest('.product-comparison-view')) {
1672    return false;
1673  }
1674  const wrap = buttonEl.closest('[data-core-add-to-cart]');
1675  const code = String(buttonEl.dataset.productCode || wrap && wrap.dataset.productCode || buttonEl.getAttribute('data-product-code') || '').trim();
1676  const sku = String(buttonEl.dataset.sku || buttonEl.getAttribute('data-sku') || '').trim();
1677  return !!code && !!sku;
1678}
1679const COOKIE_DEBUG_NAMES = (/* unused pure expression or super */ null && (['McKissock-LongExpiration', 'McKissock-Role', 'McKissock-Token', 'McKissock-UserId', 'shoppingcartid', 'STYXKEY-McKissock-LongExpiration', 'STYXKEY-McKissock-Role', 'STYXKEY-McKissock-Token', 'STYXKEY-McKissock-UserId', 'STYXKEY-partnershipid', 'STYXKEY-shoppingcartid', 'X-Credential']));
1680
1681// Capture cart cookies on partnership group pages
1682function registerCookieLogging() {
1683  const body = document.body;
1684  if (!body || !body.classList.contains('retail-group-template') && !body.classList.contains('university-group-template')) {
1685    return;
1686  }
1687  body.addEventListener('ColibriEvent:DO_ADD_TO_CART_SUCCESS', () => {
1688    COOKIE_DEBUG_NAMES.forEach(name => {
1689      const value = getCookie(name);
1690      console.log(name, ': ', value || 'Not Set');
1691    });
1692  }, {
1693    once: true
1694  });
1695}
1696;// ./src/Rendering/scripts/marketingApiRouting.ts
1697/**
1698 * Browser routing: direct upstream Marketing API vs same-origin WP REST pass-through.
1699 *
1700 * Always use the WP REST proxy when the host includes `.local` (never call `/marketing/...` from the
1701 * browser). On other non-prod hosts (`dev.*`, `test.*`, `stage.*`), use the proxy
1702 * when the configured base URI is production Marketing API (`api-ms.<domain>`).
1703 * PHP exposes the decision via `window.colibriMarketingApiConfig.useRestProxy`.
1704 */
1705
1706const WP_JSON_MARKETING_V1_SUFFIX = '/wp-json/wp-marketing-api/v1';
1707
1708/**
1709 * Same-origin WP namespace for Marketing API routes.
1710 */
1711function wpJsonMarketingV1Base() {
1712  return `${window.location.origin.replace(/\/+$/, '')}${WP_JSON_MARKETING_V1_SUFFIX}`;
1713}
1714
1715/**
1716 * True when the URL points at the WP REST proxy namespace.
1717 */
1718function isWpJsonMarketingProxyBase(base) {
1719  return /\bwp-json\/.*?wp-marketing-api\/v1\b/i.test(base.replace(/^https?:\/\//i, ''));
1720}
1721
1722/**
1723 * Non-prod site hosts that may need CORS proxying to prod Marketing API.
1724 */
1725function isNonProdMarketingProxyHost() {
1726  const host = String(window.location?.hostname || '').toLowerCase().replace(/:\d+$/, '');
1727  if (!host) {
1728    return false;
1729  }
1730  if (host === 'local' || host.includes('.local')) {
1731    return true;
1732  }
1733  return /^(dev|test|stage)\./.test(host);
1734}
1735
1736/**
1737 * Whether browser-side Marketing API calls should use the WP REST pass-through.
1738 */
1739function shouldUseMarketingRestProxy() {
1740  if (typeof window === 'undefined') {
1741    return false;
1742  }
1743  const host = String(window.location?.hostname || '').toLowerCase().replace(/:\d+$/, '');
1744  // Local always uses REST passthrough — never hit /marketing/... directly.
1745  if (host === 'local' || host.includes('.local')) {
1746    return true;
1747  }
1748  const cfg = window.colibriMarketingApiConfig;
1749  if (typeof cfg?.useRestProxy === 'boolean') {
1750    return cfg.useRestProxy;
1751  }
1752  if (cfg?.isProdApiBase === true && isNonProdMarketingProxyHost()) {
1753    return true;
1754  }
1755  return false;
1756}
1757;// ./src/Rendering/scripts/fetchPromotedDiscountCodes.ts
1758
1759
1760
1761function readMarketingApiUpstreamBaseUri() {
1762  const raw = String(window.colibriMarketingApiConfig?.apiBaseUri || window.colibri?.marketingApi?.baseUrl || window.colibri?.marketingApi?.settings?.baseUri || '').trim();
1763  return raw.replace(/\/+$/, '');
1764}
1765function buildMetaFromResponse(response) {
1766  let pagination = null;
1767  let filter = null;
1768  try {
1769    const ph = response.headers.get('X-Pagination');
1770    if (ph) {
1771      pagination = JSON.parse(ph);
1772    }
1773  } catch {
1774    /* ignore */
1775  }
1776  try {
1777    const fh = response.headers.get('X-Filter');
1778    if (fh) {
1779      filter = JSON.parse(fh);
1780    }
1781  } catch {
1782    /* ignore */
1783  }
1784  return {
1785    status: response.status,
1786    pagination,
1787    filter
1788  };
1789}
1790function buildPromotedDiscountCodesQuery(params) {
1791  const sp = new URLSearchParams();
1792  const merged = withMarketingPromotedDiscountCodesDefault(params || {});
1793  for (const [key, value] of Object.entries(merged)) {
1794    if (value === undefined || value === null) {
1795      continue;
1796    }
1797    if (typeof value === 'boolean') {
1798      sp.set(key, value ? 'true' : 'false');
1799      continue;
1800    }
1801    sp.set(key, String(value));
1802  }
1803  return sp.toString();
1804}
1805
1806/**
1807 * Fetch promoted discount codes via the WP REST proxy when CORS requires it,
1808 * otherwise call the upstream Marketing API directly.
1809 */
1810async function fetchPromotedDiscountCodes(params) {
1811  const query = buildPromotedDiscountCodesQuery(params);
1812  const useWpJsonProxy = shouldUseMarketingRestProxy();
1813  const url = useWpJsonProxy ? `${wpJsonMarketingV1Base()}/discount-codes-promoted${query ? `?${query}` : ''}` : (() => {
1814    const base = readMarketingApiUpstreamBaseUri();
1815    if (!base) {
1816      throw new Error('Marketing API base URI is not configured.');
1817    }
1818    const root = base.replace(/\/marketing$/i, '');
1819    return `${root.replace(/\/+$/, '')}/marketing/discountcodes/promoted${query ? `?${query}` : ''}`;
1820  })();
1821  const response = await fetch(url, mergeMarketingAuthHeadersForFetchUrl(url, {
1822    method: 'GET'
1823  }));
1824  if (!response.ok) {
1825    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1826  }
1827  const json = await response.json();
1828  if (useWpJsonProxy && Array.isArray(json)) {
1829    return json;
1830  }
1831  return [json, buildMetaFromResponse(response)];
1832}
1833;// ./src/Rendering/scripts/product-loader.ts
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845/**
1846 * PDP troubleshooting logs: `?debug_pdp_cta=1`, `localStorage colibri_debug_pdp_core_cta=1`,
1847 * or `window.__COLIBRI_DEBUG_PDP_CORE_CTA = true`.
1848 */
1849function shouldDebugPdpCoreCta() {
1850  try {
1851    const w = window;
1852    if (w.__COLIBRI_DEBUG_PDP_CORE_CTA === true) {
1853      return true;
1854    }
1855    if (/[?&]debug_pdp_cta=1(?:&|$)/.test(String(window.location.search || ''))) {
1856      return true;
1857    }
1858    if (window.localStorage && window.localStorage.getItem('colibri_debug_pdp_core_cta') === '1') {
1859      return true;
1860    }
1861  } catch {
1862    /* optional PDP troubleshooting */
1863  }
1864  return false;
1865}
1866function logColibriProductPropsDebugEvent(stage, payload) {
1867  if (!shouldDebugPdpCoreCta()) {
1868    return;
1869  }
1870  console.log('%c[Colibri product-props]%c ' + stage, 'color:#0a7;font-weight:bold', 'color:inherit', payload ?? '');
1871}
1872
1873/** PDP loader / `fetchOffering`: same toggle as {@link logColibriProductPropsDebugEvent}. */
1874function logColibriPdpLoaderDebug(stage, payload) {
1875  if (!shouldDebugPdpCoreCta()) {
1876    return;
1877  }
1878  console.log('%c[Colibri PDP loader]%c ' + stage, 'color:#156;font-weight:bold', 'color:inherit', payload ?? '');
1879}
1880function readPrimaryPdpEquivalentOfferingProductCode() {
1881  const w = window;
1882  const fn = w.colibri?.readPrimaryPdpEquivalentOfferingProductCode;
1883  if (typeof fn !== 'function') {
1884    return '';
1885  }
1886  try {
1887    return String(fn() ?? '').trim();
1888  } catch {
1889    return '';
1890  }
1891}
1892
1893/**
1894 * Orchestrated PDP + primary core CTA: follow the equivalent-offering `<select>` when set.
1895 * When the shopper is still on the placeholder (no `.value`), do **not** prefetch Student
1896 * ProductProperties for SSR `data-product-code` — wait until {@see window.colibri.readPrimaryPdpEquivalentOfferingProductCode}
1897 * returns a SKU (including single-offering SSR preselect).
1898 *
1899 * @param btn Core PDP cart button (`[data-core-add-to-cart] button`).
1900 */
1901function effectiveCorePdpButtonProductCodeForPrefetch(btn) {
1902  const domCode = String(btn.getAttribute('data-product-code') || '').trim();
1903  if (!document.querySelector('[data-colibri-pdp]')) {
1904    return domCode;
1905  }
1906  if (!isPrimaryColibriPdpCoreCartButton(btn)) {
1907    return domCode;
1908  }
1909  const selected = readPrimaryPdpEquivalentOfferingProductCode();
1910  if (!selected) {
1911    return '';
1912  }
1913  if (normalizeProductCodeForCacheMatch(selected) !== normalizeProductCodeForCacheMatch(domCode)) {
1914    return selected;
1915  }
1916  return domCode;
1917}
1918
1919/**
1920 * Browser catalogue loader: marketing product fetch/cache, ProductProperties POST,
1921 * `window.colibri.fetchProductProperties`, legacy CTA prefetch, and merged payloads.
1922 */
1923
1924function readFetchProductProperties() {
1925  const colibri = window.colibri;
1926  const fn = colibri?.fetchProductProperties;
1927  return typeof fn === 'function' ? fn : undefined;
1928}
1929function getOfferingFromProduct(product, productCode) {
1930  if (!product || !Array.isArray(product.offerings)) {
1931    return null;
1932  }
1933  const offerings = product.offerings;
1934  if (productCode) {
1935    const match = offerings.find(o => o && typeof o === 'object' && String(o.productCode ?? '').trim() === productCode);
1936    if (match && typeof match === 'object') {
1937      return match;
1938    }
1939  }
1940  const first = offerings[0];
1941  return first && typeof first === 'object' ? first : null;
1942}
1943
1944/**
1945 * Uses `window.colibri.fetchProductProperties` when present (AddToCart bootstrap).
1946 */
1947async function fetchProductPropertiesRow(productCode) {
1948  const code = String(productCode || '').trim();
1949  if (!code) {
1950    return null;
1951  }
1952  const fn = readFetchProductProperties();
1953  if (typeof fn !== 'function') {
1954    return null;
1955  }
1956  try {
1957    const rows = await fn(code);
1958    const list = Array.isArray(rows) ? rows : [];
1959    const hit = list.find(r => r && typeof r === 'object' && String(r.ProductCode ?? '').trim() === code);
1960    return hit && typeof hit === 'object' ? hit : null;
1961  } catch {
1962    return null;
1963  }
1964}
1965
1966/**
1967 * Shallow-merge ProductProperties into the marketing offering used for PDP render
1968 * and `PDP_PRODUCT_RESOLVED` consumers.
1969 */
1970function applyProductPropertiesToMarketingProduct(marketingProduct, productCode, propsRow) {
1971  if (!propsRow) {
1972    return marketingProduct;
1973  }
1974  const offering = getOfferingFromProduct(marketingProduct, productCode);
1975  if (!offering) {
1976    return marketingProduct;
1977  }
1978  let props = {};
1979  const p = propsRow.Properties;
1980  if (p && typeof p === 'object') {
1981    const upsellNameRaw = p.UpsellMembershipName;
1982    const hasUpsell = upsellNameRaw !== undefined && upsellNameRaw !== null && String(upsellNameRaw).trim() !== '';
1983    props = {
1984      inCart: (p.InCart ?? false) === 'True',
1985      isInvalid: (p.Invalid ?? false) === 'True',
1986      seatsRemaining: p.SeatsRemaining ?? null,
1987      upsell: hasUpsell ? {
1988        name: p.UpsellMembershipName,
1989        productCode: p.UpsellMembershipProductCode,
1990        monthlyPrice: p.UpsellMembershipMonthlyPrice,
1991        months: p.UpsellMembershipMonths
1992      } : null
1993    };
1994  }
1995  const baseProps = offering.properties && typeof offering.properties === 'object' ? {
1996    ...offering.properties
1997  } : {};
1998  const mergedOffering = {
1999    ...offering,
2000    properties: {
2001      ...baseProps,
2002      ...props
2003    }
2004  };
2005  const codeToMatch = String(productCode ?? '').trim();
2006  const result = {
2007    ...marketingProduct
2008  };
2009  if (Array.isArray(result.offerings)) {
2010    result.offerings = result.offerings.map((o, idx) => {
2011      if (!o || typeof o !== 'object') {
2012        return o;
2013      }
2014      const row = o;
2015      if (codeToMatch) {
2016        const opc = String(row.productCode ?? '').trim();
2017        return opc === codeToMatch ? mergedOffering : o;
2018      }
2019      return idx === 0 ? mergedOffering : o;
2020    });
2021  } else {
2022    result.offerings = [mergedOffering];
2023  }
2024  return result;
2025}
2026/**
2027 * Coalesce concurrent {@link fetchOffering} calls for the same SKU (multiple PDP hooks used to double-fetch).
2028 * Key includes merge flag — props merge changes the merged product/offering payload.
2029 */
2030const fetchOfferingInflight = new Map();
2031
2032/** PDP orchestrator: marketing GET plus optional ProductProperties merge. */
2033async function fetchOffering(productCode, options) {
2034  const code = String(productCode || '').trim();
2035  if (!code) {
2036    return null;
2037  }
2038  const opts = options ?? {
2039    whenRendererReady: () => Promise.resolve(),
2040    readPdpRoot: () => document.querySelector('[data-colibri-pdp]'),
2041    mergeStudentProductProperties: true
2042  };
2043  const mergeStudentProductProperties = opts.mergeStudentProductProperties !== false;
2044  const ignoreCartFetchGate = opts.ignoreCartFetchGate === true;
2045  const inflightKey = `${code}\u0000mergeProps:${mergeStudentProductProperties ? '1' : '0'}\u0000ignoreGate:${ignoreCartFetchGate ? '1' : '0'}`;
2046  const existing = fetchOfferingInflight.get(inflightKey);
2047  if (existing) {
2048    logColibriPdpLoaderDebug('fetchOffering:join_inflight', {
2049      code,
2050      mergeStudentProductProperties
2051    });
2052    return existing;
2053  }
2054  logColibriPdpLoaderDebug('fetchOffering:start', {
2055    code,
2056    mergeStudentProductProperties
2057  });
2058  const pending = (async () => {
2059    await opts.whenRendererReady();
2060    const loader = window.colibriProductLoader;
2061    const fetchByCode = loader?.fetchByCode;
2062    if (typeof fetchByCode !== 'function' || !loader) {
2063      logColibriPdpLoaderDebug('fetchOffering:abort_no_loader', {
2064        code
2065      });
2066      return null;
2067    }
2068    const pdpRoot = opts.readPdpRoot();
2069    const gateTargets = findPrimaryCartGateTargetsForProductCode(code, pdpRoot);
2070    const allowLoaderSku = ignoreCartFetchGate || !gateTargets || shouldFetchMarketingApisForAddToCartDomScope(gateTargets.scope, gateTargets.button) || isPrimaryColibriPdpCoreCartButton(gateTargets.button);
2071    logColibriPdpLoaderDebug('fetchOffering:gate', {
2072      code,
2073      allowLoaderSku,
2074      hasGateTargets: !!gateTargets,
2075      marketingScopeWouldFetch: gateTargets ? shouldFetchMarketingApisForAddToCartDomScope(gateTargets.scope, gateTargets.button) : null,
2076      isPrimaryPdpCoreButton: gateTargets ? isPrimaryColibriPdpCoreCartButton(gateTargets.button) : null
2077    });
2078    const propsPromise = allowLoaderSku && mergeStudentProductProperties ? fetchProductPropertiesRow(code) : Promise.resolve(null);
2079    const [marketingProduct, propsRow] = await Promise.all([allowLoaderSku ? fetchByCode.call(loader, code, {
2080      sourceEl: pdpRoot
2081    }) : Promise.resolve(null), propsPromise]);
2082    if (!marketingProduct || typeof marketingProduct !== 'object') {
2083      logColibriPdpLoaderDebug('fetchOffering:done_no_marketing_product', {
2084        code,
2085        allowLoaderSku
2086      });
2087      return null;
2088    }
2089    const product = applyProductPropertiesToMarketingProduct(marketingProduct, code, propsRow);
2090    const offering = getOfferingFromProduct(product, code);
2091    logColibriPdpLoaderDebug('fetchOffering:done', {
2092      code,
2093      hasOffering: !!offering,
2094      allowLoaderSku
2095    });
2096    return {
2097      product,
2098      offering
2099    };
2100  })();
2101  fetchOfferingInflight.set(inflightKey, pending);
2102  pending.finally(() => {
2103    fetchOfferingInflight.delete(inflightKey);
2104  });
2105  return pending;
2106}
2107(() => {
2108  'use strict';
2109
2110  /** @see `ProductsService::DEFAULT_LIST_SORT` (PHP) and `Gutenberg/constants/productListSort.ts`. */
2111  const DEFAULT_PRODUCT_LIST_SORT = 'deliveryMethodOrder,offerings.properties.packageSortOrder,offerings.isMandatory:desc,deliveryMethodSecondaryOrder,startTime,price,hasSchedule:desc,name,offerings.creditHours:desc';
2112  const store = {
2113    requestedCodes: new Set(),
2114    requestedProductRequests: new Set(),
2115    pendingByCode: new Map(),
2116    pendingByMany: new Map(),
2117    pendingProductRequests: new Map(),
2118    cacheByComposite: new Map(),
2119    cacheByCode: new Map(),
2120    cacheProductRequests: new Map()
2121  };
2122  const productPropsInflightRequests = new Map();
2123  function productPropsInflightKey(url, codes) {
2124    return `${url}\x00${[...codes].sort().join('\x1f')}`;
2125  }
2126
2127  /**
2128   * Bumped on login/logout (or other session boundary) so list/detail cache keys
2129   * diverge and in-flight requests cannot repopulate stale auth-sensitive entries.
2130   */
2131  let listRequestSessionEpoch = 0;
2132  function normalizeApiBaseUri(sourceEl) {
2133    if (sourceEl instanceof Element) {
2134      const ancestor = sourceEl.closest('.colibri-product-query-block, [data-api-base-uri], [data-colibri-pdp]');
2135      const fromAncestor = String(ancestor?.dataset?.apiBaseUri || '').trim();
2136      if (fromAncestor) {
2137        return fromAncestor.replace(/\/+$/, '');
2138      }
2139    }
2140    return `${window.location.origin.replace(/\/+$/, '')}/wp-json/wp-marketing-api/v1`;
2141  }
2142  function isLocalBase(apiBaseUri) {
2143    return isWpJsonMarketingProxyBase(String(apiBaseUri || ''));
2144  }
2145  function readMarketingApiUpstreamBaseUri() {
2146    const colibri = window.colibri;
2147    const raw = String(colibri?.marketingApi?.baseUrl || colibri?.marketingApi?.settings?.baseUri || '').trim();
2148    return raw.replace(/\/+$/, '');
2149  }
2150
2151  /**
2152   * Upstream Marketing catalogue base (host or host + `/marketing` prefix) for filtered list calls.
2153   * Single-product-by-code uses {@see marketingProductDetailUrl}
2153 (`/marketing/products/{productCode}`).
2154   * Uses `data-api-base-uri` when it does not point at the WP JSON proxy; otherwise
2155   * `window.colibri.marketingApi` (baseUrl / settings.baseUri); last resort is {@see normalizeApiBaseUri}.
2156   */
2157  function resolveUpstreamProductsBaseUri(sourceEl) {
2158    const fromDom = normalizeApiBaseUri(sourceEl).replace(/\/+$/, '');
2159    if (fromDom && !isWpJsonMarketingProxyBase(fromDom)) {
2160      return fromDom;
2161    }
2162    const globalBase = readMarketingApiUpstreamBaseUri();
2163    if (globalBase) {
2164      return globalBase;
2165    }
2166    return fromDom;
2167  }
2168
2169  /**
2170   * Marketing API path for fetching one product by code (browser → upstream Marketing only).
2171   * Strips a trailing `/marketing` from configured base once so callers are not doubled up.
2172   *
2173   * Path: `/marketing/products/{productCode}` (see plugin `samples/ProductsByProductCode.md`).
2174   *
2175   * @param upstreamBase Host or `{host}/marketing` from {@see resolveUpstreamProductsBaseUri}
2176   */
2177  function normalizeMarketingApiRoot(upstreamBase) {
2178    return upstreamBase.replace(/\/+$/, '').replace(/\/marketing$/i, '').replace(/\/+$/, '');
2179  }
2180
2181  /**
2182   * Upstream Marketing API list path: `/marketing/products/withfilters`.
2183   * Accepts bare host or `{host}/marketing` (see {@see marketingProductDetailUrl}).
2184   */
2185  function marketingProductsListUrl(upstreamBase) {
2186    return `${normalizeMarketingApiRoot(upstreamBase)}/marketing/products/withfilters`;
2187  }
2188
2189  /**
2190   * Full GET URL for a product list request (upstream or WP JSON proxy).
2191   */
2192  function resolveProductsListFetchUrl(base, queryString) {
2193    const trimmed = base.replace(/\/+$/, '');
2194    if (isWpJsonMarketingProxyBase(trimmed)) {
2195      return `${trimmed}/products-with-filters?${queryString}`;
2196    }
2197    return `${marketingProductsListUrl(trimmed)}?${queryString}`;
2198  }
2199
2200  /** Page-scoped pool of itemIds shown by non-remaining ProductQuery / ComparisonGrid blocks. */
2201  const displayedProductItemIdPool = new Set();
2202  function contributeDisplayedProductItemIds(ids) {
2203    const added = [];
2204    if (!Array.isArray(ids)) {
2205      return added;
2206    }
2207    ids.forEach(raw => {
2208      const id = String(raw ?? '').trim();
2209      if (!id || displayedProductItemIdPool.has(id)) {
2210        return;
2211      }
2212      displayedProductItemIdPool.add(id);
2213      added.push(id);
2214    });
2215    return added;
2216  }
2217  function getDisplayedProductItemIds() {
2218    return Array.from(displayedProductItemIdPool);
2219  }
2220  function buildItemIdExclusionFilterSegment(ids) {
2221    const clean = Array.isArray(ids) ? ids.map(v => String(v ?? '').trim()).filter(Boolean) : [];
2222    const unique = Array.from(new Set(clean));
2223    if (!unique.length) {
2224      return '';
2225    }
2226    return buildFilterSegment('itemId', '!=', unique);
2227  }
2228
2229  /**
2230   * True for a catalog product row (not a withfilters list envelope).
2231   * Package products may also expose a nested `products` array of children.
2232   */
2233  function isCatalogProductRow(value) {
2234    if (!value || typeof value !== 'object' || Array.isArray(value)) {
2235      return false;
2236    }
2237    const o = value;
2238    return 'offerings' in o || 'productCode' in o || 'itemId' in o || 'deliveryMethod' in o;
2239  }
2240
2241  /**
2242   * True for `{ products|items: [...] }` list envelopes (optionally wrapped in a 1-element array).
2243   */
2244  function isWithFiltersEnvelope(value) {
2245    if (!value || typeof value !== 'object' || Array.isArray(value)) {
2246      return false;
2247    }
2248    if (isCatalogProductRow(value)) {
2249      return false;
2250    }
2251    const o = value;
2252    return Array.isArray(o.products) || Array.isArray(o.items);
2253  }
2254  function unwrapWithFiltersPayload(responseJson) {
2255    if (Array.isArray(responseJson) && responseJson.length >= 1) {
2256      const first = responseJson[0];
2257      // Only unwrap 1-element envelopes. A bare product array must stay intact
2258      // (package rows often include a nested `products` key for children).
2259      if (isWithFiltersEnvelope(first)) {
2260        return first;
2261      }
2262      return null;
2263    }
2264    if (responseJson && typeof responseJson === 'object' && !Array.isArray(responseJson)) {
2265      const o = responseJson;
2266      if (isWithFiltersEnvelope(o) || Array.isArray(o.products) || Array.isArray(o.items)) {
2267        return o;
2268      }
2269    }
2270    return null;
2271  }
2272  function marketingProductDetailUrl(upstreamBase, productCode) {
2273    const code = encodeURIComponent(productCode);
2274    return appendMarketingPromotedDiscountCodesToUrl(`${normalizeMarketingApiRoot(upstreamBase)}/marketing/products/${code}`);
2275  }
2276  function buildFilterSegment(field, operator, values) {
2277    const clean = Array.isArray(values) ? values.map(v => String(v ?? '').trim()).filter(Boolean) : [];
2278    if (!clean.length) {
2279      return '';
2280    }
2281    if (clean.length === 1) {
2282      return `[${field}${operator}${clean[0]}]`;
2283    }
2284    return `[${field}${operator}(${clean.join(',')})]`;
2285  }
2286  function attachParentSeminarName(product) {
2287    const loc = product.location;
2288    const locRecord = loc && typeof loc === 'object' && !Array.isArray(loc) ? loc : null;
2289    const seminarName = String(locRecord?.seminarName ?? locRecord?.seminar_name ?? product['location.seminarName'] ?? product.parentSeminarName ?? '').trim();
2290    if (seminarName && !product.parentSeminarName) {
2291      product.parentSeminarName = seminarName;
2292    }
2293    return product;
2294  }
2295  function normalizeListResponsePayload(responseJson) {
2296    const withFilters = unwrapWithFiltersPayload(responseJson);
2297    if (withFilters) {
2298      if (Array.isArray(withFilters.products)) {
2299        return withFilters.products.map(attachParentSeminarName);
2300      }
2301      if (Array.isArray(withFilters.items)) {
2302        return withFilters.items.map(attachParentSeminarName);
2303      }
2304    }
2305    if (Array.isArray(responseJson)) {
2306      if (responseJson.length >= 1 && Array.isArray(responseJson[0])) {
2307        return responseJson[0].map(attachParentSeminarName);
2308      }
2309      // Bare product array (legacy /products, or already-unwrapped list).
2310      // Do not reject rows that happen to have a nested `products` key
2311      // (package/membership children) — that previously returned [].
2312      if (responseJson.length === 0 || isCatalogProductRow(responseJson[0])) {
2313        return responseJson.map(attachParentSeminarName);
2314      }
2315    }
2316    if (responseJson && typeof responseJson === 'object') {
2317      const o = responseJson;
2318      if (Array.isArray(o.products)) {
2319        return o.products.map(attachParentSeminarName);
2320      }
2321      if (Array.isArray(o.items)) {
2322        return o.items.map(attachParentSeminarName);
2323      }
2324    }
2325    return [];
2326  }
2327  function extractProductItemIds(responseJson) {
2328    const withFilters = unwrapWithFiltersPayload(responseJson);
2329    if (withFilters && Array.isArray(withFilters.productItemIds)) {
2330      return withFilters.productItemIds.map(v => String(v ?? '').trim()).filter(Boolean);
2331    }
2332    // Do not derive ids from products[] — remaining-items exclusion requires
2333    // the upstream productItemIds field on the withfilters payload.
2334    return [];
2335  }
2336  function readInlineListResponsePagination(responseJson) {
2337    if (Array.isArray(responseJson) && responseJson.length >= 2 && responseJson[1] && typeof responseJson[1] === 'object' && !Array.isArray(responseJson[1])) {
2338      const meta = responseJson[1];
2339      // WP REST withfilters/list tuple: [payload, ResponseMeta].
2340      if (meta.pagination && typeof meta.pagination === 'object' && !Array.isArray(meta.pagination)) {
2341        return meta.pagination;
2342      }
2343      return meta;
2344    }
2345    return null;
2346  }
2347  function normalizePagination(raw, page, pageSize) {
2348    const data = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
2349    const ps = Number(data.pageSize ?? data.PageSize ?? pageSize ?? 24);
2350    const tc = Number(data.totalCount ?? data.TotalCount ?? data.total ?? data.Total ?? 0);
2351    let tp = Number(data.totalPages ?? data.TotalPages ?? 0);
2352    if (!tp && tc > 0 && ps > 0) {
2353      tp = Math.ceil(tc / ps);
2354    }
2355    tp = tp || 1;
2356    let cp = Number(data.currentPage ?? data.CurrentPage ?? 0);
2357    if (!cp) {
2358      const offset = Number(data.currentOffset ?? data.CurrentOffset ?? 0);
2359      if (offset > 0 && ps > 0) {
2360        cp = Math.floor(offset / ps) + 1;
2361      } else {
2362        cp = page || 1;
2363      }
2364    }
2365    return {
2366      currentPage: cp,
2367      totalPages: tp,
2368      totalCount: tc,
2369      pageSize: ps
2370    };
2371  }
2372  function stableSerialize(value) {
2373    if (Array.isArray(value)) {
2374      return `[${value.map(stableSerialize).sort((a, b) => a.localeCompare(b)).join(',')}]`;
2375    }
2376    if (value && typeof value === 'object') {
2377      const obj = value;
2378      const keys = Object.keys(obj).sort();
2379      return `{${keys.map(k => `${JSON.stringify(k)}:${stableSerialize(obj[k])}`).join(',')}}`;
2380    }
2381    return JSON.stringify(value);
2382  }
2383  function productsListComputedSlice(params) {
2384    const page = Math.max(1, Number(params.page || 1));
2385    const pageSize = Math.max(1, Number(params.pageSize || 24));
2386    const explicitOff = params.offset;
2387    const explicitLim = params.limit;
2388    const hasExplicitOffset = typeof explicitOff === 'number' && Number.isFinite(explicitOff) && explicitOff >= 0;
2389    const hasExplicitLimit = typeof explicitLim === 'number' && Number.isFinite(explicitLim) && explicitLim > 0;
2390    const offset = hasExplicitOffset ? Math.floor(explicitOff) : (page - 1) * pageSize;
2391    const limit = hasExplicitLimit ? Math.floor(explicitLim) : pageSize;
2392    return {
2393      page,
2394      pageSize,
2395      offset,
2396      limit
2397    };
2398  }
2399  function buildProductsRequestKey(params) {
2400    const rawSort = String(params.sort || '').trim();
2401    const {
2402      page,
2403      pageSize,
2404      offset,
2405      limit
2406    } = productsListComputedSlice(params);
2407    return stableSerialize({
2408      listSessionEpoch: listRequestSessionEpoch,
2409      apiBaseUri: String(params.apiBaseUri || '').replace(/\/+$/, ''),
2410      page,
2411      pageSize,
2412      offset,
2413      limit,
2414      stateAbbvs: canonicalStateAbbvs(params.stateAbbvs),
2415      licenseTypeIds: canonicalLicenseTypeIds(params.licenseTypeIds),
2416      filter: canonicalFilterSegments(params.filter),
2417      urlFilterSegments: canonicalFilterSegments(params.urlFilterSegments),
2418      sort: rawSort || DEFAULT_PRODUCT_LIST_SORT,
2419      includeNotForIndividualSale: !!params.includeNotForIndividualSale
2420    });
2421  }
2422
2423  /**
2424   * Call when cookies/session change so ProductQuery re-fetches are not served from
2425   * a cache built under a different user.
2426   */
2427  function bumpListRequestSessionEpoch() {
2428    listRequestSessionEpoch += 1;
2429    store.cacheProductRequests.clear();
2430    store.pendingProductRequests.clear();
2431    store.cacheByCode.clear();
2432    store.cacheByComposite.clear();
2433    store.pendingByCode.clear();
2434    store.pendingByMany.clear();
2435  }
2436  function normalizeToken(value) {
2437    return String(value ?? '').trim().toLowerCase();
2438  }
2439  function readProductField(product, camel, snake) {
2440    if (!product || typeof product !== 'object') {
2441      return '';
2442    }
2443    return String(product[camel] ?? product[snake] ?? '').trim();
2444  }
2445  function readOfferingField(offering, keys) {
2446    if (!offering || typeof offering !== 'object') {
2447      return '';
2448    }
2449    for (const key of keys) {
2450      const value = offering[key];
2451      if (value != null && String(value).trim() !== '') {
2452        return String(value).trim();
2453      }
2454    }
2455    return '';
2456  }
2457  function getProductCodes(product) {
2458    const codes = new Set();
2459    const directCode = product && typeof product === 'object' ? String(product.productCode ?? '').trim() : '';
2460    if (directCode) {
2461      codes.add(directCode);
2462    }
2463    const itemId = product && typeof product === 'object' ? String(product.itemId ?? product.item_id ?? '').trim() : '';
2464    if (itemId) {
2465      codes.add(itemId);
2466    }
2467    const offerings = Array.isArray(product?.offerings) ? product.offerings : [];
2468    offerings.forEach(offering => {
2469      const code = readOfferingField(offering, ['productCode']);
2470      if (code) {
2471        codes.add(code);
2472      }
2473    });
2474    return Array.from(codes);
2475  }
2476  function makeCompositeKey(itemId, deliveryMethod, licenseType) {
2477    return [normalizeToken(itemId), normalizeToken(deliveryMethod), normalizeToken(licenseType)].join('|');
2478  }
2479
2480  /**
2481   * Broadcast SKU catalogue hydration so Core CTAs (SSR-disabled until Marketing payload exists)
2482   * can re-run {@see finalizeCoreCartButtonInteractiveState} in core-add-to-cart-view.js.
2483   *
2484   * Fired whenever {@link indexProduct} runs — covers single-code proxy/detail routes,
2485   * batched `/products` / `products-list`, and paginated list fetches.
2486   */
2487  function dispatchMarketingCatalogIndexed(codes) {
2488    const unique = Array.from(new Set(codes.map(c =>
2488 String(c || '').trim()).filter(Boolean)));
2489    if (!unique.length) {
2490      return;
2491    }
2492    window.dispatchEvent(new CustomEvent('colibri:marketing-catalog-indexed', {
2493      detail: {
2494        productCodes: unique
2495      }
2496    }));
2497  }
2498  function putCacheByComposite(compositeKey, stateCode, product) {
2499    if (!compositeKey || compositeKey === '||') {
2500      return;
2501    }
2502    if (!store.cacheByComposite.has(compositeKey)) {
2503      store.cacheByComposite.set(compositeKey, new Map());
2504    }
2505    const byState = store.cacheByComposite.get(compositeKey);
2506    byState.set(normalizeToken(stateCode), product);
2507  }
2508  function indexProduct(product) {
2509    if (!product || typeof product !== 'object') {
2510      return;
2511    }
2512    const rec = product;
2513    getProductCodes(rec).forEach(code => store.cacheByCode.set(code, rec));
2514    const itemId = readProductField(rec, 'itemId', 'item_id');
2515    const deliveryMethod = readProductField(rec, 'deliveryMethod', 'delivery_method');
2516    const offerings = Array.isArray(rec.offerings) ? rec.offerings : [];
2517    if (!offerings.length) {
2518      const licenseType = readProductField(rec, 'licenseType', 'license_type');
2519      putCacheByComposite(makeCompositeKey(itemId, deliveryMethod, licenseType), '', rec);
2520      dispatchMarketingCatalogIndexed(getProductCodes(rec));
2521      return;
2522    }
2523    offerings.forEach(offering => {
2524      const licenseType = readOfferingField(offering, ['licenseType', 'license_type', 'licenseTypeId', 'license_type_id']);
2525      const stateCode = readOfferingField(offering, ['stateAbbv', 'state_abbv', 'stateCode', 'state_code', 'state']);
2526      putCacheByComposite(makeCompositeKey(itemId, deliveryMethod, licenseType), stateCode, rec);
2527    });
2528    dispatchMarketingCatalogIndexed(getProductCodes(rec));
2529  }
2530  function getCachedByComposite(itemId, deliveryMethod, licenseType, stateCode = '') {
2531    const key = makeCompositeKey(itemId, deliveryMethod, licenseType);
2532    const byState = store.cacheByComposite.get(key);
2533    if (!byState) {
2534      return null;
2535    }
2536    const stateKey = normalizeToken(stateCode);
2537    return byState.get(stateKey) || byState.get('') || null;
2538  }
2539  function productHasCode(product, productCode) {
2540    if (!product || !productCode) {
2541      return false;
2542    }
2543    const targetCode = String(productCode).trim();
2544    return getProductCodes(product).some(code => code === targetCode);
2545  }
2546  function pickProductFromResponse(response, productCode) {
2547    const target = String(productCode || '').trim();
2548    if (response && typeof response === 'object' && !Array.isArray(response)) {
2549      const r = response;
2550      if (!r.products && !r.items && !r.product) {
2551        return productHasCode(r, target) ? r : null;
2552      }
2553    }
2554    const collections = [];
2555    if (Array.isArray(response)) {
2556      collections.push(response);
2557    }
2558    if (response && typeof response === 'object' && !Array.isArray(response)) {
2559      const r = response;
2560      if (Array.isArray(r.products)) {
2561        collections.push(r.products);
2562      }
2563      if (Array.isArray(r.items)) {
2564        collections.push(r.items);
2565      }
2566    }
2567    for (const list of collections) {
2568      const exact = list.find(product => {
2569        if (!product || typeof product !== 'object') {
2570          return false;
2571        }
2572        return productHasCode(product, target);
2573      });
2574      if (exact && typeof exact === 'object') {
2575        return exact;
2576      }
2577    }
2578    if (response && typeof response === 'object' && !Array.isArray(response)) {
2579      const r = response;
2580      if (r.product && typeof r.product === 'object') {
2581        const p = r.product;
2582        return productHasCode(p, target) ? p : null;
2583      }
2584    }
2585    const fallbackCandidates = [];
2586    if (response && typeof response === 'object' && !Array.isArray(response)) {
2587      const r = response;
2588      if (r.product && typeof r.product === 'object') {
2589        fallbackCandidates.push(r.product);
2590      }
2591    }
2592    collections.forEach(list => {
2593      if (Array.isArray(list)) {
2594        list.forEach(product => {
2595          if (product && typeof product === 'object') {
2596            fallbackCandidates.push(product);
2597          }
2598        });
2599      }
2600    });
2601    if (response && typeof response === 'object' && !Array.isArray(response)) {
2602      const r = response;
2603      if (!r.products && !r.items && !r.product) {
2604        fallbackCandidates.push(r);
2605      }
2606    }
2607    const normalized = fallbackCandidates.filter(product => product && typeof product === 'object');
2608    return normalized.length === 1 ? normalized[0] : null;
2609  }
2610  function getCachedProductByCode(productCode) {
2611    const code = String(productCode ?? '').trim();
2612    if (!code) {
2613      return null;
2614    }
2615    return store.cacheByCode.get(code) ?? null;
2616  }
2617  async function fetchByCode(productCode, options = {}) {
2618    const code = String(productCode || '').trim();
2619    if (!code) {
2620      return null;
2621    }
2622    const cached = store.cacheByCode.get(code);
2623    if (cached) {
2624      return cached;
2625    }
2626    const existingPending = store.pendingByCode.get(code);
2627    if (existingPending) {
2628      return existingPending;
2629    }
2630    const useWpJsonProductDetailProxy = shouldUseMarketingRestProxy();
2631    const base = useWpJsonProductDetailProxy ? wpJsonMarketingV1Base() : resolveUpstreamProductsBaseUri(options.sourceEl).replace(/\/+$/, '');
2632    const url = useWpJsonProductDetailProxy ? appendMarketingPromotedDiscountCodesToUrl(`${base}/products-get-by-code?product_code=${encodeURIComponent(code)}`) : marketingProductDetailUrl(base, code);
2633    store.requestedCodes.add(code);
2634    const pending = (async () => {
2635      try {
2636        const response = await fetch(url, mergeMarketingAuthHeadersForFetchUrl(url, {
2637          method: 'GET'
2638        }));
2639        if (!response.ok) {
2640          return null;
2641        }
2642        const json = await response.json();
2643        const product = pickProductFromResponse(json, code);
2644        if (product) {
2645          indexProduct(product);
2646        }
2647        return product;
2648      } catch (_err) {
2649        return null;
2650      } finally {
2651        store.pendingByCode.delete(code);
2652      }
2653    })();
2654    store.pendingByCode.set(code, pending);
2655    return pending;
2656  }
2657  async function fetchManyByCodes(productCodes, options = {}) {
2658    const codes = Array.from(new Set((Array.isArray(productCodes) ? productCodes : []).map(c => String(c || '').trim()).filter(Boolean)));
2659    if (!codes.length) {
2660      return [];
2661    }
2662    const cachedProducts = [];
2663    const missingCodes = [];
2664    codes.forEach(code => {
2665      const cached = store.cacheByCode.get(code);
2666      if (cached) {
2667        cachedProducts.push(cached);
2668      } else {
2669        missingCodes.push(code);
2670      }
2671    });
2672    if (!missingCodes.length) {
2673      return Array.from(new Set(cachedProducts));
2674    }
2675    if (missingCodes.length === 1) {
2676      const single = await fetchByCode(missingCodes[0], options);
2677      return Array.from(new Set(single ? [...cachedProducts, single] : cac
2677hedProducts));
2678    }
2679    const useWpJsonProductDetailProxy = shouldUseMarketingRestProxy();
2680    const base = useWpJsonProductDetailProxy ? wpJsonMarketingV1Base() : resolveUpstreamProductsBaseUri(options.sourceEl).replace(/\/+$/, '');
2681    const pendingKey = `${base}|${missingCodes.slice().sort().join(',')}`;
2682    const existingMany = store.pendingByMany.get(pendingKey);
2683    if (existingMany) {
2684      const pendingProducts = await existingMany;
2685      return Array.from(new Set([...cachedProducts, ...pendingProducts]));
2686    }
2687    missingCodes.forEach(code => store.requestedCodes.add(code));
2688    const pending = (async () => {
2689      const params = new URLSearchParams();
2690      const filterSegment = buildFilterSegment('offerings.productCode', '==', missingCodes);
2691      if (filterSegment) {
2692        params.append('filter', filterSegment);
2693      }
2694      params.append('limit', String(Math.max(missingCodes.length, 24)));
2695      params.append('sort', DEFAULT_PRODUCT_LIST_SORT);
2696      applyMarketingPromotedDiscountCodesDefault(params);
2697      const url = resolveProductsListFetchUrl(base, params.toString());
2698      try {
2699        const response = await fetch(url, mergeMarketingAuthHeadersForFetchUrl(url, {
2700          method: 'GET'
2701        }));
2702        if (!response.ok) {
2703          return [];
2704        }
2705        const json = await response.json();
2706        const products = normalizeListResponsePayload(json);
2707        products.forEach(indexProduct);
2708        return products;
2709      } catch (_err) {
2710        return [];
2711      } finally {
2712        store.pendingByMany.delete(pendingKey);
2713      }
2714    })();
2715    store.pendingByMany.set(pendingKey, pending);
2716    const fetchedProducts = await pending;
2717    return Array.from(new Set([...cachedProducts, ...fetchedProducts]));
2718  }
2719  async function resolveProducts(productsOrCodes, options = {}) {
2720    const input = Array.isArray(productsOrCodes) ? productsOrCodes : [productsOrCodes];
2721    const codePositions = [];
2722    const objects = [];
2723    const codes = [];
2724    input.forEach((entry, index) => {
2725      if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
2726        const value = entry;
2727        objects.push({
2728          index,
2729          value
2730        });
2731        indexProduct(value);
2732        return;
2733      }
2734      const code = String(entry || '').trim();
2735      if (!code) {
2736        return;
2737      }
2738      codePositions.push({
2739        index,
2740        code
2741      });
2742      codes.push(code);
2743    });
2744    const resolvedByIndex = new Map();
2745    objects.forEach(({
2746      index,
2747      value
2748    }) => resolvedByIndex.set(index, value));
2749    if (codes.length === 1) {
2750      const product = await fetchByCode(codes[0], options);
2751      if (product) {
2752        resolvedByIndex.set(codePositions[0].index, product);
2753      }
2754    } else if (codes.length > 1) {
2755      await fetchManyByCodes(codes, options);
2756      codePositions.forEach(({
2757        index,
2758        code
2759      }) => {
2760        const product = store.cacheByCode.get(code);
2761        if (product) {
2762          resolvedByIndex.set(index, product);
2763        }
2764      });
2765    }
2766    return input.map((_, index) => resolvedByIndex.get(index) || null).filter(p => Boolean(p));
2767  }
2768  async function fetchProductsPage(params = {}, options = {}) {
2769    const apiBaseUri = String(params.apiBaseUri || '').replace(/\/+$/, '');
2770    const {
2771      page,
2772      pageSize,
2773      offset,
2774      limit
2775    } = productsListComputedSlice(params);
2776    const filters = Array.isArray(params.filter) ? params.filter : [];
2777    const urlFilters = Array.isArray(params.urlFilterSegments) ? params.urlFilterSegments : [];
2778    const uniqueFilters = [];
2779    [...filters, ...urlFilters].forEach(segment => {
2780      const clean = String(segment || '').trim();
2781      if (clean && uniqueFilters.indexOf(clean) === -1) {
2782        uniqueFilters.push(clean);
2783      }
2784    });
2785    if (options?.excludeDisplayedItemIds) {
2786      const poolIds = getDisplayedProductItemIds();
2787      const excludeSeg = buildItemIdExclusionFilterSegment(poolIds);
2788      if (excludeSeg && uniqueFilters.indexOf(excludeSeg) === -1) {
2789        uniqueFilters.push(excludeSeg);
2790      }
2791    }
2792    const key = buildProductsRequestKey({
2793      ...params,
2794      apiBaseUri,
2795      page,
2796      pageSize,
2797      offset,
2798      limit,
2799      filter: uniqueFilters
2800    });
2801    const cached = store.cacheProductRequests.get(key);
2802    if (cached) {
2803      return {
2804        products: normalizeListResponsePayload(cached.products).slice(),
2805        pagination: cached.pagination || normalizePagination({}, page, limit),
2806        productItemIds: Array.isArray(cached.productItemIds) ? cached.productItemIds.slice() : []
2807      };
2808    }
2809    const existingPage = store.pendingProductRequests.get(key);
2810    if (!options?.signal && existingPage) {
2811      return existingPage;
2812    }
2813    const requestPromise = (async () => {
2814      const search = new URLSearchParams();
2815      const stateAbbvs = Array.isArray(params.stateAbbvs) ? params.stateAbbvs.filter(Boolean) : [];
2816      const licenseTypeIds = Array.isArray(params.licenseTypeIds) ? params.licenseTypeIds.filter(Boolean) : [];
2817      if (stateAbbvs.length) {
2818        search.append('stateAbbvs', stateAbbvs.join(','));
2819      }
2820      if (licenseTypeIds.length) {
2821        search.append('licenseTypeIds', licenseTypeIds.join(','));
2822      }
2823      uniqueFilters.forEach(segment => search.append('filter', segment));
2824      const sort = String(params.sort || '').trim() || DEFAULT_PRODUCT_LIST_SORT;
2825      search.append('sort', sort);
2826      search.append('offset', String(offset));
2827      search.append('limit', String(limit));
2828      if (params.includeNotForIndividualSale) {
2829        search.append('includeNotForIndividualSale', 'true');
2830      }
2831      applyMarketingPromotedDiscountCodesDefault(search);
2832      const fetchOpts = {};
2833      if (options.signal) {
2834        fetchOpts.signal = options.signal;
2835      }
2836      const url = resolveProductsListFetchUrl(apiBaseUri, search.toString());
2837      store.requestedProductRequests.add(key);
2838      const resp = await fetch(url, mergeMarketingAuthHeadersForFetchUrl(url, fetchOpts));
2839      if (!resp.ok) {
2840        throw new Error(`HTTP ${resp.status}`);
2841      }
2842      const productsJson = await resp.json();
2843      const paginationHeader = resp.headers.get('X-Pagination');
2844      const inlinePagination = readInlineListResponsePagination(productsJson);
2845      const rawPagination = paginationHeader ? JSON.parse(paginationHeader) : inlinePagination || {};
2846      const pagination = normalizePagination(rawPagination, page, limit);
2847      let products = normalizeListResponsePayload(productsJson);
2848      const productItemIds = extractProductItemIds(productsJson);
2849      if (options?.contributeDisplayedItemIds) {
2850        contributeDisplayedProductItemIds(productItemIds);
2851      }
2852      // Prefer productItemIds length when present (withfilters). X-Pagination
2853      // totalCount can disagree (often +1) under itemId!= exclusion.
2854      if (productItemIds.length > 0) {
2855        pagination.totalCount = productItemIds.length;
2856        pagination.totalPages = Math.max(1, Math.ceil(productItemIds.length / Math.max(1, limit)));
2857      }
2858      const orderIds = resolveIncludedItemsOrderFromQueryParts({
2859        includedItems: params.includedItems,
2860        filter: uniqueFilters
2861      });
2862      products = orderUnknownRecordsByIncludedItemIds(products, orderIds);
2863      if (!orderIds.length) {
2864        products = orderUnknownRecordsByFirstOfferingPackageSortOrder(products);
2865      }
2866      products.forEach(p => {
2867        if (p && typeof p === 'object') {
2868          indexProduct(p);
2869        }
2870      });
2871      const payload = {
2872        products,
2873        pagination,
2874        productItemIds
2875      };
2876      store.cacheProductRequests.set(key, payload);
2877      return {
2878        products: payload.products.slice(),
2879        pagination: payload.pagination,
2880        productItemIds: payload.productItemIds ? payload.productItemIds.slice() : []
2881      };
2882    })();
2883    if (!options?.signal) {
2884      store.pendingProductRequests.set(key, requestPromise);
2885    }
2886    try {
2887      return await requestPromise;
2888    } finally {
2889      if (!options?.signal) {
2890        store.pendingProductRequests.delete(key);
2891      }
2892    }
2893  }
2894  function readBrowserCookie(name) {
2895    const needle = `${name}=`;
2896    const parts = document.cookie.split(';');
2897    for (let i = 0; i < parts.length; i++) {
2898      const c = parts[i].replace(/^\s+/, '');
2899      if (c.indexOf(needle) === 0) {
2900        return c.substring(needle.length);
2901      }
2902    }
2903    return null;
2904  }
2905  function colibriProductPropsRequestContext() {
2906    const colibri = window.colibri;
2907    const med = colibri?.ctaMediator || null;
2908    const api = String(med && med.studentApiUrl || med && med.studentApi || colibri?.studentApi || '').replace(/\/+$/, '').trim();
2909    const sub = String(med && med.subsidiaryId || colibri?.subsidiaryId || '').trim();
2910    return {
2911      api,
2912      sub
2913    };
2914  }
2915  function colibriProductPropsPostUrl(api, sub) {
2916    const sid = readBrowserCookie('STYXKEY-McKissock-UserId');
2917    const cid = readBrowserCookie('STYXKEY-shoppingcartid');
2918    const baseUrl = shouldUseMarketingRestProxy() ? '/wp-json/wp-marketing-api/v1/product-properties' : `${api}
2918/productSearch/ProductProperties`;
2919    let u = `${baseUrl}?subsidiaryId=${encodeURIComponent(sub)}`;
2920    if (sid) {
2921      u += `&studentId=${encodeURIComponent(sid)}`;
2922    }
2923    if (cid) {
2924      u += `&shoppingCartID=${encodeURIComponent(cid)}`;
2925    }
2926    return u;
2927  }
2928  function mergePropsRowsIntoLoaderCache(rows) {
2929    if (!Array.isArray(rows) || !rows.length) {
2930      return;
2931    }
2932    for (const row of rows) {
2933      if (!row || typeof row !== 'object') {
2934        continue;
2935      }
2936      const code = String(row.ProductCode ?? '').trim();
2937      if (!code) {
2938        continue;
2939      }
2940      const cached = store.cacheByCode.get(code);
2941      if (cached && typeof cached === 'object') {
2942        store.cacheByCode.set(code, applyProductPropertiesToMarketingProduct(cached, code, row));
2943      }
2944    }
2945  }
2946  function writeColibriProductPropsPrefetchBucket(data, codes) {
2947    const w = window;
2948    const arr = Array.isArray(data) && data.length ? data : [];
2949    w.__colibriProductProps = {
2950      response: arr,
2951      codes: [...codes]
2952    };
2953    mergePropsRowsIntoLoaderCache(arr);
2954  }
2955  function installColibriFetchProductProperties() {
2956    const w = window;
2957    w.colibri = w.colibri || {};
2958    w.colibri.fetchProductProperties = function (products) {
2959      const ctx = colibriProductPropsRequestContext();
2960      if (!ctx.api || !ctx.sub) {
2961        return Promise.reject(new Error('fetchProductProperties: missing studentApi or subsidiaryId'));
2962      }
2963      const src = Array.isArray(products) ? products : [products];
2964      const codes = [];
2965      const seen = {};
2966      for (let i = 0; i < src.length; i++) {
2967        const v = String(src[i] == null ? '' : src[i]).trim();
2968        if (v && !seen[v]) {
2969          seen[v] = true;
2970          codes.push(v);
2971        }
2972      }
2973      if (!codes.length) {
2974        return Promise.reject(new Error('fetchProductProperties: empty product codes'));
2975      }
2976      const url = colibriProductPropsPostUrl(ctx.api, ctx.sub);
2977      const dedupeKey = productPropsInflightKey(url, codes);
2978      const inflight = productPropsInflightRequests.get(dedupeKey);
2979      if (inflight) {
2980        return inflight;
2981      }
2982      const pending = fetch(url, {
2983        method: 'POST',
2984        headers: {
2985          'Content-Type': 'application/json'
2986        },
2987        body: JSON.stringify({
2988          products: codes
2989        }),
2990        priority: 'low'
2991      }).then(r => r.ok ? r.json() : Promise.reject(r.status)).then(data => {
2992        const arr = normalizeProductPropertiesApiRows(data);
2993        mergeColibriProductPropsFromApiResponse(arr);
2994        mergePropsRowsIntoLoaderCache(arr);
2995        ensureProductPropsBucketTracksCodes(codes);
2996        const w = window;
2997        logColibriProductPropsDebugEvent('fetchProductProperties:merged', {
2998          requestedCodes: codes,
2999          bucketCodesSample: Array.isArray(w.__colibriProductProps?.codes) ? w.__colibriProductProps?.codes?.slice(0, 16) : null,
3000          bucketCodesCount: Array.isArray(w.__colibriProductProps?.codes) ? w.__colibriProductProps.codes.length : 0
3001        });
3002        window.queueMicrotask(() => {
3003          window.dispatchEvent(new CustomEvent('colibri:product-props-ready'));
3004        });
3005        return arr;
3006      }).finally(() => {
3007        productPropsInflightRequests.delete(dedupeKey);
3008      });
3009      productPropsInflightRequests.set(dedupeKey, pending);
3010      return pending;
3011    };
3012  }
3013  function collectLegacyAddToCartProductCodes() {
3014    return collectEligibleStudentPrefetchCartProductCodes(effectiveCorePdpButtonProductCodeForPrefetch);
3015  }
3016  function prefetchLegacyAddToCartProductProperties() {
3017    const w = window;
3018    if (w.__colibriProductProps) {
3019      return;
3020    }
3021    const ctx = colibriProductPropsRequestContext();
3022    if (!ctx.api || !ctx.sub) {
3023      return;
3024    }
3025    const codes = collectLegacyAddToCartProductCodes();
3026    if (!codes.length) {
3027      return;
3028    }
3029    w.__colibriProductPropsLoading = true;
3030    window.setTimeout(() => {
3031      const fetchFn = window.colibri?.fetchProductProperties;
3032      if (typeof fetchFn !== 'function') {
3033        w.__colibriProductPropsLoading = false;
3034        writeColibriProductPropsPrefetchBucket(null, codes);
3035        window.dispatchEvent(new CustomEvent('colibri:product-props-ready'));
3036        return;
3037      }
3038      void fetchFn(codes).catch(() => {
3039        writeColibriProductPropsPrefetchBucket(null, codes);
3040      }).finally(() => {
3041        w.__colibriProductPropsLoading = false;
3042        window.dispatchEvent(new CustomEvent('colibri:product-props-ready'));
3043      });
3044    }, 0);
3045  }
3046  function schedulePrefetchLegacyAddToCartProductProperties() {
3047    const run = () => {
3048      prefetchLegacyAddToCartProductProperties();
3049    };
3050    if (document.readyState === 'loading') {
3051      document.addEventListener('DOMContentLoaded', run, {
3052        once: true
3053      });
3054    } else {
3055      window.setTimeout(run, 0);
3056    }
3057  }
3058  async function fetchOfferingsForCodes(productCodes, options = {}) {
3059    const codes = [...new Set(productCodes.map(c => String(c || '').trim()).filter(Boolean))];
3060    const out = {};
3061    if (!codes.length) {
3062      return out;
3063    }
3064    await fetchManyByCodes(codes, options);
3065    const prefetchGateSets = buildCartMarketingStudentPrefetchGateSets(effectiveCorePdpButtonProductCodeForPrefetch);
3066    const propsCodes = codes.filter(c => shouldStudentPrefetchProductPropertiesForProductCode(c, prefetchGateSets));
3067    const colibri = window.colibri;
3068    const fetchFn = colibri?.fetchProductProperties;
3069    let rows = [];
3070    if (typeof fetchFn === 'function' && propsCodes.length) {
3071      try {
3072        const data = await fetchFn(propsCodes);
3073        rows = Array.isArray(data) ? data : [];
3074      } catch {
3075        rows = [];
3076      }
3077    }
3078    const rowByCode = new Map();
3079    for (const row of rows) {
3080      const pc = String(row?.ProductCode ?? '').trim();
3081      if (pc) {
3082        rowByCode.set(pc, row);
3083      }
3084    }
3085    for (const code of codes) {
3086      const marketing = store.cacheByCode.get(code) ?? null;
3087      if (!marketing) {
3088        out[code] = null;
3089        continue;
3090      }
3091      const propsRow = rowByCode.get(code) ?? null;
3092      const product = applyProductPropertiesToMarketingProduct(marketing, code, propsRow);
3093      const offering = getOfferingFromProduct(product, code);
3094      store.cacheByCode.set(code, product);
3095      out[code] = {
3096        product,
3097        offering
3098      };
3099    }
3100    return out;
3101  }
3102  installColibriFetchProductProperties();
3103  window.colibriProductLoader = window.colibriProductLoader || {};
3104  window.colibriProductLoader.normalizeApiBaseUri = normalizeApiBaseUri;
3105  window.colibriProductLoader.isLocalBase = isLocalBase;
3106  window.colibriProductLoader.buildFilterSegment = buildFilterSegment;
3107  window.colibriProductLoader.normalizeListResponsePayload = normalizeListResponsePayload;
3108  window.colibriProductLoader.productHasCode = productHasCode;
3109  window.colibriProductLoader.pickProductFromResponse = pickProductFromResponse;
3110  window.colibriProductLoader.fetchByCode = fetchByCode;
3111  window.colibriProductLoader.fetchManyByCodes = fetchManyByCodes;
3112  window.colibriProductLoader.resolveProducts = resolveProducts;
3113  window.colibriProductLoader.fetchProductsPage = fetchProductsPage;
3114  window.colibriProductLoader.contributeDisplayedProductItemIds = contributeDisplayedProductItemIds;
3115  window.colibriProductLoader.getDisplayedProductItemIds = getDisplayedProductItemIds;
3116  window.colibriProductLoader.buildItemIdExclusionFilterSegment = buildItemIdExclusionFilterSegment;
3117  window.colibriProductLoader.getCachedProductByCode = getCachedProductByCode;
3118  window.colibriProductLoader.normalizePagination = normalizePagination;
3119  window.colibriProductLoader.buildProductsRequestKey = buildProductsRequestKey;
3120  window.colibriProductLoader.bumpListRequestSessionEpoch = bumpListRequestSessionEpoch;
3121  window.colibriProductLoader.getCachedByComposite = getCachedByComposite;
3122  window.colibriProductLoader.getRequestedCodes = () => Array.from(store.requestedCodes);
3123  window.colibriProductLoader.getRequestedProductRequests = () => Array.from(store.requestedProductRequests);
3124  window.colibriProductLoader._store = store;
3125  window.colibriProductLoader.getOfferingFromProduct = getOfferingFromProduct;
3126  window.colibriProductLoader.fetchProductPropertiesRow = fetchProductPropertiesRow;
3127  window.colibriProductLoader.applyProductPropertiesToMarketingProduct = applyProductPropertiesToMarketingProduct;
3128  window.colibriProductLoader.fetchOffering = fetchOffering;
3129  window.colibriProductLoader.fetchOfferingsForCodes = fetchOfferingsForCodes;
3130  window.colibriProductLoader.fetchPromotedDiscountCodes = fetchPromotedDiscountCodes;
3131  window.colibri = window.colibri || {};
3132  window.colibri.fetchPromotedDiscountCodes = fetchPromotedDiscountCodes;
3133  window.colibriProductLoader.prefetchLegacyAddToCartProductProperties = prefetchLegacyAddToCartProductProperties;
3134  schedulePrefetchLegacyAddToCartProductProperties();
3135})();
3136/******/ })()
3137;

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.