@rebuy/rebuy 3.17.0-rc.1 → 3.18.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
  };
@@ -25,7 +25,7 @@ declare const TAXONOMY: {
25
25
  element_id: string;
26
26
  experiment_type: string;
27
27
  };
28
- buildTags: ({ experimentId }: Record<string, unknown>) => string[] | undefined;
28
+ buildTags: ({ experimentId }: Record<string, unknown>, input: AnalyticsInput) => string[] | undefined;
29
29
  noun: "cart";
30
30
  subject: "abtest";
31
31
  usesWidgetId: false;
@@ -37,7 +37,7 @@ declare const TAXONOMY: {
37
37
  buildMeta: ({ productId, variantId }: Record<string, unknown>) => {
38
38
  [x: string]: true;
39
39
  };
40
- buildTags: ({ experimentId }: Record<string, unknown>) => string[] | undefined;
40
+ buildTags: ({ experimentId }: Record<string, unknown>, input: AnalyticsInput) => string[] | undefined;
41
41
  /** Under an experiment the add belongs to the VARIANT element, not the placeholder (React parity). */
42
42
  buildWidgetId: ({ elementId }: Record<string, unknown>, input: {
43
43
  events: {
@@ -49,6 +49,11 @@ declare const TAXONOMY: {
49
49
  visitorId: string;
50
50
  cartToken?: string | undefined;
51
51
  checkoutToken?: string | undefined;
52
+ segments?: {
53
+ device?: string | undefined;
54
+ source?: string | undefined;
55
+ visitor?: string | undefined;
56
+ } | undefined;
52
57
  widgetId?: string | undefined;
53
58
  }) => string | undefined;
54
59
  noun: "widget";
@@ -77,7 +82,7 @@ declare const TAXONOMY: {
77
82
  verb: "tracking";
78
83
  };
79
84
  'widget-viewed': {
80
- buildTags: ({ experimentId }: Record<string, unknown>) => string[] | undefined;
85
+ buildTags: ({ experimentId }: Record<string, unknown>, input: AnalyticsInput) => string[] | undefined;
81
86
  /** Under an experiment the view belongs to the VARIANT element, not the placeholder (React parity, same as added-to-cart) — else views credit the placeholder while adds credit the variant and per-widget conversion is meaningless. */
82
87
  buildWidgetId: ({ elementId }: Record<string, unknown>, input: {
83
88
  events: {
@@ -89,6 +94,11 @@ declare const TAXONOMY: {
89
94
  visitorId: string;
90
95
  cartToken?: string | undefined;
91
96
  checkoutToken?: string | undefined;
97
+ segments?: {
98
+ device?: string | undefined;
99
+ source?: string | undefined;
100
+ visitor?: string | undefined;
101
+ } | undefined;
92
102
  widgetId?: string | undefined;
93
103
  }) => string | undefined;
94
104
  noun: "widget";
@@ -0,0 +1,6 @@
1
+ import { type AnalyticsSegmentsInput } from '../server/requestSchemas';
2
+ /**
3
+ * One `<baseTag>.<dimension>.<bucket>` tag per segment whose bucket passes {@link SEGMENT_BUCKETS}; invalid
4
+ * or absent dimensions contribute nothing (so valid siblings still ride along).
5
+ */
6
+ export declare const segmentTags: (baseTag: string, segments: AnalyticsSegmentsInput | undefined) => string[];
@@ -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 array — the 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 widgets — which 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[]>;