@rebuy/rebuy 3.56.1 → 3.56.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -206,6 +206,7 @@ __export(src_exports, {
206
206
  PostPurchaseOfferEligibilityContext: () => PostPurchaseOfferEligibilityContext,
207
207
  PostPurchaseOfferPresentation: () => PostPurchaseOfferPresentation,
208
208
  PostPurchaseOfferSkipReason: () => PostPurchaseOfferSkipReason,
209
+ PostPurchaseOfferUnavailableReason: () => PostPurchaseOfferUnavailableReason,
209
210
  PostPurchaseOptions: () => PostPurchaseOptions,
210
211
  PostPurchaseOrderAttributes: () => PostPurchaseOrderAttributes,
211
212
  PostPurchaseOrderIdSchema: () => PostPurchaseOrderIdSchema,
@@ -2769,6 +2770,8 @@ var Product2 = ProductBase.extend({
2769
2770
  metafields: import_zod19.z.unknown().optional(),
2770
2771
  options: import_zod19.z.unknown().optional(),
2771
2772
  productGroupVariants: import_zod19.z.array(import_zod19.z.unknown()).default([]),
2773
+ // REB-25048: the v2 data source flags a subscription-only product here and sends no selling plan groups
2774
+ requiresSellingPlan: import_zod19.z.boolean().nullish(),
2772
2775
  selectedVariant: import_zod19.z.unknown().optional(),
2773
2776
  sellingPlanGroups: import_zod19.z.array(
2774
2777
  OfferProduct.shape.sellingPlanGroups.unwrap().element.extend({
@@ -4790,6 +4793,12 @@ var OfferPreparationResponseSchema = import_zod77.z.object({
4790
4793
  var import_zod78 = require("zod");
4791
4794
  var selection2 = PreparedPostPurchaseProduct.shape.selection;
4792
4795
  var PostPurchaseOfferSkipReason = import_zod78.z.enum(["empty", "no-eligible-subscription"]);
4796
+ var PostPurchaseOfferUnavailableReason = import_zod78.z.enum([
4797
+ "catalog-unreadable",
4798
+ "context-unreadable",
4799
+ "eligible-sts",
4800
+ "settings-unreadable"
4801
+ ]);
4793
4802
  var PostPurchaseOfferEligibilityContext = import_zod78.z.object({
4794
4803
  excludedProductIds: PostPurchaseExcludedProductIds,
4795
4804
  originalLineItems: import_zod78.z.array(selection2.pick({ productId: true, sellingPlanId: true, variantId: true }))
@@ -4810,7 +4819,8 @@ var PostPurchaseOfferEligibility = import_zod78.z.discriminatedUnion("status", [
4810
4819
  status: import_zod78.z.literal("retain")
4811
4820
  }),
4812
4821
  import_zod78.z.object({ reason: PostPurchaseOfferSkipReason, status: import_zod78.z.literal("skip") }),
4813
- import_zod78.z.object({ reason: import_zod78.z.literal("eligible-sts").optional(), status: import_zod78.z.literal("unavailable") })
4822
+ /** The classifier always names a reason; it stays optional so a reasonless answer from before still parses. */
4823
+ import_zod78.z.object({ reason: PostPurchaseOfferUnavailableReason.optional(), status: import_zod78.z.literal("unavailable") })
4814
4824
  ]);
4815
4825
 
4816
4826
  // src/schema/postPurchaseOfferPresentation.ts
package/dist/index.mjs CHANGED
@@ -2290,6 +2290,8 @@ var Product2 = ProductBase.extend({
2290
2290
  metafields: z19.unknown().optional(),
2291
2291
  options: z19.unknown().optional(),
2292
2292
  productGroupVariants: z19.array(z19.unknown()).default([]),
2293
+ // REB-25048: the v2 data source flags a subscription-only product here and sends no selling plan groups
2294
+ requiresSellingPlan: z19.boolean().nullish(),
2293
2295
  selectedVariant: z19.unknown().optional(),
2294
2296
  sellingPlanGroups: z19.array(
2295
2297
  OfferProduct.shape.sellingPlanGroups.unwrap().element.extend({
@@ -4311,6 +4313,12 @@ var OfferPreparationResponseSchema = z77.object({
4311
4313
  import { z as z78 } from "zod";
4312
4314
  var selection2 = PreparedPostPurchaseProduct.shape.selection;
4313
4315
  var PostPurchaseOfferSkipReason = z78.enum(["empty", "no-eligible-subscription"]);
4316
+ var PostPurchaseOfferUnavailableReason = z78.enum([
4317
+ "catalog-unreadable",
4318
+ "context-unreadable",
4319
+ "eligible-sts",
4320
+ "settings-unreadable"
4321
+ ]);
4314
4322
  var PostPurchaseOfferEligibilityContext = z78.object({
4315
4323
  excludedProductIds: PostPurchaseExcludedProductIds,
4316
4324
  originalLineItems: z78.array(selection2.pick({ productId: true, sellingPlanId: true, variantId: true }))
@@ -4331,7 +4339,8 @@ var PostPurchaseOfferEligibility = z78.discriminatedUnion("status", [
4331
4339
  status: z78.literal("retain")
4332
4340
  }),
4333
4341
  z78.object({ reason: PostPurchaseOfferSkipReason, status: z78.literal("skip") }),
4334
- z78.object({ reason: z78.literal("eligible-sts").optional(), status: z78.literal("unavailable") })
4342
+ /** The classifier always names a reason; it stays optional so a reasonless answer from before still parses. */
4343
+ z78.object({ reason: PostPurchaseOfferUnavailableReason.optional(), status: z78.literal("unavailable") })
4335
4344
  ]);
4336
4345
 
4337
4346
  // src/schema/postPurchaseOfferPresentation.ts
@@ -6770,6 +6779,7 @@ export {
6770
6779
  PostPurchaseOfferEligibilityContext,
6771
6780
  PostPurchaseOfferPresentation,
6772
6781
  PostPurchaseOfferSkipReason,
6782
+ PostPurchaseOfferUnavailableReason,
6773
6783
  PostPurchaseOptions,
6774
6784
  PostPurchaseOrderAttributes,
6775
6785
  PostPurchaseOrderIdSchema,
@@ -3,6 +3,19 @@ export declare const PostPurchaseOfferSkipReason: z.ZodEnum<{
3
3
  empty: "empty";
4
4
  "no-eligible-subscription": "no-eligible-subscription";
5
5
  }>;
6
+ /**
7
+ * Why an Offer could not be classified (REB-25054). `eligible-sts` is an STS candidate classified without the STS
8
+ * opt-in; each other reason names what was unreadable: the caller's context, the merchant's settings, or the
9
+ * catalog (the data source's products, or its metadata overrides). An ordinary product reason (sold out,
10
+ * plan-only, no variants, undeclared options) never answers `unavailable`: that member is ineligible instead.
11
+ */
12
+ export declare const PostPurchaseOfferUnavailableReason: z.ZodEnum<{
13
+ "catalog-unreadable": "catalog-unreadable";
14
+ "context-unreadable": "context-unreadable";
15
+ "eligible-sts": "eligible-sts";
16
+ "settings-unreadable": "settings-unreadable";
17
+ }>;
18
+ export type PostPurchaseOfferUnavailableReason = z.infer<typeof PostPurchaseOfferUnavailableReason>;
6
19
  export declare const PostPurchaseOfferEligibilityContext: z.ZodObject<{
7
20
  excludedProductIds: z.ZodDefault<z.ZodArray<z.ZodNumber>>;
8
21
  originalLineItems: z.ZodArray<z.ZodObject<{
@@ -49,7 +62,12 @@ export declare const PostPurchaseOfferEligibility: z.ZodDiscriminatedUnion<[z.Zo
49
62
  }>;
50
63
  status: z.ZodLiteral<"skip">;
51
64
  }, z.core.$strip>, z.ZodObject<{
52
- reason: z.ZodOptional<z.ZodLiteral<"eligible-sts">>;
65
+ reason: z.ZodOptional<z.ZodEnum<{
66
+ "catalog-unreadable": "catalog-unreadable";
67
+ "context-unreadable": "context-unreadable";
68
+ "eligible-sts": "eligible-sts";
69
+ "settings-unreadable": "settings-unreadable";
70
+ }>>;
53
71
  status: z.ZodLiteral<"unavailable">;
54
72
  }, z.core.$strip>], "status">;
55
73
  export type PostPurchaseOfferEligibility = z.infer<typeof PostPurchaseOfferEligibility>;
@@ -42,6 +42,7 @@ export declare const PostPurchaseProductSource: z.ZodObject<{
42
42
  metafields: z.ZodOptional<z.ZodUnknown>;
43
43
  options: z.ZodOptional<z.ZodUnknown>;
44
44
  productGroupVariants: z.ZodDefault<z.ZodArray<z.ZodUnknown>>;
45
+ requiresSellingPlan: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
45
46
  selectedVariant: z.ZodOptional<z.ZodUnknown>;
46
47
  sellingPlanGroups: z.ZodDefault<z.ZodArray<z.ZodObject<{
47
48
  discountAmount: z.ZodOptional<z.ZodUnknown>;
@@ -6128,6 +6128,8 @@ var Product2 = ProductBase.extend({
6128
6128
  metafields: import_zod43.z.unknown().optional(),
6129
6129
  options: import_zod43.z.unknown().optional(),
6130
6130
  productGroupVariants: import_zod43.z.array(import_zod43.z.unknown()).default([]),
6131
+ // REB-25048: the v2 data source flags a subscription-only product here and sends no selling plan groups
6132
+ requiresSellingPlan: import_zod43.z.boolean().nullish(),
6131
6133
  selectedVariant: import_zod43.z.unknown().optional(),
6132
6134
  sellingPlanGroups: import_zod43.z.array(
6133
6135
  OfferProduct.shape.sellingPlanGroups.unwrap().element.extend({
@@ -6187,7 +6189,14 @@ var readPostPurchaseProductSource = (response, report) => {
6187
6189
  const dropped = [];
6188
6190
  const products = data.flatMap((raw, productIndex) => {
6189
6191
  const parsed = Product3.safeParse(raw);
6190
- if (parsed.success) return [{ product: parsed.data, productIndex }];
6192
+ if (parsed.success) {
6193
+ if (parsed.data.requiresSellingPlan && !parsed.data.sellingPlanGroups.length)
6194
+ drift.push({
6195
+ detail: `Dropped post-purchase product ${parsed.data.id}: data.${productIndex}.requiresSellingPlan`,
6196
+ kind: "product"
6197
+ });
6198
+ return [{ product: parsed.data, productIndex }];
6199
+ }
6191
6200
  const id = Identified.safeParse(raw);
6192
6201
  const issues = parsed.error.issues.map((issue) => ({ ...issue, path: ["data", productIndex, ...issue.path] }));
6193
6202
  const path = issues[0].path.join(".");
@@ -6398,7 +6407,8 @@ var projectPostPurchasePlanCatalog = (normalized, report) => {
6398
6407
  discountPercentage: postPurchasePlanDiscountPercentage(variant2.price, plan.adjustment)
6399
6408
  })),
6400
6409
  productId: product.id,
6401
- requiresSellingPlan: required[0] ?? false,
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,
6402
6412
  variantId: variant2.id
6403
6413
  });
6404
6414
  } catch {
@@ -6951,7 +6961,7 @@ var oneTimeUnavailable = (normalized, allowCollection, quantity3) => {
6951
6961
  return "Only fixed one-time configuration is supported";
6952
6962
  const product = data[0];
6953
6963
  const variant2 = product.variants[0];
6954
- if (product.productGroupVariants.length || product.sellingPlanGroups?.some((group) => group.requiresSellingPlan))
6964
+ if (product.productGroupVariants.length || product.requiresSellingPlan === true || product.sellingPlanGroups?.some((group) => group.requiresSellingPlan))
6955
6965
  return "A one-time variant without product groups or a required selling plan is required";
6956
6966
  if (!postPurchaseVariantAvailable(variant2)) return VARIANT_UNAVAILABLE;
6957
6967
  if (variant2.productId !== product.id) throw new Error("Product variant identity mismatch");
@@ -7491,11 +7501,11 @@ var PostPurchaseMemberPointer = PostPurchaseProgressSchema.shape.members.element
7491
7501
 
7492
7502
  // src/server/normalizePostPurchaseCollection.ts
7493
7503
  var import_zod82 = require("zod");
7504
+ var PostPurchaseProductLimit = import_zod82.z.number().int().positive();
7494
7505
  var normalizePostPurchaseCollection = (settings, response, excludedProductIds, report) => {
7495
7506
  const { dropped, normalized, products } = normalizePostPurchaseSource(settings, response, report);
7496
- const limit = import_zod82.z.number().int().positive();
7497
- limit.parse(normalized.original.productLimit);
7498
- const cap = limit.parse(normalized.effective.productLimit);
7507
+ PostPurchaseProductLimit.parse(normalized.original.productLimit);
7508
+ const cap = PostPurchaseProductLimit.parse(normalized.effective.productLimit);
7499
7509
  const prefix = underPostPurchaseCap(products, cap);
7500
7510
  const unread = underPostPurchaseCap(dropped, cap);
7501
7511
  if (unread.length && !prefix.length) throw new import_zod82.ZodError(unread.flatMap(({ issues }) => issues));
@@ -7507,6 +7517,8 @@ var normalizePostPurchaseCollection = (settings, response, excludedProductIds, r
7507
7517
  };
7508
7518
 
7509
7519
  // src/server/postPurchaseVariantCatalog.ts
7520
+ var PostPurchaseVariantOptionMismatch = class extends Error {
7521
+ };
7510
7522
  var projectPostPurchaseVariantCatalog = (product, settings) => {
7511
7523
  const options2 = PostPurchaseVariantCatalog.shape.options.parse(product.options === void 0 ? [] : product.options).sort((a, b) => a.position - b.position);
7512
7524
  const variants = product.variants.map((variant2) => ({
@@ -7520,7 +7532,7 @@ var projectPostPurchaseVariantCatalog = (product, settings) => {
7520
7532
  throw new Error("Ambiguous post-purchase variant catalog");
7521
7533
  for (const tuple of tuples) {
7522
7534
  if (options2.some(({ position, values: values2 }) => !values2.some((value) => value === tuple[position - 1])) || tuple.some((value, index) => value != null && !options2[index]))
7523
- throw new Error("Variant option tuple does not match the catalog");
7535
+ throw new PostPurchaseVariantOptionMismatch("Variant option tuple does not match the catalog");
7524
7536
  }
7525
7537
  return PostPurchaseVariantCatalog.parse({
7526
7538
  displayMode: settings.productOptions.variantSelectorDisplayMode,
@@ -7543,6 +7555,12 @@ var projectPostPurchaseVariantCatalog = (product, settings) => {
7543
7555
  var import_zod83 = require("zod");
7544
7556
  var selection2 = PreparedPostPurchaseProduct.shape.selection;
7545
7557
  var PostPurchaseOfferSkipReason = import_zod83.z.enum(["empty", "no-eligible-subscription"]);
7558
+ var PostPurchaseOfferUnavailableReason = import_zod83.z.enum([
7559
+ "catalog-unreadable",
7560
+ "context-unreadable",
7561
+ "eligible-sts",
7562
+ "settings-unreadable"
7563
+ ]);
7546
7564
  var PostPurchaseOfferEligibilityContext = import_zod83.z.object({
7547
7565
  excludedProductIds: PostPurchaseExcludedProductIds,
7548
7566
  originalLineItems: import_zod83.z.array(selection2.pick({ productId: true, sellingPlanId: true, variantId: true }))
@@ -7563,7 +7581,8 @@ var PostPurchaseOfferEligibility = import_zod83.z.discriminatedUnion("status", [
7563
7581
  status: import_zod83.z.literal("retain")
7564
7582
  }),
7565
7583
  import_zod83.z.object({ reason: PostPurchaseOfferSkipReason, status: import_zod83.z.literal("skip") }),
7566
- import_zod83.z.object({ reason: import_zod83.z.literal("eligible-sts").optional(), status: import_zod83.z.literal("unavailable") })
7584
+ /** The classifier always names a reason; it stays optional so a reasonless answer from before still parses. */
7585
+ import_zod83.z.object({ reason: PostPurchaseOfferUnavailableReason.optional(), status: import_zod83.z.literal("unavailable") })
7567
7586
  ]);
7568
7587
 
7569
7588
  // src/schema/postPurchaseQuantity.ts
@@ -8047,14 +8066,13 @@ var preparePostPurchaseCollectionView = (widgetId, settings, response, report, r
8047
8066
  };
8048
8067
 
8049
8068
  // src/server/postPurchaseMemberEligibility.ts
8050
- var PostPurchaseInventoryUnavailable = class extends Error {
8051
- };
8052
- var postPurchaseMemberEligibility = (normalized, product, excluded) => {
8069
+ var eligibility = (normalized, product, excluded) => {
8053
8070
  if (product.productGroupVariants.length) {
8054
8071
  const group = readPostPurchaseGroup(product, normalized.effective, excluded);
8072
+ if (!group.entries.length) return void 0;
8055
8073
  return group.entries.map(({ product: option }) => postPurchaseMemberEligibility(normalized, option)).some(Boolean);
8056
8074
  }
8057
- if (!product.variants.length) throw new Error("Unsupported catalog");
8075
+ if (!product.variants.length) return false;
8058
8076
  const variants = projectPostPurchaseVariantCatalog(product, normalized.effective).variants;
8059
8077
  const catalogs = product.variants.map((variant2) => {
8060
8078
  const catalog = projectPostPurchasePlanCatalog({
@@ -8062,23 +8080,31 @@ var postPurchaseMemberEligibility = (normalized, product, excluded) => {
8062
8080
  data: [{ ...product, variants: [variant2] }]
8063
8081
  });
8064
8082
  if (!catalog) throw new Error("Unreadable subscription catalog");
8065
- if (normalized.effective.productType === "one-time" && catalog.requiresSellingPlan)
8066
- throw new Error("Unsupported purchase mode");
8067
8083
  return catalog;
8068
8084
  });
8069
- if (!variants.some((variant2) => variant2.available))
8070
- throw new PostPurchaseInventoryUnavailable("Unavailable inventory");
8071
8085
  return catalogs.some(
8072
8086
  (catalog, index) => variants[index].available && (normalized.effective.productType !== "subscription" && !catalog.requiresSellingPlan || normalized.effective.productType !== "one-time" && catalog.plans.length > 0)
8073
8087
  );
8074
8088
  };
8089
+ var postPurchaseMemberEligibility = (normalized, product, excluded) => {
8090
+ try {
8091
+ return eligibility(normalized, product, excluded);
8092
+ } catch (error) {
8093
+ if (error instanceof PostPurchaseVariantOptionMismatch) return false;
8094
+ throw error;
8095
+ }
8096
+ };
8075
8097
 
8076
8098
  // src/server/classifyPostPurchaseOffer.ts
8077
8099
  var identities = (members) => members.map(({ product, productIndex }) => ({ productId: product.id, productIndex }));
8078
8100
  var classifyPostPurchaseOffer = (settings, response, context, options2) => {
8101
+ let unreadable = "context-unreadable";
8079
8102
  try {
8080
8103
  const trusted = PostPurchaseOfferEligibilityContext.parse(context);
8081
8104
  const { sts: stsMode = false } = PostPurchaseStsOptions.parse(options2 ?? {});
8105
+ unreadable = "settings-unreadable";
8106
+ PostPurchaseProductLimit.parse(PostPurchaseProductSettings.parse(deepCamelCase(settings)).productLimit);
8107
+ unreadable = "catalog-unreadable";
8082
8108
  const { members, normalized } = normalizePostPurchaseCollection(settings, response);
8083
8109
  const configurations = [normalized.original, normalized.effective];
8084
8110
  const stsEnabled = configurations.some((config) => config.upgradeToSubscription.allow);
@@ -8091,33 +8117,28 @@ var classifyPostPurchaseOffer = (settings, response, context, options2) => {
8091
8117
  ))
8092
8118
  return { reason: "eligible-sts", status: "unavailable" };
8093
8119
  if (normalized.effective.productType !== "one-time" || sts) {
8094
- configurations.forEach((config) => PostPurchaseSubscriptionSettings.parse(config.subscription));
8120
+ unreadable = "settings-unreadable";
8121
+ PostPurchaseSubscriptionSettings.parse(normalized.original.subscription);
8122
+ unreadable = "catalog-unreadable";
8123
+ PostPurchaseSubscriptionSettings.parse(normalized.effective.subscription);
8095
8124
  }
8096
8125
  const lines = trusted.originalLineItems;
8097
8126
  const stsMembers = sts ? members.filter(({ product }) => postPurchaseStsLine(normalized, product, lines)) : [];
8098
8127
  if (stsMembers.length) return { members: identities(stsMembers), mode: "sts", status: "retain" };
8099
- const classified = members.map((member) => {
8100
- try {
8101
- return {
8102
- ...member,
8103
- eligible: postPurchaseMemberEligibility(normalized, member.product, trusted.excludedProductIds)
8104
- };
8105
- } catch (error) {
8106
- if (error instanceof PostPurchaseInventoryUnavailable && !member.product.productGroupVariants.length && trusted.excludedProductIds.includes(member.product.id))
8107
- return { ...member, eligible: false };
8108
- throw error;
8109
- }
8110
- });
8128
+ const classified = members.map((member) => ({
8129
+ ...member,
8130
+ eligible: postPurchaseMemberEligibility(normalized, member.product, trusted.excludedProductIds)
8131
+ }));
8111
8132
  if (!members.length) return { reason: "empty", status: "skip" };
8112
8133
  if (stsEnabled) return { reason: "no-eligible-subscription", status: "skip" };
8113
8134
  const candidates = classified.filter(
8114
- ({ product }) => product.productGroupVariants.length ? readPostPurchaseGroup(product, normalized.effective, trusted.excludedProductIds).entries.length > 0 : !trusted.excludedProductIds.includes(product.id)
8135
+ ({ eligible, product }) => product.productGroupVariants.length ? eligible !== void 0 : !trusted.excludedProductIds.includes(product.id)
8115
8136
  );
8116
8137
  if (!candidates.length) return { reason: "empty", status: "skip" };
8117
8138
  const retained = candidates.filter(({ eligible }) => eligible);
8118
8139
  return retained.length ? { members: identities(retained), status: "retain" } : { reason: "no-eligible-subscription", status: "skip" };
8119
8140
  } catch {
8120
- return { status: "unavailable" };
8141
+ return { reason: unreadable, status: "unavailable" };
8121
8142
  }
8122
8143
  };
8123
8144
 
@@ -6055,6 +6055,8 @@ var Product2 = ProductBase.extend({
6055
6055
  metafields: z43.unknown().optional(),
6056
6056
  options: z43.unknown().optional(),
6057
6057
  productGroupVariants: z43.array(z43.unknown()).default([]),
6058
+ // REB-25048: the v2 data source flags a subscription-only product here and sends no selling plan groups
6059
+ requiresSellingPlan: z43.boolean().nullish(),
6058
6060
  selectedVariant: z43.unknown().optional(),
6059
6061
  sellingPlanGroups: z43.array(
6060
6062
  OfferProduct.shape.sellingPlanGroups.unwrap().element.extend({
@@ -6114,7 +6116,14 @@ var readPostPurchaseProductSource = (response, report) => {
6114
6116
  const dropped = [];
6115
6117
  const products = data.flatMap((raw, productIndex) => {
6116
6118
  const parsed = Product3.safeParse(raw);
6117
- if (parsed.success) return [{ product: parsed.data, productIndex }];
6119
+ if (parsed.success) {
6120
+ if (parsed.data.requiresSellingPlan && !parsed.data.sellingPlanGroups.length)
6121
+ drift.push({
6122
+ detail: `Dropped post-purchase product ${parsed.data.id}: data.${productIndex}.requiresSellingPlan`,
6123
+ kind: "product"
6124
+ });
6125
+ return [{ product: parsed.data, productIndex }];
6126
+ }
6118
6127
  const id = Identified.safeParse(raw);
6119
6128
  const issues = parsed.error.issues.map((issue) => ({ ...issue, path: ["data", productIndex, ...issue.path] }));
6120
6129
  const path = issues[0].path.join(".");
@@ -6325,7 +6334,8 @@ var projectPostPurchasePlanCatalog = (normalized, report) => {
6325
6334
  discountPercentage: postPurchasePlanDiscountPercentage(variant2.price, plan.adjustment)
6326
6335
  })),
6327
6336
  productId: product.id,
6328
- requiresSellingPlan: required[0] ?? false,
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,
6329
6339
  variantId: variant2.id
6330
6340
  });
6331
6341
  } catch {
@@ -6878,7 +6888,7 @@ var oneTimeUnavailable = (normalized, allowCollection, quantity3) => {
6878
6888
  return "Only fixed one-time configuration is supported";
6879
6889
  const product = data[0];
6880
6890
  const variant2 = product.variants[0];
6881
- if (product.productGroupVariants.length || product.sellingPlanGroups?.some((group) => group.requiresSellingPlan))
6891
+ if (product.productGroupVariants.length || product.requiresSellingPlan === true || product.sellingPlanGroups?.some((group) => group.requiresSellingPlan))
6882
6892
  return "A one-time variant without product groups or a required selling plan is required";
6883
6893
  if (!postPurchaseVariantAvailable(variant2)) return VARIANT_UNAVAILABLE;
6884
6894
  if (variant2.productId !== product.id) throw new Error("Product variant identity mismatch");
@@ -7418,11 +7428,11 @@ var PostPurchaseMemberPointer = PostPurchaseProgressSchema.shape.members.element
7418
7428
 
7419
7429
  // src/server/normalizePostPurchaseCollection.ts
7420
7430
  import { z as z82, ZodError } from "zod";
7431
+ var PostPurchaseProductLimit = z82.number().int().positive();
7421
7432
  var normalizePostPurchaseCollection = (settings, response, excludedProductIds, report) => {
7422
7433
  const { dropped, normalized, products } = normalizePostPurchaseSource(settings, response, report);
7423
- const limit = z82.number().int().positive();
7424
- limit.parse(normalized.original.productLimit);
7425
- const cap = limit.parse(normalized.effective.productLimit);
7434
+ PostPurchaseProductLimit.parse(normalized.original.productLimit);
7435
+ const cap = PostPurchaseProductLimit.parse(normalized.effective.productLimit);
7426
7436
  const prefix = underPostPurchaseCap(products, cap);
7427
7437
  const unread = underPostPurchaseCap(dropped, cap);
7428
7438
  if (unread.length && !prefix.length) throw new ZodError(unread.flatMap(({ issues }) => issues));
@@ -7434,6 +7444,8 @@ var normalizePostPurchaseCollection = (settings, response, excludedProductIds, r
7434
7444
  };
7435
7445
 
7436
7446
  // src/server/postPurchaseVariantCatalog.ts
7447
+ var PostPurchaseVariantOptionMismatch = class extends Error {
7448
+ };
7437
7449
  var projectPostPurchaseVariantCatalog = (product, settings) => {
7438
7450
  const options2 = PostPurchaseVariantCatalog.shape.options.parse(product.options === void 0 ? [] : product.options).sort((a, b) => a.position - b.position);
7439
7451
  const variants = product.variants.map((variant2) => ({
@@ -7447,7 +7459,7 @@ var projectPostPurchaseVariantCatalog = (product, settings) => {
7447
7459
  throw new Error("Ambiguous post-purchase variant catalog");
7448
7460
  for (const tuple of tuples) {
7449
7461
  if (options2.some(({ position, values: values2 }) => !values2.some((value) => value === tuple[position - 1])) || tuple.some((value, index) => value != null && !options2[index]))
7450
- throw new Error("Variant option tuple does not match the catalog");
7462
+ throw new PostPurchaseVariantOptionMismatch("Variant option tuple does not match the catalog");
7451
7463
  }
7452
7464
  return PostPurchaseVariantCatalog.parse({
7453
7465
  displayMode: settings.productOptions.variantSelectorDisplayMode,
@@ -7470,6 +7482,12 @@ var projectPostPurchaseVariantCatalog = (product, settings) => {
7470
7482
  import { z as z83 } from "zod";
7471
7483
  var selection2 = PreparedPostPurchaseProduct.shape.selection;
7472
7484
  var PostPurchaseOfferSkipReason = z83.enum(["empty", "no-eligible-subscription"]);
7485
+ var PostPurchaseOfferUnavailableReason = z83.enum([
7486
+ "catalog-unreadable",
7487
+ "context-unreadable",
7488
+ "eligible-sts",
7489
+ "settings-unreadable"
7490
+ ]);
7473
7491
  var PostPurchaseOfferEligibilityContext = z83.object({
7474
7492
  excludedProductIds: PostPurchaseExcludedProductIds,
7475
7493
  originalLineItems: z83.array(selection2.pick({ productId: true, sellingPlanId: true, variantId: true }))
@@ -7490,7 +7508,8 @@ var PostPurchaseOfferEligibility = z83.discriminatedUnion("status", [
7490
7508
  status: z83.literal("retain")
7491
7509
  }),
7492
7510
  z83.object({ reason: PostPurchaseOfferSkipReason, status: z83.literal("skip") }),
7493
- z83.object({ reason: z83.literal("eligible-sts").optional(), status: z83.literal("unavailable") })
7511
+ /** The classifier always names a reason; it stays optional so a reasonless answer from before still parses. */
7512
+ z83.object({ reason: PostPurchaseOfferUnavailableReason.optional(), status: z83.literal("unavailable") })
7494
7513
  ]);
7495
7514
 
7496
7515
  // src/schema/postPurchaseQuantity.ts
@@ -7974,14 +7993,13 @@ var preparePostPurchaseCollectionView = (widgetId, settings, response, report, r
7974
7993
  };
7975
7994
 
7976
7995
  // src/server/postPurchaseMemberEligibility.ts
7977
- var PostPurchaseInventoryUnavailable = class extends Error {
7978
- };
7979
- var postPurchaseMemberEligibility = (normalized, product, excluded) => {
7996
+ var eligibility = (normalized, product, excluded) => {
7980
7997
  if (product.productGroupVariants.length) {
7981
7998
  const group = readPostPurchaseGroup(product, normalized.effective, excluded);
7999
+ if (!group.entries.length) return void 0;
7982
8000
  return group.entries.map(({ product: option }) => postPurchaseMemberEligibility(normalized, option)).some(Boolean);
7983
8001
  }
7984
- if (!product.variants.length) throw new Error("Unsupported catalog");
8002
+ if (!product.variants.length) return false;
7985
8003
  const variants = projectPostPurchaseVariantCatalog(product, normalized.effective).variants;
7986
8004
  const catalogs = product.variants.map((variant2) => {
7987
8005
  const catalog = projectPostPurchasePlanCatalog({
@@ -7989,23 +8007,31 @@ var postPurchaseMemberEligibility = (normalized, product, excluded) => {
7989
8007
  data: [{ ...product, variants: [variant2] }]
7990
8008
  });
7991
8009
  if (!catalog) throw new Error("Unreadable subscription catalog");
7992
- if (normalized.effective.productType === "one-time" && catalog.requiresSellingPlan)
7993
- throw new Error("Unsupported purchase mode");
7994
8010
  return catalog;
7995
8011
  });
7996
- if (!variants.some((variant2) => variant2.available))
7997
- throw new PostPurchaseInventoryUnavailable("Unavailable inventory");
7998
8012
  return catalogs.some(
7999
8013
  (catalog, index) => variants[index].available && (normalized.effective.productType !== "subscription" && !catalog.requiresSellingPlan || normalized.effective.productType !== "one-time" && catalog.plans.length > 0)
8000
8014
  );
8001
8015
  };
8016
+ var postPurchaseMemberEligibility = (normalized, product, excluded) => {
8017
+ try {
8018
+ return eligibility(normalized, product, excluded);
8019
+ } catch (error) {
8020
+ if (error instanceof PostPurchaseVariantOptionMismatch) return false;
8021
+ throw error;
8022
+ }
8023
+ };
8002
8024
 
8003
8025
  // src/server/classifyPostPurchaseOffer.ts
8004
8026
  var identities = (members) => members.map(({ product, productIndex }) => ({ productId: product.id, productIndex }));
8005
8027
  var classifyPostPurchaseOffer = (settings, response, context, options2) => {
8028
+ let unreadable = "context-unreadable";
8006
8029
  try {
8007
8030
  const trusted = PostPurchaseOfferEligibilityContext.parse(context);
8008
8031
  const { sts: stsMode = false } = PostPurchaseStsOptions.parse(options2 ?? {});
8032
+ unreadable = "settings-unreadable";
8033
+ PostPurchaseProductLimit.parse(PostPurchaseProductSettings.parse(deepCamelCase(settings)).productLimit);
8034
+ unreadable = "catalog-unreadable";
8009
8035
  const { members, normalized } = normalizePostPurchaseCollection(settings, response);
8010
8036
  const configurations = [normalized.original, normalized.effective];
8011
8037
  const stsEnabled = configurations.some((config) => config.upgradeToSubscription.allow);
@@ -8018,33 +8044,28 @@ var classifyPostPurchaseOffer = (settings, response, context, options2) => {
8018
8044
  ))
8019
8045
  return { reason: "eligible-sts", status: "unavailable" };
8020
8046
  if (normalized.effective.productType !== "one-time" || sts) {
8021
- configurations.forEach((config) => PostPurchaseSubscriptionSettings.parse(config.subscription));
8047
+ unreadable = "settings-unreadable";
8048
+ PostPurchaseSubscriptionSettings.parse(normalized.original.subscription);
8049
+ unreadable = "catalog-unreadable";
8050
+ PostPurchaseSubscriptionSettings.parse(normalized.effective.subscription);
8022
8051
  }
8023
8052
  const lines = trusted.originalLineItems;
8024
8053
  const stsMembers = sts ? members.filter(({ product }) => postPurchaseStsLine(normalized, product, lines)) : [];
8025
8054
  if (stsMembers.length) return { members: identities(stsMembers), mode: "sts", status: "retain" };
8026
- const classified = members.map((member) => {
8027
- try {
8028
- return {
8029
- ...member,
8030
- eligible: postPurchaseMemberEligibility(normalized, member.product, trusted.excludedProductIds)
8031
- };
8032
- } catch (error) {
8033
- if (error instanceof PostPurchaseInventoryUnavailable && !member.product.productGroupVariants.length && trusted.excludedProductIds.includes(member.product.id))
8034
- return { ...member, eligible: false };
8035
- throw error;
8036
- }
8037
- });
8055
+ const classified = members.map((member) => ({
8056
+ ...member,
8057
+ eligible: postPurchaseMemberEligibility(normalized, member.product, trusted.excludedProductIds)
8058
+ }));
8038
8059
  if (!members.length) return { reason: "empty", status: "skip" };
8039
8060
  if (stsEnabled) return { reason: "no-eligible-subscription", status: "skip" };
8040
8061
  const candidates = classified.filter(
8041
- ({ product }) => product.productGroupVariants.length ? readPostPurchaseGroup(product, normalized.effective, trusted.excludedProductIds).entries.length > 0 : !trusted.excludedProductIds.includes(product.id)
8062
+ ({ eligible, product }) => product.productGroupVariants.length ? eligible !== void 0 : !trusted.excludedProductIds.includes(product.id)
8042
8063
  );
8043
8064
  if (!candidates.length) return { reason: "empty", status: "skip" };
8044
8065
  const retained = candidates.filter(({ eligible }) => eligible);
8045
8066
  return retained.length ? { members: identities(retained), status: "retain" } : { reason: "no-eligible-subscription", status: "skip" };
8046
8067
  } catch {
8047
- return { status: "unavailable" };
8068
+ return { reason: unreadable, status: "unavailable" };
8048
8069
  }
8049
8070
  };
8050
8071
 
@@ -1,5 +1,8 @@
1
+ import { z } from 'zod';
1
2
  import { PostPurchaseExcludedProductIds } from '../schema/postPurchaseProductCollectionView';
2
3
  import type { Reporter } from '../server/shared';
4
+ /** A collection's `productLimit`, original or effective: a positive whole number. */
5
+ export declare const PostPurchaseProductLimit: z.ZodNumber;
3
6
  /**
4
7
  * Preserve the original capped positions before applying product-wide exclusions. `productLimit` caps raw
5
8
  * positions, and a member's `productIndex` is its raw position: a product the schema dropped leaves its slot
@@ -51,6 +54,7 @@ export declare const normalizePostPurchaseCollection: (settings: unknown, respon
51
54
  } | undefined;
52
55
  metafields?: unknown;
53
56
  options?: unknown;
57
+ requiresSellingPlan?: boolean | null | undefined;
54
58
  selectedVariant?: unknown;
55
59
  };
56
60
  productIndex: number;
@@ -96,6 +100,7 @@ export declare const normalizePostPurchaseCollection: (settings: unknown, respon
96
100
  } | undefined;
97
101
  metafields?: unknown;
98
102
  options?: unknown;
103
+ requiresSellingPlan?: boolean | null | undefined;
99
104
  selectedVariant?: unknown;
100
105
  }[];
101
106
  effective: {
@@ -52,6 +52,7 @@ export declare const normalizePostPurchaseSource: (settings: unknown, response:
52
52
  } | undefined;
53
53
  metafields?: unknown;
54
54
  options?: unknown;
55
+ requiresSellingPlan?: boolean | null | undefined;
55
56
  selectedVariant?: unknown;
56
57
  }[];
57
58
  effective: {
@@ -254,6 +255,7 @@ export declare const normalizePostPurchaseSource: (settings: unknown, response:
254
255
  } | undefined;
255
256
  metafields?: unknown;
256
257
  options?: unknown;
258
+ requiresSellingPlan?: boolean | null | undefined;
257
259
  selectedVariant?: unknown;
258
260
  };
259
261
  productIndex: number;
@@ -317,6 +319,7 @@ export declare const normalizePostPurchaseProduct: (settings: unknown, response:
317
319
  } | undefined;
318
320
  metafields?: unknown;
319
321
  options?: unknown;
322
+ requiresSellingPlan?: boolean | null | undefined;
320
323
  selectedVariant?: unknown;
321
324
  }[];
322
325
  effective: {
@@ -1,7 +1,13 @@
1
1
  import type { PostPurchaseExcludedProductIds } from '../schema/postPurchaseProductCollectionView';
2
2
  import type { normalizePostPurchaseProduct } from '../server/normalizePostPurchaseProduct';
3
- /** The member's catalog is readable, but no variant is in stock. */
4
- export declare class PostPurchaseInventoryUnavailable extends Error {
5
- }
6
- /** Catalog facts only: preparation still owns defaults, quantity, terms, and presentation. */
7
- export declare const postPurchaseMemberEligibility: (normalized: ReturnType<typeof normalizePostPurchaseProduct>, product: ReturnType<typeof normalizePostPurchaseProduct>["data"][number], excluded?: PostPurchaseExcludedProductIds) => boolean;
3
+ type Normalized = ReturnType<typeof normalizePostPurchaseProduct>;
4
+ /**
5
+ * Catalog facts only: preparation still owns defaults, quantity, terms, and presentation. `false` is an ordinary
6
+ * product reason (REB-25054): no available variant, plan-only on a one-time Offer, or option values the product
7
+ * does not declare, for the member or, for a group, every sibling. A sibling with undeclared options leaves its
8
+ * whole group ineligible, as preparation cannot read that group either. `undefined` is a group whose every
9
+ * sibling is excluded, so nothing is left to offer; the group is read before exclusions apply, though, so an
10
+ * excluded sibling with undeclared options still answers `false`. A throw is unreadable data.
11
+ */
12
+ export declare const postPurchaseMemberEligibility: (normalized: Normalized, product: Normalized["data"][number], excluded?: PostPurchaseExcludedProductIds) => boolean | undefined;
13
+ export {};
@@ -60,6 +60,7 @@ export declare const postPurchaseStsLine: <T extends Line>(normalized: Normalize
60
60
  } | undefined;
61
61
  metafields?: unknown;
62
62
  options?: unknown;
63
+ requiresSellingPlan?: boolean | null | undefined;
63
64
  selectedVariant?: unknown;
64
65
  }[];
65
66
  line: T;
@@ -1,3 +1,9 @@
1
1
  import { PostPurchaseVariantCatalog } from '../schema/postPurchaseVariantCatalog';
2
2
  import type { normalizePostPurchaseProduct } from '../server/normalizePostPurchaseProduct';
3
+ /**
4
+ * A variant carries an option value its product does not declare (an optionless product, say): the merchandise is
5
+ * readable but cannot be offered as it stands, so member eligibility leaves it out rather than failing the Offer.
6
+ */
7
+ export declare class PostPurchaseVariantOptionMismatch extends Error {
8
+ }
3
9
  export declare const projectPostPurchaseVariantCatalog: (product: ReturnType<typeof normalizePostPurchaseProduct>["data"][number], settings: ReturnType<typeof normalizePostPurchaseProduct>["effective"]) => PostPurchaseVariantCatalog;
@@ -651,6 +651,7 @@ export declare const prepareMember: (widgetId: number, settings: unknown, respon
651
651
  } | undefined;
652
652
  metafields?: unknown;
653
653
  options?: unknown;
654
+ requiresSellingPlan?: boolean | null | undefined;
654
655
  selectedVariant?: unknown;
655
656
  }[];
656
657
  effective: {
@@ -1468,6 +1469,7 @@ export declare const prepareMember: (widgetId: number, settings: unknown, respon
1468
1469
  } | undefined;
1469
1470
  metafields?: unknown;
1470
1471
  options?: unknown;
1472
+ requiresSellingPlan?: boolean | null | undefined;
1471
1473
  selectedVariant?: unknown;
1472
1474
  }[];
1473
1475
  effective: {
@@ -656,6 +656,7 @@ export declare const preparePostPurchaseStsMember: (widgetId: number, settings:
656
656
  } | undefined;
657
657
  metafields?: unknown;
658
658
  options?: unknown;
659
+ requiresSellingPlan?: boolean | null | undefined;
659
660
  selectedVariant?: unknown;
660
661
  }[];
661
662
  effective: {
@@ -159,6 +159,7 @@ export declare const projectPostPurchaseProduct: ({ data, effective, metadata, o
159
159
  } | undefined;
160
160
  metafields?: unknown;
161
161
  options?: unknown;
162
+ requiresSellingPlan?: boolean | null | undefined;
162
163
  selectedVariant?: unknown;
163
164
  };
164
165
  selection: {
@@ -75,6 +75,7 @@ export declare const readPostPurchaseGroup: (product: ReturnType<typeof normaliz
75
75
  productQuantity?: number | undefined;
76
76
  } | undefined;
77
77
  metafields?: unknown;
78
+ requiresSellingPlan?: boolean | null | undefined;
78
79
  selectedVariant?: unknown;
79
80
  };
80
81
  variantCatalog: {
@@ -81,6 +81,7 @@ export declare const readPostPurchaseGroupSelection: (settings: unknown, respons
81
81
  productQuantity?: number | undefined;
82
82
  } | undefined;
83
83
  metafields?: unknown;
84
+ requiresSellingPlan?: boolean | null | undefined;
84
85
  selectedVariant?: unknown;
85
86
  }[];
86
87
  metadata: {
@@ -9,6 +9,10 @@ import type { Reporter } from '../server/shared';
9
9
  * one request (a re-parse or clone per projection reports the drops again). The envelope (an object, `data`
10
10
  * an array, readable `metadata`) still throws.
11
11
  *
12
+ * A product that parses but requires a selling plan on itself with no selling plan groups (a v2 subscription-only
13
+ * product, REB-25048) is kept for eligibility to drop, and reported here as the same once-per-response drift,
14
+ * naming its `requiresSellingPlan` path: it has no plan to sell it with and may never be sold one-time.
15
+ *
12
16
  * Every survivor keeps `productIndex`, its position in the raw `data` array: Decision, Render and Offer
13
17
  * members identify a product by that index, so a drop never renumbers the products after it. `dropped` keeps
14
18
  * each drop's `productIndex` and its zod issues with paths from the source root, so a caller can fail a source
@@ -63,6 +67,7 @@ export declare const readPostPurchaseProductSource: (response: unknown, report?:
63
67
  } | undefined;
64
68
  metafields?: unknown;
65
69
  options?: unknown;
70
+ requiresSellingPlan?: boolean | null | undefined;
66
71
  selectedVariant?: unknown;
67
72
  };
68
73
  productIndex: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebuy/rebuy",
3
- "version": "3.56.1",
3
+ "version": "3.56.3",
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.",