@rebuy/rebuy 3.56.3 → 3.58.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.
@@ -37,8 +37,8 @@ var getValidGwpProductIds = (matchedRules = []) => new Set(
37
37
 
38
38
  // src/gwp/hasUnsupportedRuleTypes.ts
39
39
  var UNSUPPORTED_RULE_TYPES = ["url", "order_tag"];
40
- var UNSUPPORTED_RULE_TYPE_PREFIX = "geolocation_";
41
- var isUnsupportedType = (type = "") => UNSUPPORTED_RULE_TYPES.includes(type) || type.startsWith(UNSUPPORTED_RULE_TYPE_PREFIX);
40
+ var UNSUPPORTED_RULE_TYPE_FRAGMENT = "geolocation";
41
+ var isUnsupportedType = (type = "") => UNSUPPORTED_RULE_TYPES.includes(type) || type.includes(UNSUPPORTED_RULE_TYPE_FRAGMENT);
42
42
  var ruleHasUnsupportedType = (rule) => (rule.logic ?? []).some((group) => (group.rules ?? []).some((item) => isUnsupportedType(item.type)));
43
43
  var hasUnsupportedRuleTypes = (metadata) => [...metadata?.matchedRules ?? [], ...metadata?.unmatchedRules ?? []].some(ruleHasUnsupportedType);
44
44
 
@@ -5,8 +5,8 @@ var getValidGwpProductIds = (matchedRules = []) => new Set(
5
5
 
6
6
  // src/gwp/hasUnsupportedRuleTypes.ts
7
7
  var UNSUPPORTED_RULE_TYPES = ["url", "order_tag"];
8
- var UNSUPPORTED_RULE_TYPE_PREFIX = "geolocation_";
9
- var isUnsupportedType = (type = "") => UNSUPPORTED_RULE_TYPES.includes(type) || type.startsWith(UNSUPPORTED_RULE_TYPE_PREFIX);
8
+ var UNSUPPORTED_RULE_TYPE_FRAGMENT = "geolocation";
9
+ var isUnsupportedType = (type = "") => UNSUPPORTED_RULE_TYPES.includes(type) || type.includes(UNSUPPORTED_RULE_TYPE_FRAGMENT);
10
10
  var ruleHasUnsupportedType = (rule) => (rule.logic ?? []).some((group) => (group.rules ?? []).some((item) => isUnsupportedType(item.type)));
11
11
  var hasUnsupportedRuleTypes = (metadata) => [...metadata?.matchedRules ?? [], ...metadata?.unmatchedRules ?? []].some(ruleHasUnsupportedType);
12
12
 
package/dist/index.cjs CHANGED
@@ -1698,7 +1698,7 @@ var OfferProductDiscount = import_zod5.z.object({
1698
1698
  type: import_zod5.z.string()
1699
1699
  });
1700
1700
  var OfferProductOption = import_zod5.z.object({
1701
- id: import_zod5.z.number(),
1701
+ id: import_zod5.z.number().optional(),
1702
1702
  name: import_zod5.z.string(),
1703
1703
  position: import_zod5.z.number(),
1704
1704
  values: import_zod5.z.array(import_zod5.z.string())
@@ -1710,7 +1710,7 @@ var OfferProductVariant = import_zod5.z.object({
1710
1710
  inventoryManagement: import_zod5.z.string().nullable(),
1711
1711
  inventoryPolicy: import_zod5.z.string(),
1712
1712
  inventoryQuantity: import_zod5.z.number(),
1713
- link: import_zod5.z.string(),
1713
+ link: import_zod5.z.string().optional(),
1714
1714
  option1: import_zod5.z.string(),
1715
1715
  option2: import_zod5.z.string().nullable(),
1716
1716
  option3: import_zod5.z.string().nullable(),
@@ -1723,12 +1723,12 @@ var OfferProduct = import_zod5.z.object({
1723
1723
  id: import_zod5.z.number(),
1724
1724
  image: import_zod5.z.object({ src: import_zod5.z.string() }).nullish(),
1725
1725
  /**
1726
- * `variantIds` is optional: the engine omits `variant_ids` (rather than sending `[]`) on an image tied to no
1726
+ * `variantIds` is nullish: the engine omits `variant_ids` (rather than sending `[]`) on an image tied to no
1727
1727
  * variant — seen live on the `recommended` data source while `top_sellers` sends `[]` for the same image
1728
- * (REB-24958). A missing list means "not variant-specific", exactly like `[]`, so the image keeps its `src`
1729
- * and the product is never dropped over it.
1728
+ * (REB-24958). A missing (or `null`) list means "not variant-specific", exactly like `[]`, so the image
1729
+ * keeps its `src` and the product is never dropped over it.
1730
1730
  */
1731
- images: import_zod5.z.array(import_zod5.z.object({ src: import_zod5.z.string(), variantIds: import_zod5.z.array(import_zod5.z.number()).optional() })).nullish(),
1731
+ images: import_zod5.z.array(import_zod5.z.object({ src: import_zod5.z.string(), variantIds: import_zod5.z.array(import_zod5.z.number()).nullish() })).nullish(),
1732
1732
  link: import_zod5.z.string().optional(),
1733
1733
  metafields: import_zod5.z.array(import_zod5.z.object({ key: import_zod5.z.string(), namespace: import_zod5.z.string(), value: import_zod5.z.string() })).optional(),
1734
1734
  options: import_zod5.z.array(OfferProductOption),
@@ -3313,7 +3313,15 @@ var StsCompensationProgressSchema = import_zod42.z.union([
3313
3313
  AttemptSchema2,
3314
3314
  FinishedSchema.extend({ status: import_zod42.z.enum(["removed", "failed", "unknown"]) })
3315
3315
  ]);
3316
- var StsReviewReasonSchema = import_zod42.z.enum(["refund-unknown", "compensation-failed", "compensation-unknown"]);
3316
+ var StsReviewReasonSchema = import_zod42.z.enum([
3317
+ "refund-unknown",
3318
+ "compensation-failed",
3319
+ "compensation-unknown",
3320
+ "charge-unconfirmed",
3321
+ "refund-lost",
3322
+ "compensation-lost",
3323
+ "refund-owed"
3324
+ ]);
3317
3325
  var StsReviewProgressSchema = import_zod42.z.strictObject({
3318
3326
  markedAt: PostPurchaseTimestampSchema,
3319
3327
  marker: AcceptedMetafieldResultSchema.shape.status,
@@ -3325,6 +3333,12 @@ var reviewReason = (refund, compensation) => {
3325
3333
  return `compensation-${compensation.status}`;
3326
3334
  return null;
3327
3335
  };
3336
+ var judgedReason = (refund, compensation) => {
3337
+ if (refund === null) return "charge-unconfirmed";
3338
+ if (refund.status === "attempted") return "refund-lost";
3339
+ if (compensation?.status === "attempted") return "compensation-lost";
3340
+ return compensation?.status === "removed" ? "refund-owed" : null;
3341
+ };
3328
3342
  var StsProgressSchema = import_zod42.z.strictObject({
3329
3343
  compensation: StsCompensationProgressSchema.nullable(),
3330
3344
  operationId: PurchaseReceiptSchema.shape.operationId,
@@ -3333,10 +3347,11 @@ var StsProgressSchema = import_zod42.z.strictObject({
3333
3347
  }).refine(
3334
3348
  ({ compensation, refund }) => compensation === null || refund?.status === "failed",
3335
3349
  "Compensation follows a known refund failure only"
3336
- ).refine(
3337
- ({ compensation, refund, review }) => (review?.reason ?? null) === reviewReason(refund, compensation),
3338
- "Review must match the unresolved outcome"
3339
- );
3350
+ ).refine(({ compensation, refund, review }) => {
3351
+ const resolved = reviewReason(refund, compensation);
3352
+ const reason = review?.reason ?? null;
3353
+ return reason === resolved || resolved === null && reason === judgedReason(refund, compensation);
3354
+ }, "Review must match the unresolved outcome");
3340
3355
 
3341
3356
  // src/schema/postPurchaseFollowups.ts
3342
3357
  var RequiredHealthSchema = import_zod43.z.union([
@@ -4918,8 +4933,10 @@ var PurchaseRecoveryRequestSchema = DecideRequestSchema.strict();
4918
4933
  var PurchaseRecoveryResponseSchema = import_zod84.z.object({
4919
4934
  data: import_zod84.z.object({
4920
4935
  /**
4921
- * Strict, so a client pinned before a new optional field rejects a reply that carries it rather than
4922
- * stripping it. Release the client before rebuy-api starts emitting a field added here.
4936
+ * Strict, so rebuy-api (which validates its own reply against this schema) can never emit a field the
4937
+ * contract does not name. cab-post-purchase reads recovery with hand guards that ignore extra keys, so
4938
+ * the strictness never reaches it: release rebuy-api (the pin bump) first, then the client that reads a
4939
+ * field added here.
4923
4940
  */
4924
4941
  operation: import_zod84.z.object({
4925
4942
  /** Absent from a rebuy-api that predates it. */
package/dist/index.mjs CHANGED
@@ -1218,7 +1218,7 @@ var OfferProductDiscount = z5.object({
1218
1218
  type: z5.string()
1219
1219
  });
1220
1220
  var OfferProductOption = z5.object({
1221
- id: z5.number(),
1221
+ id: z5.number().optional(),
1222
1222
  name: z5.string(),
1223
1223
  position: z5.number(),
1224
1224
  values: z5.array(z5.string())
@@ -1230,7 +1230,7 @@ var OfferProductVariant = z5.object({
1230
1230
  inventoryManagement: z5.string().nullable(),
1231
1231
  inventoryPolicy: z5.string(),
1232
1232
  inventoryQuantity: z5.number(),
1233
- link: z5.string(),
1233
+ link: z5.string().optional(),
1234
1234
  option1: z5.string(),
1235
1235
  option2: z5.string().nullable(),
1236
1236
  option3: z5.string().nullable(),
@@ -1243,12 +1243,12 @@ var OfferProduct = z5.object({
1243
1243
  id: z5.number(),
1244
1244
  image: z5.object({ src: z5.string() }).nullish(),
1245
1245
  /**
1246
- * `variantIds` is optional: the engine omits `variant_ids` (rather than sending `[]`) on an image tied to no
1246
+ * `variantIds` is nullish: the engine omits `variant_ids` (rather than sending `[]`) on an image tied to no
1247
1247
  * variant — seen live on the `recommended` data source while `top_sellers` sends `[]` for the same image
1248
- * (REB-24958). A missing list means "not variant-specific", exactly like `[]`, so the image keeps its `src`
1249
- * and the product is never dropped over it.
1248
+ * (REB-24958). A missing (or `null`) list means "not variant-specific", exactly like `[]`, so the image
1249
+ * keeps its `src` and the product is never dropped over it.
1250
1250
  */
1251
- images: z5.array(z5.object({ src: z5.string(), variantIds: z5.array(z5.number()).optional() })).nullish(),
1251
+ images: z5.array(z5.object({ src: z5.string(), variantIds: z5.array(z5.number()).nullish() })).nullish(),
1252
1252
  link: z5.string().optional(),
1253
1253
  metafields: z5.array(z5.object({ key: z5.string(), namespace: z5.string(), value: z5.string() })).optional(),
1254
1254
  options: z5.array(OfferProductOption),
@@ -2833,7 +2833,15 @@ var StsCompensationProgressSchema = z42.union([
2833
2833
  AttemptSchema2,
2834
2834
  FinishedSchema.extend({ status: z42.enum(["removed", "failed", "unknown"]) })
2835
2835
  ]);
2836
- var StsReviewReasonSchema = z42.enum(["refund-unknown", "compensation-failed", "compensation-unknown"]);
2836
+ var StsReviewReasonSchema = z42.enum([
2837
+ "refund-unknown",
2838
+ "compensation-failed",
2839
+ "compensation-unknown",
2840
+ "charge-unconfirmed",
2841
+ "refund-lost",
2842
+ "compensation-lost",
2843
+ "refund-owed"
2844
+ ]);
2837
2845
  var StsReviewProgressSchema = z42.strictObject({
2838
2846
  markedAt: PostPurchaseTimestampSchema,
2839
2847
  marker: AcceptedMetafieldResultSchema.shape.status,
@@ -2845,6 +2853,12 @@ var reviewReason = (refund, compensation) => {
2845
2853
  return `compensation-${compensation.status}`;
2846
2854
  return null;
2847
2855
  };
2856
+ var judgedReason = (refund, compensation) => {
2857
+ if (refund === null) return "charge-unconfirmed";
2858
+ if (refund.status === "attempted") return "refund-lost";
2859
+ if (compensation?.status === "attempted") return "compensation-lost";
2860
+ return compensation?.status === "removed" ? "refund-owed" : null;
2861
+ };
2848
2862
  var StsProgressSchema = z42.strictObject({
2849
2863
  compensation: StsCompensationProgressSchema.nullable(),
2850
2864
  operationId: PurchaseReceiptSchema.shape.operationId,
@@ -2853,10 +2867,11 @@ var StsProgressSchema = z42.strictObject({
2853
2867
  }).refine(
2854
2868
  ({ compensation, refund }) => compensation === null || refund?.status === "failed",
2855
2869
  "Compensation follows a known refund failure only"
2856
- ).refine(
2857
- ({ compensation, refund, review }) => (review?.reason ?? null) === reviewReason(refund, compensation),
2858
- "Review must match the unresolved outcome"
2859
- );
2870
+ ).refine(({ compensation, refund, review }) => {
2871
+ const resolved = reviewReason(refund, compensation);
2872
+ const reason = review?.reason ?? null;
2873
+ return reason === resolved || resolved === null && reason === judgedReason(refund, compensation);
2874
+ }, "Review must match the unresolved outcome");
2860
2875
 
2861
2876
  // src/schema/postPurchaseFollowups.ts
2862
2877
  var RequiredHealthSchema = z43.union([
@@ -4438,8 +4453,10 @@ var PurchaseRecoveryRequestSchema = DecideRequestSchema.strict();
4438
4453
  var PurchaseRecoveryResponseSchema = z84.object({
4439
4454
  data: z84.object({
4440
4455
  /**
4441
- * Strict, so a client pinned before a new optional field rejects a reply that carries it rather than
4442
- * stripping it. Release the client before rebuy-api starts emitting a field added here.
4456
+ * Strict, so rebuy-api (which validates its own reply against this schema) can never emit a field the
4457
+ * contract does not name. cab-post-purchase reads recovery with hand guards that ignore extra keys, so
4458
+ * the strictness never reaches it: release rebuy-api (the pin bump) first, then the client that reads a
4459
+ * field added here.
4443
4460
  */
4444
4461
  operation: z84.object({
4445
4462
  /** Absent from a rebuy-api that predates it. */
@@ -21,7 +21,7 @@ export declare const OfferProduct: z.ZodObject<{
21
21
  }, z.core.$strip>>>;
22
22
  images: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
23
23
  src: z.ZodString;
24
- variantIds: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
24
+ variantIds: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodNumber>>>;
25
25
  }, z.core.$strip>>>>;
26
26
  link: z.ZodOptional<z.ZodString>;
27
27
  metafields: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -30,7 +30,7 @@ export declare const OfferProduct: z.ZodObject<{
30
30
  value: z.ZodString;
31
31
  }, z.core.$strip>>>;
32
32
  options: z.ZodArray<z.ZodObject<{
33
- id: z.ZodNumber;
33
+ id: z.ZodOptional<z.ZodNumber>;
34
34
  name: z.ZodString;
35
35
  position: z.ZodNumber;
36
36
  values: z.ZodArray<z.ZodString>;
@@ -46,7 +46,7 @@ export declare const OfferProduct: z.ZodObject<{
46
46
  inventoryManagement: z.ZodNullable<z.ZodString>;
47
47
  inventoryPolicy: z.ZodString;
48
48
  inventoryQuantity: z.ZodNumber;
49
- link: z.ZodString;
49
+ link: z.ZodOptional<z.ZodString>;
50
50
  option1: z.ZodString;
51
51
  option2: z.ZodNullable<z.ZodString>;
52
52
  option3: z.ZodNullable<z.ZodString>;
@@ -169,6 +169,10 @@ export declare const AcceptedFollowupsResponseSchema: z.ZodObject<{
169
169
  "refund-unknown": "refund-unknown";
170
170
  "compensation-failed": "compensation-failed";
171
171
  "compensation-unknown": "compensation-unknown";
172
+ "charge-unconfirmed": "charge-unconfirmed";
173
+ "refund-lost": "refund-lost";
174
+ "compensation-lost": "compensation-lost";
175
+ "refund-owed": "refund-owed";
172
176
  }>;
173
177
  }, z.core.$strict>>;
174
178
  }, z.core.$strict>>;
@@ -152,6 +152,10 @@ export declare const PurchaseRecoveryResponseSchema: z.ZodObject<{
152
152
  "refund-unknown": "refund-unknown";
153
153
  "compensation-failed": "compensation-failed";
154
154
  "compensation-unknown": "compensation-unknown";
155
+ "charge-unconfirmed": "charge-unconfirmed";
156
+ "refund-lost": "refund-lost";
157
+ "compensation-lost": "compensation-lost";
158
+ "refund-owed": "refund-owed";
155
159
  }>;
156
160
  }, z.core.$strict>>;
157
161
  }, z.core.$strict>>;
@@ -36,11 +36,25 @@ export declare const StsCompensationProgressSchema: z.ZodUnion<readonly [z.ZodOb
36
36
  * Why the STS is left for a person; `marker` is the `subscription_rollback_failed` metafield write. A
37
37
  * `marker` of `unavailable` means the marker could not be written, NOT that no review is needed: the review
38
38
  * stands whatever the marker status.
39
+ *
40
+ * The first three follow from a finished step. The rest are a server's judgement the steps cannot show, so
41
+ * each is allowed (never required) beside one state only:
42
+ * - `charge-unconfirmed`: the server sweep could not confirm the charge (no result reported in time, or an
43
+ * Unknown one), so no step will ever start. Without it a late `/purchases/accepted` reads as not started,
44
+ * which looks pending forever.
45
+ * - `refund-lost` / `compensation-lost`: that step was sent but its outcome was never saved, so it stays
46
+ * `attempted` and may have moved money.
47
+ * - `refund-owed`: the rollback removed the subscription line, but apiv2's removal never refunds, so the
48
+ * buyer may still have been charged for it.
39
49
  */
40
50
  export declare const StsReviewReasonSchema: z.ZodEnum<{
41
51
  "refund-unknown": "refund-unknown";
42
52
  "compensation-failed": "compensation-failed";
43
53
  "compensation-unknown": "compensation-unknown";
54
+ "charge-unconfirmed": "charge-unconfirmed";
55
+ "refund-lost": "refund-lost";
56
+ "compensation-lost": "compensation-lost";
57
+ "refund-owed": "refund-owed";
44
58
  }>;
45
59
  export declare const StsReviewProgressSchema: z.ZodObject<{
46
60
  markedAt: z.ZodNumber;
@@ -56,12 +70,18 @@ export declare const StsReviewProgressSchema: z.ZodObject<{
56
70
  "refund-unknown": "refund-unknown";
57
71
  "compensation-failed": "compensation-failed";
58
72
  "compensation-unknown": "compensation-unknown";
73
+ "charge-unconfirmed": "charge-unconfirmed";
74
+ "refund-lost": "refund-lost";
75
+ "compensation-lost": "compensation-lost";
76
+ "refund-owed": "refund-owed";
59
77
  }>;
60
78
  }, z.core.$strict>;
61
79
  /**
62
80
  * The refund-compensation saga of one admitted STS, `null` steps not yet started. Compensation follows a
63
81
  * known refund failure only, and a review is recorded exactly when the outcome is unresolved (an unknown
64
- * refund, or a failed or unknown rollback), so a compensated or unresolved STS never reads as switched.
82
+ * refund, or a failed or unknown rollback), so a compensated or unresolved STS never reads as switched. A
83
+ * server may also record a review the steps cannot show (an unconfirmed charge, a lost step, a refund owed
84
+ * after the rollback), only beside the one state it applies to.
65
85
  */
66
86
  export declare const StsProgressSchema: z.ZodObject<{
67
87
  compensation: z.ZodNullable<z.ZodUnion<readonly [z.ZodObject<{
@@ -107,6 +127,10 @@ export declare const StsProgressSchema: z.ZodObject<{
107
127
  "refund-unknown": "refund-unknown";
108
128
  "compensation-failed": "compensation-failed";
109
129
  "compensation-unknown": "compensation-unknown";
130
+ "charge-unconfirmed": "charge-unconfirmed";
131
+ "refund-lost": "refund-lost";
132
+ "compensation-lost": "compensation-lost";
133
+ "refund-owed": "refund-owed";
110
134
  }>;
111
135
  }, z.core.$strict>>;
112
136
  }, z.core.$strict>;
@@ -1,5 +1,8 @@
1
1
  import { type z } from 'zod';
2
2
  import { type PostPurchaseOfferEligibility, PostPurchaseOfferEligibilityContext, PostPurchaseStsOptions } from '../schema/postPurchaseOfferEligibility';
3
+ import type { Reporter } from '../server/shared';
3
4
  export declare const classifyPostPurchaseOffer: (settings: unknown, response: unknown, context: z.input<typeof PostPurchaseOfferEligibilityContext>,
4
5
  /** STS opt-in; absent or `{ sts: false }` keeps every pre-STS outcome. */
5
- options?: PostPurchaseStsOptions) => PostPurchaseOfferEligibility;
6
+ options?: PostPurchaseStsOptions,
7
+ /** Hears each purchased STS candidate whose plan data cannot be read, once per response (REB-25050). */
8
+ report?: Reporter) => PostPurchaseOfferEligibility;
@@ -81,8 +81,8 @@ var getValidGwpProductIds = (matchedRules = []) => new Set(
81
81
 
82
82
  // src/gwp/hasUnsupportedRuleTypes.ts
83
83
  var UNSUPPORTED_RULE_TYPES = ["url", "order_tag"];
84
- var UNSUPPORTED_RULE_TYPE_PREFIX = "geolocation_";
85
- var isUnsupportedType = (type = "") => UNSUPPORTED_RULE_TYPES.includes(type) || type.startsWith(UNSUPPORTED_RULE_TYPE_PREFIX);
84
+ var UNSUPPORTED_RULE_TYPE_FRAGMENT = "geolocation";
85
+ var isUnsupportedType = (type = "") => UNSUPPORTED_RULE_TYPES.includes(type) || type.includes(UNSUPPORTED_RULE_TYPE_FRAGMENT);
86
86
  var ruleHasUnsupportedType = (rule) => (rule.logic ?? []).some((group) => (group.rules ?? []).some((item) => isUnsupportedType(item.type)));
87
87
  var hasUnsupportedRuleTypes = (metadata) => [...metadata?.matchedRules ?? [], ...metadata?.unmatchedRules ?? []].some(ruleHasUnsupportedType);
88
88
 
@@ -364,7 +364,7 @@ var OfferProductDiscount = import_zod3.z.object({
364
364
  type: import_zod3.z.string()
365
365
  });
366
366
  var OfferProductOption = import_zod3.z.object({
367
- id: import_zod3.z.number(),
367
+ id: import_zod3.z.number().optional(),
368
368
  name: import_zod3.z.string(),
369
369
  position: import_zod3.z.number(),
370
370
  values: import_zod3.z.array(import_zod3.z.string())
@@ -376,7 +376,7 @@ var OfferProductVariant = import_zod3.z.object({
376
376
  inventoryManagement: import_zod3.z.string().nullable(),
377
377
  inventoryPolicy: import_zod3.z.string(),
378
378
  inventoryQuantity: import_zod3.z.number(),
379
- link: import_zod3.z.string(),
379
+ link: import_zod3.z.string().optional(),
380
380
  option1: import_zod3.z.string(),
381
381
  option2: import_zod3.z.string().nullable(),
382
382
  option3: import_zod3.z.string().nullable(),
@@ -389,12 +389,12 @@ var OfferProduct = import_zod3.z.object({
389
389
  id: import_zod3.z.number(),
390
390
  image: import_zod3.z.object({ src: import_zod3.z.string() }).nullish(),
391
391
  /**
392
- * `variantIds` is optional: the engine omits `variant_ids` (rather than sending `[]`) on an image tied to no
392
+ * `variantIds` is nullish: the engine omits `variant_ids` (rather than sending `[]`) on an image tied to no
393
393
  * variant — seen live on the `recommended` data source while `top_sellers` sends `[]` for the same image
394
- * (REB-24958). A missing list means "not variant-specific", exactly like `[]`, so the image keeps its `src`
395
- * and the product is never dropped over it.
394
+ * (REB-24958). A missing (or `null`) list means "not variant-specific", exactly like `[]`, so the image
395
+ * keeps its `src` and the product is never dropped over it.
396
396
  */
397
- images: import_zod3.z.array(import_zod3.z.object({ src: import_zod3.z.string(), variantIds: import_zod3.z.array(import_zod3.z.number()).optional() })).nullish(),
397
+ images: import_zod3.z.array(import_zod3.z.object({ src: import_zod3.z.string(), variantIds: import_zod3.z.array(import_zod3.z.number()).nullish() })).nullish(),
398
398
  link: import_zod3.z.string().optional(),
399
399
  metafields: import_zod3.z.array(import_zod3.z.object({ key: import_zod3.z.string(), namespace: import_zod3.z.string(), value: import_zod3.z.string() })).optional(),
400
400
  options: import_zod3.z.array(OfferProductOption),
@@ -6172,17 +6172,34 @@ var PostPurchaseProductSettings = PostPurchaseSettings.pick({
6172
6172
 
6173
6173
  // src/server/readPostPurchaseProductSource.ts
6174
6174
  var import_zod45 = require("zod");
6175
- var Envelope = PostPurchaseProductSource.extend({ data: import_zod45.z.array(import_zod45.z.unknown()) });
6176
- var Product3 = PostPurchaseProductSource.shape.data.element;
6177
- var Identified = import_zod45.z.object({ id: PostPurchaseProductId });
6175
+
6176
+ // src/server/postPurchaseResponseDrift.ts
6178
6177
  var heard = /* @__PURE__ */ new WeakMap();
6179
- var firstHearing = (response, report) => {
6180
- const reporters = heard.get(response) ?? /* @__PURE__ */ new WeakSet();
6178
+ var sources = /* @__PURE__ */ new WeakMap();
6179
+ var rememberPostPurchaseProductSource = (product, response, productIndex) => {
6180
+ sources.set(product, { productIndex, response });
6181
+ };
6182
+ var postPurchaseProductSourceOf = (product) => sources.get(product);
6183
+ var reportOncePerResponse = (response, report, events) => {
6184
+ const reporters = heard.get(response) ?? /* @__PURE__ */ new WeakMap();
6181
6185
  heard.set(response, reporters);
6182
- if (reporters.has(report)) return false;
6183
- reporters.add(report);
6184
- return true;
6186
+ const details = reporters.get(report) ?? /* @__PURE__ */ new Set();
6187
+ reporters.set(report, details);
6188
+ for (const event of events) {
6189
+ if (details.has(event.detail)) continue;
6190
+ details.add(event.detail);
6191
+ report(event);
6192
+ }
6185
6193
  };
6194
+
6195
+ // src/server/postPurchaseSellingPlanRequirement.ts
6196
+ var requiresOwnSellingPlan = (product) => product.requiresSellingPlan === true;
6197
+ var requiresUnsuppliedSellingPlan = (product) => requiresOwnSellingPlan(product) && !product.sellingPlanGroups?.length;
6198
+
6199
+ // src/server/readPostPurchaseProductSource.ts
6200
+ var Envelope = PostPurchaseProductSource.extend({ data: import_zod45.z.array(import_zod45.z.unknown()) });
6201
+ var Product3 = PostPurchaseProductSource.shape.data.element;
6202
+ var Identified = import_zod45.z.object({ id: PostPurchaseProductId });
6186
6203
  var readPostPurchaseProductSource = (response, report) => {
6187
6204
  const { data, metadata } = Envelope.parse(deepCamelCase(response));
6188
6205
  const drift = [];
@@ -6190,9 +6207,10 @@ var readPostPurchaseProductSource = (response, report) => {
6190
6207
  const products = data.flatMap((raw, productIndex) => {
6191
6208
  const parsed = Product3.safeParse(raw);
6192
6209
  if (parsed.success) {
6193
- if (parsed.data.requiresSellingPlan && !parsed.data.sellingPlanGroups.length)
6210
+ rememberPostPurchaseProductSource(parsed.data, response, productIndex);
6211
+ if (requiresUnsuppliedSellingPlan(parsed.data))
6194
6212
  drift.push({
6195
- detail: `Dropped post-purchase product ${parsed.data.id}: data.${productIndex}.requiresSellingPlan`,
6213
+ detail: `Ineligible post-purchase product ${parsed.data.id}: data.${productIndex}.requiresSellingPlan`,
6196
6214
  kind: "product"
6197
6215
  });
6198
6216
  return [{ product: parsed.data, productIndex }];
@@ -6207,7 +6225,7 @@ var readPostPurchaseProductSource = (response, report) => {
6207
6225
  });
6208
6226
  return [];
6209
6227
  });
6210
- if (report && drift.length && firstHearing(response, report)) drift.forEach((event) => report(event));
6228
+ if (report && drift.length) reportOncePerResponse(response, report, drift);
6211
6229
  return { dropped, metadata, products };
6212
6230
  };
6213
6231
 
@@ -6353,7 +6371,8 @@ var Groups = import_zod52.z.array(
6353
6371
  sellingPlans: import_zod52.z.unknown()
6354
6372
  })
6355
6373
  );
6356
- var Allocations = import_zod52.z.array(import_zod52.z.object({ sellingPlanGroupId: import_zod52.z.string().min(1) })).default([]);
6374
+ var PostPurchasePlanAllocations = import_zod52.z.array(import_zod52.z.object({ sellingPlanGroupId: import_zod52.z.string().min(1) }));
6375
+ var Allocations = PostPurchasePlanAllocations.default([]);
6357
6376
  var Plans = import_zod52.z.array(
6358
6377
  PostPurchasePlanOption.pick({ id: true, name: true }).extend({
6359
6378
  options: import_zod52.z.array(import_zod52.z.object({ value: import_zod52.z.string().min(1) })).min(1),
@@ -6382,19 +6401,20 @@ var readPostPurchaseModernPlans = (raw, allocations) => {
6382
6401
  };
6383
6402
 
6384
6403
  // src/server/postPurchasePlanCatalog.ts
6404
+ var hasLegacyPlanGroups = ({ sellingPlanGroups }) => typeof sellingPlanGroups[0]?.id === "number";
6385
6405
  var projectPostPurchasePlanCatalog = (normalized, report) => {
6386
6406
  const { data } = normalized;
6387
- if (data.length === 1 && data[0].variants.length > 1) return null;
6388
6407
  if (normalized.emptiedByDrop && !data.length) return null;
6408
+ if (data.length === 1 && data[0].variants.length > 1) return null;
6389
6409
  try {
6390
- if (data.length !== 1 || data[0].variants.length !== 1) throw new Error("Ambiguous product");
6410
+ if (data.length !== 1 || data[0].variants.length !== 1) throw new Error("No single product and variant");
6391
6411
  const product = data[0];
6392
6412
  const variant2 = product.variants[0];
6393
6413
  if (variant2.productId !== product.id) throw new Error("Foreign variant");
6394
6414
  const groups = product.sellingPlanGroups;
6395
6415
  const required = groups.map((group) => group.requiresSellingPlan);
6396
6416
  if (required.some((flag) => flag !== required[0])) throw new Error("Conflicting requirement");
6397
- const plans = typeof groups[0]?.id === "number" ? readPostPurchaseLegacyPlans(groups[0], variant2.id) : readPostPurchaseModernPlans(groups, variant2.sellingPlanAllocations);
6417
+ const plans = hasLegacyPlanGroups(product) ? readPostPurchaseLegacyPlans(groups[0], variant2.id) : readPostPurchaseModernPlans(groups, variant2.sellingPlanAllocations);
6398
6418
  const unique = /* @__PURE__ */ new Map();
6399
6419
  for (const plan of plans) {
6400
6420
  const previous = unique.get(plan.id);
@@ -6407,8 +6427,7 @@ var projectPostPurchasePlanCatalog = (normalized, report) => {
6407
6427
  discountPercentage: postPurchasePlanDiscountPercentage(variant2.price, plan.adjustment)
6408
6428
  })),
6409
6429
  productId: product.id,
6410
- // REB-25048: a v2 product flags the requirement on itself and carries no selling plan groups
6411
- requiresSellingPlan: (required[0] ?? false) || product.requiresSellingPlan === true,
6430
+ requiresSellingPlan: (required[0] ?? false) || requiresOwnSellingPlan(product),
6412
6431
  variantId: variant2.id
6413
6432
  });
6414
6433
  } catch {
@@ -6961,7 +6980,7 @@ var oneTimeUnavailable = (normalized, allowCollection, quantity3) => {
6961
6980
  return "Only fixed one-time configuration is supported";
6962
6981
  const product = data[0];
6963
6982
  const variant2 = product.variants[0];
6964
- if (product.productGroupVariants.length || product.requiresSellingPlan === true || product.sellingPlanGroups?.some((group) => group.requiresSellingPlan))
6983
+ if (product.productGroupVariants.length || requiresOwnSellingPlan(product) || product.sellingPlanGroups?.some((group) => group.requiresSellingPlan))
6965
6984
  return "A one-time variant without product groups or a required selling plan is required";
6966
6985
  if (!postPurchaseVariantAvailable(variant2)) return VARIANT_UNAVAILABLE;
6967
6986
  if (variant2.productId !== product.id) throw new Error("Product variant identity mismatch");
@@ -7624,18 +7643,30 @@ var readPostPurchaseQuantity = ({
7624
7643
 
7625
7644
  // src/server/postPurchaseSts.ts
7626
7645
  var stsAllowed = ({ effective, original }) => [original, effective].some((config) => config.upgradeToSubscription.allow);
7627
- var postPurchaseStsLine = (normalized, product, lines, variantId) => {
7646
+ var reportUnreadableStsLine = (product, field, report) => {
7647
+ const source2 = postPurchaseProductSourceOf(product);
7648
+ const path = source2 ? `data.${source2.productIndex}.${field}` : field;
7649
+ const detail = `Unreadable post-purchase STS product ${product.id}: ${path}`;
7650
+ reportOncePerResponse(source2?.response ?? product, report, [{ detail, kind: "product" }]);
7651
+ };
7652
+ var postPurchaseStsLine = (normalized, product, lines, variantId, report) => {
7628
7653
  if (!product.sellingPlanGroups?.length || product.productGroupVariants.length) return void 0;
7629
- for (const variant2 of product.variants) {
7654
+ const modern = !hasLegacyPlanGroups(product);
7655
+ for (const [index, variant2] of product.variants.entries()) {
7630
7656
  const matched = lines.filter((line) => line.sellingPlanId === void 0 && line.variantId === variant2.id);
7631
7657
  const data = [{ ...product, variants: [variant2] }];
7632
- if ((variantId === void 0 || variant2.id === variantId) && matched.length === 1 && postPurchaseVariantAvailable(variant2) && projectPostPurchasePlanCatalog({ ...normalized, data })?.plans.length)
7633
- return { data, line: matched[0] };
7658
+ if ((variantId === void 0 || variant2.id === variantId) && matched.length === 1 && postPurchaseVariantAvailable(variant2)) {
7659
+ const unread = [];
7660
+ if (projectPostPurchasePlanCatalog({ ...normalized, data }, (event) => unread.push(event))?.plans.length)
7661
+ return { data, line: matched[0] };
7662
+ const field = modern && !PostPurchasePlanAllocations.safeParse(variant2.sellingPlanAllocations).success ? `variants.${index}.sellingPlanAllocations` : !unread.length ? void 0 : variant2.productId === product.id ? "sellingPlanGroups" : `variants.${index}.productId`;
7663
+ if (report && field) reportUnreadableStsLine(product, field, report);
7664
+ }
7634
7665
  }
7635
7666
  };
7636
7667
  var preparePostPurchaseStsView = (widgetId, normalized, product, context, requested, report) => {
7637
7668
  const { originalLineItems } = PostPurchaseStsContext.parse(context);
7638
- const found = postPurchaseStsLine(normalized, product, originalLineItems, requested.variantId);
7669
+ const found = postPurchaseStsLine(normalized, product, originalLineItems, requested.variantId, report);
7639
7670
  if (!found) return null;
7640
7671
  const whole = found.line.quantity;
7641
7672
  if (requested.quantity !== void 0 && requested.quantity !== whole)
@@ -8070,7 +8101,7 @@ var eligibility = (normalized, product, excluded) => {
8070
8101
  if (product.productGroupVariants.length) {
8071
8102
  const group = readPostPurchaseGroup(product, normalized.effective, excluded);
8072
8103
  if (!group.entries.length) return void 0;
8073
- return group.entries.map(({ product: option }) => postPurchaseMemberEligibility(normalized, option)).some(Boolean);
8104
+ return group.entries.map(({ product: option }) => eligibility(normalized, option)).some(Boolean);
8074
8105
  }
8075
8106
  if (!product.variants.length) return false;
8076
8107
  const variants = projectPostPurchaseVariantCatalog(product, normalized.effective).variants;
@@ -8097,7 +8128,7 @@ var postPurchaseMemberEligibility = (normalized, product, excluded) => {
8097
8128
 
8098
8129
  // src/server/classifyPostPurchaseOffer.ts
8099
8130
  var identities = (members) => members.map(({ product, productIndex }) => ({ productId: product.id, productIndex }));
8100
- var classifyPostPurchaseOffer = (settings, response, context, options2) => {
8131
+ var classifyPostPurchaseOffer = (settings, response, context, options2, report) => {
8101
8132
  let unreadable = "context-unreadable";
8102
8133
  try {
8103
8134
  const trusted = PostPurchaseOfferEligibilityContext.parse(context);
@@ -8123,7 +8154,7 @@ var classifyPostPurchaseOffer = (settings, response, context, options2) => {
8123
8154
  PostPurchaseSubscriptionSettings.parse(normalized.effective.subscription);
8124
8155
  }
8125
8156
  const lines = trusted.originalLineItems;
8126
- const stsMembers = sts ? members.filter(({ product }) => postPurchaseStsLine(normalized, product, lines)) : [];
8157
+ const stsMembers = sts ? members.filter(({ product }) => postPurchaseStsLine(normalized, product, lines, void 0, report)) : [];
8127
8158
  if (stsMembers.length) return { members: identities(stsMembers), mode: "sts", status: "retain" };
8128
8159
  const classified = members.map((member) => ({
8129
8160
  ...member,
@@ -8,8 +8,8 @@ var getValidGwpProductIds = (matchedRules = []) => new Set(
8
8
 
9
9
  // src/gwp/hasUnsupportedRuleTypes.ts
10
10
  var UNSUPPORTED_RULE_TYPES = ["url", "order_tag"];
11
- var UNSUPPORTED_RULE_TYPE_PREFIX = "geolocation_";
12
- var isUnsupportedType = (type = "") => UNSUPPORTED_RULE_TYPES.includes(type) || type.startsWith(UNSUPPORTED_RULE_TYPE_PREFIX);
11
+ var UNSUPPORTED_RULE_TYPE_FRAGMENT = "geolocation";
12
+ var isUnsupportedType = (type = "") => UNSUPPORTED_RULE_TYPES.includes(type) || type.includes(UNSUPPORTED_RULE_TYPE_FRAGMENT);
13
13
  var ruleHasUnsupportedType = (rule) => (rule.logic ?? []).some((group) => (group.rules ?? []).some((item) => isUnsupportedType(item.type)));
14
14
  var hasUnsupportedRuleTypes = (metadata) => [...metadata?.matchedRules ?? [], ...metadata?.unmatchedRules ?? []].some(ruleHasUnsupportedType);
15
15
 
@@ -291,7 +291,7 @@ var OfferProductDiscount = z3.object({
291
291
  type: z3.string()
292
292
  });
293
293
  var OfferProductOption = z3.object({
294
- id: z3.number(),
294
+ id: z3.number().optional(),
295
295
  name: z3.string(),
296
296
  position: z3.number(),
297
297
  values: z3.array(z3.string())
@@ -303,7 +303,7 @@ var OfferProductVariant = z3.object({
303
303
  inventoryManagement: z3.string().nullable(),
304
304
  inventoryPolicy: z3.string(),
305
305
  inventoryQuantity: z3.number(),
306
- link: z3.string(),
306
+ link: z3.string().optional(),
307
307
  option1: z3.string(),
308
308
  option2: z3.string().nullable(),
309
309
  option3: z3.string().nullable(),
@@ -316,12 +316,12 @@ var OfferProduct = z3.object({
316
316
  id: z3.number(),
317
317
  image: z3.object({ src: z3.string() }).nullish(),
318
318
  /**
319
- * `variantIds` is optional: the engine omits `variant_ids` (rather than sending `[]`) on an image tied to no
319
+ * `variantIds` is nullish: the engine omits `variant_ids` (rather than sending `[]`) on an image tied to no
320
320
  * variant — seen live on the `recommended` data source while `top_sellers` sends `[]` for the same image
321
- * (REB-24958). A missing list means "not variant-specific", exactly like `[]`, so the image keeps its `src`
322
- * and the product is never dropped over it.
321
+ * (REB-24958). A missing (or `null`) list means "not variant-specific", exactly like `[]`, so the image
322
+ * keeps its `src` and the product is never dropped over it.
323
323
  */
324
- images: z3.array(z3.object({ src: z3.string(), variantIds: z3.array(z3.number()).optional() })).nullish(),
324
+ images: z3.array(z3.object({ src: z3.string(), variantIds: z3.array(z3.number()).nullish() })).nullish(),
325
325
  link: z3.string().optional(),
326
326
  metafields: z3.array(z3.object({ key: z3.string(), namespace: z3.string(), value: z3.string() })).optional(),
327
327
  options: z3.array(OfferProductOption),
@@ -6099,17 +6099,34 @@ var PostPurchaseProductSettings = PostPurchaseSettings.pick({
6099
6099
 
6100
6100
  // src/server/readPostPurchaseProductSource.ts
6101
6101
  import { z as z45 } from "zod";
6102
- var Envelope = PostPurchaseProductSource.extend({ data: z45.array(z45.unknown()) });
6103
- var Product3 = PostPurchaseProductSource.shape.data.element;
6104
- var Identified = z45.object({ id: PostPurchaseProductId });
6102
+
6103
+ // src/server/postPurchaseResponseDrift.ts
6105
6104
  var heard = /* @__PURE__ */ new WeakMap();
6106
- var firstHearing = (response, report) => {
6107
- const reporters = heard.get(response) ?? /* @__PURE__ */ new WeakSet();
6105
+ var sources = /* @__PURE__ */ new WeakMap();
6106
+ var rememberPostPurchaseProductSource = (product, response, productIndex) => {
6107
+ sources.set(product, { productIndex, response });
6108
+ };
6109
+ var postPurchaseProductSourceOf = (product) => sources.get(product);
6110
+ var reportOncePerResponse = (response, report, events) => {
6111
+ const reporters = heard.get(response) ?? /* @__PURE__ */ new WeakMap();
6108
6112
  heard.set(response, reporters);
6109
- if (reporters.has(report)) return false;
6110
- reporters.add(report);
6111
- return true;
6113
+ const details = reporters.get(report) ?? /* @__PURE__ */ new Set();
6114
+ reporters.set(report, details);
6115
+ for (const event of events) {
6116
+ if (details.has(event.detail)) continue;
6117
+ details.add(event.detail);
6118
+ report(event);
6119
+ }
6112
6120
  };
6121
+
6122
+ // src/server/postPurchaseSellingPlanRequirement.ts
6123
+ var requiresOwnSellingPlan = (product) => product.requiresSellingPlan === true;
6124
+ var requiresUnsuppliedSellingPlan = (product) => requiresOwnSellingPlan(product) && !product.sellingPlanGroups?.length;
6125
+
6126
+ // src/server/readPostPurchaseProductSource.ts
6127
+ var Envelope = PostPurchaseProductSource.extend({ data: z45.array(z45.unknown()) });
6128
+ var Product3 = PostPurchaseProductSource.shape.data.element;
6129
+ var Identified = z45.object({ id: PostPurchaseProductId });
6113
6130
  var readPostPurchaseProductSource = (response, report) => {
6114
6131
  const { data, metadata } = Envelope.parse(deepCamelCase(response));
6115
6132
  const drift = [];
@@ -6117,9 +6134,10 @@ var readPostPurchaseProductSource = (response, report) => {
6117
6134
  const products = data.flatMap((raw, productIndex) => {
6118
6135
  const parsed = Product3.safeParse(raw);
6119
6136
  if (parsed.success) {
6120
- if (parsed.data.requiresSellingPlan && !parsed.data.sellingPlanGroups.length)
6137
+ rememberPostPurchaseProductSource(parsed.data, response, productIndex);
6138
+ if (requiresUnsuppliedSellingPlan(parsed.data))
6121
6139
  drift.push({
6122
- detail: `Dropped post-purchase product ${parsed.data.id}: data.${productIndex}.requiresSellingPlan`,
6140
+ detail: `Ineligible post-purchase product ${parsed.data.id}: data.${productIndex}.requiresSellingPlan`,
6123
6141
  kind: "product"
6124
6142
  });
6125
6143
  return [{ product: parsed.data, productIndex }];
@@ -6134,7 +6152,7 @@ var readPostPurchaseProductSource = (response, report) => {
6134
6152
  });
6135
6153
  return [];
6136
6154
  });
6137
- if (report && drift.length && firstHearing(response, report)) drift.forEach((event) => report(event));
6155
+ if (report && drift.length) reportOncePerResponse(response, report, drift);
6138
6156
  return { dropped, metadata, products };
6139
6157
  };
6140
6158
 
@@ -6280,7 +6298,8 @@ var Groups = z52.array(
6280
6298
  sellingPlans: z52.unknown()
6281
6299
  })
6282
6300
  );
6283
- var Allocations = z52.array(z52.object({ sellingPlanGroupId: z52.string().min(1) })).default([]);
6301
+ var PostPurchasePlanAllocations = z52.array(z52.object({ sellingPlanGroupId: z52.string().min(1) }));
6302
+ var Allocations = PostPurchasePlanAllocations.default([]);
6284
6303
  var Plans = z52.array(
6285
6304
  PostPurchasePlanOption.pick({ id: true, name: true }).extend({
6286
6305
  options: z52.array(z52.object({ value: z52.string().min(1) })).min(1),
@@ -6309,19 +6328,20 @@ var readPostPurchaseModernPlans = (raw, allocations) => {
6309
6328
  };
6310
6329
 
6311
6330
  // src/server/postPurchasePlanCatalog.ts
6331
+ var hasLegacyPlanGroups = ({ sellingPlanGroups }) => typeof sellingPlanGroups[0]?.id === "number";
6312
6332
  var projectPostPurchasePlanCatalog = (normalized, report) => {
6313
6333
  const { data } = normalized;
6314
- if (data.length === 1 && data[0].variants.length > 1) return null;
6315
6334
  if (normalized.emptiedByDrop && !data.length) return null;
6335
+ if (data.length === 1 && data[0].variants.length > 1) return null;
6316
6336
  try {
6317
- if (data.length !== 1 || data[0].variants.length !== 1) throw new Error("Ambiguous product");
6337
+ if (data.length !== 1 || data[0].variants.length !== 1) throw new Error("No single product and variant");
6318
6338
  const product = data[0];
6319
6339
  const variant2 = product.variants[0];
6320
6340
  if (variant2.productId !== product.id) throw new Error("Foreign variant");
6321
6341
  const groups = product.sellingPlanGroups;
6322
6342
  const required = groups.map((group) => group.requiresSellingPlan);
6323
6343
  if (required.some((flag) => flag !== required[0])) throw new Error("Conflicting requirement");
6324
- const plans = typeof groups[0]?.id === "number" ? readPostPurchaseLegacyPlans(groups[0], variant2.id) : readPostPurchaseModernPlans(groups, variant2.sellingPlanAllocations);
6344
+ const plans = hasLegacyPlanGroups(product) ? readPostPurchaseLegacyPlans(groups[0], variant2.id) : readPostPurchaseModernPlans(groups, variant2.sellingPlanAllocations);
6325
6345
  const unique = /* @__PURE__ */ new Map();
6326
6346
  for (const plan of plans) {
6327
6347
  const previous = unique.get(plan.id);
@@ -6334,8 +6354,7 @@ var projectPostPurchasePlanCatalog = (normalized, report) => {
6334
6354
  discountPercentage: postPurchasePlanDiscountPercentage(variant2.price, plan.adjustment)
6335
6355
  })),
6336
6356
  productId: product.id,
6337
- // REB-25048: a v2 product flags the requirement on itself and carries no selling plan groups
6338
- requiresSellingPlan: (required[0] ?? false) || product.requiresSellingPlan === true,
6357
+ requiresSellingPlan: (required[0] ?? false) || requiresOwnSellingPlan(product),
6339
6358
  variantId: variant2.id
6340
6359
  });
6341
6360
  } catch {
@@ -6888,7 +6907,7 @@ var oneTimeUnavailable = (normalized, allowCollection, quantity3) => {
6888
6907
  return "Only fixed one-time configuration is supported";
6889
6908
  const product = data[0];
6890
6909
  const variant2 = product.variants[0];
6891
- if (product.productGroupVariants.length || product.requiresSellingPlan === true || product.sellingPlanGroups?.some((group) => group.requiresSellingPlan))
6910
+ if (product.productGroupVariants.length || requiresOwnSellingPlan(product) || product.sellingPlanGroups?.some((group) => group.requiresSellingPlan))
6892
6911
  return "A one-time variant without product groups or a required selling plan is required";
6893
6912
  if (!postPurchaseVariantAvailable(variant2)) return VARIANT_UNAVAILABLE;
6894
6913
  if (variant2.productId !== product.id) throw new Error("Product variant identity mismatch");
@@ -7551,18 +7570,30 @@ var readPostPurchaseQuantity = ({
7551
7570
 
7552
7571
  // src/server/postPurchaseSts.ts
7553
7572
  var stsAllowed = ({ effective, original }) => [original, effective].some((config) => config.upgradeToSubscription.allow);
7554
- var postPurchaseStsLine = (normalized, product, lines, variantId) => {
7573
+ var reportUnreadableStsLine = (product, field, report) => {
7574
+ const source2 = postPurchaseProductSourceOf(product);
7575
+ const path = source2 ? `data.${source2.productIndex}.${field}` : field;
7576
+ const detail = `Unreadable post-purchase STS product ${product.id}: ${path}`;
7577
+ reportOncePerResponse(source2?.response ?? product, report, [{ detail, kind: "product" }]);
7578
+ };
7579
+ var postPurchaseStsLine = (normalized, product, lines, variantId, report) => {
7555
7580
  if (!product.sellingPlanGroups?.length || product.productGroupVariants.length) return void 0;
7556
- for (const variant2 of product.variants) {
7581
+ const modern = !hasLegacyPlanGroups(product);
7582
+ for (const [index, variant2] of product.variants.entries()) {
7557
7583
  const matched = lines.filter((line) => line.sellingPlanId === void 0 && line.variantId === variant2.id);
7558
7584
  const data = [{ ...product, variants: [variant2] }];
7559
- if ((variantId === void 0 || variant2.id === variantId) && matched.length === 1 && postPurchaseVariantAvailable(variant2) && projectPostPurchasePlanCatalog({ ...normalized, data })?.plans.length)
7560
- return { data, line: matched[0] };
7585
+ if ((variantId === void 0 || variant2.id === variantId) && matched.length === 1 && postPurchaseVariantAvailable(variant2)) {
7586
+ const unread = [];
7587
+ if (projectPostPurchasePlanCatalog({ ...normalized, data }, (event) => unread.push(event))?.plans.length)
7588
+ return { data, line: matched[0] };
7589
+ const field = modern && !PostPurchasePlanAllocations.safeParse(variant2.sellingPlanAllocations).success ? `variants.${index}.sellingPlanAllocations` : !unread.length ? void 0 : variant2.productId === product.id ? "sellingPlanGroups" : `variants.${index}.productId`;
7590
+ if (report && field) reportUnreadableStsLine(product, field, report);
7591
+ }
7561
7592
  }
7562
7593
  };
7563
7594
  var preparePostPurchaseStsView = (widgetId, normalized, product, context, requested, report) => {
7564
7595
  const { originalLineItems } = PostPurchaseStsContext.parse(context);
7565
- const found = postPurchaseStsLine(normalized, product, originalLineItems, requested.variantId);
7596
+ const found = postPurchaseStsLine(normalized, product, originalLineItems, requested.variantId, report);
7566
7597
  if (!found) return null;
7567
7598
  const whole = found.line.quantity;
7568
7599
  if (requested.quantity !== void 0 && requested.quantity !== whole)
@@ -7997,7 +8028,7 @@ var eligibility = (normalized, product, excluded) => {
7997
8028
  if (product.productGroupVariants.length) {
7998
8029
  const group = readPostPurchaseGroup(product, normalized.effective, excluded);
7999
8030
  if (!group.entries.length) return void 0;
8000
- return group.entries.map(({ product: option }) => postPurchaseMemberEligibility(normalized, option)).some(Boolean);
8031
+ return group.entries.map(({ product: option }) => eligibility(normalized, option)).some(Boolean);
8001
8032
  }
8002
8033
  if (!product.variants.length) return false;
8003
8034
  const variants = projectPostPurchaseVariantCatalog(product, normalized.effective).variants;
@@ -8024,7 +8055,7 @@ var postPurchaseMemberEligibility = (normalized, product, excluded) => {
8024
8055
 
8025
8056
  // src/server/classifyPostPurchaseOffer.ts
8026
8057
  var identities = (members) => members.map(({ product, productIndex }) => ({ productId: product.id, productIndex }));
8027
- var classifyPostPurchaseOffer = (settings, response, context, options2) => {
8058
+ var classifyPostPurchaseOffer = (settings, response, context, options2, report) => {
8028
8059
  let unreadable = "context-unreadable";
8029
8060
  try {
8030
8061
  const trusted = PostPurchaseOfferEligibilityContext.parse(context);
@@ -8050,7 +8081,7 @@ var classifyPostPurchaseOffer = (settings, response, context, options2) => {
8050
8081
  PostPurchaseSubscriptionSettings.parse(normalized.effective.subscription);
8051
8082
  }
8052
8083
  const lines = trusted.originalLineItems;
8053
- const stsMembers = sts ? members.filter(({ product }) => postPurchaseStsLine(normalized, product, lines)) : [];
8084
+ const stsMembers = sts ? members.filter(({ product }) => postPurchaseStsLine(normalized, product, lines, void 0, report)) : [];
8054
8085
  if (stsMembers.length) return { members: identities(stsMembers), mode: "sts", status: "retain" };
8055
8086
  const classified = members.map((member) => ({
8056
8087
  ...member,
@@ -1,4 +1,9 @@
1
+ import { z } from 'zod';
1
2
  import { PostPurchasePlanOption } from '../schema/postPurchasePlanCatalog';
3
+ /** A variant's readable `selling_plan_allocations`; a missing list reads as none here, but STS reports it. */
4
+ export declare const PostPurchasePlanAllocations: z.ZodArray<z.ZodObject<{
5
+ sellingPlanGroupId: z.ZodString;
6
+ }, z.core.$strip>>;
2
7
  /**
3
8
  * Match allocation groups in product order, then preserve each matching group's plan order. Membership is
4
9
  * group-level by design (frozen-branch parity): Shopify allocates every plan of a group to a member variant, and
@@ -1,6 +1,9 @@
1
1
  import { PostPurchasePlanCatalog } from '../schema/postPurchasePlanCatalog';
2
2
  import type { normalizePostPurchaseProduct } from '../server/normalizePostPurchaseProduct';
3
3
  import type { Reporter } from '../server/shared';
4
+ type Product = ReturnType<typeof normalizePostPurchaseProduct>['data'][number];
5
+ /** v1 selling-plan groups carry numeric ids and their plans on the group; modern ones key plans per variant. */
6
+ export declare const hasLegacyPlanGroups: ({ sellingPlanGroups }: Pick<Product, "sellingPlanGroups">) => boolean;
4
7
  /**
5
8
  * Optional inspection data survives unsupported preparation without changing its purchase authority. Each plan
6
9
  * carries its own display saving on this variant's price, so the switch label needs no client maths.
@@ -12,3 +15,4 @@ import type { Reporter } from '../server/shared';
12
15
  * not a second fault: each drop was already reported, so it answers null without another report.
13
16
  */
14
17
  export declare const projectPostPurchasePlanCatalog: (normalized: ReturnType<typeof normalizePostPurchaseProduct>, report?: Reporter) => PostPurchasePlanCatalog | null;
18
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { Reporter, ServerDrift } from '../server/shared';
2
+ /** Records where a parsed product came from, so drift found about it later still names its response and path. */
3
+ export declare const rememberPostPurchaseProductSource: (product: object, response: object, productIndex: number) => void;
4
+ /** The response and raw `productIndex` a parsed product was read from, when it was read by the product source. */
5
+ export declare const postPurchaseProductSourceOf: (product: object) => {
6
+ productIndex: number;
7
+ response: object;
8
+ } | undefined;
9
+ /**
10
+ * Reports each event `report` has not yet heard about `response`, keyed on the `response` object itself: every
11
+ * projection of one request rereads the same object, so a drift is heard once however many projections find it.
12
+ */
13
+ export declare const reportOncePerResponse: (response: object, report: Reporter, events: ServerDrift[]) => void;
@@ -0,0 +1,14 @@
1
+ type Requiring = {
2
+ requiresSellingPlan?: boolean | null;
3
+ sellingPlanGroups?: unknown[];
4
+ };
5
+ /**
6
+ * REB-25048: a v2 product flags `requires_selling_plan` on itself rather than on its selling plan groups. It is
7
+ * never sold one-time, and a product that also has groups is offered subscription-only. Legacy read only the first
8
+ * group's flag, so under Product Type `both` it dropped such a product where the rebuild offers it subscription-only
9
+ * (accepted deviation, connscoding on npm-rebuy #117: neither sells it one-time).
10
+ */
11
+ export declare const requiresOwnSellingPlan: (product: Requiring) => boolean;
12
+ /** Requires a selling plan with no group to supply one: no purchasable mode is left, so eligibility drops it. */
13
+ export declare const requiresUnsuppliedSellingPlan: (product: Requiring) => boolean;
14
+ export {};
@@ -17,9 +17,17 @@ export declare const stsAllowed: ({ effective, original }: Pick<Normalized, "eff
17
17
  * plan; (2) it sits on exactly one one-time line, since two make the refunded quantity ambiguous; (3) a grouped
18
18
  * product is never an STS member; (4) the bound variant is the purchased line's, not the product's selected
19
19
  * variant, and subscription is the only mode. The first such variant in catalog order wins, unless `variantId`
20
- * names one.
20
+ * names one. Two more differences from legacy are accepted (connscoding, npm-rebuy #109): a subscription line
21
+ * beside a one-time line of the same variant still gets STS, where legacy's first match on the variant offered
22
+ * none; and the quantity is fixed at the whole one-time line, where legacy let the buyer change it.
23
+ *
24
+ * `report` hears a candidate (a purchased, in-stock variant on exactly one one-time line) whose plan data cannot be
25
+ * read, since it then silently switches nothing: a modern selling-plan variant whose `sellingPlanAllocations` is
26
+ * missing or malformed, or plan data the catalog cannot read. Each is a `product` drift naming the product id and
27
+ * the dotted path from the source root, never a value, heard once per response like the source's own drops. A
28
+ * readable variant that simply has no plan, or no candidate at all, stays silent.
21
29
  */
22
- export declare const postPurchaseStsLine: <T extends Line>(normalized: Normalized, product: Normalized["data"][number], lines: T[], variantId?: number) => {
30
+ export declare const postPurchaseStsLine: <T extends Line>(normalized: Normalized, product: Normalized["data"][number], lines: T[], variantId?: number, report?: Reporter) => {
23
31
  data: {
24
32
  variants: {
25
33
  title: string;
@@ -28,12 +28,12 @@ export type UpstreamFetch = (path: string, params: Record<string, string>, optio
28
28
  /**
29
29
  * A drift signal the orchestrators emit when a per-item payload salvage drops something: a malformed
30
30
  * offer product (or a post-purchase data-source product, `product`, named by id and dotted path), an
31
- * invalid/unknown section stubbed out, an unreadable rule verdict, a shop-config
32
- * entry the shared schema inerted (`shopConfig`), unreadable selectable-gift milestones
33
- * (`giftMilestones` — the widget's gifts are then kept), or a post-purchase product's malformed selected review
34
- * metafield (`reviews` — the reviews are then omitted; it names the product and the field, never the value). Distinct from a hard {@link UpstreamError} — the
35
- * request still succeeds (degraded), so this is the only trace that an upstream contract moved. Optional
36
- * on {@link ServerContext}; the host (rebuy-api) wires it to its logger.
31
+ * invalid/unknown section stubbed out, an unreadable rule verdict, a shop-config entry the shared schema
32
+ * inerted (`shopConfig`), unreadable selectable-gift milestones (`giftMilestones` — the widget's gifts are
33
+ * then kept), or a post-purchase product's malformed selected review metafield (`reviews` — the reviews are
34
+ * then omitted; it names the product and the field, never the value). Distinct from a hard
35
+ * {@link UpstreamError} — the request still succeeds (degraded), so this is the only trace that an upstream
36
+ * contract moved. Optional on {@link ServerContext}; the host (rebuy-api) wires it to its logger.
37
37
  */
38
38
  export type ServerDrift = {
39
39
  detail: string;
@@ -55,7 +55,10 @@ export declare const isEmptyObject: (x: unknown) => boolean;
55
55
  export type ServerContext = {
56
56
  fetchUpstream: UpstreamFetch;
57
57
  host: string;
58
- /** Buyer request IP forwarded to the ads engine (the checkout worker can't read it; rebuy-api extracts it). Without it DSPs see one datacenter IP for all traffic — geo/IVT-flagging risk. Monetize only. */
58
+ /**
59
+ * Buyer request IP forwarded to the ads engine (the checkout worker can't read it; rebuy-api extracts it).
60
+ * Without it DSPs see one datacenter IP for all traffic — geo/IVT-flagging risk. Monetize only.
61
+ */
59
62
  ipAddress?: string;
60
63
  /** Optional sink for {@link ServerDrift} — a degraded-but-successful payload salvage. */
61
64
  reportDrift?: (event: ServerDrift) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebuy/rebuy",
3
- "version": "3.56.3",
3
+ "version": "3.58.0",
4
4
  "description": "Shared zod schemas, legacy-to-CAB widget transforms, and server orchestrators for Rebuy's Shopify consumers (rebuy-api, admin-nextjs, rebuy-shopify-extensions)",
5
5
  "license": "MIT",
6
6
  "author": "Rebuy, Inc.",