@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.
|
@@ -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.
|
|
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 : {},
|
|
@@ -25,38 +25,39 @@ const deepFreeze = (value) => {
|
|
|
25
25
|
}
|
|
26
26
|
return value;
|
|
27
27
|
};
|
|
28
|
-
const
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|| (
|
|
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 : (
|
|
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
|
-
|| (
|
|
58
|
-
const intervalCount = (_b = pickNumber(metadataMapping, 'billingIntervalCount')) !== null && _b !== void 0 ? _b : (
|
|
59
|
-
const currency =
|
|
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
|
|
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
|
|
101
|
-
const testMapping = buildPlanModeConfig(item,
|
|
102
|
-
const liveMapping = buildPlanModeConfig(item,
|
|
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
|
-
|| (
|
|
106
|
+
|| (legacyPlan ? pickString(legacyPlan, 'displayName', 'display_name') : null);
|
|
106
107
|
const description = pickString(item, 'description')
|
|
107
108
|
|| pickString(metadata, 'description')
|
|
108
|
-
|| (
|
|
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
|
|
175
|
-
|
|
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
|
-
]
|
|
180
|
-
|
|
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,
|
|
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
|
|
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
|
-
||
|
|
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;
|