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

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,321 @@ 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
+ canada: "CA",
3261
+ "united states": "US",
3262
+ usa: "US",
3263
+ "united states of america": "US",
3264
+ uk: "GB",
3265
+ "great britain": "GB",
3266
+ england: "GB",
3267
+ uae: "AE",
3268
+ can: "CA",
3269
+ mex: "MX",
3270
+ aus: "AU",
3271
+ gbr: "GB"
3272
+ };
3273
+ var NAME_LOCALES = ["en", "fr"];
3274
+ function regionNames(locale) {
3275
+ try {
3276
+ return new Intl.DisplayNames([locale], { type: "region", fallback: "none" });
3277
+ } catch {
3278
+ return null;
3279
+ }
3280
+ }
3281
+ function canonicalCode(code) {
3282
+ try {
3283
+ return new Intl.Locale(`und-${code}`).maximize().region ?? code;
3284
+ } catch {
3285
+ return code;
3286
+ }
3287
+ }
3288
+ function allCodes() {
3289
+ const names = regionNames("en");
3290
+ if (!names) return [];
3291
+ const codes2 = [];
3292
+ for (let a = 65; a <= 90; a++) {
3293
+ for (let b = 65; b <= 90; b++) {
3294
+ const code = String.fromCharCode(a, b);
3295
+ if (!NOT_COUNTRIES.has(code) && names.of(code) && canonicalCode(code) === code) codes2.push(code);
3296
+ }
3297
+ }
3298
+ return codes2;
3299
+ }
3300
+ var lookup = null;
3301
+ var codes = null;
3302
+ function countryCodes() {
3303
+ codes ??= allCodes();
3304
+ return codes;
3305
+ }
3306
+ function countryLookup() {
3307
+ if (lookup) return lookup;
3308
+ lookup = new Map(Object.entries(ALIASES));
3309
+ for (const locale of NAME_LOCALES) {
3310
+ const names = regionNames(locale);
3311
+ if (!names) continue;
3312
+ for (const code of countryCodes()) {
3313
+ const name = names.of(code);
3314
+ if (name) lookup.set(foldText(name), code);
3315
+ }
3316
+ }
3317
+ return lookup;
3318
+ }
3319
+ function normalizeCountry(value) {
3320
+ const text = (value ?? "").trim();
3321
+ if (!text) return void 0;
3322
+ const upper = text.toUpperCase();
3323
+ if (upper.length === 2) {
3324
+ if (NOT_COUNTRIES.has(upper)) return void 0;
3325
+ if (countryCodes().length === 0) return /^[A-Z]{2}$/.test(upper) ? upper : void 0;
3326
+ const code = canonicalCode(upper);
3327
+ if (countryCodes().includes(code)) return code;
3328
+ }
3329
+ return countryLookup().get(foldText(text));
3330
+ }
3331
+ function listCountries(locale = "en") {
3332
+ const names = regionNames(locale) ?? regionNames("en");
3333
+ return countryCodes().map((code) => ({ code, name: names?.of(code) ?? code })).sort((a, b) => a.name.localeCompare(b.name, locale));
3334
+ }
3335
+
3336
+ // shipping/provinces.ts
3337
+ var CANADA_PROVINCES = [
3338
+ { code: "AB", name: "Alberta", nameFr: "Alberta", postalLetters: ["T"] },
3339
+ { code: "BC", name: "British Columbia", nameFr: "Colombie-Britannique", postalLetters: ["V"] },
3340
+ { code: "MB", name: "Manitoba", nameFr: "Manitoba", postalLetters: ["R"] },
3341
+ { code: "NB", name: "New Brunswick", nameFr: "Nouveau-Brunswick", postalLetters: ["E"] },
3342
+ {
3343
+ code: "NL",
3344
+ name: "Newfoundland and Labrador",
3345
+ nameFr: "Terre-Neuve-et-Labrador",
3346
+ postalLetters: ["A"],
3347
+ aliases: ["NF", "LB", "Newfoundland", "Labrador"]
3348
+ },
3349
+ { code: "NS", name: "Nova Scotia", nameFr: "Nouvelle-\xC9cosse", postalLetters: ["B"] },
3350
+ { code: "NT", name: "Northwest Territories", nameFr: "Territoires du Nord-Ouest", postalLetters: ["X"] },
3351
+ { code: "NU", name: "Nunavut", nameFr: "Nunavut", postalLetters: ["X"] },
3352
+ { code: "ON", name: "Ontario", nameFr: "Ontario", postalLetters: ["K", "L", "M", "N", "P"] },
3353
+ {
3354
+ code: "PE",
3355
+ name: "Prince Edward Island",
3356
+ nameFr: "\xCEle-du-Prince-\xC9douard",
3357
+ postalLetters: ["C"],
3358
+ aliases: ["PEI"]
3359
+ },
3360
+ { code: "QC", name: "Quebec", nameFr: "Qu\xE9bec", postalLetters: ["G", "H", "J"], aliases: ["PQ"] },
3361
+ { code: "SK", name: "Saskatchewan", nameFr: "Saskatchewan", postalLetters: ["S"] },
3362
+ { code: "YT", name: "Yukon", nameFr: "Yukon", postalLetters: ["Y"], aliases: ["YK"] }
3363
+ ];
3364
+ var PROVINCES_BY_COUNTRY = { CA: CANADA_PROVINCES };
3365
+ var lookups = /* @__PURE__ */ new Map();
3366
+ function provinceLookup(country) {
3367
+ const provinces = PROVINCES_BY_COUNTRY[country];
3368
+ if (!provinces) return void 0;
3369
+ let lookup2 = lookups.get(country);
3370
+ if (!lookup2) {
3371
+ lookup2 = /* @__PURE__ */ new Map();
3372
+ for (const p of provinces) {
3373
+ for (const label of [p.code, p.name, p.nameFr, ...p.aliases ?? []]) lookup2.set(foldText(label), p.code);
3374
+ }
3375
+ lookups.set(country, lookup2);
3376
+ }
3377
+ return lookup2;
3378
+ }
3379
+ function normalizeProvince(country, value) {
3380
+ if (!country || !value?.trim()) return void 0;
3381
+ return provinceLookup(country)?.get(foldText(value));
3382
+ }
3383
+ function listProvinces(country) {
3384
+ return PROVINCES_BY_COUNTRY[country] ?? [];
3385
+ }
3386
+ function provincesForPostCode(country, postCode) {
3387
+ const letter = postCode?.charAt(0);
3388
+ if (!country || !letter) return [];
3389
+ return listProvinces(country).filter((p) => p.postalLetters.includes(letter)).map((p) => p.code);
3390
+ }
3391
+
3392
+ // shipping/normalize.ts
3393
+ var EVERYWHERE_KEY = "*";
3394
+ function normalizeCityKey(value) {
3395
+ const words = foldText(value ?? "").split(" ").filter(Boolean).map((word) => word === "saint" ? "st" : word === "sainte" ? "ste" : word);
3396
+ return words.length ? words.join("") : void 0;
3397
+ }
3398
+ function normalizePostCode(value) {
3399
+ const compact = (value ?? "").replace(/[^0-9a-z]/gi, "").toUpperCase();
3400
+ return compact || void 0;
3401
+ }
3402
+ function normalizeAddress(address) {
3403
+ if (!address) return {};
3404
+ const country = normalizeCountry(address.country);
3405
+ const postCode = normalizePostCode(address.postCode ?? address.postalCode ?? address.zip);
3406
+ const fromPostCode = provincesForPostCode(country, postCode);
3407
+ const province = normalizeProvince(country, address.state ?? address.province) ?? (fromPostCode.length === 1 ? fromPostCode[0] : void 0);
3408
+ return { country, province, cityKey: normalizeCityKey(address.city), postCode };
3409
+ }
3410
+ function postCodeMatchesProvince(address) {
3411
+ const candidates = provincesForPostCode(address.country, address.postCode);
3412
+ if (!address.province || candidates.length === 0) return true;
3413
+ return candidates.includes(address.province);
3414
+ }
3415
+ function placeKey(place) {
3416
+ const country = normalizeCountry(place.country);
3417
+ if (!country) return void 0;
3418
+ if (!place.province) return country;
3419
+ const province = normalizeProvince(country, place.province);
3420
+ if (!province) return void 0;
3421
+ if (!place.city) return `${country}-${province}`;
3422
+ const city = normalizeCityKey(place.city);
3423
+ return city ? `${country}-${province}/${city}` : void 0;
3424
+ }
3425
+ function addressPlaceKeys(address) {
3426
+ const keys = [];
3427
+ if (address.country && address.province && address.cityKey) {
3428
+ keys.push(`${address.country}-${address.province}/${address.cityKey}`);
3429
+ }
3430
+ if (address.country && address.province) keys.push(`${address.country}-${address.province}`);
3431
+ if (address.country) keys.push(address.country);
3432
+ keys.push(EVERYWHERE_KEY);
3433
+ return keys;
3434
+ }
3435
+
3436
+ // shipping/resolve.ts
3437
+ function resolveShipping({ zones, address, itemsTotal, channel }) {
3438
+ const shipsToCountries = countriesShippedTo(zones);
3439
+ const none = (reason, zone2) => ({
3440
+ zoneId: zone2?.id ?? null,
3441
+ options: [],
3442
+ reason,
3443
+ message: zone2?.message?.trim() || null,
3444
+ shipsToCountries
3445
+ });
3446
+ const normalized = normalizeAddress(address);
3447
+ if (!normalized.country) return none("INCOMPLETE" /* INCOMPLETE */);
3448
+ if (!postCodeMatchesProvince(normalized)) return none("ADDRESS_MISMATCH" /* ADDRESS_MISMATCH */);
3449
+ const zone = findZone(zones, addressPlaceKeys(normalized));
3450
+ if (!zone) return none("NO_COVERAGE" /* NO_COVERAGE */);
3451
+ const offered = zone.options.filter((option) => isOffered(option, zone, channel));
3452
+ if (offered.length === 0) {
3453
+ return none(zone.ships ? "NO_OPTION" /* NO_OPTION */ : "BLOCKED" /* BLOCKED */, zone);
3454
+ }
3455
+ const quotes = offered.map((option) => quote(option, zone.id, itemsTotal));
3456
+ const ship = quotes.filter((q) => q.type === "ship" /* SHIP */).sort((a, b) => a.amount - b.amount);
3457
+ const pickup = quotes.filter((q) => q.type === "pickup" /* PICKUP */).sort((a, b) => a.amount - b.amount);
3458
+ return {
3459
+ zoneId: zone.id,
3460
+ options: [...ship, ...pickup],
3461
+ reason: null,
3462
+ message: zone.ships ? null : zone.message?.trim() || null,
3463
+ shipsToCountries
3464
+ };
3465
+ }
3466
+ function findZone(zones, keys) {
3467
+ const byKey = /* @__PURE__ */ new Map();
3468
+ for (const zone of zones) {
3469
+ for (const key of zoneKeys(zone)) if (!byKey.has(key)) byKey.set(key, zone);
3470
+ }
3471
+ for (const key of keys) {
3472
+ const zone = byKey.get(key);
3473
+ if (zone) return zone;
3474
+ }
3475
+ return void 0;
3476
+ }
3477
+ function zoneKeys(zone) {
3478
+ if (zone.kind === "everywhere" /* EVERYWHERE */) return [EVERYWHERE_KEY];
3479
+ return zone.places.map(placeKey).filter((key) => Boolean(key));
3480
+ }
3481
+ function isOffered(option, zone, channel) {
3482
+ if (!option.channels.some((c) => c === channel)) return false;
3483
+ if (option.type === "pickup" /* PICKUP */) return channel !== "pos" /* POS */;
3484
+ return zone.ships;
3485
+ }
3486
+ function priceForTotal(option, itemsTotal) {
3487
+ const tiers = [...option.priceByTotal ?? []].sort((a, b) => a.from - b.from);
3488
+ let price = option.price;
3489
+ for (const tier of tiers) if (itemsTotal >= tier.from) price = tier.price;
3490
+ if (option.freeOver != null && itemsTotal >= option.freeOver) return 0;
3491
+ return price;
3492
+ }
3493
+ function quote(option, zoneId, itemsTotal) {
3494
+ const amount = priceForTotal(option, itemsTotal);
3495
+ const freeOver = option.freeOver ?? null;
3496
+ return {
3497
+ optionId: option.id,
3498
+ zoneId,
3499
+ type: option.type,
3500
+ name: option.name?.trim() || null,
3501
+ amount,
3502
+ amountToFree: freeOver != null && amount > 0 ? freeOver - itemsTotal : null,
3503
+ minDays: option.minDays ?? null,
3504
+ maxDays: option.maxDays ?? null,
3505
+ outletId: option.outletId ?? null
3506
+ };
3507
+ }
3508
+ function countriesShippedTo(zones) {
3509
+ const countries = /* @__PURE__ */ new Set();
3510
+ for (const zone of zones) {
3511
+ if (!zone.ships) continue;
3512
+ for (const key of zoneKeys(zone)) if (key !== EVERYWHERE_KEY) countries.add(key.slice(0, 2));
3513
+ }
3514
+ return [...countries].sort();
3515
+ }
3516
+
3517
+ // shipping/validate.ts
3518
+ function validateShippingZone(zone) {
3519
+ const problems = [...placeProblems(zone)];
3520
+ for (const option of zone.options) problems.push(...optionProblems(option));
3521
+ const reachesWebsite = zone.options.some((option) => option.channels.includes("web" /* WEB */));
3522
+ if (zone.ships && !reachesWebsite) problems.push({ code: "NO_WEBSITE_OPTION" /* NO_WEBSITE_OPTION */ });
3523
+ return problems;
3524
+ }
3525
+ function placeProblems(zone) {
3526
+ if (zone.kind === "everywhere" /* EVERYWHERE */) {
3527
+ return zone.places.length ? [{ code: "INVALID_PLACE" /* INVALID_PLACE */ }] : [];
3528
+ }
3529
+ if (zone.places.length === 0) return [{ code: "NO_PLACES" /* NO_PLACES */ }];
3530
+ if (!zone.places.every((place) => fitsKind(place, zone.kind))) return [{ code: "INVALID_PLACE" /* INVALID_PLACE */ }];
3531
+ const keys = zone.places.map(placeKey);
3532
+ if (keys.some((key) => !key)) return [{ code: "INVALID_PLACE" /* INVALID_PLACE */ }];
3533
+ if (new Set(keys).size !== keys.length) return [{ code: "DUPLICATE_PLACE" /* DUPLICATE_PLACE */ }];
3534
+ return [];
3535
+ }
3536
+ function fitsKind(place, kind) {
3537
+ const hasProvince = Boolean(place.province?.trim());
3538
+ const hasCity = Boolean(place.city?.trim());
3539
+ if (kind === "country" /* COUNTRY */) return !hasProvince && !hasCity;
3540
+ if (kind === "province" /* PROVINCE */) return hasProvince && !hasCity;
3541
+ if (kind === "city" /* CITY */) return hasProvince && hasCity;
3542
+ return false;
3543
+ }
3544
+ var isAmount = (value) => Number.isInteger(value) && value >= 0;
3545
+ var isDays = (value) => value == null || isAmount(value);
3546
+ function optionProblems(option) {
3547
+ const problems = [];
3548
+ const isPickup = option.type === "pickup" /* PICKUP */;
3549
+ if (!isAmount(option.price) || option.freeOver != null && !isAmount(option.freeOver)) {
3550
+ problems.push("INVALID_PRICE" /* INVALID_PRICE */);
3551
+ }
3552
+ if (!isPickup && !option.name?.trim()) problems.push("MISSING_NAME" /* MISSING_NAME */);
3553
+ if (!isDays(option.minDays) || !isDays(option.maxDays) || (option.minDays ?? 0) > (option.maxDays ?? Infinity)) {
3554
+ problems.push("INVALID_DAYS" /* INVALID_DAYS */);
3555
+ }
3556
+ if (!tiersAreValid(option)) problems.push("INVALID_TIERS" /* INVALID_TIERS */);
3557
+ if (option.channels.length === 0) problems.push("NO_CHANNEL" /* NO_CHANNEL */);
3558
+ if (isPickup && !option.outletId) problems.push("PICKUP_WITHOUT_STORE" /* PICKUP_WITHOUT_STORE */);
3559
+ return problems.map((code) => ({ code, optionId: option.id }));
3560
+ }
3561
+ function tiersAreValid(option) {
3562
+ const tiers = option.priceByTotal ?? [];
3563
+ const froms = tiers.map((tier) => tier.from);
3564
+ return tiers.every((tier) => Number.isInteger(tier.from) && tier.from > 0 && isAmount(tier.price)) && new Set(froms).size === froms.length;
3565
+ }
3566
+
3208
3567
  // constants/adyen.constants.ts
3209
3568
  var ADYEN_ROLLOUT_COUNTRY_CODES = /* @__PURE__ */ new Set(["CA"]);
3210
3569
  function isAdyenRolloutCountry(alpha2) {
@@ -3262,6 +3621,7 @@ export {
3262
3621
  BookingResourceStatus,
3263
3622
  BookingRulesError,
3264
3623
  BookingType,
3624
+ CANADA_PROVINCES,
3265
3625
  CONDITION_OPERATORS,
3266
3626
  COUNTRY_CURRENCIES,
3267
3627
  CURRENCY_CONFIG,
@@ -3278,6 +3638,7 @@ export {
3278
3638
  DualWriteStrategy,
3279
3639
  EUFixedRates,
3280
3640
  EUROPE,
3641
+ EVERYWHERE_KEY,
3281
3642
  ExtensionCategory,
3282
3643
  FINANCIAL_INVARIANTS,
3283
3644
  FULFILLMENT_STATES,
@@ -3328,6 +3689,12 @@ export {
3328
3689
  SYSTEM_FAMILY_IDS,
3329
3690
  SYSTEM_UNITS,
3330
3691
  SYSTEM_UNIT_IDS,
3692
+ ShippingChannel,
3693
+ ShippingNoOptionReason,
3694
+ ShippingOptionType,
3695
+ ShippingRateType,
3696
+ ShippingZoneError,
3697
+ ShippingZoneKind,
3331
3698
  StationStatus,
3332
3699
  StripeCurrencyCode,
3333
3700
  StripeStatus,
@@ -3335,6 +3702,7 @@ export {
3335
3702
  TopMetericsSort,
3336
3703
  UserTypes,
3337
3704
  addAmounts,
3705
+ addressPlaceKeys,
3338
3706
  amountToString,
3339
3707
  assertUnitSane,
3340
3708
  availableIn,
@@ -3395,19 +3763,31 @@ export {
3395
3763
  isSystemFamilyCode,
3396
3764
  isValidQuantity,
3397
3765
  isWhole,
3766
+ listCountries,
3767
+ listProvinces,
3398
3768
  mapToLegacyStatus,
3399
3769
  maxPrecisionFor,
3400
3770
  mergeFragment,
3401
3771
  multiplyAmount,
3772
+ normalizeAddress,
3773
+ normalizeCityKey,
3774
+ normalizeCountry,
3402
3775
  normalizeMongoIdString,
3776
+ normalizePostCode,
3777
+ normalizeProvince,
3403
3778
  occupancyIn,
3404
3779
  parseAmountToMinorUnits,
3405
3780
  parseCurrencyCode,
3406
3781
  parseStripeCurrencyCode,
3782
+ placeKey,
3407
3783
  planWindow,
3784
+ postCodeMatchesProvince,
3408
3785
  presentAt,
3786
+ priceForTotal,
3787
+ provincesForPostCode,
3409
3788
  removeFragment,
3410
3789
  resolveFamily,
3790
+ resolveShipping,
3411
3791
  resolveTemplate,
3412
3792
  resolveUnit,
3413
3793
  roundToPrecision,
@@ -3423,6 +3803,7 @@ export {
3423
3803
  toMinorUnits,
3424
3804
  validateConfig,
3425
3805
  validateFragmentCompatibility,
3806
+ validateShippingZone,
3426
3807
  violatesFinancialInvariant,
3427
3808
  windowEndsAt,
3428
3809
  withinPresence