@rebuy/rebuy 3.57.0 → 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>;
@@ -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),
@@ -6192,6 +6192,10 @@ var reportOncePerResponse = (response, report, events) => {
6192
6192
  }
6193
6193
  };
6194
6194
 
6195
+ // src/server/postPurchaseSellingPlanRequirement.ts
6196
+ var requiresOwnSellingPlan = (product) => product.requiresSellingPlan === true;
6197
+ var requiresUnsuppliedSellingPlan = (product) => requiresOwnSellingPlan(product) && !product.sellingPlanGroups?.length;
6198
+
6195
6199
  // src/server/readPostPurchaseProductSource.ts
6196
6200
  var Envelope = PostPurchaseProductSource.extend({ data: import_zod45.z.array(import_zod45.z.unknown()) });
6197
6201
  var Product3 = PostPurchaseProductSource.shape.data.element;
@@ -6204,9 +6208,9 @@ var readPostPurchaseProductSource = (response, report) => {
6204
6208
  const parsed = Product3.safeParse(raw);
6205
6209
  if (parsed.success) {
6206
6210
  rememberPostPurchaseProductSource(parsed.data, response, productIndex);
6207
- if (parsed.data.requiresSellingPlan && !parsed.data.sellingPlanGroups.length)
6211
+ if (requiresUnsuppliedSellingPlan(parsed.data))
6208
6212
  drift.push({
6209
- detail: `Dropped post-purchase product ${parsed.data.id}: data.${productIndex}.requiresSellingPlan`,
6213
+ detail: `Ineligible post-purchase product ${parsed.data.id}: data.${productIndex}.requiresSellingPlan`,
6210
6214
  kind: "product"
6211
6215
  });
6212
6216
  return [{ product: parsed.data, productIndex }];
@@ -6397,19 +6401,20 @@ var readPostPurchaseModernPlans = (raw, allocations) => {
6397
6401
  };
6398
6402
 
6399
6403
  // src/server/postPurchasePlanCatalog.ts
6404
+ var hasLegacyPlanGroups = ({ sellingPlanGroups }) => typeof sellingPlanGroups[0]?.id === "number";
6400
6405
  var projectPostPurchasePlanCatalog = (normalized, report) => {
6401
6406
  const { data } = normalized;
6402
- if (data.length === 1 && data[0].variants.length > 1) return null;
6403
6407
  if (normalized.emptiedByDrop && !data.length) return null;
6408
+ if (data.length === 1 && data[0].variants.length > 1) return null;
6404
6409
  try {
6405
- 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");
6406
6411
  const product = data[0];
6407
6412
  const variant2 = product.variants[0];
6408
6413
  if (variant2.productId !== product.id) throw new Error("Foreign variant");
6409
6414
  const groups = product.sellingPlanGroups;
6410
6415
  const required = groups.map((group) => group.requiresSellingPlan);
6411
6416
  if (required.some((flag) => flag !== required[0])) throw new Error("Conflicting requirement");
6412
- 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);
6413
6418
  const unique = /* @__PURE__ */ new Map();
6414
6419
  for (const plan of plans) {
6415
6420
  const previous = unique.get(plan.id);
@@ -6422,8 +6427,7 @@ var projectPostPurchasePlanCatalog = (normalized, report) => {
6422
6427
  discountPercentage: postPurchasePlanDiscountPercentage(variant2.price, plan.adjustment)
6423
6428
  })),
6424
6429
  productId: product.id,
6425
- // REB-25048: a v2 product flags the requirement on itself and carries no selling plan groups
6426
- requiresSellingPlan: (required[0] ?? false) || product.requiresSellingPlan === true,
6430
+ requiresSellingPlan: (required[0] ?? false) || requiresOwnSellingPlan(product),
6427
6431
  variantId: variant2.id
6428
6432
  });
6429
6433
  } catch {
@@ -6976,7 +6980,7 @@ var oneTimeUnavailable = (normalized, allowCollection, quantity3) => {
6976
6980
  return "Only fixed one-time configuration is supported";
6977
6981
  const product = data[0];
6978
6982
  const variant2 = product.variants[0];
6979
- 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))
6980
6984
  return "A one-time variant without product groups or a required selling plan is required";
6981
6985
  if (!postPurchaseVariantAvailable(variant2)) return VARIANT_UNAVAILABLE;
6982
6986
  if (variant2.productId !== product.id) throw new Error("Product variant identity mismatch");
@@ -7647,7 +7651,7 @@ var reportUnreadableStsLine = (product, field, report) => {
7647
7651
  };
7648
7652
  var postPurchaseStsLine = (normalized, product, lines, variantId, report) => {
7649
7653
  if (!product.sellingPlanGroups?.length || product.productGroupVariants.length) return void 0;
7650
- const modern = typeof product.sellingPlanGroups[0].id !== "number";
7654
+ const modern = !hasLegacyPlanGroups(product);
7651
7655
  for (const [index, variant2] of product.variants.entries()) {
7652
7656
  const matched = lines.filter((line) => line.sellingPlanId === void 0 && line.variantId === variant2.id);
7653
7657
  const data = [{ ...product, variants: [variant2] }];
@@ -7655,7 +7659,7 @@ var postPurchaseStsLine = (normalized, product, lines, variantId, report) => {
7655
7659
  const unread = [];
7656
7660
  if (projectPostPurchasePlanCatalog({ ...normalized, data }, (event) => unread.push(event))?.plans.length)
7657
7661
  return { data, line: matched[0] };
7658
- const field = modern && !PostPurchasePlanAllocations.safeParse(variant2.sellingPlanAllocations).success ? `variants.${index}.sellingPlanAllocations` : unread.length ? "sellingPlanGroups" : void 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`;
7659
7663
  if (report && field) reportUnreadableStsLine(product, field, report);
7660
7664
  }
7661
7665
  }
@@ -8097,7 +8101,7 @@ var eligibility = (normalized, product, excluded) => {
8097
8101
  if (product.productGroupVariants.length) {
8098
8102
  const group = readPostPurchaseGroup(product, normalized.effective, excluded);
8099
8103
  if (!group.entries.length) return void 0;
8100
- return group.entries.map(({ product: option }) => postPurchaseMemberEligibility(normalized, option)).some(Boolean);
8104
+ return group.entries.map(({ product: option }) => eligibility(normalized, option)).some(Boolean);
8101
8105
  }
8102
8106
  if (!product.variants.length) return false;
8103
8107
  const variants = projectPostPurchaseVariantCatalog(product, normalized.effective).variants;
@@ -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),
@@ -6119,6 +6119,10 @@ var reportOncePerResponse = (response, report, events) => {
6119
6119
  }
6120
6120
  };
6121
6121
 
6122
+ // src/server/postPurchaseSellingPlanRequirement.ts
6123
+ var requiresOwnSellingPlan = (product) => product.requiresSellingPlan === true;
6124
+ var requiresUnsuppliedSellingPlan = (product) => requiresOwnSellingPlan(product) && !product.sellingPlanGroups?.length;
6125
+
6122
6126
  // src/server/readPostPurchaseProductSource.ts
6123
6127
  var Envelope = PostPurchaseProductSource.extend({ data: z45.array(z45.unknown()) });
6124
6128
  var Product3 = PostPurchaseProductSource.shape.data.element;
@@ -6131,9 +6135,9 @@ var readPostPurchaseProductSource = (response, report) => {
6131
6135
  const parsed = Product3.safeParse(raw);
6132
6136
  if (parsed.success) {
6133
6137
  rememberPostPurchaseProductSource(parsed.data, response, productIndex);
6134
- if (parsed.data.requiresSellingPlan && !parsed.data.sellingPlanGroups.length)
6138
+ if (requiresUnsuppliedSellingPlan(parsed.data))
6135
6139
  drift.push({
6136
- detail: `Dropped post-purchase product ${parsed.data.id}: data.${productIndex}.requiresSellingPlan`,
6140
+ detail: `Ineligible post-purchase product ${parsed.data.id}: data.${productIndex}.requiresSellingPlan`,
6137
6141
  kind: "product"
6138
6142
  });
6139
6143
  return [{ product: parsed.data, productIndex }];
@@ -6324,19 +6328,20 @@ var readPostPurchaseModernPlans = (raw, allocations) => {
6324
6328
  };
6325
6329
 
6326
6330
  // src/server/postPurchasePlanCatalog.ts
6331
+ var hasLegacyPlanGroups = ({ sellingPlanGroups }) => typeof sellingPlanGroups[0]?.id === "number";
6327
6332
  var projectPostPurchasePlanCatalog = (normalized, report) => {
6328
6333
  const { data } = normalized;
6329
- if (data.length === 1 && data[0].variants.length > 1) return null;
6330
6334
  if (normalized.emptiedByDrop && !data.length) return null;
6335
+ if (data.length === 1 && data[0].variants.length > 1) return null;
6331
6336
  try {
6332
- 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");
6333
6338
  const product = data[0];
6334
6339
  const variant2 = product.variants[0];
6335
6340
  if (variant2.productId !== product.id) throw new Error("Foreign variant");
6336
6341
  const groups = product.sellingPlanGroups;
6337
6342
  const required = groups.map((group) => group.requiresSellingPlan);
6338
6343
  if (required.some((flag) => flag !== required[0])) throw new Error("Conflicting requirement");
6339
- 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);
6340
6345
  const unique = /* @__PURE__ */ new Map();
6341
6346
  for (const plan of plans) {
6342
6347
  const previous = unique.get(plan.id);
@@ -6349,8 +6354,7 @@ var projectPostPurchasePlanCatalog = (normalized, report) => {
6349
6354
  discountPercentage: postPurchasePlanDiscountPercentage(variant2.price, plan.adjustment)
6350
6355
  })),
6351
6356
  productId: product.id,
6352
- // REB-25048: a v2 product flags the requirement on itself and carries no selling plan groups
6353
- requiresSellingPlan: (required[0] ?? false) || product.requiresSellingPlan === true,
6357
+ requiresSellingPlan: (required[0] ?? false) || requiresOwnSellingPlan(product),
6354
6358
  variantId: variant2.id
6355
6359
  });
6356
6360
  } catch {
@@ -6903,7 +6907,7 @@ var oneTimeUnavailable = (normalized, allowCollection, quantity3) => {
6903
6907
  return "Only fixed one-time configuration is supported";
6904
6908
  const product = data[0];
6905
6909
  const variant2 = product.variants[0];
6906
- 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))
6907
6911
  return "A one-time variant without product groups or a required selling plan is required";
6908
6912
  if (!postPurchaseVariantAvailable(variant2)) return VARIANT_UNAVAILABLE;
6909
6913
  if (variant2.productId !== product.id) throw new Error("Product variant identity mismatch");
@@ -7574,7 +7578,7 @@ var reportUnreadableStsLine = (product, field, report) => {
7574
7578
  };
7575
7579
  var postPurchaseStsLine = (normalized, product, lines, variantId, report) => {
7576
7580
  if (!product.sellingPlanGroups?.length || product.productGroupVariants.length) return void 0;
7577
- const modern = typeof product.sellingPlanGroups[0].id !== "number";
7581
+ const modern = !hasLegacyPlanGroups(product);
7578
7582
  for (const [index, variant2] of product.variants.entries()) {
7579
7583
  const matched = lines.filter((line) => line.sellingPlanId === void 0 && line.variantId === variant2.id);
7580
7584
  const data = [{ ...product, variants: [variant2] }];
@@ -7582,7 +7586,7 @@ var postPurchaseStsLine = (normalized, product, lines, variantId, report) => {
7582
7586
  const unread = [];
7583
7587
  if (projectPostPurchasePlanCatalog({ ...normalized, data }, (event) => unread.push(event))?.plans.length)
7584
7588
  return { data, line: matched[0] };
7585
- const field = modern && !PostPurchasePlanAllocations.safeParse(variant2.sellingPlanAllocations).success ? `variants.${index}.sellingPlanAllocations` : unread.length ? "sellingPlanGroups" : void 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`;
7586
7590
  if (report && field) reportUnreadableStsLine(product, field, report);
7587
7591
  }
7588
7592
  }
@@ -8024,7 +8028,7 @@ var eligibility = (normalized, product, excluded) => {
8024
8028
  if (product.productGroupVariants.length) {
8025
8029
  const group = readPostPurchaseGroup(product, normalized.effective, excluded);
8026
8030
  if (!group.entries.length) return void 0;
8027
- return group.entries.map(({ product: option }) => postPurchaseMemberEligibility(normalized, option)).some(Boolean);
8031
+ return group.entries.map(({ product: option }) => eligibility(normalized, option)).some(Boolean);
8028
8032
  }
8029
8033
  if (!product.variants.length) return false;
8030
8034
  const variants = projectPostPurchaseVariantCatalog(product, normalized.effective).variants;
@@ -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,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,7 +17,9 @@ 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.
21
23
  *
22
24
  * `report` hears a candidate (a purchased, in-stock variant on exactly one one-time line) whose plan data cannot be
23
25
  * read, since it then silently switches nothing: a modern selling-plan variant whose `sellingPlanAllocations` is
@@ -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.57.0",
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.",