@rebuy/rebuy 3.56.2 → 3.57.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.
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,
@@ -4792,6 +4793,12 @@ var OfferPreparationResponseSchema = import_zod77.z.object({
4792
4793
  var import_zod78 = require("zod");
4793
4794
  var selection2 = PreparedPostPurchaseProduct.shape.selection;
4794
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
+ ]);
4795
4802
  var PostPurchaseOfferEligibilityContext = import_zod78.z.object({
4796
4803
  excludedProductIds: PostPurchaseExcludedProductIds,
4797
4804
  originalLineItems: import_zod78.z.array(selection2.pick({ productId: true, sellingPlanId: true, variantId: true }))
@@ -4812,7 +4819,8 @@ var PostPurchaseOfferEligibility = import_zod78.z.discriminatedUnion("status", [
4812
4819
  status: import_zod78.z.literal("retain")
4813
4820
  }),
4814
4821
  import_zod78.z.object({ reason: PostPurchaseOfferSkipReason, status: import_zod78.z.literal("skip") }),
4815
- 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") })
4816
4824
  ]);
4817
4825
 
4818
4826
  // src/schema/postPurchaseOfferPresentation.ts
package/dist/index.mjs CHANGED
@@ -4313,6 +4313,12 @@ var OfferPreparationResponseSchema = z77.object({
4313
4313
  import { z as z78 } from "zod";
4314
4314
  var selection2 = PreparedPostPurchaseProduct.shape.selection;
4315
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
+ ]);
4316
4322
  var PostPurchaseOfferEligibilityContext = z78.object({
4317
4323
  excludedProductIds: PostPurchaseExcludedProductIds,
4318
4324
  originalLineItems: z78.array(selection2.pick({ productId: true, sellingPlanId: true, variantId: true }))
@@ -4333,7 +4339,8 @@ var PostPurchaseOfferEligibility = z78.discriminatedUnion("status", [
4333
4339
  status: z78.literal("retain")
4334
4340
  }),
4335
4341
  z78.object({ reason: PostPurchaseOfferSkipReason, status: z78.literal("skip") }),
4336
- 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") })
4337
4344
  ]);
4338
4345
 
4339
4346
  // src/schema/postPurchaseOfferPresentation.ts
@@ -6772,6 +6779,7 @@ export {
6772
6779
  PostPurchaseOfferEligibilityContext,
6773
6780
  PostPurchaseOfferPresentation,
6774
6781
  PostPurchaseOfferSkipReason,
6782
+ PostPurchaseOfferUnavailableReason,
6775
6783
  PostPurchaseOptions,
6776
6784
  PostPurchaseOrderAttributes,
6777
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>;
@@ -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;
@@ -6172,17 +6172,30 @@ 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/readPostPurchaseProductSource.ts
6196
+ var Envelope = PostPurchaseProductSource.extend({ data: import_zod45.z.array(import_zod45.z.unknown()) });
6197
+ var Product3 = PostPurchaseProductSource.shape.data.element;
6198
+ var Identified = import_zod45.z.object({ id: PostPurchaseProductId });
6186
6199
  var readPostPurchaseProductSource = (response, report) => {
6187
6200
  const { data, metadata } = Envelope.parse(deepCamelCase(response));
6188
6201
  const drift = [];
@@ -6190,6 +6203,7 @@ var readPostPurchaseProductSource = (response, report) => {
6190
6203
  const products = data.flatMap((raw, productIndex) => {
6191
6204
  const parsed = Product3.safeParse(raw);
6192
6205
  if (parsed.success) {
6206
+ rememberPostPurchaseProductSource(parsed.data, response, productIndex);
6193
6207
  if (parsed.data.requiresSellingPlan && !parsed.data.sellingPlanGroups.length)
6194
6208
  drift.push({
6195
6209
  detail: `Dropped post-purchase product ${parsed.data.id}: data.${productIndex}.requiresSellingPlan`,
@@ -6207,7 +6221,7 @@ var readPostPurchaseProductSource = (response, report) => {
6207
6221
  });
6208
6222
  return [];
6209
6223
  });
6210
- if (report && drift.length && firstHearing(response, report)) drift.forEach((event) => report(event));
6224
+ if (report && drift.length) reportOncePerResponse(response, report, drift);
6211
6225
  return { dropped, metadata, products };
6212
6226
  };
6213
6227
 
@@ -6353,7 +6367,8 @@ var Groups = import_zod52.z.array(
6353
6367
  sellingPlans: import_zod52.z.unknown()
6354
6368
  })
6355
6369
  );
6356
- var Allocations = import_zod52.z.array(import_zod52.z.object({ sellingPlanGroupId: import_zod52.z.string().min(1) })).default([]);
6370
+ var PostPurchasePlanAllocations = import_zod52.z.array(import_zod52.z.object({ sellingPlanGroupId: import_zod52.z.string().min(1) }));
6371
+ var Allocations = PostPurchasePlanAllocations.default([]);
6357
6372
  var Plans = import_zod52.z.array(
6358
6373
  PostPurchasePlanOption.pick({ id: true, name: true }).extend({
6359
6374
  options: import_zod52.z.array(import_zod52.z.object({ value: import_zod52.z.string().min(1) })).min(1),
@@ -7501,11 +7516,11 @@ var PostPurchaseMemberPointer = PostPurchaseProgressSchema.shape.members.element
7501
7516
 
7502
7517
  // src/server/normalizePostPurchaseCollection.ts
7503
7518
  var import_zod82 = require("zod");
7519
+ var PostPurchaseProductLimit = import_zod82.z.number().int().positive();
7504
7520
  var normalizePostPurchaseCollection = (settings, response, excludedProductIds, report) => {
7505
7521
  const { dropped, normalized, products } = normalizePostPurchaseSource(settings, response, report);
7506
- const limit = import_zod82.z.number().int().positive();
7507
- limit.parse(normalized.original.productLimit);
7508
- const cap = limit.parse(normalized.effective.productLimit);
7522
+ PostPurchaseProductLimit.parse(normalized.original.productLimit);
7523
+ const cap = PostPurchaseProductLimit.parse(normalized.effective.productLimit);
7509
7524
  const prefix = underPostPurchaseCap(products, cap);
7510
7525
  const unread = underPostPurchaseCap(dropped, cap);
7511
7526
  if (unread.length && !prefix.length) throw new import_zod82.ZodError(unread.flatMap(({ issues }) => issues));
@@ -7517,6 +7532,8 @@ var normalizePostPurchaseCollection = (settings, response, excludedProductIds, r
7517
7532
  };
7518
7533
 
7519
7534
  // src/server/postPurchaseVariantCatalog.ts
7535
+ var PostPurchaseVariantOptionMismatch = class extends Error {
7536
+ };
7520
7537
  var projectPostPurchaseVariantCatalog = (product, settings) => {
7521
7538
  const options2 = PostPurchaseVariantCatalog.shape.options.parse(product.options === void 0 ? [] : product.options).sort((a, b) => a.position - b.position);
7522
7539
  const variants = product.variants.map((variant2) => ({
@@ -7530,7 +7547,7 @@ var projectPostPurchaseVariantCatalog = (product, settings) => {
7530
7547
  throw new Error("Ambiguous post-purchase variant catalog");
7531
7548
  for (const tuple of tuples) {
7532
7549
  if (options2.some(({ position, values: values2 }) => !values2.some((value) => value === tuple[position - 1])) || tuple.some((value, index) => value != null && !options2[index]))
7533
- throw new Error("Variant option tuple does not match the catalog");
7550
+ throw new PostPurchaseVariantOptionMismatch("Variant option tuple does not match the catalog");
7534
7551
  }
7535
7552
  return PostPurchaseVariantCatalog.parse({
7536
7553
  displayMode: settings.productOptions.variantSelectorDisplayMode,
@@ -7553,6 +7570,12 @@ var projectPostPurchaseVariantCatalog = (product, settings) => {
7553
7570
  var import_zod83 = require("zod");
7554
7571
  var selection2 = PreparedPostPurchaseProduct.shape.selection;
7555
7572
  var PostPurchaseOfferSkipReason = import_zod83.z.enum(["empty", "no-eligible-subscription"]);
7573
+ var PostPurchaseOfferUnavailableReason = import_zod83.z.enum([
7574
+ "catalog-unreadable",
7575
+ "context-unreadable",
7576
+ "eligible-sts",
7577
+ "settings-unreadable"
7578
+ ]);
7556
7579
  var PostPurchaseOfferEligibilityContext = import_zod83.z.object({
7557
7580
  excludedProductIds: PostPurchaseExcludedProductIds,
7558
7581
  originalLineItems: import_zod83.z.array(selection2.pick({ productId: true, sellingPlanId: true, variantId: true }))
@@ -7573,7 +7596,8 @@ var PostPurchaseOfferEligibility = import_zod83.z.discriminatedUnion("status", [
7573
7596
  status: import_zod83.z.literal("retain")
7574
7597
  }),
7575
7598
  import_zod83.z.object({ reason: PostPurchaseOfferSkipReason, status: import_zod83.z.literal("skip") }),
7576
- import_zod83.z.object({ reason: import_zod83.z.literal("eligible-sts").optional(), status: import_zod83.z.literal("unavailable") })
7599
+ /** The classifier always names a reason; it stays optional so a reasonless answer from before still parses. */
7600
+ import_zod83.z.object({ reason: PostPurchaseOfferUnavailableReason.optional(), status: import_zod83.z.literal("unavailable") })
7577
7601
  ]);
7578
7602
 
7579
7603
  // src/schema/postPurchaseQuantity.ts
@@ -7615,18 +7639,30 @@ var readPostPurchaseQuantity = ({
7615
7639
 
7616
7640
  // src/server/postPurchaseSts.ts
7617
7641
  var stsAllowed = ({ effective, original }) => [original, effective].some((config) => config.upgradeToSubscription.allow);
7618
- var postPurchaseStsLine = (normalized, product, lines, variantId) => {
7642
+ var reportUnreadableStsLine = (product, field, report) => {
7643
+ const source2 = postPurchaseProductSourceOf(product);
7644
+ const path = source2 ? `data.${source2.productIndex}.${field}` : field;
7645
+ const detail = `Unreadable post-purchase STS product ${product.id}: ${path}`;
7646
+ reportOncePerResponse(source2?.response ?? product, report, [{ detail, kind: "product" }]);
7647
+ };
7648
+ var postPurchaseStsLine = (normalized, product, lines, variantId, report) => {
7619
7649
  if (!product.sellingPlanGroups?.length || product.productGroupVariants.length) return void 0;
7620
- for (const variant2 of product.variants) {
7650
+ const modern = typeof product.sellingPlanGroups[0].id !== "number";
7651
+ for (const [index, variant2] of product.variants.entries()) {
7621
7652
  const matched = lines.filter((line) => line.sellingPlanId === void 0 && line.variantId === variant2.id);
7622
7653
  const data = [{ ...product, variants: [variant2] }];
7623
- if ((variantId === void 0 || variant2.id === variantId) && matched.length === 1 && postPurchaseVariantAvailable(variant2) && projectPostPurchasePlanCatalog({ ...normalized, data })?.plans.length)
7624
- return { data, line: matched[0] };
7654
+ if ((variantId === void 0 || variant2.id === variantId) && matched.length === 1 && postPurchaseVariantAvailable(variant2)) {
7655
+ const unread = [];
7656
+ if (projectPostPurchasePlanCatalog({ ...normalized, data }, (event) => unread.push(event))?.plans.length)
7657
+ return { data, line: matched[0] };
7658
+ const field = modern && !PostPurchasePlanAllocations.safeParse(variant2.sellingPlanAllocations).success ? `variants.${index}.sellingPlanAllocations` : unread.length ? "sellingPlanGroups" : void 0;
7659
+ if (report && field) reportUnreadableStsLine(product, field, report);
7660
+ }
7625
7661
  }
7626
7662
  };
7627
7663
  var preparePostPurchaseStsView = (widgetId, normalized, product, context, requested, report) => {
7628
7664
  const { originalLineItems } = PostPurchaseStsContext.parse(context);
7629
- const found = postPurchaseStsLine(normalized, product, originalLineItems, requested.variantId);
7665
+ const found = postPurchaseStsLine(normalized, product, originalLineItems, requested.variantId, report);
7630
7666
  if (!found) return null;
7631
7667
  const whole = found.line.quantity;
7632
7668
  if (requested.quantity !== void 0 && requested.quantity !== whole)
@@ -8057,14 +8093,13 @@ var preparePostPurchaseCollectionView = (widgetId, settings, response, report, r
8057
8093
  };
8058
8094
 
8059
8095
  // src/server/postPurchaseMemberEligibility.ts
8060
- var PostPurchaseInventoryUnavailable = class extends Error {
8061
- };
8062
- var postPurchaseMemberEligibility = (normalized, product, excluded) => {
8096
+ var eligibility = (normalized, product, excluded) => {
8063
8097
  if (product.productGroupVariants.length) {
8064
8098
  const group = readPostPurchaseGroup(product, normalized.effective, excluded);
8099
+ if (!group.entries.length) return void 0;
8065
8100
  return group.entries.map(({ product: option }) => postPurchaseMemberEligibility(normalized, option)).some(Boolean);
8066
8101
  }
8067
- if (!product.variants.length) throw new Error("Unsupported catalog");
8102
+ if (!product.variants.length) return false;
8068
8103
  const variants = projectPostPurchaseVariantCatalog(product, normalized.effective).variants;
8069
8104
  const catalogs = product.variants.map((variant2) => {
8070
8105
  const catalog = projectPostPurchasePlanCatalog({
@@ -8072,23 +8107,31 @@ var postPurchaseMemberEligibility = (normalized, product, excluded) => {
8072
8107
  data: [{ ...product, variants: [variant2] }]
8073
8108
  });
8074
8109
  if (!catalog) throw new Error("Unreadable subscription catalog");
8075
- if (normalized.effective.productType === "one-time" && product.sellingPlanGroups.some((group) => group.requiresSellingPlan))
8076
- throw new Error("Unsupported purchase mode");
8077
8110
  return catalog;
8078
8111
  });
8079
- if (!variants.some((variant2) => variant2.available))
8080
- throw new PostPurchaseInventoryUnavailable("Unavailable inventory");
8081
8112
  return catalogs.some(
8082
8113
  (catalog, index) => variants[index].available && (normalized.effective.productType !== "subscription" && !catalog.requiresSellingPlan || normalized.effective.productType !== "one-time" && catalog.plans.length > 0)
8083
8114
  );
8084
8115
  };
8116
+ var postPurchaseMemberEligibility = (normalized, product, excluded) => {
8117
+ try {
8118
+ return eligibility(normalized, product, excluded);
8119
+ } catch (error) {
8120
+ if (error instanceof PostPurchaseVariantOptionMismatch) return false;
8121
+ throw error;
8122
+ }
8123
+ };
8085
8124
 
8086
8125
  // src/server/classifyPostPurchaseOffer.ts
8087
8126
  var identities = (members) => members.map(({ product, productIndex }) => ({ productId: product.id, productIndex }));
8088
- var classifyPostPurchaseOffer = (settings, response, context, options2) => {
8127
+ var classifyPostPurchaseOffer = (settings, response, context, options2, report) => {
8128
+ let unreadable = "context-unreadable";
8089
8129
  try {
8090
8130
  const trusted = PostPurchaseOfferEligibilityContext.parse(context);
8091
8131
  const { sts: stsMode = false } = PostPurchaseStsOptions.parse(options2 ?? {});
8132
+ unreadable = "settings-unreadable";
8133
+ PostPurchaseProductLimit.parse(PostPurchaseProductSettings.parse(deepCamelCase(settings)).productLimit);
8134
+ unreadable = "catalog-unreadable";
8092
8135
  const { members, normalized } = normalizePostPurchaseCollection(settings, response);
8093
8136
  const configurations = [normalized.original, normalized.effective];
8094
8137
  const stsEnabled = configurations.some((config) => config.upgradeToSubscription.allow);
@@ -8101,33 +8144,28 @@ var classifyPostPurchaseOffer = (settings, response, context, options2) => {
8101
8144
  ))
8102
8145
  return { reason: "eligible-sts", status: "unavailable" };
8103
8146
  if (normalized.effective.productType !== "one-time" || sts) {
8104
- configurations.forEach((config) => PostPurchaseSubscriptionSettings.parse(config.subscription));
8147
+ unreadable = "settings-unreadable";
8148
+ PostPurchaseSubscriptionSettings.parse(normalized.original.subscription);
8149
+ unreadable = "catalog-unreadable";
8150
+ PostPurchaseSubscriptionSettings.parse(normalized.effective.subscription);
8105
8151
  }
8106
8152
  const lines = trusted.originalLineItems;
8107
- const stsMembers = sts ? members.filter(({ product }) => postPurchaseStsLine(normalized, product, lines)) : [];
8153
+ const stsMembers = sts ? members.filter(({ product }) => postPurchaseStsLine(normalized, product, lines, void 0, report)) : [];
8108
8154
  if (stsMembers.length) return { members: identities(stsMembers), mode: "sts", status: "retain" };
8109
- const classified = members.map((member) => {
8110
- try {
8111
- return {
8112
- ...member,
8113
- eligible: postPurchaseMemberEligibility(normalized, member.product, trusted.excludedProductIds)
8114
- };
8115
- } catch (error) {
8116
- if (error instanceof PostPurchaseInventoryUnavailable && !member.product.productGroupVariants.length && trusted.excludedProductIds.includes(member.product.id))
8117
- return { ...member, eligible: false };
8118
- throw error;
8119
- }
8120
- });
8155
+ const classified = members.map((member) => ({
8156
+ ...member,
8157
+ eligible: postPurchaseMemberEligibility(normalized, member.product, trusted.excludedProductIds)
8158
+ }));
8121
8159
  if (!members.length) return { reason: "empty", status: "skip" };
8122
8160
  if (stsEnabled) return { reason: "no-eligible-subscription", status: "skip" };
8123
8161
  const candidates = classified.filter(
8124
- ({ product }) => product.productGroupVariants.length ? readPostPurchaseGroup(product, normalized.effective, trusted.excludedProductIds).entries.length > 0 : !trusted.excludedProductIds.includes(product.id)
8162
+ ({ eligible, product }) => product.productGroupVariants.length ? eligible !== void 0 : !trusted.excludedProductIds.includes(product.id)
8125
8163
  );
8126
8164
  if (!candidates.length) return { reason: "empty", status: "skip" };
8127
8165
  const retained = candidates.filter(({ eligible }) => eligible);
8128
8166
  return retained.length ? { members: identities(retained), status: "retain" } : { reason: "no-eligible-subscription", status: "skip" };
8129
8167
  } catch {
8130
- return { status: "unavailable" };
8168
+ return { reason: unreadable, status: "unavailable" };
8131
8169
  }
8132
8170
  };
8133
8171
 
@@ -6099,17 +6099,30 @@ 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/readPostPurchaseProductSource.ts
6123
+ var Envelope = PostPurchaseProductSource.extend({ data: z45.array(z45.unknown()) });
6124
+ var Product3 = PostPurchaseProductSource.shape.data.element;
6125
+ var Identified = z45.object({ id: PostPurchaseProductId });
6113
6126
  var readPostPurchaseProductSource = (response, report) => {
6114
6127
  const { data, metadata } = Envelope.parse(deepCamelCase(response));
6115
6128
  const drift = [];
@@ -6117,6 +6130,7 @@ var readPostPurchaseProductSource = (response, report) => {
6117
6130
  const products = data.flatMap((raw, productIndex) => {
6118
6131
  const parsed = Product3.safeParse(raw);
6119
6132
  if (parsed.success) {
6133
+ rememberPostPurchaseProductSource(parsed.data, response, productIndex);
6120
6134
  if (parsed.data.requiresSellingPlan && !parsed.data.sellingPlanGroups.length)
6121
6135
  drift.push({
6122
6136
  detail: `Dropped post-purchase product ${parsed.data.id}: data.${productIndex}.requiresSellingPlan`,
@@ -6134,7 +6148,7 @@ var readPostPurchaseProductSource = (response, report) => {
6134
6148
  });
6135
6149
  return [];
6136
6150
  });
6137
- if (report && drift.length && firstHearing(response, report)) drift.forEach((event) => report(event));
6151
+ if (report && drift.length) reportOncePerResponse(response, report, drift);
6138
6152
  return { dropped, metadata, products };
6139
6153
  };
6140
6154
 
@@ -6280,7 +6294,8 @@ var Groups = z52.array(
6280
6294
  sellingPlans: z52.unknown()
6281
6295
  })
6282
6296
  );
6283
- var Allocations = z52.array(z52.object({ sellingPlanGroupId: z52.string().min(1) })).default([]);
6297
+ var PostPurchasePlanAllocations = z52.array(z52.object({ sellingPlanGroupId: z52.string().min(1) }));
6298
+ var Allocations = PostPurchasePlanAllocations.default([]);
6284
6299
  var Plans = z52.array(
6285
6300
  PostPurchasePlanOption.pick({ id: true, name: true }).extend({
6286
6301
  options: z52.array(z52.object({ value: z52.string().min(1) })).min(1),
@@ -7428,11 +7443,11 @@ var PostPurchaseMemberPointer = PostPurchaseProgressSchema.shape.members.element
7428
7443
 
7429
7444
  // src/server/normalizePostPurchaseCollection.ts
7430
7445
  import { z as z82, ZodError } from "zod";
7446
+ var PostPurchaseProductLimit = z82.number().int().positive();
7431
7447
  var normalizePostPurchaseCollection = (settings, response, excludedProductIds, report) => {
7432
7448
  const { dropped, normalized, products } = normalizePostPurchaseSource(settings, response, report);
7433
- const limit = z82.number().int().positive();
7434
- limit.parse(normalized.original.productLimit);
7435
- const cap = limit.parse(normalized.effective.productLimit);
7449
+ PostPurchaseProductLimit.parse(normalized.original.productLimit);
7450
+ const cap = PostPurchaseProductLimit.parse(normalized.effective.productLimit);
7436
7451
  const prefix = underPostPurchaseCap(products, cap);
7437
7452
  const unread = underPostPurchaseCap(dropped, cap);
7438
7453
  if (unread.length && !prefix.length) throw new ZodError(unread.flatMap(({ issues }) => issues));
@@ -7444,6 +7459,8 @@ var normalizePostPurchaseCollection = (settings, response, excludedProductIds, r
7444
7459
  };
7445
7460
 
7446
7461
  // src/server/postPurchaseVariantCatalog.ts
7462
+ var PostPurchaseVariantOptionMismatch = class extends Error {
7463
+ };
7447
7464
  var projectPostPurchaseVariantCatalog = (product, settings) => {
7448
7465
  const options2 = PostPurchaseVariantCatalog.shape.options.parse(product.options === void 0 ? [] : product.options).sort((a, b) => a.position - b.position);
7449
7466
  const variants = product.variants.map((variant2) => ({
@@ -7457,7 +7474,7 @@ var projectPostPurchaseVariantCatalog = (product, settings) => {
7457
7474
  throw new Error("Ambiguous post-purchase variant catalog");
7458
7475
  for (const tuple of tuples) {
7459
7476
  if (options2.some(({ position, values: values2 }) => !values2.some((value) => value === tuple[position - 1])) || tuple.some((value, index) => value != null && !options2[index]))
7460
- throw new Error("Variant option tuple does not match the catalog");
7477
+ throw new PostPurchaseVariantOptionMismatch("Variant option tuple does not match the catalog");
7461
7478
  }
7462
7479
  return PostPurchaseVariantCatalog.parse({
7463
7480
  displayMode: settings.productOptions.variantSelectorDisplayMode,
@@ -7480,6 +7497,12 @@ var projectPostPurchaseVariantCatalog = (product, settings) => {
7480
7497
  import { z as z83 } from "zod";
7481
7498
  var selection2 = PreparedPostPurchaseProduct.shape.selection;
7482
7499
  var PostPurchaseOfferSkipReason = z83.enum(["empty", "no-eligible-subscription"]);
7500
+ var PostPurchaseOfferUnavailableReason = z83.enum([
7501
+ "catalog-unreadable",
7502
+ "context-unreadable",
7503
+ "eligible-sts",
7504
+ "settings-unreadable"
7505
+ ]);
7483
7506
  var PostPurchaseOfferEligibilityContext = z83.object({
7484
7507
  excludedProductIds: PostPurchaseExcludedProductIds,
7485
7508
  originalLineItems: z83.array(selection2.pick({ productId: true, sellingPlanId: true, variantId: true }))
@@ -7500,7 +7523,8 @@ var PostPurchaseOfferEligibility = z83.discriminatedUnion("status", [
7500
7523
  status: z83.literal("retain")
7501
7524
  }),
7502
7525
  z83.object({ reason: PostPurchaseOfferSkipReason, status: z83.literal("skip") }),
7503
- z83.object({ reason: z83.literal("eligible-sts").optional(), status: z83.literal("unavailable") })
7526
+ /** The classifier always names a reason; it stays optional so a reasonless answer from before still parses. */
7527
+ z83.object({ reason: PostPurchaseOfferUnavailableReason.optional(), status: z83.literal("unavailable") })
7504
7528
  ]);
7505
7529
 
7506
7530
  // src/schema/postPurchaseQuantity.ts
@@ -7542,18 +7566,30 @@ var readPostPurchaseQuantity = ({
7542
7566
 
7543
7567
  // src/server/postPurchaseSts.ts
7544
7568
  var stsAllowed = ({ effective, original }) => [original, effective].some((config) => config.upgradeToSubscription.allow);
7545
- var postPurchaseStsLine = (normalized, product, lines, variantId) => {
7569
+ var reportUnreadableStsLine = (product, field, report) => {
7570
+ const source2 = postPurchaseProductSourceOf(product);
7571
+ const path = source2 ? `data.${source2.productIndex}.${field}` : field;
7572
+ const detail = `Unreadable post-purchase STS product ${product.id}: ${path}`;
7573
+ reportOncePerResponse(source2?.response ?? product, report, [{ detail, kind: "product" }]);
7574
+ };
7575
+ var postPurchaseStsLine = (normalized, product, lines, variantId, report) => {
7546
7576
  if (!product.sellingPlanGroups?.length || product.productGroupVariants.length) return void 0;
7547
- for (const variant2 of product.variants) {
7577
+ const modern = typeof product.sellingPlanGroups[0].id !== "number";
7578
+ for (const [index, variant2] of product.variants.entries()) {
7548
7579
  const matched = lines.filter((line) => line.sellingPlanId === void 0 && line.variantId === variant2.id);
7549
7580
  const data = [{ ...product, variants: [variant2] }];
7550
- if ((variantId === void 0 || variant2.id === variantId) && matched.length === 1 && postPurchaseVariantAvailable(variant2) && projectPostPurchasePlanCatalog({ ...normalized, data })?.plans.length)
7551
- return { data, line: matched[0] };
7581
+ if ((variantId === void 0 || variant2.id === variantId) && matched.length === 1 && postPurchaseVariantAvailable(variant2)) {
7582
+ const unread = [];
7583
+ if (projectPostPurchasePlanCatalog({ ...normalized, data }, (event) => unread.push(event))?.plans.length)
7584
+ return { data, line: matched[0] };
7585
+ const field = modern && !PostPurchasePlanAllocations.safeParse(variant2.sellingPlanAllocations).success ? `variants.${index}.sellingPlanAllocations` : unread.length ? "sellingPlanGroups" : void 0;
7586
+ if (report && field) reportUnreadableStsLine(product, field, report);
7587
+ }
7552
7588
  }
7553
7589
  };
7554
7590
  var preparePostPurchaseStsView = (widgetId, normalized, product, context, requested, report) => {
7555
7591
  const { originalLineItems } = PostPurchaseStsContext.parse(context);
7556
- const found = postPurchaseStsLine(normalized, product, originalLineItems, requested.variantId);
7592
+ const found = postPurchaseStsLine(normalized, product, originalLineItems, requested.variantId, report);
7557
7593
  if (!found) return null;
7558
7594
  const whole = found.line.quantity;
7559
7595
  if (requested.quantity !== void 0 && requested.quantity !== whole)
@@ -7984,14 +8020,13 @@ var preparePostPurchaseCollectionView = (widgetId, settings, response, report, r
7984
8020
  };
7985
8021
 
7986
8022
  // src/server/postPurchaseMemberEligibility.ts
7987
- var PostPurchaseInventoryUnavailable = class extends Error {
7988
- };
7989
- var postPurchaseMemberEligibility = (normalized, product, excluded) => {
8023
+ var eligibility = (normalized, product, excluded) => {
7990
8024
  if (product.productGroupVariants.length) {
7991
8025
  const group = readPostPurchaseGroup(product, normalized.effective, excluded);
8026
+ if (!group.entries.length) return void 0;
7992
8027
  return group.entries.map(({ product: option }) => postPurchaseMemberEligibility(normalized, option)).some(Boolean);
7993
8028
  }
7994
- if (!product.variants.length) throw new Error("Unsupported catalog");
8029
+ if (!product.variants.length) return false;
7995
8030
  const variants = projectPostPurchaseVariantCatalog(product, normalized.effective).variants;
7996
8031
  const catalogs = product.variants.map((variant2) => {
7997
8032
  const catalog = projectPostPurchasePlanCatalog({
@@ -7999,23 +8034,31 @@ var postPurchaseMemberEligibility = (normalized, product, excluded) => {
7999
8034
  data: [{ ...product, variants: [variant2] }]
8000
8035
  });
8001
8036
  if (!catalog) throw new Error("Unreadable subscription catalog");
8002
- if (normalized.effective.productType === "one-time" && product.sellingPlanGroups.some((group) => group.requiresSellingPlan))
8003
- throw new Error("Unsupported purchase mode");
8004
8037
  return catalog;
8005
8038
  });
8006
- if (!variants.some((variant2) => variant2.available))
8007
- throw new PostPurchaseInventoryUnavailable("Unavailable inventory");
8008
8039
  return catalogs.some(
8009
8040
  (catalog, index) => variants[index].available && (normalized.effective.productType !== "subscription" && !catalog.requiresSellingPlan || normalized.effective.productType !== "one-time" && catalog.plans.length > 0)
8010
8041
  );
8011
8042
  };
8043
+ var postPurchaseMemberEligibility = (normalized, product, excluded) => {
8044
+ try {
8045
+ return eligibility(normalized, product, excluded);
8046
+ } catch (error) {
8047
+ if (error instanceof PostPurchaseVariantOptionMismatch) return false;
8048
+ throw error;
8049
+ }
8050
+ };
8012
8051
 
8013
8052
  // src/server/classifyPostPurchaseOffer.ts
8014
8053
  var identities = (members) => members.map(({ product, productIndex }) => ({ productId: product.id, productIndex }));
8015
- var classifyPostPurchaseOffer = (settings, response, context, options2) => {
8054
+ var classifyPostPurchaseOffer = (settings, response, context, options2, report) => {
8055
+ let unreadable = "context-unreadable";
8016
8056
  try {
8017
8057
  const trusted = PostPurchaseOfferEligibilityContext.parse(context);
8018
8058
  const { sts: stsMode = false } = PostPurchaseStsOptions.parse(options2 ?? {});
8059
+ unreadable = "settings-unreadable";
8060
+ PostPurchaseProductLimit.parse(PostPurchaseProductSettings.parse(deepCamelCase(settings)).productLimit);
8061
+ unreadable = "catalog-unreadable";
8019
8062
  const { members, normalized } = normalizePostPurchaseCollection(settings, response);
8020
8063
  const configurations = [normalized.original, normalized.effective];
8021
8064
  const stsEnabled = configurations.some((config) => config.upgradeToSubscription.allow);
@@ -8028,33 +8071,28 @@ var classifyPostPurchaseOffer = (settings, response, context, options2) => {
8028
8071
  ))
8029
8072
  return { reason: "eligible-sts", status: "unavailable" };
8030
8073
  if (normalized.effective.productType !== "one-time" || sts) {
8031
- configurations.forEach((config) => PostPurchaseSubscriptionSettings.parse(config.subscription));
8074
+ unreadable = "settings-unreadable";
8075
+ PostPurchaseSubscriptionSettings.parse(normalized.original.subscription);
8076
+ unreadable = "catalog-unreadable";
8077
+ PostPurchaseSubscriptionSettings.parse(normalized.effective.subscription);
8032
8078
  }
8033
8079
  const lines = trusted.originalLineItems;
8034
- const stsMembers = sts ? members.filter(({ product }) => postPurchaseStsLine(normalized, product, lines)) : [];
8080
+ const stsMembers = sts ? members.filter(({ product }) => postPurchaseStsLine(normalized, product, lines, void 0, report)) : [];
8035
8081
  if (stsMembers.length) return { members: identities(stsMembers), mode: "sts", status: "retain" };
8036
- const classified = members.map((member) => {
8037
- try {
8038
- return {
8039
- ...member,
8040
- eligible: postPurchaseMemberEligibility(normalized, member.product, trusted.excludedProductIds)
8041
- };
8042
- } catch (error) {
8043
- if (error instanceof PostPurchaseInventoryUnavailable && !member.product.productGroupVariants.length && trusted.excludedProductIds.includes(member.product.id))
8044
- return { ...member, eligible: false };
8045
- throw error;
8046
- }
8047
- });
8082
+ const classified = members.map((member) => ({
8083
+ ...member,
8084
+ eligible: postPurchaseMemberEligibility(normalized, member.product, trusted.excludedProductIds)
8085
+ }));
8048
8086
  if (!members.length) return { reason: "empty", status: "skip" };
8049
8087
  if (stsEnabled) return { reason: "no-eligible-subscription", status: "skip" };
8050
8088
  const candidates = classified.filter(
8051
- ({ product }) => product.productGroupVariants.length ? readPostPurchaseGroup(product, normalized.effective, trusted.excludedProductIds).entries.length > 0 : !trusted.excludedProductIds.includes(product.id)
8089
+ ({ eligible, product }) => product.productGroupVariants.length ? eligible !== void 0 : !trusted.excludedProductIds.includes(product.id)
8052
8090
  );
8053
8091
  if (!candidates.length) return { reason: "empty", status: "skip" };
8054
8092
  const retained = candidates.filter(({ eligible }) => eligible);
8055
8093
  return retained.length ? { members: identities(retained), status: "retain" } : { reason: "no-eligible-subscription", status: "skip" };
8056
8094
  } catch {
8057
- return { status: "unavailable" };
8095
+ return { reason: unreadable, status: "unavailable" };
8058
8096
  }
8059
8097
  };
8060
8098
 
@@ -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
@@ -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 {};
@@ -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
@@ -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;
@@ -18,8 +18,14 @@ export declare const stsAllowed: ({ effective, original }: Pick<Normalized, "eff
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
20
  * names one.
21
+ *
22
+ * `report` hears a candidate (a purchased, in-stock variant on exactly one one-time line) whose plan data cannot be
23
+ * read, since it then silently switches nothing: a modern selling-plan variant whose `sellingPlanAllocations` is
24
+ * missing or malformed, or plan data the catalog cannot read. Each is a `product` drift naming the product id and
25
+ * the dotted path from the source root, never a value, heard once per response like the source's own drops. A
26
+ * readable variant that simply has no plan, or no candidate at all, stays silent.
21
27
  */
22
- export declare const postPurchaseStsLine: <T extends Line>(normalized: Normalized, product: Normalized["data"][number], lines: T[], variantId?: number) => {
28
+ export declare const postPurchaseStsLine: <T extends Line>(normalized: Normalized, product: Normalized["data"][number], lines: T[], variantId?: number, report?: Reporter) => {
23
29
  data: {
24
30
  variants: {
25
31
  title: string;
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebuy/rebuy",
3
- "version": "3.56.2",
3
+ "version": "3.57.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.",