@funnelsgrove/runtime 0.6.0 → 0.7.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.
@@ -15,7 +15,7 @@ type FunnelRouteExperimentVariantDefinition = {
15
15
  };
16
16
  type FunnelPricingExperimentVariantDefinition = {
17
17
  variantKey?: string;
18
- offerSetKey: string;
18
+ offerSetId: string;
19
19
  label?: string;
20
20
  trafficPercent: number;
21
21
  };
@@ -101,6 +101,8 @@ export type OfferSetRuntimePlanOverrides = {
101
101
  };
102
102
  export type OfferSetRuntimeInput<TPlan extends Record<string, unknown>> = {
103
103
  offerSets: readonly GeneratedOfferSet[];
104
+ offerSetId?: string;
105
+ /** @deprecated Prefer the immutable offerSetId. */
104
106
  offerSetKey?: string;
105
107
  baseCatalogsByMode?: Partial<Record<RuntimeMode, Record<string, TPlan>>>;
106
108
  fallbackDiscounts?: readonly OfferSetRuntimeDiscount[];
@@ -116,7 +118,7 @@ export type OfferSetRuntimeResult<TPlan extends Record<string, unknown>> = {
116
118
  getPlanCatalog: (mode: RuntimeMode) => Record<string, TPlan & OfferSetRuntimePlanOverrides>;
117
119
  getDiscountList: (mode: RuntimeMode) => readonly OfferSetRuntimeDiscount[];
118
120
  };
119
- export declare const findGeneratedOfferSet: (offerSets: readonly GeneratedOfferSet[], offerSetKey?: string) => GeneratedOfferSet | null;
121
+ export declare const findGeneratedOfferSet: (offerSets: readonly GeneratedOfferSet[], offerSetKey?: string, offerSetId?: string) => GeneratedOfferSet | null;
120
122
  export declare const applyGeneratedOfferSetToPlanCatalog: <TPlan extends Record<string, unknown>>(baseCatalog: Record<string, TPlan>, mode: RuntimeMode, offerSet: GeneratedOfferSet | null) => Record<string, TPlan & OfferSetRuntimePlanOverrides>;
121
123
  export declare const buildGeneratedOfferSetDiscountList: (offerSet: GeneratedOfferSet | null, mode: RuntimeMode, fallbackDiscounts?: readonly OfferSetRuntimeDiscount[]) => readonly OfferSetRuntimeDiscount[];
122
124
  export declare const resolveGeneratedOfferSetRuntime: <TPlan extends Record<string, unknown> = Record<string, unknown>>(input: OfferSetRuntimeInput<TPlan>) => OfferSetRuntimeResult<TPlan>;
@@ -58,10 +58,10 @@ const getSortedItemsByKind = (offerSet, kind) => {
58
58
  return leftPosition - rightPosition;
59
59
  })) !== null && _a !== void 0 ? _a : []);
60
60
  };
61
- export const findGeneratedOfferSet = (offerSets, offerSetKey) => {
62
- var _a, _b;
61
+ export const findGeneratedOfferSet = (offerSets, offerSetKey, offerSetId) => {
62
+ var _a, _b, _c;
63
63
  const activeOfferSets = offerSets.filter((offerSet) => offerSet.isActive !== false);
64
- return ((_b = (_a = activeOfferSets.find((offerSet) => offerSet.key === offerSetKey)) !== null && _a !== void 0 ? _a : activeOfferSets[0]) !== null && _b !== void 0 ? _b : null);
64
+ return ((_c = (_b = (_a = activeOfferSets.find((offerSet) => offerSet.id === offerSetId)) !== null && _a !== void 0 ? _a : activeOfferSets.find((offerSet) => offerSet.key === offerSetKey)) !== null && _b !== void 0 ? _b : activeOfferSets[0]) !== null && _c !== void 0 ? _c : null);
65
65
  };
66
66
  const resolveCatalogKey = (baseCatalog, funnelPlanKey) => {
67
67
  var _a;
@@ -248,7 +248,7 @@ export const buildGeneratedOfferSetDiscountList = (offerSet, mode, fallbackDisco
248
248
  };
249
249
  export const resolveGeneratedOfferSetRuntime = (input) => {
250
250
  var _a, _b, _c, _d, _e, _f;
251
- const offerSet = findGeneratedOfferSet(input.offerSets, input.offerSetKey);
251
+ const offerSet = findGeneratedOfferSet(input.offerSets, input.offerSetKey, input.offerSetId);
252
252
  const baseCatalogsByMode = {
253
253
  test: (_b = (_a = input.baseCatalogsByMode) === null || _a === void 0 ? void 0 : _a.test) !== null && _b !== void 0 ? _b : {},
254
254
  live: (_d = (_c = input.baseCatalogsByMode) === null || _c === void 0 ? void 0 : _c.live) !== null && _d !== void 0 ? _d : {},
@@ -4,7 +4,8 @@ export type FunnelRuntimeConfig = {
4
4
  schemaVersion: number;
5
5
  funnelId: string;
6
6
  projectId: string;
7
- plans: unknown[];
7
+ /** Present only on historical schema-v1 artifacts. */
8
+ plans?: unknown[];
8
9
  offerSets: unknown[];
9
10
  experiments: unknown[];
10
11
  };
@@ -25,38 +25,39 @@ const deepFreeze = (value) => {
25
25
  }
26
26
  return value;
27
27
  };
28
- const getPlanById = (plans, projectBillingPlanId) => {
28
+ const getLegacyPlanById = (plans, projectBillingPlanId) => {
29
29
  var _a;
30
30
  return (projectBillingPlanId
31
31
  ? (_a = plans.find((plan) => pickString(plan, 'id') === projectBillingPlanId)) !== null && _a !== void 0 ? _a : null
32
32
  : null);
33
33
  };
34
- const getProviderMapping = (plan, mode) => {
34
+ const getLegacyProviderMapping = (plan, mode) => {
35
35
  var _a;
36
36
  return ((_a = readRecordArray(plan === null || plan === void 0 ? void 0 : plan.providerMappings)
37
37
  .find((mapping) => (pickString(mapping, 'environment') === mode
38
38
  && pickBoolean(mapping, 'isActive', 'is_active') !== false))) !== null && _a !== void 0 ? _a : null);
39
39
  };
40
- const buildPlanModeConfig = (item, plan, mode) => {
40
+ const buildPlanModeConfig = (item, mode, legacyPlan = null) => {
41
41
  var _a, _b;
42
42
  const metadata = isRecord(item.metadata) ? item.metadata : {};
43
43
  const metadataMappings = isRecord(metadata.providerMappingsByMode)
44
44
  ? metadata.providerMappingsByMode
45
45
  : {};
46
46
  const metadataMapping = isRecord(metadataMappings[mode]) ? metadataMappings[mode] : {};
47
- const planMapping = getProviderMapping(plan, mode);
47
+ const legacyMapping = getLegacyProviderMapping(legacyPlan, mode);
48
48
  const prefix = mode === 'test' ? 'test' : 'live';
49
49
  const providerPlanId = (pickString(item, `${prefix}ProviderPlanId`, `${prefix}_provider_plan_id`)
50
50
  || pickString(metadataMapping, 'providerPlanId')
51
- || (planMapping ? pickString(planMapping, 'providerPlanId', 'provider_plan_id') : null));
51
+ || (legacyMapping ? pickString(legacyMapping, 'providerPlanId', 'provider_plan_id') : null));
52
52
  if (!providerPlanId) {
53
53
  return null;
54
54
  }
55
- const amountMinor = (_a = pickNumber(metadataMapping, 'amountCents')) !== null && _a !== void 0 ? _a : (planMapping ? pickNumber(planMapping, 'amountMinor', 'amount_minor') : null);
55
+ const amountMinor = (_a = pickNumber(metadataMapping, 'amountCents')) !== null && _a !== void 0 ? _a : (legacyMapping ? pickNumber(legacyMapping, 'amountMinor', 'amount_minor') : null);
56
56
  const interval = pickString(metadataMapping, 'billingInterval')
57
- || (planMapping ? pickString(planMapping, 'interval') : null);
58
- const intervalCount = (_b = pickNumber(metadataMapping, 'billingIntervalCount')) !== null && _b !== void 0 ? _b : (planMapping ? pickNumber(planMapping, 'intervalCount', 'interval_count') : null);
59
- const currency = planMapping ? pickString(planMapping, 'currency') : null;
57
+ || (legacyMapping ? pickString(legacyMapping, 'interval') : null);
58
+ const intervalCount = (_b = pickNumber(metadataMapping, 'billingIntervalCount')) !== null && _b !== void 0 ? _b : (legacyMapping ? pickNumber(legacyMapping, 'intervalCount', 'interval_count') : null);
59
+ const currency = pickString(metadataMapping, 'currency')
60
+ || (legacyMapping ? pickString(legacyMapping, 'currency') : null);
60
61
  const amountMajor = amountMinor === null ? null : amountMinor / 100;
61
62
  const priceLabel = pickString(metadataMapping, 'priceLabel') || (amountMajor === null
62
63
  ? null
@@ -82,7 +83,7 @@ const buildPlanModeConfig = (item, plan, mode) => {
82
83
  }).format(amountMajor)} today` })), (interval ? { billingInterval: interval } : {})), (intervalCount === null ? {} : { billingIntervalCount: intervalCount }));
83
84
  };
84
85
  const normalizePublishedOfferSets = (config) => {
85
- const plans = readRecordArray(config.plans);
86
+ const legacyPlans = readRecordArray(config.plans);
86
87
  return readRecordArray(config.offerSets).map((offerSet) => {
87
88
  var _a;
88
89
  const key = pickString(offerSet, 'key');
@@ -97,15 +98,15 @@ const normalizePublishedOfferSets = (config) => {
97
98
  throw new Error(`Published runtime offer set "${key}" has an item without a plan key`);
98
99
  }
99
100
  const projectBillingPlanId = pickString(item, 'projectBillingPlanId', 'project_billing_plan_id');
100
- const plan = getPlanById(plans, projectBillingPlanId);
101
- const testMapping = buildPlanModeConfig(item, plan, 'test');
102
- const liveMapping = buildPlanModeConfig(item, plan, 'live');
101
+ const legacyPlan = getLegacyPlanById(legacyPlans, projectBillingPlanId);
102
+ const testMapping = buildPlanModeConfig(item, 'test', legacyPlan);
103
+ const liveMapping = buildPlanModeConfig(item, 'live', legacyPlan);
103
104
  const title = pickString(item, 'title', 'displayNameOverride', 'display_name_override')
104
105
  || pickString(metadata, 'title', 'publicPlanName', 'public_plan_name')
105
- || (plan ? pickString(plan, 'displayName', 'display_name') : null);
106
+ || (legacyPlan ? pickString(legacyPlan, 'displayName', 'display_name') : null);
106
107
  const description = pickString(item, 'description')
107
108
  || pickString(metadata, 'description')
108
- || (plan ? pickString(plan, 'description') : null);
109
+ || (legacyPlan ? pickString(legacyPlan, 'description') : null);
109
110
  return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ funnelPlanKey }, (pickString(metadata, 'runtimePlanKey', 'planKey', 'billingPlanKey')
110
111
  ? { runtimePlanKey: pickString(metadata, 'runtimePlanKey', 'planKey', 'billingPlanKey') }
111
112
  : {})), (projectBillingPlanId ? { projectBillingPlanId } : {})), (pickString(item, 'testProviderPlanId', 'test_provider_plan_id')
@@ -135,7 +136,7 @@ const normalizePublishedOfferSets = (config) => {
135
136
  return Object.assign(Object.assign({}, (pickString(offerSet, 'id') ? { id: pickString(offerSet, 'id') } : {})), { key, isActive: (_a = pickBoolean(offerSet, 'isActive', 'is_active')) !== null && _a !== void 0 ? _a : true, metadata: isRecord(offerSet.metadata) ? offerSet.metadata : {}, items });
136
137
  });
137
138
  };
138
- const normalizePublishedExperiments = (config, publishedAt) => readRecordArray(config.experiments).map((experiment) => {
139
+ const normalizePublishedExperiments = (config, publishedAt, offerSets) => readRecordArray(config.experiments).map((experiment) => {
139
140
  const conversionConfig = isRecord(experiment.conversionConfig)
140
141
  ? experiment.conversionConfig
141
142
  : isRecord(experiment.conversion_config)
@@ -171,16 +172,26 @@ const normalizePublishedExperiments = (config, publishedAt) => readRecordArray(c
171
172
  || pickString(experiment, 'startedAt', 'started_at')
172
173
  || publishedAt).slice(0, 10);
173
174
  if (type === 'pricing') {
174
- const configuredKeys = Array.isArray(conversionConfig.offerSetKeys)
175
- ? conversionConfig.offerSetKeys.map(readString).filter((value) => Boolean(value))
175
+ const directIds = normalizedVariants.map((variant, index) => (pickString(variants[index] || {}, 'offerSetId', 'offer_set_id')));
176
+ const legacyKeys = Array.isArray(conversionConfig.offerSetKeys)
177
+ ? conversionConfig.offerSetKeys.map(readString)
176
178
  : [
177
179
  pickString(conversionConfig, 'controlOfferSetKey'),
178
180
  pickString(conversionConfig, 'variantOfferSetKey'),
179
- ].filter((value) => Boolean(value));
180
- if (configuredKeys.length !== normalizedVariants.length) {
181
+ ];
182
+ const configuredIds = directIds.map((offerSetId, index) => {
183
+ var _a, _b;
184
+ if (offerSetId)
185
+ return offerSetId;
186
+ const legacyKey = legacyKeys[index];
187
+ return legacyKey
188
+ ? (_b = (_a = offerSets.find((offerSet) => offerSet.key === legacyKey)) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null
189
+ : null;
190
+ });
191
+ if (configuredIds.some((offerSetId) => !offerSetId)) {
181
192
  throw new Error(`Published pricing experiment "${id}" does not map every variant to an offer set`);
182
193
  }
183
- const pricingVariants = normalizedVariants.map((variant, index) => (Object.assign(Object.assign({ variantKey: variant.variantKey }, (variant.label ? { label: variant.label } : {})), { trafficPercent: variant.trafficPercent, offerSetKey: configuredKeys[index] })));
194
+ const pricingVariants = normalizedVariants.map((variant, index) => (Object.assign(Object.assign({ variantKey: variant.variantKey }, (variant.label ? { label: variant.label } : {})), { trafficPercent: variant.trafficPercent, offerSetId: configuredIds[index] })));
184
195
  return {
185
196
  id,
186
197
  name,
@@ -214,13 +225,14 @@ export const resolvePublishedFunnelRuntimeConfig = (published, options = {}) =>
214
225
  && published.sourceVersionId !== options.expectedFunnelVersionId) {
215
226
  throw new Error('Published runtime config belongs to a different funnel version');
216
227
  }
228
+ const offerSets = normalizePublishedOfferSets(published.config);
217
229
  return deepFreeze({
218
230
  revisionId: published.revisionId,
219
231
  sourceDeploymentId: published.sourceDeploymentId,
220
232
  sourceVersionId: published.sourceVersionId,
221
233
  publishedAt: published.publishedAt,
222
- offerSets: normalizePublishedOfferSets(published.config),
223
- experiments: normalizePublishedExperiments(published.config, published.publishedAt),
234
+ offerSets,
235
+ experiments: normalizePublishedExperiments(published.config, published.publishedAt, offerSets),
224
236
  });
225
237
  };
226
238
  const parsePublishedRuntimeConfig = (value) => {
@@ -228,17 +240,18 @@ const parsePublishedRuntimeConfig = (value) => {
228
240
  throw new Error('Funnel runtime config response is invalid');
229
241
  }
230
242
  const config = value.config;
243
+ const schemaVersion = typeof config.schemaVersion === 'number' ? config.schemaVersion : null;
231
244
  if (typeof value.revisionId !== 'string'
232
245
  || typeof value.sourceDeploymentId !== 'string'
233
246
  || typeof value.sourceVersionId !== 'string'
234
247
  || typeof value.schemaVersion !== 'number'
235
248
  || typeof value.publishedAt !== 'string'
236
- || typeof config.schemaVersion !== 'number'
249
+ || (schemaVersion !== 1 && schemaVersion !== 2)
237
250
  || typeof config.funnelId !== 'string'
238
251
  || typeof config.projectId !== 'string'
239
- || !Array.isArray(config.plans)
240
252
  || !Array.isArray(config.offerSets)
241
- || !Array.isArray(config.experiments)) {
253
+ || !Array.isArray(config.experiments)
254
+ || (schemaVersion === 1 && !Array.isArray(config.plans))) {
242
255
  throw new Error('Funnel runtime config response is invalid');
243
256
  }
244
257
  return value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/runtime",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",