@drawbridge/drawbridge-utils 0.0.93 → 0.0.95

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/plans.cjs CHANGED
@@ -31,7 +31,8 @@ var plans_exports = {};
31
31
  __export(plans_exports, {
32
32
  conversionRate: () => conversionRate,
33
33
  free: () => free,
34
- plans: () => plans
34
+ plans: () => plans,
35
+ resolvePlan: () => resolvePlan
35
36
  });
36
37
  module.exports = __toCommonJS(plans_exports);
37
38
 
@@ -420,70 +421,33 @@ var plans = {
420
421
  overages: { actions: "1.5" },
421
422
  title: "Elite",
422
423
  conversion: 0.5
423
- },
424
- // Negotiated tier. Unlike the four sellable plans it has NO catalog price:
425
- // each enterprise subscription bills from its own Stripe price, minted
426
- // under STRIPE_PRODUCT_ENTERPRISE and recorded on items.plan.price.
427
- //
428
- // That absent price is the whole marker — this entry carries no flag of its
429
- // own, deliberately. A plan with no stripePriceId cannot be sold self-serve
430
- // because subscribe and change would hand Stripe `price : undefined`, so
431
- // "is it selectable" reads straight off the thing that makes it true. Two
432
- // flags were considered and dropped: `status : drafted | published` (it
433
- // collides with subscription.status and organization.status, which extend.js
434
- // attaches to the same payload) and `type : default | custom` (its one real
435
- // job — catching a sellable plan whose STRIPE_PRICE_* env went missing — is
436
- // already done by assertEnv at api boot). Negotiated-ness is recorded where
437
- // it actually varies: subscription.type on the document, set by the admin.
438
- //
439
- // Every value below is a FALLBACK, not the deal. Real allowances come from
440
- // subscription.overrides per customer. They deliberately mirror Elite
441
- // rather than being infinite or zero: a half-configured enterprise
442
- // subscription then degrades to the best sellable tier — a known-good,
443
- // billable state — instead of billing nothing or granting everything.
444
- //
445
- // Placed LAST so the array-position direction logic in drawbridge-api
446
- // (organization-subscription.js `plans.findIndex`) reads a move onto
447
- // enterprise as an upgrade and a move off it as a downgrade.
448
- DBENTERPRISE: {
449
- features: all.features([
450
- organization.advertisements.key,
451
- organization.analytics.key,
452
- organization.members.key,
453
- organization.subdomain.key,
454
- page.slug.key
455
- ]),
456
- limits: all.limits({ actions: 1e5, members: infinite, storage: gigabyte * 100 }),
457
- marketing: {
458
- description: "Negotiated terms tailored to your organization.",
459
- features: [
460
- "Analytics",
461
- "Custom subdomain / URLs",
462
- "Confirmation page ads",
463
- "Negotiated pricing and limits"
464
- ],
465
- limits: [
466
- ["Actions per month", "Custom"],
467
- ["Affiliates", "Unlimited"],
468
- ["Brands", "Unlimited"],
469
- ["Campaigns", "Unlimited"],
470
- ["Pages", "Unlimited"],
471
- ["Members", "Custom"],
472
- ["Storage", "Custom"]
473
- ]
474
- },
475
- overages: { actions: "1.5" },
476
- title: "Enterprise",
477
- conversion: 0.5
478
424
  }
479
425
  };
480
- var conversionRate = (subscription) => {
426
+ var resolvePlan = (subscription) => {
481
427
  var _a, _b;
482
- return ((_a = subscription == null ? void 0 : subscription.overrides) == null ? void 0 : _a.conversion) ?? (subscription == null ? void 0 : subscription.conversion) ?? ((_b = plans[subscription == null ? void 0 : subscription.plan]) == null ? void 0 : _b.conversion) ?? free.conversion;
428
+ const custom = subscription == null ? void 0 : subscription.custom;
429
+ if (!custom) return plans[subscription == null ? void 0 : subscription.plan] ?? free;
430
+ return {
431
+ // Reusing all.features / all.limits is what keeps a custom plan the same
432
+ // SHAPE as a catalog one: the base feature grants every plan carries, the
433
+ // campaign limits that are always infinite, and members defaulting to
434
+ // infinite when a deal does not name it.
435
+ conversion: custom.conversion ?? free.conversion,
436
+ custom: true,
437
+ features: all.features(((_a = custom.features) == null ? void 0 : _a.granted) || []),
438
+ limits: all.limits(((_b = custom.limits) == null ? void 0 : _b.organization) || {}),
439
+ overages: { actions: custom.overages },
440
+ title: custom.title || "Custom"
441
+ };
442
+ };
443
+ var conversionRate = (subscription) => {
444
+ var _a;
445
+ return ((_a = resolvePlan(subscription)) == null ? void 0 : _a.conversion) ?? free.conversion;
483
446
  };
484
447
  // Annotate the CommonJS export names for ESM import in node:
485
448
  0 && (module.exports = {
486
449
  conversionRate,
487
450
  free,
488
- plans
451
+ plans,
452
+ resolvePlan
489
453
  });
package/dist/plans.d.cts CHANGED
@@ -194,73 +194,51 @@ const plans = {
194
194
  conversion : 0.5
195
195
  },
196
196
 
197
- // Negotiated tier. Unlike the four sellable plans it has NO catalog price:
198
- // each enterprise subscription bills from its own Stripe price, minted
199
- // under STRIPE_PRODUCT_ENTERPRISE and recorded on items.plan.price.
200
- //
201
- // That absent price is the whole marker this entry carries no flag of its
202
- // own, deliberately. A plan with no stripePriceId cannot be sold self-serve
203
- // because subscribe and change would hand Stripe `price : undefined`, so
204
- // "is it selectable" reads straight off the thing that makes it true. Two
205
- // flags were considered and dropped: `status : drafted | published` (it
206
- // collides with subscription.status and organization.status, which extend.js
207
- // attaches to the same payload) and `type : default | custom` (its one real
208
- // job catching a sellable plan whose STRIPE_PRICE_* env went missing is
209
- // already done by assertEnv at api boot). Negotiated-ness is recorded where
210
- // it actually varies: subscription.type on the document, set by the admin.
211
- //
212
- // Every value below is a FALLBACK, not the deal. Real allowances come from
213
- // subscription.overrides per customer. They deliberately mirror Elite
214
- // rather than being infinite or zero: a half-configured enterprise
215
- // subscription then degrades to the best sellable tier — a known-good,
216
- // billable state — instead of billing nothing or granting everything.
217
- //
218
- // Placed LAST so the array-position direction logic in drawbridge-api
219
- // (organization-subscription.js `plans.findIndex`) reads a move onto
220
- // enterprise as an upgrade and a move off it as a downgrade.
221
- DBENTERPRISE : {
222
- features : all.features([
223
- organization.advertisements.key,
224
- organization.analytics.key,
225
- organization.members.key,
226
- organization.subdomain.key,
227
- page.slug.key
228
- ]),
229
- limits : all.limits({ actions : 100000, members : infinite, storage : gigabyte * 100 }),
230
- marketing : {
231
- description : 'Negotiated terms tailored to your organization.',
232
- features : [
233
- 'Analytics',
234
- 'Custom subdomain / URLs',
235
- 'Confirmation page ads',
236
- 'Negotiated pricing and limits'
237
- ],
238
- limits : [
239
- [ 'Actions per month', 'Custom' ],
240
- [ 'Affiliates', 'Unlimited' ],
241
- [ 'Brands', 'Unlimited' ],
242
- [ 'Campaigns', 'Unlimited' ],
243
- [ 'Pages', 'Unlimited' ],
244
- [ 'Members', 'Custom' ],
245
- [ 'Storage', 'Custom' ]
246
- ]
247
- },
248
- overages : { actions : '1.5' },
249
- title : 'Enterprise',
250
- conversion : 0.5
251
- }
197
+ };
198
+
199
+ // The effective plan for a subscription.
200
+ //
201
+ // A CUSTOM plan is not in this catalog and never will be: it belongs to one
202
+ // organization, so it is stored on that organization's subscription and is
203
+ // self-describing. Everything a plan needs is built here from what it carries,
204
+ // which is why `subscription.plan` is empty for one.
205
+ //
206
+ // This is the single resolver. drawbridge-api mirrors it as an aggregation
207
+ // expression for the organization payload, and drawbridge-sync calls it
208
+ // directly on raw documents sync never touches extend, so without this it
209
+ // would resolve `plans[ null ]`, bill every custom-plan cycle at $0 and charge
210
+ // the free 3% conversion rate. A parity test pins the two expressions together.
211
+ const resolvePlan = ( subscription ) => {
212
+
213
+ const custom = subscription?.custom;
214
+
215
+ if( ! custom ) return plans[ subscription?.plan ] ?? free;
216
+
217
+ return {
218
+ // Reusing all.features / all.limits is what keeps a custom plan the same
219
+ // SHAPE as a catalog one: the base feature grants every plan carries, the
220
+ // campaign limits that are always infinite, and members defaulting to
221
+ // infinite when a deal does not name it.
222
+ conversion : custom.conversion ?? free.conversion,
223
+ custom : true,
224
+ features : all.features( custom.features?.granted || [] ),
225
+ limits : all.limits( custom.limits?.organization || {} ),
226
+ overages : { actions : custom.overages },
227
+ title : custom.title || 'Custom'
228
+ };
229
+
252
230
  };
253
231
 
254
232
  // Resolve the conversion-fee percentage for an order's owning org.
255
- // Precedence: explicit per-subscription override (admin/negotiated) in
256
- // subscription.overrides the legacy top-level field (pre-migration docs;
257
- // remove after the overrides cleanup release) the plan's catalog default
258
- // the free-org rate. `??` throughout a stored 0 is a valid 0% override.
259
- // `subscription` is null for free orgs (they have no subscription doc).
233
+ //
234
+ // The plan is the only source. A negotiated rate is not a per-subscription
235
+ // override any more it is a custom plan, and `resolvePlan` reads that from
236
+ // `subscription.custom.conversion`. The catalog answers for everyone else.
237
+ //
238
+ // `??` not `||`, because a plan's 0 is a valid 0% rate. `subscription` is null
239
+ // for free orgs (they have no subscription document).
260
240
  const conversionRate = ( subscription ) =>
261
- subscription?.overrides?.conversion
262
- ?? subscription?.conversion
263
- ?? plans[ subscription?.plan ]?.conversion
241
+ resolvePlan( subscription )?.conversion
264
242
  ?? free.conversion;
265
243
 
266
- export { conversionRate, free, plans };
244
+ export { conversionRate, free, plans, resolvePlan };
package/dist/plans.d.ts CHANGED
@@ -194,73 +194,51 @@ const plans = {
194
194
  conversion : 0.5
195
195
  },
196
196
 
197
- // Negotiated tier. Unlike the four sellable plans it has NO catalog price:
198
- // each enterprise subscription bills from its own Stripe price, minted
199
- // under STRIPE_PRODUCT_ENTERPRISE and recorded on items.plan.price.
200
- //
201
- // That absent price is the whole marker this entry carries no flag of its
202
- // own, deliberately. A plan with no stripePriceId cannot be sold self-serve
203
- // because subscribe and change would hand Stripe `price : undefined`, so
204
- // "is it selectable" reads straight off the thing that makes it true. Two
205
- // flags were considered and dropped: `status : drafted | published` (it
206
- // collides with subscription.status and organization.status, which extend.js
207
- // attaches to the same payload) and `type : default | custom` (its one real
208
- // job catching a sellable plan whose STRIPE_PRICE_* env went missing is
209
- // already done by assertEnv at api boot). Negotiated-ness is recorded where
210
- // it actually varies: subscription.type on the document, set by the admin.
211
- //
212
- // Every value below is a FALLBACK, not the deal. Real allowances come from
213
- // subscription.overrides per customer. They deliberately mirror Elite
214
- // rather than being infinite or zero: a half-configured enterprise
215
- // subscription then degrades to the best sellable tier — a known-good,
216
- // billable state — instead of billing nothing or granting everything.
217
- //
218
- // Placed LAST so the array-position direction logic in drawbridge-api
219
- // (organization-subscription.js `plans.findIndex`) reads a move onto
220
- // enterprise as an upgrade and a move off it as a downgrade.
221
- DBENTERPRISE : {
222
- features : all.features([
223
- organization.advertisements.key,
224
- organization.analytics.key,
225
- organization.members.key,
226
- organization.subdomain.key,
227
- page.slug.key
228
- ]),
229
- limits : all.limits({ actions : 100000, members : infinite, storage : gigabyte * 100 }),
230
- marketing : {
231
- description : 'Negotiated terms tailored to your organization.',
232
- features : [
233
- 'Analytics',
234
- 'Custom subdomain / URLs',
235
- 'Confirmation page ads',
236
- 'Negotiated pricing and limits'
237
- ],
238
- limits : [
239
- [ 'Actions per month', 'Custom' ],
240
- [ 'Affiliates', 'Unlimited' ],
241
- [ 'Brands', 'Unlimited' ],
242
- [ 'Campaigns', 'Unlimited' ],
243
- [ 'Pages', 'Unlimited' ],
244
- [ 'Members', 'Custom' ],
245
- [ 'Storage', 'Custom' ]
246
- ]
247
- },
248
- overages : { actions : '1.5' },
249
- title : 'Enterprise',
250
- conversion : 0.5
251
- }
197
+ };
198
+
199
+ // The effective plan for a subscription.
200
+ //
201
+ // A CUSTOM plan is not in this catalog and never will be: it belongs to one
202
+ // organization, so it is stored on that organization's subscription and is
203
+ // self-describing. Everything a plan needs is built here from what it carries,
204
+ // which is why `subscription.plan` is empty for one.
205
+ //
206
+ // This is the single resolver. drawbridge-api mirrors it as an aggregation
207
+ // expression for the organization payload, and drawbridge-sync calls it
208
+ // directly on raw documents sync never touches extend, so without this it
209
+ // would resolve `plans[ null ]`, bill every custom-plan cycle at $0 and charge
210
+ // the free 3% conversion rate. A parity test pins the two expressions together.
211
+ const resolvePlan = ( subscription ) => {
212
+
213
+ const custom = subscription?.custom;
214
+
215
+ if( ! custom ) return plans[ subscription?.plan ] ?? free;
216
+
217
+ return {
218
+ // Reusing all.features / all.limits is what keeps a custom plan the same
219
+ // SHAPE as a catalog one: the base feature grants every plan carries, the
220
+ // campaign limits that are always infinite, and members defaulting to
221
+ // infinite when a deal does not name it.
222
+ conversion : custom.conversion ?? free.conversion,
223
+ custom : true,
224
+ features : all.features( custom.features?.granted || [] ),
225
+ limits : all.limits( custom.limits?.organization || {} ),
226
+ overages : { actions : custom.overages },
227
+ title : custom.title || 'Custom'
228
+ };
229
+
252
230
  };
253
231
 
254
232
  // Resolve the conversion-fee percentage for an order's owning org.
255
- // Precedence: explicit per-subscription override (admin/negotiated) in
256
- // subscription.overrides the legacy top-level field (pre-migration docs;
257
- // remove after the overrides cleanup release) the plan's catalog default
258
- // the free-org rate. `??` throughout a stored 0 is a valid 0% override.
259
- // `subscription` is null for free orgs (they have no subscription doc).
233
+ //
234
+ // The plan is the only source. A negotiated rate is not a per-subscription
235
+ // override any more it is a custom plan, and `resolvePlan` reads that from
236
+ // `subscription.custom.conversion`. The catalog answers for everyone else.
237
+ //
238
+ // `??` not `||`, because a plan's 0 is a valid 0% rate. `subscription` is null
239
+ // for free orgs (they have no subscription document).
260
240
  const conversionRate = ( subscription ) =>
261
- subscription?.overrides?.conversion
262
- ?? subscription?.conversion
263
- ?? plans[ subscription?.plan ]?.conversion
241
+ resolvePlan( subscription )?.conversion
264
242
  ?? free.conversion;
265
243
 
266
- export { conversionRate, free, plans };
244
+ export { conversionRate, free, plans, resolvePlan };
package/dist/plans.js CHANGED
@@ -383,69 +383,32 @@ var plans = {
383
383
  overages: { actions: "1.5" },
384
384
  title: "Elite",
385
385
  conversion: 0.5
386
- },
387
- // Negotiated tier. Unlike the four sellable plans it has NO catalog price:
388
- // each enterprise subscription bills from its own Stripe price, minted
389
- // under STRIPE_PRODUCT_ENTERPRISE and recorded on items.plan.price.
390
- //
391
- // That absent price is the whole marker — this entry carries no flag of its
392
- // own, deliberately. A plan with no stripePriceId cannot be sold self-serve
393
- // because subscribe and change would hand Stripe `price : undefined`, so
394
- // "is it selectable" reads straight off the thing that makes it true. Two
395
- // flags were considered and dropped: `status : drafted | published` (it
396
- // collides with subscription.status and organization.status, which extend.js
397
- // attaches to the same payload) and `type : default | custom` (its one real
398
- // job — catching a sellable plan whose STRIPE_PRICE_* env went missing — is
399
- // already done by assertEnv at api boot). Negotiated-ness is recorded where
400
- // it actually varies: subscription.type on the document, set by the admin.
401
- //
402
- // Every value below is a FALLBACK, not the deal. Real allowances come from
403
- // subscription.overrides per customer. They deliberately mirror Elite
404
- // rather than being infinite or zero: a half-configured enterprise
405
- // subscription then degrades to the best sellable tier — a known-good,
406
- // billable state — instead of billing nothing or granting everything.
407
- //
408
- // Placed LAST so the array-position direction logic in drawbridge-api
409
- // (organization-subscription.js `plans.findIndex`) reads a move onto
410
- // enterprise as an upgrade and a move off it as a downgrade.
411
- DBENTERPRISE: {
412
- features: all.features([
413
- organization.advertisements.key,
414
- organization.analytics.key,
415
- organization.members.key,
416
- organization.subdomain.key,
417
- page.slug.key
418
- ]),
419
- limits: all.limits({ actions: 1e5, members: infinite, storage: gigabyte * 100 }),
420
- marketing: {
421
- description: "Negotiated terms tailored to your organization.",
422
- features: [
423
- "Analytics",
424
- "Custom subdomain / URLs",
425
- "Confirmation page ads",
426
- "Negotiated pricing and limits"
427
- ],
428
- limits: [
429
- ["Actions per month", "Custom"],
430
- ["Affiliates", "Unlimited"],
431
- ["Brands", "Unlimited"],
432
- ["Campaigns", "Unlimited"],
433
- ["Pages", "Unlimited"],
434
- ["Members", "Custom"],
435
- ["Storage", "Custom"]
436
- ]
437
- },
438
- overages: { actions: "1.5" },
439
- title: "Enterprise",
440
- conversion: 0.5
441
386
  }
442
387
  };
443
- var conversionRate = (subscription) => {
388
+ var resolvePlan = (subscription) => {
444
389
  var _a, _b;
445
- return ((_a = subscription == null ? void 0 : subscription.overrides) == null ? void 0 : _a.conversion) ?? (subscription == null ? void 0 : subscription.conversion) ?? ((_b = plans[subscription == null ? void 0 : subscription.plan]) == null ? void 0 : _b.conversion) ?? free.conversion;
390
+ const custom = subscription == null ? void 0 : subscription.custom;
391
+ if (!custom) return plans[subscription == null ? void 0 : subscription.plan] ?? free;
392
+ return {
393
+ // Reusing all.features / all.limits is what keeps a custom plan the same
394
+ // SHAPE as a catalog one: the base feature grants every plan carries, the
395
+ // campaign limits that are always infinite, and members defaulting to
396
+ // infinite when a deal does not name it.
397
+ conversion: custom.conversion ?? free.conversion,
398
+ custom: true,
399
+ features: all.features(((_a = custom.features) == null ? void 0 : _a.granted) || []),
400
+ limits: all.limits(((_b = custom.limits) == null ? void 0 : _b.organization) || {}),
401
+ overages: { actions: custom.overages },
402
+ title: custom.title || "Custom"
403
+ };
404
+ };
405
+ var conversionRate = (subscription) => {
406
+ var _a;
407
+ return ((_a = resolvePlan(subscription)) == null ? void 0 : _a.conversion) ?? free.conversion;
446
408
  };
447
409
  export {
448
410
  conversionRate,
449
411
  free,
450
- plans
412
+ plans,
413
+ resolvePlan
451
414
  };
package/package.json CHANGED
@@ -169,5 +169,5 @@
169
169
  "test": ". \"$HOME/.nvm/nvm.sh\" && nvm use && node --test"
170
170
  },
171
171
  "types": "dist/index.d.ts",
172
- "version": "0.0.93"
172
+ "version": "0.0.95"
173
173
  }