@rebuy/rebuy 3.16.3 → 3.17.0

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.
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/gwp/index.ts
21
21
  var gwp_exports = {};
22
22
  __export(gwp_exports, {
23
+ GIFT_KINDS: () => GIFT_KINDS,
23
24
  getValidGwpProductIds: () => getValidGwpProductIds,
24
25
  hasLostDiscount: () => hasLostDiscount,
25
26
  hasUnsupportedRuleTypes: () => hasUnsupportedRuleTypes,
@@ -50,3 +51,6 @@ var isDefinitiveRuleAnswer = (metadata) => metadata != null && (metadata.matched
50
51
  // src/gwp/shouldRemoveGift.ts
51
52
  var hasLostDiscount = (gift) => !gift.discounted && gift.cost > 0;
52
53
  var shouldRemoveGift = (gift, validIds) => !validIds.has(gift.productId) || hasLostDiscount(gift);
54
+
55
+ // src/gwp/types.ts
56
+ var GIFT_KINDS = ["gwp", "sgwp"];
@@ -19,7 +19,11 @@ var isDefinitiveRuleAnswer = (metadata) => metadata != null && (metadata.matched
19
19
  // src/gwp/shouldRemoveGift.ts
20
20
  var hasLostDiscount = (gift) => !gift.discounted && gift.cost > 0;
21
21
  var shouldRemoveGift = (gift, validIds) => !validIds.has(gift.productId) || hasLostDiscount(gift);
22
+
23
+ // src/gwp/types.ts
24
+ var GIFT_KINDS = ["gwp", "sgwp"];
22
25
  export {
26
+ GIFT_KINDS,
23
27
  getValidGwpProductIds,
24
28
  hasLostDiscount,
25
29
  hasUnsupportedRuleTypes,
@@ -1,17 +1,39 @@
1
- /** A gift-with-purchase cart line the client asks the server to validate. */
1
+ /**
2
+ * The attribution a gift line carries, as the wire tags it: `gwp` = `Rebuy Gift with Purchase`, `sgwp` =
3
+ * `Rebuy Selectable Gift with Purchase`. `gwp` lines run the GWP rules; `sgwp` lines are judged against their
4
+ * widget's milestones (REB-24363).
5
+ */
6
+ export declare const GIFT_KINDS: readonly ["gwp", "sgwp"];
7
+ export type GiftKind = (typeof GIFT_KINDS)[number];
8
+ /**
9
+ * A gift cart line the client asks the server to validate (GWP or selectable GWP — see {@link GiftKind}).
10
+ * This is the WIRE shape — what a client sends, `z.input` of the server's `GiftInputSchema` — so the two
11
+ * REB-24363 fields are optional here: a client that predates them sends GWP lines only, and the schema
12
+ * defaults fill them (`kind: 'gwp'`, `quantity: 1`) before the server reads either. The parsed twin
13
+ * (`GiftInput`, `./server`) carries both as required.
14
+ */
2
15
  export type GwpGift = {
3
16
  /**
4
- * The gift line's total in the buyer's MARKET currency, MAJOR units (e.g. `19.99`) — a
5
- * Function-discounted (free) gift is `0`. Only the zero/non-zero distinction is read
6
- * (`cost > 0` the gift lost its discount), so the exact unit isn't load-bearing; send the
7
- * Shopify cart-line total as-is.
17
+ * The gift line's total in the buyer's MARKET (presentment) currency, MAJOR units (e.g. `19.99`) — a
18
+ * Function-discounted (free) gift is `0`. Send the Shopify cart-line total as-is; both the unit and the
19
+ * magnitude are contractual. A `gwp` line only has its zero/non-zero read (`cost > 0` the gift lost
20
+ * its discount), but an `sgwp` line's cost is subtracted from the subtotal its milestones are judged
21
+ * against (×100, ÷ the FX rate), so a minor-unit or unit-price value there would over-subtract and
22
+ * silently strip earned gifts (REB-24363).
8
23
  */
9
24
  cost: number;
10
25
  /** Whether the Shopify Function discount is currently allocated to the line. */
11
26
  discounted: boolean;
27
+ /** Which attribution the line carries; omitted by clients that predate the field (reads as `gwp`). */
28
+ kind?: GiftKind;
12
29
  /** Shopify cart-line id to remove. */
13
30
  lineId: string;
14
31
  productId: number;
32
+ /**
33
+ * The line's unit count; the selectable-GWP verdict trims surplus units against milestone capacity.
34
+ * Omitted by clients that predate the field (reads as one unit — a GWP verdict is whole-line anyway).
35
+ */
36
+ quantity?: number;
15
37
  /** The Rebuy widget that added the gift (`_widget_id`). */
16
38
  widgetId: number;
17
39
  };
@@ -1,22 +1,20 @@
1
- import { type GiftValidationInput, type ServerContext } from '../server/shared';
1
+ import { type GiftValidationInput, type GiftValidationResponse, type ServerContext } from '../server/shared';
2
2
  /**
3
- * Validates gift lines per widget and returns the cart-line ids to remove (the complete receipt).
4
- * Per widget: resolve its data-source path, evaluate its rules against the cart (`limit: 0`), then
5
- * drop gifts that no longer qualify OR lost their discount.
3
+ * Validates the gift lines and returns the receipt. The two gift kinds get independent verdicts, each
4
+ * filling its own list so a line id can never land in both: GWP (`gwp`) lines are judged whole-line by
5
+ * their widget's rules and fill `remove` ({@link validateGwpGifts}); selectable GWP (`sgwp`) lines are
6
+ * trimmed to their widget's milestone capacity and fill `adjust` (target quantity, `0` = remove —
7
+ * {@link validateSelectableGifts}). A selectable gift may be legitimately priced, so the GWP charged-gift
8
+ * rule never touches it. Both halves fail safe per widget, so a degraded answer for one widget never
9
+ * disturbs another's verdict.
6
10
  *
7
- * The two removal reasons are NOT symmetric. **Qualification loss** (a gift's product no longer matches
8
- * the rules) needs the engine's answer, so it is **fail-safe**: any error, a missing endpoint, a
9
- * non-definitive rule answer (no metadata, or metadata with neither rule arraythe tolerated-empty-2xx
10
- * path), or unsupported rule types (`url`/`order_tag`) don't drop on qualification (never remove a
11
- * genuinely-qualified gift on uncertainty). A definitive answer removes a gift its matched-rule product
12
- * output omits; when that output yields NO ids at all, only a confirmed miss ({@link isConfirmedRuleMiss}:
13
- * `unmatchedRules` populated, `matchedRules` empty) removes a matched rule with a non-product output
14
- * (collection/endpoint/tag) or a zero-rule ruleset proves nothing, so those keep the gifts (REB-23842,
15
- * matching the React extension). **Discount loss** ({@link hasLostDiscount}: `!discounted && cost > 0`) is cart-observable
16
- * and needs NO engine answer, so it removes the gift on EVERY path, degraded or not — a buyer must not keep
17
- * paying for a "gift" because the rule engine was degraded. (React removed on any successful fetch; this
18
- * splits the observable discount-loss out of the qualification fail-safe rather than gating both behind it.)
11
+ * The receipt's one-verdict-per-line invariant (`GiftValidationResponseSchema`) is upheld HERE, not by the
12
+ * client: nothing stops a buggy watcher sending one `lineId` twice under both kinds (a line carrying
13
+ * both attributions, or a double push), or as `sgwp` under two widgetswhich would otherwise land it in
14
+ * both lists, or twice in `adjust` with conflicting targets, and fail the response schema at the route — a
15
+ * 500 where the feature promises fail-safe. The strictest verdict wins: removal over any target (a removed
16
+ * line has no quantity left), and the smallest target among duplicates (each is a trim, so the smallest can
17
+ * never exceed what was sent). `remove` is likewise named once per line (a `gwp` line sent under two
18
+ * widgets is removed by both legs), so a client issuing one cart mutation per entry never removes twice.
19
19
  */
20
- export declare const validateGifts: (input: GiftValidationInput, ctx: ServerContext) => Promise<{
21
- remove: string[];
22
- }>;
20
+ export declare const validateGifts: (input: GiftValidationInput, ctx: ServerContext) => Promise<GiftValidationResponse>;
@@ -0,0 +1,37 @@
1
+ import { z } from 'zod';
2
+ import { type GiftKind } from '../gwp';
3
+ import { type GiftInput, type Reporter, type ServerContext } from '../server/shared';
4
+ /**
5
+ * One selectable-GWP milestone, RAW (snake_case) as the retired React validator read it: `unlock_price` in
6
+ * shop MAJOR units, `selectable_product_quantity` in units (a non-negative whole count — a fractional or
7
+ * negative capacity is nonsense, so it is malformed rather than summed).
8
+ *
9
+ * `unlock_price` is deliberately NOT bounded below. A negative or zero price is a legitimate "always
10
+ * unlocked" tier (the progress-bar converter tolerates the same via `Number(...) || 0`), and it only
11
+ * grants ITS OWN capacity. Rejecting it would mark the whole list malformed, which keeps every gift the
12
+ * widget added — unlimited capacity, a strictly worse degradation than the one avoided.
13
+ */
14
+ export declare const SgwpMilestoneSchema: z.ZodObject<{
15
+ selectable_product_quantity: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodCoercedNumber<string | number>>, z.ZodNumber>;
16
+ unlock_price: z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodCoercedNumber<string | number>>;
17
+ }, z.core.$strip>;
18
+ /** One parsed selectable-GWP milestone: the shop-major-unit unlock price and the gift units it grants. */
19
+ export type SgwpMilestone = z.infer<typeof SgwpMilestoneSchema>;
20
+ /**
21
+ * One gift widget's raw settings from the engine (the `data` envelope unwrapped, so a deleted widget's
22
+ * `{ data: null }` reads as `null`). Throws on an upstream failure — every caller fails safe (keeps the
23
+ * widget's gifts), so the throw is the signal, not a bug.
24
+ */
25
+ export declare const fetchGiftWidgetSettings: (widgetId: number, shop: string, ctx: ServerContext) => Promise<unknown>;
26
+ /** A GWP widget's data-source path from its raw settings `endpoint` (uniform across widget types). */
27
+ export declare const readDataSourcePath: (settings: unknown) => string | undefined;
28
+ /**
29
+ * A selectable-GWP widget's milestones from its raw settings. Absent or empty milestones are an empty list
30
+ * (the widget has nothing to judge against — silent). A MALFORMED list also reads as empty, because the
31
+ * fail-safe direction is to keep the gifts, but that is a settings-contract drift, so it is reported — with
32
+ * the first issue's path AND message, since the likeliest real drift (`milestones` is an object, not an
33
+ * array) fails at the top level, where the path is empty and only the message says what arrived.
34
+ */
35
+ export declare const readMilestones: (settings: unknown, report?: Reporter) => SgwpMilestone[];
36
+ /** The gifts of one `kind`, grouped by widget in request order (insertion-ordered map; lines keep their order within a widget). */
37
+ export declare const groupGiftsByWidget: (gifts: GiftInput[], kind: GiftKind) => Map<number, GiftInput[]>;
@@ -25,8 +25,10 @@ __export(server_exports, {
25
25
  CartInputSchema: () => CartInputSchema,
26
26
  CartLineInputSchema: () => CartLineInputSchema,
27
27
  DataSourceInputSchema: () => DataSourceInputSchema,
28
+ GiftAdjustmentSchema: () => GiftAdjustmentSchema,
28
29
  GiftInputSchema: () => GiftInputSchema,
29
30
  GiftValidationInputSchema: () => GiftValidationInputSchema,
31
+ GiftValidationResponseSchema: () => GiftValidationResponseSchema,
30
32
  MonetizeInputSchema: () => MonetizeInputSchema,
31
33
  MonetizeTransactionItemSchema: () => MonetizeTransactionItemSchema,
32
34
  NotFoundError: () => NotFoundError,
@@ -71,6 +73,9 @@ var isDefinitiveRuleAnswer = (metadata) => metadata != null && (metadata.matched
71
73
  var hasLostDiscount = (gift) => !gift.discounted && gift.cost > 0;
72
74
  var shouldRemoveGift = (gift, validIds) => !validIds.has(gift.productId) || hasLostDiscount(gift);
73
75
 
76
+ // src/gwp/types.ts
77
+ var GIFT_KINDS = ["gwp", "sgwp"];
78
+
74
79
  // src/schema/metadata.ts
75
80
  var import_zod2 = require("zod");
76
81
 
@@ -430,6 +435,14 @@ var NotFoundError = class extends Error {
430
435
  }
431
436
  };
432
437
 
438
+ // src/server/shopSubtotal.ts
439
+ var shopCurrencyRate = (cart) => {
440
+ const rate = cart.currencyRate ?? 1;
441
+ return Number.isFinite(rate) && rate > 0 ? rate : 1;
442
+ };
443
+ var shopSubtotalCents = (cart) => Math.round((cart.subtotal - (cart.discountTotal ?? 0)) / shopCurrencyRate(cart));
444
+ var toShopCents = (majorPresentment, rate) => Math.round(majorPresentment * 100 / rate);
445
+
433
446
  // src/schema/cabShopConfig.ts
434
447
  var import_zod6 = require("zod");
435
448
 
@@ -3923,9 +3936,7 @@ var buildEngineParams = (input, key) => {
3923
3936
  variant_id: variantId
3924
3937
  }));
3925
3938
  const itemCount = cart.items.reduce((total, { quantity }) => total + quantity, 0);
3926
- const currencyRate = cart.currencyRate ?? 1;
3927
- const rate = Number.isFinite(currencyRate) && currencyRate > 0 ? currencyRate : 1;
3928
- const subtotal = Math.round((cart.subtotal - (cart.discountTotal ?? 0)) / rate);
3939
+ const subtotal = shopSubtotalCents(cart);
3929
3940
  return {
3930
3941
  cart: {
3931
3942
  attributes: encodeAttributes(cart.attributes),
@@ -4398,26 +4409,92 @@ var convertWidgetSettings = async (input, ctx) => {
4398
4409
  return experiment && isCABTree(result) ? { ...result, experiment } : result;
4399
4410
  };
4400
4411
 
4401
- // src/server/giftValidation.ts
4402
- var resolveDataSourcePath = async (widgetId, shop, ctx) => {
4403
- const raw = await ctx.fetchUpstream("/api/v1/widgets/settings", { id: String(widgetId), shop }, { host: ctx.host });
4404
- const data = raw && typeof raw === "object" && "data" in raw ? raw.data : raw;
4405
- const endpoint = deepCamelCase(data)?.endpoint;
4412
+ // src/server/giftWidgets.ts
4413
+ var import_zod29 = require("zod");
4414
+ var SettingsNumber = import_zod29.z.union([
4415
+ import_zod29.z.number(),
4416
+ import_zod29.z.string().trim().regex(/^-?\d+(\.\d+)?$/)
4417
+ ]).pipe(import_zod29.z.coerce.number());
4418
+ var SgwpMilestoneSchema = import_zod29.z.object({
4419
+ selectable_product_quantity: SettingsNumber.pipe(import_zod29.z.number().int().nonnegative()),
4420
+ unlock_price: SettingsNumber
4421
+ });
4422
+ var fetchGiftWidgetSettings = async (widgetId, shop, ctx) => unwrapData(await ctx.fetchUpstream("/api/v1/widgets/settings", { id: String(widgetId), shop }, { host: ctx.host }));
4423
+ var readDataSourcePath = (settings) => {
4424
+ const endpoint = deepCamelCase(settings)?.endpoint;
4406
4425
  return typeof endpoint === "string" && endpoint ? endpoint : void 0;
4407
4426
  };
4408
- var groupByWidget = (gifts) => {
4427
+ var readMilestones = (settings, report) => {
4428
+ const raw = settings && typeof settings === "object" ? settings.milestones : void 0;
4429
+ if (raw == null) return [];
4430
+ const parsed = import_zod29.z.array(SgwpMilestoneSchema).safeParse(raw);
4431
+ if (parsed.success) return parsed.data;
4432
+ report?.({
4433
+ detail: [
4434
+ "selectable-gift milestones unreadable",
4435
+ ...parsed.error.issues.slice(0, 1).map((issue) => [issue.path.join("."), issue.message].filter(Boolean).join(": "))
4436
+ ].join(": "),
4437
+ kind: "giftMilestones"
4438
+ });
4439
+ return [];
4440
+ };
4441
+ var groupGiftsByWidget = (gifts, kind) => {
4409
4442
  const byWidget = /* @__PURE__ */ new Map();
4410
4443
  for (const gift of gifts) {
4444
+ if (gift.kind !== kind) continue;
4411
4445
  byWidget.set(gift.widgetId, [...byWidget.get(gift.widgetId) ?? [], gift]);
4412
4446
  }
4413
4447
  return byWidget;
4414
4448
  };
4415
- var validateGifts = async (input, ctx) => {
4449
+
4450
+ // src/server/sgwpCapacity.ts
4451
+ var sgwpQualifyingSubtotalCents = (cart, gifts) => {
4452
+ const giftMajor = gifts.filter((gift) => gift.kind === "sgwp").reduce((total, gift) => total + gift.cost, 0);
4453
+ return shopSubtotalCents(cart) - toShopCents(giftMajor, shopCurrencyRate(cart));
4454
+ };
4455
+ var unlockedCapacity = (milestones, subtotalCents) => milestones.reduce(
4456
+ (capacity, milestone) => Math.round(milestone.unlock_price * 100) <= subtotalCents ? capacity + milestone.selectable_product_quantity : capacity,
4457
+ 0
4458
+ );
4459
+
4460
+ // src/server/sgwpValidation.ts
4461
+ var computeSgwpAdjustments = (gifts, capacity) => {
4462
+ let surplus = gifts.reduce((units, gift) => units + gift.quantity, 0) - capacity;
4463
+ const adjust = [];
4464
+ for (const gift of gifts) {
4465
+ if (surplus <= 0) break;
4466
+ const trimmed = Math.min(gift.quantity, surplus);
4467
+ adjust.push({ lineId: gift.lineId, quantity: gift.quantity - trimmed });
4468
+ surplus -= trimmed;
4469
+ }
4470
+ return adjust;
4471
+ };
4472
+ var validateSelectableGifts = async (input, ctx) => {
4473
+ const byWidget = groupGiftsByWidget(input.gifts, "sgwp");
4474
+ if (byWidget.size === 0) return [];
4475
+ const subtotalCents = sgwpQualifyingSubtotalCents(input.cart, input.gifts);
4416
4476
  const perWidget = await Promise.all(
4417
- [...groupByWidget(input.gifts)].map(async ([widgetId, gifts]) => {
4477
+ [...byWidget].map(async ([widgetId, gifts]) => {
4478
+ try {
4479
+ const settings = await fetchGiftWidgetSettings(widgetId, input.shop, ctx);
4480
+ const milestones = readMilestones(settings, ctx.reportDrift);
4481
+ if (milestones.length === 0) return [];
4482
+ return computeSgwpAdjustments(gifts, unlockedCapacity(milestones, subtotalCents));
4483
+ } catch {
4484
+ return [];
4485
+ }
4486
+ })
4487
+ );
4488
+ return perWidget.flat();
4489
+ };
4490
+
4491
+ // src/server/giftValidation.ts
4492
+ var validateGwpGifts = async (input, ctx) => {
4493
+ const perWidget = await Promise.all(
4494
+ [...groupGiftsByWidget(input.gifts, "gwp")].map(async ([widgetId, gifts]) => {
4418
4495
  const discountLost = gifts.filter(hasLostDiscount).map((gift) => gift.lineId);
4419
4496
  try {
4420
- const dataSourcePath = await resolveDataSourcePath(widgetId, input.shop, ctx);
4497
+ const dataSourcePath = readDataSourcePath(await fetchGiftWidgetSettings(widgetId, input.shop, ctx));
4421
4498
  if (!dataSourcePath) return discountLost;
4422
4499
  const { metadata } = await fetchDataSourceResults(
4423
4500
  {
@@ -4450,7 +4527,17 @@ var validateGifts = async (input, ctx) => {
4450
4527
  }
4451
4528
  })
4452
4529
  );
4453
- return { remove: perWidget.flat() };
4530
+ return perWidget.flat();
4531
+ };
4532
+ var validateGifts = async (input, ctx) => {
4533
+ const [remove, adjust] = await Promise.all([validateGwpGifts(input, ctx), validateSelectableGifts(input, ctx)]);
4534
+ const removed = new Set(remove);
4535
+ const targets = /* @__PURE__ */ new Map();
4536
+ for (const { lineId, quantity } of adjust) {
4537
+ if (removed.has(lineId)) continue;
4538
+ targets.set(lineId, Math.min(quantity, targets.get(lineId) ?? quantity));
4539
+ }
4540
+ return { adjust: [...targets].map(([lineId, quantity]) => ({ lineId, quantity })), remove: [...removed] };
4454
4541
  };
4455
4542
 
4456
4543
  // src/server/analytics.ts
@@ -4572,7 +4659,7 @@ var postAnalyticsEvents = async (input, ctx) => {
4572
4659
  };
4573
4660
 
4574
4661
  // src/server/requestSchemas.ts
4575
- var import_zod29 = require("zod");
4662
+ var import_zod30 = require("zod");
4576
4663
  var REQUEST_BOUNDS = {
4577
4664
  /** Cart-level attributes per cart. */
4578
4665
  CART_ATTRIBUTES_MAX: 100,
@@ -4603,42 +4690,42 @@ var REQUEST_BOUNDS = {
4603
4690
  /** Rich ads transaction lines — the engine's ads contract. */
4604
4691
  TRANSACTION_ITEMS_MAX: 50
4605
4692
  };
4606
- var KeyValue = import_zod29.z.object({
4607
- key: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_KEY_MAX),
4608
- value: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_VALUE_MAX).optional()
4693
+ var KeyValue = import_zod30.z.object({
4694
+ key: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_KEY_MAX),
4695
+ value: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_VALUE_MAX).optional()
4609
4696
  });
4610
- var CartLineInputSchema = import_zod29.z.object({
4611
- productId: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4612
- properties: import_zod29.z.array(KeyValue).max(REQUEST_BOUNDS.LINE_PROPERTIES_MAX).optional(),
4613
- quantity: import_zod29.z.number(),
4614
- subscriptionId: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4615
- variantId: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX)
4697
+ var CartLineInputSchema = import_zod30.z.object({
4698
+ productId: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4699
+ properties: import_zod30.z.array(KeyValue).max(REQUEST_BOUNDS.LINE_PROPERTIES_MAX).optional(),
4700
+ quantity: import_zod30.z.number(),
4701
+ subscriptionId: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4702
+ variantId: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX)
4616
4703
  });
4617
- var CartInputSchema = import_zod29.z.object({
4618
- attributes: import_zod29.z.array(KeyValue).max(REQUEST_BOUNDS.CART_ATTRIBUTES_MAX).optional(),
4704
+ var CartInputSchema = import_zod30.z.object({
4705
+ attributes: import_zod30.z.array(KeyValue).max(REQUEST_BOUNDS.CART_ATTRIBUTES_MAX).optional(),
4619
4706
  /**
4620
4707
  * Presentment-per-shop-currency FX rate (buyer price ÷ shop price, e.g. `1.35` for a USD shop
4621
4708
  * selling in CAD). Money rule thresholds are authored in SHOP currency, so the engine subtotal is
4622
4709
  * divided by this before evaluation. Omit (or send 1) when the cart is already in the shop's currency.
4623
4710
  */
4624
- currencyRate: import_zod29.z.number().optional(),
4711
+ currencyRate: import_zod30.z.number().optional(),
4625
4712
  /** Applied discount codes; the engine evaluates `discount_code` rules against them (sent as `[{code}]`). */
4626
- discountCodes: import_zod29.z.array(import_zod29.z.string().max(REQUEST_BOUNDS.STRING_KEY_MAX)).max(REQUEST_BOUNDS.DISCOUNT_CODES_MAX).optional(),
4713
+ discountCodes: import_zod30.z.array(import_zod30.z.string().max(REQUEST_BOUNDS.STRING_KEY_MAX)).max(REQUEST_BOUNDS.DISCOUNT_CODES_MAX).optional(),
4627
4714
  /** Presentment cents of cart-level discount allocations, subtracted from `subtotal` for the engine. */
4628
- discountTotal: import_zod29.z.number().optional(),
4629
- items: import_zod29.z.array(CartLineInputSchema).max(REQUEST_BOUNDS.CART_ITEMS_MAX),
4715
+ discountTotal: import_zod30.z.number().optional(),
4716
+ items: import_zod30.z.array(CartLineInputSchema).max(REQUEST_BOUNDS.CART_ITEMS_MAX),
4630
4717
  /** Presentment subtotal in cents. */
4631
- subtotal: import_zod29.z.number()
4718
+ subtotal: import_zod30.z.number()
4632
4719
  });
4633
- var UserConfigInputSchema = import_zod29.z.object({ shop: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX) });
4634
- var DataSourceInputSchema = import_zod29.z.object({
4720
+ var UserConfigInputSchema = import_zod30.z.object({ shop: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX) });
4721
+ var DataSourceInputSchema = import_zod30.z.object({
4635
4722
  cart: CartInputSchema,
4636
4723
  /**
4637
4724
  * Numeric Shopify company-location id when the buyer is a B2B session (`buyerIdentity.purchasingCompany`).
4638
4725
  * Present ⇒ the orchestrator filters results by the location's catalog via admin-api (REB-23713); the
4639
4726
  * client's country-context market check is bypassed for B2B (it false-negatives B2B-catalog products).
4640
4727
  */
4641
- companyLocationId: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4728
+ companyLocationId: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4642
4729
  /**
4643
4730
  * Buyer's market country (ISO 3166-1 alpha-2) — forwarded to the engine as the `country_code` PARAM
4644
4731
  * when the shop's markets toggle is on (REB-21688; the sink was the `X-Country-Code` header under
@@ -4647,16 +4734,16 @@ var DataSourceInputSchema = import_zod29.z.object({
4647
4734
  * uppercase (the wire value is forwarded verbatim as the param) so a case-sensitive engine lookup
4648
4735
  * can't silently miss a lowercase code — `adsEngine.ts` uppercases for the same reason.
4649
4736
  */
4650
- country: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).regex(/^[A-Za-z]{2}$/, "must be an ISO 3166-1 alpha-2 country code (e.g. CA)").transform((value) => value.toUpperCase()).optional(),
4651
- customerId: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4652
- dataSourcePath: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_KEY_MAX),
4737
+ country: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).regex(/^[A-Za-z]{2}$/, "must be an ISO 3166-1 alpha-2 country code (e.g. CA)").transform((value) => value.toUpperCase()).optional(),
4738
+ customerId: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4739
+ dataSourcePath: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_KEY_MAX),
4653
4740
  /**
4654
4741
  * Engine `filter_inputs` override: `'no'` keeps in-cart (input) products in the results/rule
4655
4742
  * outputs — gift validation sends it so a gift's own product can appear in its answer. Omitted = the
4656
4743
  * engine's default filtering.
4657
4744
  */
4658
- filterInputs: import_zod29.z.enum(["no", "yes"]).optional(),
4659
- integrations: import_zod29.z.record(import_zod29.z.string().max(REQUEST_BOUNDS.STRING_KEY_MAX), import_zod29.z.boolean()).refine((record) => Object.keys(record).length <= REQUEST_BOUNDS.INTEGRATION_KEYS_MAX, {
4745
+ filterInputs: import_zod30.z.enum(["no", "yes"]).optional(),
4746
+ integrations: import_zod30.z.record(import_zod30.z.string().max(REQUEST_BOUNDS.STRING_KEY_MAX), import_zod30.z.boolean()).refine((record) => Object.keys(record).length <= REQUEST_BOUNDS.INTEGRATION_KEYS_MAX, {
4660
4747
  message: `Too many integration keys (max ${REQUEST_BOUNDS.INTEGRATION_KEYS_MAX})`
4661
4748
  }).optional(),
4662
4749
  /**
@@ -4669,20 +4756,31 @@ var DataSourceInputSchema = import_zod29.z.object({
4669
4756
  * request header) fails the regex. Callers should send the buyer's resolved locale (e.g. Shopify
4670
4757
  * checkout's `localization.language.isoCode`), not the request header verbatim.
4671
4758
  */
4672
- language: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).regex(/^[A-Za-z0-9-]+$/, "must be a single BCP-47 tag (e.g. fr-CA), not a q-value list").optional(),
4673
- limit: import_zod29.z.number().max(REQUEST_BOUNDS.LIMIT_MAX),
4674
- productType: import_zod29.z.enum(["both", "one-time", "subscription"]).optional(),
4675
- shop: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4676
- visitorId: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional()
4759
+ language: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).regex(/^[A-Za-z0-9-]+$/, "must be a single BCP-47 tag (e.g. fr-CA), not a q-value list").optional(),
4760
+ limit: import_zod30.z.number().max(REQUEST_BOUNDS.LIMIT_MAX),
4761
+ productType: import_zod30.z.enum(["both", "one-time", "subscription"]).optional(),
4762
+ shop: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4763
+ visitorId: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional()
4677
4764
  });
4678
- var GiftInputSchema = import_zod29.z.object({
4679
- cost: import_zod29.z.number(),
4680
- discounted: import_zod29.z.boolean(),
4681
- lineId: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4682
- productId: import_zod29.z.number(),
4683
- widgetId: import_zod29.z.number()
4765
+ var GiftInputSchema = import_zod30.z.object({
4766
+ cost: import_zod30.z.number(),
4767
+ discounted: import_zod30.z.boolean(),
4768
+ /**
4769
+ * Which gift attribution the line carries (REB-24363). Defaults to `gwp` because a client that predates
4770
+ * the field only ever sent GWP lines — without the default an older extension's request would 422.
4771
+ */
4772
+ kind: import_zod30.z.enum(GIFT_KINDS).default("gwp"),
4773
+ lineId: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4774
+ productId: import_zod30.z.number(),
4775
+ /**
4776
+ * The line's unit count (REB-24363) — the selectable-GWP verdict trims surplus units against milestone
4777
+ * capacity. The second deliberate default: a client predating the field sent only GWP lines, whose
4778
+ * verdict is whole-line, so a missing quantity reads as one unit rather than 422ing the older request.
4779
+ */
4780
+ quantity: import_zod30.z.number().int().min(1).default(1),
4781
+ widgetId: import_zod30.z.number()
4684
4782
  });
4685
- var GiftValidationInputSchema = import_zod29.z.object({
4783
+ var GiftValidationInputSchema = import_zod30.z.object({
4686
4784
  cart: CartInputSchema,
4687
4785
  /**
4688
4786
  * Buyer's market country — threaded into the per-widget data-source evaluations, which forward it to
@@ -4691,59 +4789,76 @@ var GiftValidationInputSchema = import_zod29.z.object({
4691
4789
  * turns markets off); regex-bound and uppercase-normalized for the same reasons as
4692
4790
  * `DataSourceInputSchema`'s `country`.
4693
4791
  */
4694
- country: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).regex(/^[A-Za-z]{2}$/, "must be an ISO 3166-1 alpha-2 country code (e.g. CA)").transform((value) => value.toUpperCase()).optional(),
4695
- customerId: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4696
- gifts: import_zod29.z.array(GiftInputSchema).max(REQUEST_BOUNDS.GIFTS_MAX),
4792
+ country: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).regex(/^[A-Za-z]{2}$/, "must be an ISO 3166-1 alpha-2 country code (e.g. CA)").transform((value) => value.toUpperCase()).optional(),
4793
+ customerId: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4794
+ gifts: import_zod30.z.array(GiftInputSchema).max(REQUEST_BOUNDS.GIFTS_MAX),
4697
4795
  /**
4698
4796
  * Buyer's checkout language — threaded into the per-widget data-source evaluations, which normalize
4699
4797
  * and forward it as the engine `locale` param under the same markets-toggle gate (REB-21688). MUST be
4700
4798
  * a single BCP-47 tag, for the same reason as `DataSourceInputSchema`'s `language` above.
4701
4799
  */
4702
- language: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).regex(/^[A-Za-z0-9-]+$/, "must be a single BCP-47 tag (e.g. fr-CA), not a q-value list").optional(),
4703
- shop: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4704
- visitorId: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional()
4800
+ language: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).regex(/^[A-Za-z0-9-]+$/, "must be a single BCP-47 tag (e.g. fr-CA), not a q-value list").optional(),
4801
+ shop: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4802
+ visitorId: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional()
4803
+ });
4804
+ var GiftAdjustmentSchema = import_zod30.z.object({
4805
+ lineId: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4806
+ quantity: import_zod30.z.number().int().nonnegative()
4705
4807
  });
4706
- var MonetizeTransactionItemSchema = import_zod29.z.object({
4707
- id: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4708
- priceCents: import_zod29.z.number(),
4709
- quantity: import_zod29.z.number(),
4710
- title: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_TEXT_MAX),
4711
- type: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_TEXT_MAX)
4808
+ var GiftValidationResponseSchema = import_zod30.z.object({
4809
+ adjust: import_zod30.z.array(GiftAdjustmentSchema).max(REQUEST_BOUNDS.GIFTS_MAX).default([]),
4810
+ remove: import_zod30.z.array(import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX)).max(REQUEST_BOUNDS.GIFTS_MAX).refine(
4811
+ (remove) => new Set(remove).size === remove.length,
4812
+ "a line id may appear at most once in `remove`"
4813
+ )
4814
+ }).refine(
4815
+ ({ adjust, remove }) => !adjust.some(({ lineId }) => remove.includes(lineId)),
4816
+ "a line id may not appear in both `remove` and `adjust`"
4817
+ ).refine(
4818
+ ({ adjust }) => new Set(adjust.map(({ lineId }) => lineId)).size === adjust.length,
4819
+ "a line id may appear at most once in `adjust`"
4820
+ );
4821
+ var MonetizeTransactionItemSchema = import_zod30.z.object({
4822
+ id: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4823
+ priceCents: import_zod30.z.number(),
4824
+ quantity: import_zod30.z.number(),
4825
+ title: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_TEXT_MAX),
4826
+ type: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_TEXT_MAX)
4712
4827
  });
4713
- var MonetizeInputSchema = import_zod29.z.object({
4828
+ var MonetizeInputSchema = import_zod30.z.object({
4714
4829
  cart: CartInputSchema,
4715
4830
  /** Client-minted render-beacon correlation id; forwarded to the ads engine as `client_request_id`. */
4716
- clientRequestId: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4717
- country: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4718
- currency: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4831
+ clientRequestId: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4832
+ country: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4833
+ currency: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4719
4834
  /** Ads-targeting identity/geo/locale fields (React parity); folded into the engine query verbatim. */
4720
- customerEmail: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_TEXT_MAX).optional(),
4721
- customerFirstName: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_TEXT_MAX).optional(),
4722
- customerId: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4723
- customerLastName: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_TEXT_MAX).optional(),
4724
- language: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4835
+ customerEmail: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_TEXT_MAX).optional(),
4836
+ customerFirstName: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_TEXT_MAX).optional(),
4837
+ customerId: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4838
+ customerLastName: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_TEXT_MAX).optional(),
4839
+ language: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4725
4840
  /** Buyer-facing order name (e.g. `#1001`) for the server-composed "Order … Confirmed" chrome line. */
4726
- orderName: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_TEXT_MAX).optional(),
4841
+ orderName: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_TEXT_MAX).optional(),
4727
4842
  /** Ads surface label, forwarded verbatim; `customer_account` is measured/floored as its own surface (the account-area order list). */
4728
- placement: import_zod29.z.enum(["customer_account", "order_status", "thank_you_page"]),
4843
+ placement: import_zod30.z.enum(["customer_account", "order_status", "thank_you_page"]),
4729
4844
  /** Privacy Policy footer href — the Monetize widget's `privacy_policy_url` setting (React parity). */
4730
- privacyPolicyUrl: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_URL_MAX).optional(),
4845
+ privacyPolicyUrl: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_URL_MAX).optional(),
4731
4846
  /** 30-min rolling session id (React parity); used for the ads `session_id`, else the visitor id. */
4732
- sessionId: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4733
- shop: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4847
+ sessionId: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4848
+ shop: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4734
4849
  /** Editor/QA indicator; OR-ed into the ads-engine `testing` flag so it stays out of billing/reporting. */
4735
- testing: import_zod29.z.boolean().optional(),
4736
- timezone: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4850
+ testing: import_zod30.z.boolean().optional(),
4851
+ timezone: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4737
4852
  /** Decorated (numeric) order id → the ads `transaction_id`. */
4738
- transactionId: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4853
+ transactionId: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4739
4854
  /** Rich per-line ads items; capped at the engine's ads contract. Preferred over `cart.items`. */
4740
- transactionItems: import_zod29.z.array(MonetizeTransactionItemSchema).max(REQUEST_BOUNDS.TRANSACTION_ITEMS_MAX).optional(),
4855
+ transactionItems: import_zod30.z.array(MonetizeTransactionItemSchema).max(REQUEST_BOUNDS.TRANSACTION_ITEMS_MAX).optional(),
4741
4856
  /** Order total in presentment cents (React prefers total over subtotal); else `cart.subtotal`. */
4742
- transactionTotal: import_zod29.z.number().optional(),
4743
- visitorId: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4744
- zipCode: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional()
4857
+ transactionTotal: import_zod30.z.number().optional(),
4858
+ visitorId: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4859
+ zipCode: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional()
4745
4860
  });
4746
- var WidgetSettingsInputSchema = import_zod29.z.object({
4861
+ var WidgetSettingsInputSchema = import_zod30.z.object({
4747
4862
  /**
4748
4863
  * Same domain as `CabShopConfig.cacheKey`, for the same reason: this side WINS the orchestrator's
4749
4864
  * `input.cacheKey ?? config.shop.cacheKey` (and an explicit value skips the config read entirely), so
@@ -4751,9 +4866,9 @@ var WidgetSettingsInputSchema = import_zod29.z.object({
4751
4866
  * and pin the edge copy permanently stale. `.catch(undefined)` because a bogus buster should degrade
4752
4867
  * the read to un-busted, never 400 a widget render.
4753
4868
  */
4754
- cacheKey: import_zod29.z.number().int().positive().optional().catch(void 0),
4755
- id: import_zod29.z.number(),
4756
- key: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4869
+ cacheKey: import_zod30.z.number().int().positive().optional().catch(void 0),
4870
+ id: import_zod30.z.number(),
4871
+ key: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4757
4872
  /**
4758
4873
  * Monetize context for inline hydration: when present AND the widget converts to a Monetize marker,
4759
4874
  * the server fetches/composes the offer cards inside this same request (no second `/cab/monetize`
@@ -4762,23 +4877,23 @@ var WidgetSettingsInputSchema = import_zod29.z.object({
4762
4877
  * `privacyPolicyUrl` comes from the widget settings server-side (more authoritative than a relay).
4763
4878
  */
4764
4879
  monetize: MonetizeInputSchema.omit({ privacyPolicyUrl: true, shop: true }).optional(),
4765
- shop: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4880
+ shop: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4766
4881
  /** Stable per-buyer id; when present, enables sticky server-side A/B variant selection. */
4767
- visitorId: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional()
4882
+ visitorId: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional()
4768
4883
  });
4769
- var AnalyticsEventInputSchema = import_zod29.z.object({
4770
- name: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_KEY_MAX),
4771
- properties: import_zod29.z.record(import_zod29.z.string().max(REQUEST_BOUNDS.STRING_KEY_MAX), import_zod29.z.unknown()).optional(),
4772
- sentAt: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional()
4884
+ var AnalyticsEventInputSchema = import_zod30.z.object({
4885
+ name: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_KEY_MAX),
4886
+ properties: import_zod30.z.record(import_zod30.z.string().max(REQUEST_BOUNDS.STRING_KEY_MAX), import_zod30.z.unknown()).optional(),
4887
+ sentAt: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional()
4773
4888
  });
4774
- var AnalyticsInputSchema = import_zod29.z.object({
4889
+ var AnalyticsInputSchema = import_zod30.z.object({
4775
4890
  /** Rebuy cart token (the `r_cart_token` cart attribute), when available — primary aggregation key. */
4776
- cartToken: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4891
+ cartToken: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4777
4892
  /** Shopify checkout token, when available — drives cart/checkout aggregation downstream. */
4778
- checkoutToken: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4779
- events: import_zod29.z.array(AnalyticsEventInputSchema).max(REQUEST_BOUNDS.EVENTS_MAX),
4780
- shop: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4781
- visitorId: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4893
+ checkoutToken: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional(),
4894
+ events: import_zod30.z.array(AnalyticsEventInputSchema).max(REQUEST_BOUNDS.EVENTS_MAX),
4895
+ shop: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4896
+ visitorId: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX),
4782
4897
  /** The widget id (smartBlockId) the events belong to. */
4783
- widgetId: import_zod29.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional()
4898
+ widgetId: import_zod30.z.string().max(REQUEST_BOUNDS.STRING_ID_MAX).optional()
4784
4899
  });