@final-commerce/common 2.5.1-online.1 → 2.5.1-online.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -486,6 +486,50 @@ var ResourceAvailabilityState = /* @__PURE__ */ ((ResourceAvailabilityState2) =>
486
486
  return ResourceAvailabilityState2;
487
487
  })(ResourceAvailabilityState || {});
488
488
 
489
+ // enums/shipping.enum.ts
490
+ var ShippingZoneKind = /* @__PURE__ */ ((ShippingZoneKind2) => {
491
+ ShippingZoneKind2["COUNTRY"] = "country";
492
+ ShippingZoneKind2["PROVINCE"] = "province";
493
+ ShippingZoneKind2["CITY"] = "city";
494
+ ShippingZoneKind2["EVERYWHERE"] = "everywhere";
495
+ return ShippingZoneKind2;
496
+ })(ShippingZoneKind || {});
497
+ var ShippingOptionType = /* @__PURE__ */ ((ShippingOptionType2) => {
498
+ ShippingOptionType2["SHIP"] = "ship";
499
+ ShippingOptionType2["PICKUP"] = "pickup";
500
+ return ShippingOptionType2;
501
+ })(ShippingOptionType || {});
502
+ var ShippingRateType = /* @__PURE__ */ ((ShippingRateType2) => {
503
+ ShippingRateType2["FLAT"] = "flat";
504
+ return ShippingRateType2;
505
+ })(ShippingRateType || {});
506
+ var ShippingChannel = /* @__PURE__ */ ((ShippingChannel2) => {
507
+ ShippingChannel2["WEB"] = "web";
508
+ ShippingChannel2["POS"] = "pos";
509
+ return ShippingChannel2;
510
+ })(ShippingChannel || {});
511
+ var ShippingNoOptionReason = /* @__PURE__ */ ((ShippingNoOptionReason2) => {
512
+ ShippingNoOptionReason2["INCOMPLETE"] = "INCOMPLETE";
513
+ ShippingNoOptionReason2["ADDRESS_MISMATCH"] = "ADDRESS_MISMATCH";
514
+ ShippingNoOptionReason2["NO_COVERAGE"] = "NO_COVERAGE";
515
+ ShippingNoOptionReason2["BLOCKED"] = "BLOCKED";
516
+ ShippingNoOptionReason2["NO_OPTION"] = "NO_OPTION";
517
+ return ShippingNoOptionReason2;
518
+ })(ShippingNoOptionReason || {});
519
+ var ShippingZoneError = /* @__PURE__ */ ((ShippingZoneError2) => {
520
+ ShippingZoneError2["NO_PLACES"] = "NO_PLACES";
521
+ ShippingZoneError2["INVALID_PLACE"] = "INVALID_PLACE";
522
+ ShippingZoneError2["DUPLICATE_PLACE"] = "DUPLICATE_PLACE";
523
+ ShippingZoneError2["NO_WEBSITE_OPTION"] = "NO_WEBSITE_OPTION";
524
+ ShippingZoneError2["INVALID_PRICE"] = "INVALID_PRICE";
525
+ ShippingZoneError2["INVALID_DAYS"] = "INVALID_DAYS";
526
+ ShippingZoneError2["INVALID_TIERS"] = "INVALID_TIERS";
527
+ ShippingZoneError2["NO_CHANNEL"] = "NO_CHANNEL";
528
+ ShippingZoneError2["PICKUP_WITHOUT_STORE"] = "PICKUP_WITHOUT_STORE";
529
+ ShippingZoneError2["MISSING_NAME"] = "MISSING_NAME";
530
+ return ShippingZoneError2;
531
+ })(ShippingZoneError || {});
532
+
489
533
  // utils/currency.util.ts
490
534
  var CURRENCY_CONFIG = {
491
535
  // Major currencies with 2 decimal places
@@ -3205,6 +3249,318 @@ function planWindow({ rule, timeZone, presence, startAt, endAt, now, busy = [] }
3205
3249
  }
3206
3250
  var holdExpiresAt = (rule, now) => new Date(now.getTime() + (rule.window.holdMinutes ?? DEFAULT_HOLD_MINUTES) * 6e4);
3207
3251
 
3252
+ // shipping/fold-text.ts
3253
+ function foldText(value) {
3254
+ return value.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/&/g, " and ").replace(/[^a-z0-9]+/g, " ").trim();
3255
+ }
3256
+
3257
+ // shipping/countries.ts
3258
+ var NOT_COUNTRIES = /* @__PURE__ */ new Set(["AC", "CP", "CQ", "DG", "EA", "EU", "EZ", "IC", "QO", "TA", "UN", "XA", "XB", "ZZ"]);
3259
+ var ALIASES = {
3260
+ usa: "US",
3261
+ "united states of america": "US",
3262
+ uk: "GB",
3263
+ "great britain": "GB",
3264
+ england: "GB",
3265
+ uae: "AE",
3266
+ can: "CA",
3267
+ mex: "MX",
3268
+ aus: "AU",
3269
+ gbr: "GB"
3270
+ };
3271
+ var NAME_LOCALES = ["en", "fr"];
3272
+ function regionNames(locale) {
3273
+ try {
3274
+ return new Intl.DisplayNames([locale], { type: "region", fallback: "none" });
3275
+ } catch {
3276
+ return null;
3277
+ }
3278
+ }
3279
+ function canonicalCode(code) {
3280
+ try {
3281
+ return new Intl.Locale(`und-${code}`).maximize().region ?? code;
3282
+ } catch {
3283
+ return code;
3284
+ }
3285
+ }
3286
+ function allCodes() {
3287
+ const names = regionNames("en");
3288
+ if (!names) return [];
3289
+ const codes2 = [];
3290
+ for (let a = 65; a <= 90; a++) {
3291
+ for (let b = 65; b <= 90; b++) {
3292
+ const code = String.fromCharCode(a, b);
3293
+ if (!NOT_COUNTRIES.has(code) && names.of(code) && canonicalCode(code) === code) codes2.push(code);
3294
+ }
3295
+ }
3296
+ return codes2;
3297
+ }
3298
+ var lookup = null;
3299
+ var codes = null;
3300
+ function countryCodes() {
3301
+ codes ??= allCodes();
3302
+ return codes;
3303
+ }
3304
+ function countryLookup() {
3305
+ if (lookup) return lookup;
3306
+ lookup = new Map(Object.entries(ALIASES));
3307
+ for (const locale of NAME_LOCALES) {
3308
+ const names = regionNames(locale);
3309
+ if (!names) continue;
3310
+ for (const code of countryCodes()) {
3311
+ const name = names.of(code);
3312
+ if (name) lookup.set(foldText(name), code);
3313
+ }
3314
+ }
3315
+ return lookup;
3316
+ }
3317
+ function normalizeCountry(value) {
3318
+ const text = (value ?? "").trim();
3319
+ if (!text) return void 0;
3320
+ const upper = text.toUpperCase();
3321
+ if (upper.length === 2) {
3322
+ if (NOT_COUNTRIES.has(upper)) return void 0;
3323
+ const code = canonicalCode(upper);
3324
+ if (countryCodes().includes(code)) return code;
3325
+ }
3326
+ return countryLookup().get(foldText(text));
3327
+ }
3328
+ function listCountries(locale = "en") {
3329
+ const names = regionNames(locale) ?? regionNames("en");
3330
+ return countryCodes().map((code) => ({ code, name: names?.of(code) ?? code })).sort((a, b) => a.name.localeCompare(b.name, locale));
3331
+ }
3332
+
3333
+ // shipping/provinces.ts
3334
+ var CANADA_PROVINCES = [
3335
+ { code: "AB", name: "Alberta", nameFr: "Alberta", postalLetters: ["T"] },
3336
+ { code: "BC", name: "British Columbia", nameFr: "Colombie-Britannique", postalLetters: ["V"] },
3337
+ { code: "MB", name: "Manitoba", nameFr: "Manitoba", postalLetters: ["R"] },
3338
+ { code: "NB", name: "New Brunswick", nameFr: "Nouveau-Brunswick", postalLetters: ["E"] },
3339
+ {
3340
+ code: "NL",
3341
+ name: "Newfoundland and Labrador",
3342
+ nameFr: "Terre-Neuve-et-Labrador",
3343
+ postalLetters: ["A"],
3344
+ aliases: ["NF", "LB", "Newfoundland", "Labrador"]
3345
+ },
3346
+ { code: "NS", name: "Nova Scotia", nameFr: "Nouvelle-\xC9cosse", postalLetters: ["B"] },
3347
+ { code: "NT", name: "Northwest Territories", nameFr: "Territoires du Nord-Ouest", postalLetters: ["X"] },
3348
+ { code: "NU", name: "Nunavut", nameFr: "Nunavut", postalLetters: ["X"] },
3349
+ { code: "ON", name: "Ontario", nameFr: "Ontario", postalLetters: ["K", "L", "M", "N", "P"] },
3350
+ {
3351
+ code: "PE",
3352
+ name: "Prince Edward Island",
3353
+ nameFr: "\xCEle-du-Prince-\xC9douard",
3354
+ postalLetters: ["C"],
3355
+ aliases: ["PEI"]
3356
+ },
3357
+ { code: "QC", name: "Quebec", nameFr: "Qu\xE9bec", postalLetters: ["G", "H", "J"], aliases: ["PQ"] },
3358
+ { code: "SK", name: "Saskatchewan", nameFr: "Saskatchewan", postalLetters: ["S"] },
3359
+ { code: "YT", name: "Yukon", nameFr: "Yukon", postalLetters: ["Y"], aliases: ["YK"] }
3360
+ ];
3361
+ var PROVINCES_BY_COUNTRY = { CA: CANADA_PROVINCES };
3362
+ var lookups = /* @__PURE__ */ new Map();
3363
+ function provinceLookup(country) {
3364
+ const provinces = PROVINCES_BY_COUNTRY[country];
3365
+ if (!provinces) return void 0;
3366
+ let lookup2 = lookups.get(country);
3367
+ if (!lookup2) {
3368
+ lookup2 = /* @__PURE__ */ new Map();
3369
+ for (const p of provinces) {
3370
+ for (const label of [p.code, p.name, p.nameFr, ...p.aliases ?? []]) lookup2.set(foldText(label), p.code);
3371
+ }
3372
+ lookups.set(country, lookup2);
3373
+ }
3374
+ return lookup2;
3375
+ }
3376
+ function normalizeProvince(country, value) {
3377
+ if (!country || !value?.trim()) return void 0;
3378
+ return provinceLookup(country)?.get(foldText(value));
3379
+ }
3380
+ function listProvinces(country) {
3381
+ return PROVINCES_BY_COUNTRY[country] ?? [];
3382
+ }
3383
+ function provincesForPostCode(country, postCode) {
3384
+ const letter = postCode?.charAt(0);
3385
+ if (!country || !letter) return [];
3386
+ return listProvinces(country).filter((p) => p.postalLetters.includes(letter)).map((p) => p.code);
3387
+ }
3388
+
3389
+ // shipping/normalize.ts
3390
+ var EVERYWHERE_KEY = "*";
3391
+ function normalizeCityKey(value) {
3392
+ const words = foldText(value ?? "").split(" ").filter(Boolean).map((word) => word === "saint" ? "st" : word === "sainte" ? "ste" : word);
3393
+ return words.length ? words.join("") : void 0;
3394
+ }
3395
+ function normalizePostCode(value) {
3396
+ const compact = (value ?? "").replace(/[^0-9a-z]/gi, "").toUpperCase();
3397
+ return compact || void 0;
3398
+ }
3399
+ function normalizeAddress(address) {
3400
+ if (!address) return {};
3401
+ const country = normalizeCountry(address.country);
3402
+ const postCode = normalizePostCode(address.postCode ?? address.postalCode ?? address.zip);
3403
+ const fromPostCode = provincesForPostCode(country, postCode);
3404
+ const province = normalizeProvince(country, address.state ?? address.province) ?? (fromPostCode.length === 1 ? fromPostCode[0] : void 0);
3405
+ return { country, province, cityKey: normalizeCityKey(address.city), postCode };
3406
+ }
3407
+ function postCodeMatchesProvince(address) {
3408
+ const candidates = provincesForPostCode(address.country, address.postCode);
3409
+ if (!address.province || candidates.length === 0) return true;
3410
+ return candidates.includes(address.province);
3411
+ }
3412
+ function placeKey(place) {
3413
+ const country = normalizeCountry(place.country);
3414
+ if (!country) return void 0;
3415
+ if (!place.province) return country;
3416
+ const province = normalizeProvince(country, place.province);
3417
+ if (!province) return void 0;
3418
+ if (!place.city) return `${country}-${province}`;
3419
+ const city = normalizeCityKey(place.city);
3420
+ return city ? `${country}-${province}/${city}` : void 0;
3421
+ }
3422
+ function addressPlaceKeys(address) {
3423
+ const keys = [];
3424
+ if (address.country && address.province && address.cityKey) {
3425
+ keys.push(`${address.country}-${address.province}/${address.cityKey}`);
3426
+ }
3427
+ if (address.country && address.province) keys.push(`${address.country}-${address.province}`);
3428
+ if (address.country) keys.push(address.country);
3429
+ keys.push(EVERYWHERE_KEY);
3430
+ return keys;
3431
+ }
3432
+
3433
+ // shipping/resolve.ts
3434
+ function resolveShipping({ zones, address, itemsTotal, channel }) {
3435
+ const shipsToCountries = countriesShippedTo(zones);
3436
+ const none = (reason, zone2) => ({
3437
+ zoneId: zone2?.id ?? null,
3438
+ options: [],
3439
+ reason,
3440
+ message: zone2?.message?.trim() || null,
3441
+ shipsToCountries
3442
+ });
3443
+ const normalized = normalizeAddress(address);
3444
+ if (!normalized.country) return none("INCOMPLETE" /* INCOMPLETE */);
3445
+ if (!postCodeMatchesProvince(normalized)) return none("ADDRESS_MISMATCH" /* ADDRESS_MISMATCH */);
3446
+ const zone = findZone(zones, addressPlaceKeys(normalized));
3447
+ if (!zone) return none("NO_COVERAGE" /* NO_COVERAGE */);
3448
+ const offered = zone.options.filter((option) => isOffered(option, zone, channel));
3449
+ if (offered.length === 0) {
3450
+ return none(zone.ships ? "NO_OPTION" /* NO_OPTION */ : "BLOCKED" /* BLOCKED */, zone);
3451
+ }
3452
+ const quotes = offered.map((option) => quote(option, zone.id, itemsTotal));
3453
+ const ship = quotes.filter((q) => q.type === "ship" /* SHIP */).sort((a, b) => a.amount - b.amount);
3454
+ const pickup = quotes.filter((q) => q.type === "pickup" /* PICKUP */).sort((a, b) => a.amount - b.amount);
3455
+ return {
3456
+ zoneId: zone.id,
3457
+ options: [...ship, ...pickup],
3458
+ reason: null,
3459
+ message: zone.ships ? null : zone.message?.trim() || null,
3460
+ shipsToCountries
3461
+ };
3462
+ }
3463
+ function findZone(zones, keys) {
3464
+ const byKey = /* @__PURE__ */ new Map();
3465
+ for (const zone of zones) {
3466
+ for (const key of zoneKeys(zone)) if (!byKey.has(key)) byKey.set(key, zone);
3467
+ }
3468
+ for (const key of keys) {
3469
+ const zone = byKey.get(key);
3470
+ if (zone) return zone;
3471
+ }
3472
+ return void 0;
3473
+ }
3474
+ function zoneKeys(zone) {
3475
+ if (zone.kind === "everywhere" /* EVERYWHERE */) return [EVERYWHERE_KEY];
3476
+ return zone.places.map(placeKey).filter((key) => Boolean(key));
3477
+ }
3478
+ function isOffered(option, zone, channel) {
3479
+ if (!option.channels.some((c) => c === channel)) return false;
3480
+ if (option.type === "pickup" /* PICKUP */) return channel !== "pos" /* POS */;
3481
+ return zone.ships;
3482
+ }
3483
+ function priceForTotal(option, itemsTotal) {
3484
+ const tiers = [...option.priceByTotal ?? []].sort((a, b) => a.from - b.from);
3485
+ let price = option.price;
3486
+ for (const tier of tiers) if (itemsTotal >= tier.from) price = tier.price;
3487
+ if (option.freeOver != null && itemsTotal >= option.freeOver) return 0;
3488
+ return price;
3489
+ }
3490
+ function quote(option, zoneId, itemsTotal) {
3491
+ const amount = priceForTotal(option, itemsTotal);
3492
+ const freeOver = option.freeOver ?? null;
3493
+ return {
3494
+ optionId: option.id,
3495
+ zoneId,
3496
+ type: option.type,
3497
+ name: option.name?.trim() || null,
3498
+ amount,
3499
+ amountToFree: freeOver != null && amount > 0 ? freeOver - itemsTotal : null,
3500
+ minDays: option.minDays ?? null,
3501
+ maxDays: option.maxDays ?? null,
3502
+ outletId: option.outletId ?? null
3503
+ };
3504
+ }
3505
+ function countriesShippedTo(zones) {
3506
+ const countries = /* @__PURE__ */ new Set();
3507
+ for (const zone of zones) {
3508
+ if (!zone.ships) continue;
3509
+ for (const key of zoneKeys(zone)) if (key !== EVERYWHERE_KEY) countries.add(key.slice(0, 2));
3510
+ }
3511
+ return [...countries].sort();
3512
+ }
3513
+
3514
+ // shipping/validate.ts
3515
+ function validateShippingZone(zone) {
3516
+ const problems = [...placeProblems(zone)];
3517
+ for (const option of zone.options) problems.push(...optionProblems(option));
3518
+ const reachesWebsite = zone.options.some((option) => option.channels.includes("web" /* WEB */));
3519
+ if (zone.ships && !reachesWebsite) problems.push({ code: "NO_WEBSITE_OPTION" /* NO_WEBSITE_OPTION */ });
3520
+ return problems;
3521
+ }
3522
+ function placeProblems(zone) {
3523
+ if (zone.kind === "everywhere" /* EVERYWHERE */) {
3524
+ return zone.places.length ? [{ code: "INVALID_PLACE" /* INVALID_PLACE */ }] : [];
3525
+ }
3526
+ if (zone.places.length === 0) return [{ code: "NO_PLACES" /* NO_PLACES */ }];
3527
+ if (!zone.places.every((place) => fitsKind(place, zone.kind))) return [{ code: "INVALID_PLACE" /* INVALID_PLACE */ }];
3528
+ const keys = zone.places.map(placeKey);
3529
+ if (keys.some((key) => !key)) return [{ code: "INVALID_PLACE" /* INVALID_PLACE */ }];
3530
+ if (new Set(keys).size !== keys.length) return [{ code: "DUPLICATE_PLACE" /* DUPLICATE_PLACE */ }];
3531
+ return [];
3532
+ }
3533
+ function fitsKind(place, kind) {
3534
+ const hasProvince = Boolean(place.province?.trim());
3535
+ const hasCity = Boolean(place.city?.trim());
3536
+ if (kind === "country" /* COUNTRY */) return !hasProvince && !hasCity;
3537
+ if (kind === "province" /* PROVINCE */) return hasProvince && !hasCity;
3538
+ if (kind === "city" /* CITY */) return hasProvince && hasCity;
3539
+ return false;
3540
+ }
3541
+ var isAmount = (value) => Number.isInteger(value) && value >= 0;
3542
+ var isDays = (value) => value == null || isAmount(value);
3543
+ function optionProblems(option) {
3544
+ const problems = [];
3545
+ const isPickup = option.type === "pickup" /* PICKUP */;
3546
+ if (!isAmount(option.price) || option.freeOver != null && !isAmount(option.freeOver)) {
3547
+ problems.push("INVALID_PRICE" /* INVALID_PRICE */);
3548
+ }
3549
+ if (!isPickup && !option.name?.trim()) problems.push("MISSING_NAME" /* MISSING_NAME */);
3550
+ if (!isDays(option.minDays) || !isDays(option.maxDays) || (option.minDays ?? 0) > (option.maxDays ?? Infinity)) {
3551
+ problems.push("INVALID_DAYS" /* INVALID_DAYS */);
3552
+ }
3553
+ if (!tiersAreValid(option)) problems.push("INVALID_TIERS" /* INVALID_TIERS */);
3554
+ if (option.channels.length === 0) problems.push("NO_CHANNEL" /* NO_CHANNEL */);
3555
+ if (isPickup && !option.outletId) problems.push("PICKUP_WITHOUT_STORE" /* PICKUP_WITHOUT_STORE */);
3556
+ return problems.map((code) => ({ code, optionId: option.id }));
3557
+ }
3558
+ function tiersAreValid(option) {
3559
+ const tiers = option.priceByTotal ?? [];
3560
+ const froms = tiers.map((tier) => tier.from);
3561
+ return tiers.every((tier) => Number.isInteger(tier.from) && tier.from > 0 && isAmount(tier.price)) && new Set(froms).size === froms.length;
3562
+ }
3563
+
3208
3564
  // constants/adyen.constants.ts
3209
3565
  var ADYEN_ROLLOUT_COUNTRY_CODES = /* @__PURE__ */ new Set(["CA"]);
3210
3566
  function isAdyenRolloutCountry(alpha2) {
@@ -3262,6 +3618,7 @@ export {
3262
3618
  BookingResourceStatus,
3263
3619
  BookingRulesError,
3264
3620
  BookingType,
3621
+ CANADA_PROVINCES,
3265
3622
  CONDITION_OPERATORS,
3266
3623
  COUNTRY_CURRENCIES,
3267
3624
  CURRENCY_CONFIG,
@@ -3278,6 +3635,7 @@ export {
3278
3635
  DualWriteStrategy,
3279
3636
  EUFixedRates,
3280
3637
  EUROPE,
3638
+ EVERYWHERE_KEY,
3281
3639
  ExtensionCategory,
3282
3640
  FINANCIAL_INVARIANTS,
3283
3641
  FULFILLMENT_STATES,
@@ -3328,6 +3686,12 @@ export {
3328
3686
  SYSTEM_FAMILY_IDS,
3329
3687
  SYSTEM_UNITS,
3330
3688
  SYSTEM_UNIT_IDS,
3689
+ ShippingChannel,
3690
+ ShippingNoOptionReason,
3691
+ ShippingOptionType,
3692
+ ShippingRateType,
3693
+ ShippingZoneError,
3694
+ ShippingZoneKind,
3331
3695
  StationStatus,
3332
3696
  StripeCurrencyCode,
3333
3697
  StripeStatus,
@@ -3335,6 +3699,7 @@ export {
3335
3699
  TopMetericsSort,
3336
3700
  UserTypes,
3337
3701
  addAmounts,
3702
+ addressPlaceKeys,
3338
3703
  amountToString,
3339
3704
  assertUnitSane,
3340
3705
  availableIn,
@@ -3395,19 +3760,31 @@ export {
3395
3760
  isSystemFamilyCode,
3396
3761
  isValidQuantity,
3397
3762
  isWhole,
3763
+ listCountries,
3764
+ listProvinces,
3398
3765
  mapToLegacyStatus,
3399
3766
  maxPrecisionFor,
3400
3767
  mergeFragment,
3401
3768
  multiplyAmount,
3769
+ normalizeAddress,
3770
+ normalizeCityKey,
3771
+ normalizeCountry,
3402
3772
  normalizeMongoIdString,
3773
+ normalizePostCode,
3774
+ normalizeProvince,
3403
3775
  occupancyIn,
3404
3776
  parseAmountToMinorUnits,
3405
3777
  parseCurrencyCode,
3406
3778
  parseStripeCurrencyCode,
3779
+ placeKey,
3407
3780
  planWindow,
3781
+ postCodeMatchesProvince,
3408
3782
  presentAt,
3783
+ priceForTotal,
3784
+ provincesForPostCode,
3409
3785
  removeFragment,
3410
3786
  resolveFamily,
3787
+ resolveShipping,
3411
3788
  resolveTemplate,
3412
3789
  resolveUnit,
3413
3790
  roundToPrecision,
@@ -3423,6 +3800,7 @@ export {
3423
3800
  toMinorUnits,
3424
3801
  validateConfig,
3425
3802
  validateFragmentCompatibility,
3803
+ validateShippingZone,
3426
3804
  violatesFinancialInvariant,
3427
3805
  windowEndsAt,
3428
3806
  withinPresence