@drawbridge/drawbridge-utils 0.0.103 → 0.0.104
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/ai.cjs +4 -0
- package/dist/ai.js +4 -0
- package/dist/billing.cjs +16 -0
- package/dist/billing.d.cts +7 -1
- package/dist/billing.d.ts +7 -1
- package/dist/billing.js +10 -0
- package/dist/features.cjs +5 -0
- package/dist/features.d.cts +5 -0
- package/dist/features.d.ts +5 -0
- package/dist/features.js +5 -0
- package/dist/plans.cjs +40 -8
- package/dist/plans.d.cts +58 -8
- package/dist/plans.d.ts +58 -8
- package/dist/plans.js +38 -7
- package/dist/pricing.cjs +807 -0
- package/dist/pricing.d.cts +72 -0
- package/dist/pricing.d.ts +72 -0
- package/dist/pricing.js +757 -0
- package/package.json +6 -1
package/dist/ai.cjs
CHANGED
|
@@ -268,6 +268,10 @@ var billRequest = async ({ db, user, model, usage, tools }) => {
|
|
|
268
268
|
});
|
|
269
269
|
};
|
|
270
270
|
var ai = {
|
|
271
|
+
// The rate tables themselves, in cents per 1,000,000 tokens (`tools` in cents
|
|
272
|
+
// per request). Read-only surface for lib/pricing.js — the fee math above is
|
|
273
|
+
// still the only thing that should compute from them.
|
|
274
|
+
rates: { wholesale: cost, retail: pricing, tools: { wholesale: toolCost, retail: toolPricing } },
|
|
271
275
|
// Retail cents for a request (what the user pays).
|
|
272
276
|
price: priceForRequest,
|
|
273
277
|
// Wholesale cents for a request (what we pay the provider).
|
package/dist/ai.js
CHANGED
|
@@ -234,6 +234,10 @@ var billRequest = async ({ db, user, model, usage, tools }) => {
|
|
|
234
234
|
});
|
|
235
235
|
};
|
|
236
236
|
var ai = {
|
|
237
|
+
// The rate tables themselves, in cents per 1,000,000 tokens (`tools` in cents
|
|
238
|
+
// per request). Read-only surface for lib/pricing.js — the fee math above is
|
|
239
|
+
// still the only thing that should compute from them.
|
|
240
|
+
rates: { wholesale: cost, retail: pricing, tools: { wholesale: toolCost, retail: toolPricing } },
|
|
237
241
|
// Retail cents for a request (what the user pays).
|
|
238
242
|
price: priceForRequest,
|
|
239
243
|
// Wholesale cents for a request (what we pay the provider).
|
package/dist/billing.cjs
CHANGED
|
@@ -19,7 +19,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
// lib/billing.js
|
|
20
20
|
var billing_exports = {};
|
|
21
21
|
__export(billing_exports, {
|
|
22
|
+
LOCAL_FLAT_CENTS: () => LOCAL_FLAT_CENTS,
|
|
22
23
|
MARKUP: () => MARKUP,
|
|
24
|
+
PREMIUM_CPM_CENTS: () => PREMIUM_CPM_CENTS,
|
|
25
|
+
PREMIUM_RATE_PER_GB: () => PREMIUM_RATE_PER_GB,
|
|
26
|
+
REUSE_FLAT_CENTS: () => REUSE_FLAT_CENTS,
|
|
27
|
+
STANDARD_CPM_CENTS: () => STANDARD_CPM_CENTS,
|
|
28
|
+
STANDARD_RATE_PER_GB: () => STANDARD_RATE_PER_GB,
|
|
23
29
|
action: () => action,
|
|
24
30
|
ai: () => ai,
|
|
25
31
|
balance: () => balance,
|
|
@@ -268,6 +274,10 @@ var billRequest = async ({ db, user, model, usage, tools }) => {
|
|
|
268
274
|
});
|
|
269
275
|
};
|
|
270
276
|
var ai = {
|
|
277
|
+
// The rate tables themselves, in cents per 1,000,000 tokens (`tools` in cents
|
|
278
|
+
// per request). Read-only surface for lib/pricing.js — the fee math above is
|
|
279
|
+
// still the only thing that should compute from them.
|
|
280
|
+
rates: { wholesale: cost, retail: pricing, tools: { wholesale: toolCost, retail: toolPricing } },
|
|
271
281
|
// Retail cents for a request (what the user pays).
|
|
272
282
|
price: priceForRequest,
|
|
273
283
|
// Wholesale cents for a request (what we pay the provider).
|
|
@@ -511,7 +521,13 @@ var action = {
|
|
|
511
521
|
};
|
|
512
522
|
// Annotate the CommonJS export names for ESM import in node:
|
|
513
523
|
0 && (module.exports = {
|
|
524
|
+
LOCAL_FLAT_CENTS,
|
|
514
525
|
MARKUP,
|
|
526
|
+
PREMIUM_CPM_CENTS,
|
|
527
|
+
PREMIUM_RATE_PER_GB,
|
|
528
|
+
REUSE_FLAT_CENTS,
|
|
529
|
+
STANDARD_CPM_CENTS,
|
|
530
|
+
STANDARD_RATE_PER_GB,
|
|
515
531
|
action,
|
|
516
532
|
ai,
|
|
517
533
|
balance,
|
package/dist/billing.d.cts
CHANGED
|
@@ -185,6 +185,11 @@ const billRequest = async ({ db, user, model, usage, tools }) => {
|
|
|
185
185
|
|
|
186
186
|
const ai = {
|
|
187
187
|
|
|
188
|
+
// The rate tables themselves, in cents per 1,000,000 tokens (`tools` in cents
|
|
189
|
+
// per request). Read-only surface for lib/pricing.js — the fee math above is
|
|
190
|
+
// still the only thing that should compute from them.
|
|
191
|
+
rates : { wholesale : cost, retail : pricing, tools : { wholesale : toolCost, retail : toolPricing } },
|
|
192
|
+
|
|
188
193
|
// Retail cents for a request (what the user pays).
|
|
189
194
|
price : priceForRequest,
|
|
190
195
|
|
|
@@ -211,6 +216,7 @@ const ai = {
|
|
|
211
216
|
// Both carry the same 30% margin (MARKUP). A cold local render and a cache reuse
|
|
212
217
|
// have no external cost, so they're small flat fees (set to 0 to make either
|
|
213
218
|
// free). Tune the rates against real BrightData invoices.
|
|
219
|
+
// Exported so lib/pricing.js can index every customer-facing rate in one place.
|
|
214
220
|
const STANDARD_RATE_PER_GB = 8;
|
|
215
221
|
const PREMIUM_RATE_PER_GB = 11;
|
|
216
222
|
// Web Unlocker CPM in cents-per-request: $1.50/1,000 = 0.15¢, $2.50/1,000 = 0.25¢.
|
|
@@ -577,4 +583,4 @@ const action = {
|
|
|
577
583
|
|
|
578
584
|
};
|
|
579
585
|
|
|
580
|
-
export { MARKUP, action, ai, balance, grant, scrape };
|
|
586
|
+
export { LOCAL_FLAT_CENTS, MARKUP, PREMIUM_CPM_CENTS, PREMIUM_RATE_PER_GB, REUSE_FLAT_CENTS, STANDARD_CPM_CENTS, STANDARD_RATE_PER_GB, action, ai, balance, grant, scrape };
|
package/dist/billing.d.ts
CHANGED
|
@@ -185,6 +185,11 @@ const billRequest = async ({ db, user, model, usage, tools }) => {
|
|
|
185
185
|
|
|
186
186
|
const ai = {
|
|
187
187
|
|
|
188
|
+
// The rate tables themselves, in cents per 1,000,000 tokens (`tools` in cents
|
|
189
|
+
// per request). Read-only surface for lib/pricing.js — the fee math above is
|
|
190
|
+
// still the only thing that should compute from them.
|
|
191
|
+
rates : { wholesale : cost, retail : pricing, tools : { wholesale : toolCost, retail : toolPricing } },
|
|
192
|
+
|
|
188
193
|
// Retail cents for a request (what the user pays).
|
|
189
194
|
price : priceForRequest,
|
|
190
195
|
|
|
@@ -211,6 +216,7 @@ const ai = {
|
|
|
211
216
|
// Both carry the same 30% margin (MARKUP). A cold local render and a cache reuse
|
|
212
217
|
// have no external cost, so they're small flat fees (set to 0 to make either
|
|
213
218
|
// free). Tune the rates against real BrightData invoices.
|
|
219
|
+
// Exported so lib/pricing.js can index every customer-facing rate in one place.
|
|
214
220
|
const STANDARD_RATE_PER_GB = 8;
|
|
215
221
|
const PREMIUM_RATE_PER_GB = 11;
|
|
216
222
|
// Web Unlocker CPM in cents-per-request: $1.50/1,000 = 0.15¢, $2.50/1,000 = 0.25¢.
|
|
@@ -577,4 +583,4 @@ const action = {
|
|
|
577
583
|
|
|
578
584
|
};
|
|
579
585
|
|
|
580
|
-
export { MARKUP, action, ai, balance, grant, scrape };
|
|
586
|
+
export { LOCAL_FLAT_CENTS, MARKUP, PREMIUM_CPM_CENTS, PREMIUM_RATE_PER_GB, REUSE_FLAT_CENTS, STANDARD_CPM_CENTS, STANDARD_RATE_PER_GB, action, ai, balance, grant, scrape };
|
package/dist/billing.js
CHANGED
|
@@ -238,6 +238,10 @@ var billRequest = async ({ db, user, model, usage, tools }) => {
|
|
|
238
238
|
});
|
|
239
239
|
};
|
|
240
240
|
var ai = {
|
|
241
|
+
// The rate tables themselves, in cents per 1,000,000 tokens (`tools` in cents
|
|
242
|
+
// per request). Read-only surface for lib/pricing.js — the fee math above is
|
|
243
|
+
// still the only thing that should compute from them.
|
|
244
|
+
rates: { wholesale: cost, retail: pricing, tools: { wholesale: toolCost, retail: toolPricing } },
|
|
241
245
|
// Retail cents for a request (what the user pays).
|
|
242
246
|
price: priceForRequest,
|
|
243
247
|
// Wholesale cents for a request (what we pay the provider).
|
|
@@ -480,7 +484,13 @@ var action = {
|
|
|
480
484
|
}
|
|
481
485
|
};
|
|
482
486
|
export {
|
|
487
|
+
LOCAL_FLAT_CENTS,
|
|
483
488
|
MARKUP,
|
|
489
|
+
PREMIUM_CPM_CENTS,
|
|
490
|
+
PREMIUM_RATE_PER_GB,
|
|
491
|
+
REUSE_FLAT_CENTS,
|
|
492
|
+
STANDARD_CPM_CENTS,
|
|
493
|
+
STANDARD_RATE_PER_GB,
|
|
484
494
|
action,
|
|
485
495
|
ai,
|
|
486
496
|
balance,
|
package/dist/features.cjs
CHANGED
|
@@ -99,6 +99,11 @@ var connection = {
|
|
|
99
99
|
error: "Plan does not include Mailchimp connection",
|
|
100
100
|
feature: "Mailchimp connection"
|
|
101
101
|
},
|
|
102
|
+
sender: {
|
|
103
|
+
key: "organization:connection:sender",
|
|
104
|
+
error: "Plan does not include a verified sending domain",
|
|
105
|
+
feature: "Sending domain connection"
|
|
106
|
+
},
|
|
102
107
|
sendgrid: {
|
|
103
108
|
key: "organization:connection:sendgrid",
|
|
104
109
|
error: "Plan does not include SendGrid connection",
|
package/dist/features.d.cts
CHANGED
|
@@ -73,6 +73,11 @@ const connection = {
|
|
|
73
73
|
error : 'Plan does not include Mailchimp connection',
|
|
74
74
|
feature : 'Mailchimp connection'
|
|
75
75
|
},
|
|
76
|
+
sender : {
|
|
77
|
+
key : 'organization:connection:sender',
|
|
78
|
+
error : 'Plan does not include a verified sending domain',
|
|
79
|
+
feature : 'Sending domain connection'
|
|
80
|
+
},
|
|
76
81
|
sendgrid : {
|
|
77
82
|
key : 'organization:connection:sendgrid',
|
|
78
83
|
error : 'Plan does not include SendGrid connection',
|
package/dist/features.d.ts
CHANGED
|
@@ -73,6 +73,11 @@ const connection = {
|
|
|
73
73
|
error : 'Plan does not include Mailchimp connection',
|
|
74
74
|
feature : 'Mailchimp connection'
|
|
75
75
|
},
|
|
76
|
+
sender : {
|
|
77
|
+
key : 'organization:connection:sender',
|
|
78
|
+
error : 'Plan does not include a verified sending domain',
|
|
79
|
+
feature : 'Sending domain connection'
|
|
80
|
+
},
|
|
76
81
|
sendgrid : {
|
|
77
82
|
key : 'organization:connection:sendgrid',
|
|
78
83
|
error : 'Plan does not include SendGrid connection',
|
package/dist/features.js
CHANGED
|
@@ -71,6 +71,11 @@ var connection = {
|
|
|
71
71
|
error: "Plan does not include Mailchimp connection",
|
|
72
72
|
feature: "Mailchimp connection"
|
|
73
73
|
},
|
|
74
|
+
sender: {
|
|
75
|
+
key: "organization:connection:sender",
|
|
76
|
+
error: "Plan does not include a verified sending domain",
|
|
77
|
+
feature: "Sending domain connection"
|
|
78
|
+
},
|
|
74
79
|
sendgrid: {
|
|
75
80
|
key: "organization:connection:sendgrid",
|
|
76
81
|
error: "Plan does not include SendGrid connection",
|
package/dist/plans.cjs
CHANGED
|
@@ -32,7 +32,8 @@ __export(plans_exports, {
|
|
|
32
32
|
conversionRate: () => conversionRate,
|
|
33
33
|
free: () => free,
|
|
34
34
|
plans: () => plans,
|
|
35
|
-
resolvePlan: () => resolvePlan
|
|
35
|
+
resolvePlan: () => resolvePlan,
|
|
36
|
+
unitAmountDecimal: () => unitAmountDecimal
|
|
36
37
|
});
|
|
37
38
|
module.exports = __toCommonJS(plans_exports);
|
|
38
39
|
|
|
@@ -109,6 +110,11 @@ var connection = {
|
|
|
109
110
|
error: "Plan does not include Mailchimp connection",
|
|
110
111
|
feature: "Mailchimp connection"
|
|
111
112
|
},
|
|
113
|
+
sender: {
|
|
114
|
+
key: "organization:connection:sender",
|
|
115
|
+
error: "Plan does not include a verified sending domain",
|
|
116
|
+
feature: "Sending domain connection"
|
|
117
|
+
},
|
|
112
118
|
sendgrid: {
|
|
113
119
|
key: "organization:connection:sendgrid",
|
|
114
120
|
error: "Plan does not include SendGrid connection",
|
|
@@ -262,6 +268,15 @@ var featuresFor = (array = []) => Object.values({
|
|
|
262
268
|
},
|
|
263
269
|
{ denied: {}, granted: {} }
|
|
264
270
|
);
|
|
271
|
+
var overage = (actionCents) => ({
|
|
272
|
+
actionCents,
|
|
273
|
+
overages: { actions: String(actionCents) }
|
|
274
|
+
});
|
|
275
|
+
var unitAmountDecimal = (cents) => {
|
|
276
|
+
const value = Number(cents);
|
|
277
|
+
if (cents == null || !Number.isFinite(value)) throw new Error(`Invalid overage rate: ${cents}`);
|
|
278
|
+
return String(value);
|
|
279
|
+
};
|
|
265
280
|
var all = {
|
|
266
281
|
features: (array = []) => featuresFor([
|
|
267
282
|
connection.mailchimp.key,
|
|
@@ -322,7 +337,11 @@ var free = {
|
|
|
322
337
|
};
|
|
323
338
|
var plans = {
|
|
324
339
|
DB00002: {
|
|
325
|
-
|
|
340
|
+
// A verified sending domain is a PAID capability: free plans cannot send
|
|
341
|
+
// lead-facing email at all (the send path gates on an active
|
|
342
|
+
// subscription), so granting it there would offer a domain that can
|
|
343
|
+
// never send from.
|
|
344
|
+
features: all.features([connection.sender.key, organization.members.key]),
|
|
326
345
|
limits: all.limits({ actions: 5e3, members: 3, storage: gigabyte * 10 }),
|
|
327
346
|
marketing: {
|
|
328
347
|
description: "Tools to fine-tune campaigns and improve lead quality.",
|
|
@@ -337,12 +356,13 @@ var plans = {
|
|
|
337
356
|
["Storage", "10GB"]
|
|
338
357
|
]
|
|
339
358
|
},
|
|
340
|
-
|
|
359
|
+
...overage(2.5),
|
|
341
360
|
title: "Starter",
|
|
342
361
|
conversion: 2
|
|
343
362
|
},
|
|
344
363
|
DB00003: {
|
|
345
364
|
features: all.features([
|
|
365
|
+
connection.sender.key,
|
|
346
366
|
organization.advertisements.key,
|
|
347
367
|
organization.analytics.key,
|
|
348
368
|
organization.members.key,
|
|
@@ -367,12 +387,13 @@ var plans = {
|
|
|
367
387
|
["Storage", "20GB"]
|
|
368
388
|
]
|
|
369
389
|
},
|
|
370
|
-
|
|
390
|
+
...overage(2),
|
|
371
391
|
title: "Pro",
|
|
372
392
|
conversion: 1.5
|
|
373
393
|
},
|
|
374
394
|
DB00004: {
|
|
375
395
|
features: all.features([
|
|
396
|
+
connection.sender.key,
|
|
376
397
|
organization.advertisements.key,
|
|
377
398
|
organization.analytics.key,
|
|
378
399
|
organization.members.key,
|
|
@@ -397,12 +418,13 @@ var plans = {
|
|
|
397
418
|
["Storage", "50GB"]
|
|
398
419
|
]
|
|
399
420
|
},
|
|
400
|
-
|
|
421
|
+
...overage(1.85),
|
|
401
422
|
title: "Premium",
|
|
402
423
|
conversion: 1
|
|
403
424
|
},
|
|
404
425
|
DB00005: {
|
|
405
426
|
features: all.features([
|
|
427
|
+
connection.sender.key,
|
|
406
428
|
organization.advertisements.key,
|
|
407
429
|
organization.analytics.key,
|
|
408
430
|
organization.members.key,
|
|
@@ -427,7 +449,7 @@ var plans = {
|
|
|
427
449
|
["Storage", "100GB"]
|
|
428
450
|
]
|
|
429
451
|
},
|
|
430
|
-
|
|
452
|
+
...overage(1.5),
|
|
431
453
|
title: "Elite",
|
|
432
454
|
conversion: 0.5
|
|
433
455
|
}
|
|
@@ -443,8 +465,17 @@ var resolvePlan = (subscription) => {
|
|
|
443
465
|
// infinite when a deal does not name it.
|
|
444
466
|
conversion: custom.conversion ?? free.conversion,
|
|
445
467
|
custom: true,
|
|
446
|
-
|
|
468
|
+
// A custom plan is a negotiated PAID deal, so it carries the paid-tier
|
|
469
|
+
// baseline whether or not the deal thought to name it. Today that is the
|
|
470
|
+
// sending domain: every catalog paid tier grants it, and a custom plan
|
|
471
|
+
// silently lacking it would be a support ticket, not a pricing decision.
|
|
472
|
+
features: all.features([connection.sender.key, ...((_a = custom.features) == null ? void 0 : _a.granted) || []]),
|
|
447
473
|
limits: all.limits(((_b = custom.limits) == null ? void 0 : _b.organization) || {}),
|
|
474
|
+
// A custom plan stores its overage BARE on `custom.overages` — a different
|
|
475
|
+
// shape from the catalog's nested one. Number() so a deal stored as a string
|
|
476
|
+
// still resolves to cents-per-action; an unnamed overage stays undefined
|
|
477
|
+
// (it bills nothing) rather than becoming NaN.
|
|
478
|
+
actionCents: custom.overages == null ? void 0 : Number(custom.overages),
|
|
448
479
|
overages: { actions: custom.overages },
|
|
449
480
|
title: custom.title || "Custom"
|
|
450
481
|
};
|
|
@@ -458,5 +489,6 @@ var conversionRate = (subscription) => {
|
|
|
458
489
|
conversionRate,
|
|
459
490
|
free,
|
|
460
491
|
plans,
|
|
461
|
-
resolvePlan
|
|
492
|
+
resolvePlan,
|
|
493
|
+
unitAmountDecimal
|
|
462
494
|
});
|
package/dist/plans.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { organization, page,
|
|
1
|
+
import { connection, organization, page, fields, field } from './features.cjs';
|
|
2
2
|
import { infinite, gigabyte } from './index.cjs';
|
|
3
3
|
import 'currency-codes';
|
|
4
4
|
import 'nanoid';
|
|
@@ -31,6 +31,40 @@ const featuresFor = ( array = [] ) => Object.values({
|
|
|
31
31
|
{ denied : {}, granted : {} }
|
|
32
32
|
);
|
|
33
33
|
|
|
34
|
+
// Overage pricing, unit-explicit.
|
|
35
|
+
//
|
|
36
|
+
// `actionCents` is CENTS PER ACTION, as a number: 2.5 = $0.025. Before
|
|
37
|
+
// 2026-05-31 the same rate was encoded as dollars-per-1,000-actions, Stripe read
|
|
38
|
+
// it as per-action, and every paid tier billed 10x. `overages.actions` — the
|
|
39
|
+
// string this used to be the only name for — is kept as a byte-identical alias
|
|
40
|
+
// so consumers can be pin-bumped in any order; drop it once they all read
|
|
41
|
+
// `actionCents`.
|
|
42
|
+
//
|
|
43
|
+
// Sub-cent precision is deliberate: Premium's 1.85¢ does not fit integer cents
|
|
44
|
+
// (2¢ is +8%, 1¢ is −46%).
|
|
45
|
+
const overage = ( actionCents ) => ({
|
|
46
|
+
actionCents,
|
|
47
|
+
overages : { actions : String( actionCents ) }
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Cents → Stripe's `unit_amount_decimal`.
|
|
51
|
+
//
|
|
52
|
+
// The Stripe boundary MUST use `unit_amount_decimal`, not `unit_amount`:
|
|
53
|
+
// standalone invoice items reject a top-level `unit_amount` with "Received
|
|
54
|
+
// unknown parameter", which is what left invoice drafts stuck on 2026-08-06.
|
|
55
|
+
// It is a decimal string of CENTS, so 2.5 → '2.5' — no scaling, no rounding.
|
|
56
|
+
const unitAmountDecimal = ( cents ) => {
|
|
57
|
+
|
|
58
|
+
const value = Number( cents );
|
|
59
|
+
|
|
60
|
+
// `cents == null` is checked separately because Number( null ) is 0 — an
|
|
61
|
+
// unnamed rate must fail loudly, not silently bill nothing.
|
|
62
|
+
if( cents == null || ! Number.isFinite( value ) ) throw new Error( `Invalid overage rate: ${ cents }` );
|
|
63
|
+
|
|
64
|
+
return String( value );
|
|
65
|
+
|
|
66
|
+
};
|
|
67
|
+
|
|
34
68
|
const all = {
|
|
35
69
|
features : ( array = [] ) => featuresFor([
|
|
36
70
|
connection.mailchimp.key,
|
|
@@ -93,7 +127,11 @@ const free = {
|
|
|
93
127
|
|
|
94
128
|
const plans = {
|
|
95
129
|
DB00002 : {
|
|
96
|
-
|
|
130
|
+
// A verified sending domain is a PAID capability: free plans cannot send
|
|
131
|
+
// lead-facing email at all (the send path gates on an active
|
|
132
|
+
// subscription), so granting it there would offer a domain that can
|
|
133
|
+
// never send from.
|
|
134
|
+
features : all.features([ connection.sender.key, organization.members.key ]),
|
|
97
135
|
limits : all.limits({ actions : 5000, members : 3, storage : gigabyte * 10 }),
|
|
98
136
|
marketing : {
|
|
99
137
|
description : 'Tools to fine-tune campaigns and improve lead quality.',
|
|
@@ -108,12 +146,13 @@ const plans = {
|
|
|
108
146
|
[ 'Storage', '10GB' ]
|
|
109
147
|
]
|
|
110
148
|
},
|
|
111
|
-
|
|
149
|
+
...overage( 2.5 ),
|
|
112
150
|
title : 'Starter',
|
|
113
151
|
conversion : 2
|
|
114
152
|
},
|
|
115
153
|
DB00003 : {
|
|
116
154
|
features : all.features([
|
|
155
|
+
connection.sender.key,
|
|
117
156
|
organization.advertisements.key,
|
|
118
157
|
organization.analytics.key,
|
|
119
158
|
organization.members.key,
|
|
@@ -138,12 +177,13 @@ const plans = {
|
|
|
138
177
|
[ 'Storage', '20GB' ]
|
|
139
178
|
]
|
|
140
179
|
},
|
|
141
|
-
|
|
180
|
+
...overage( 2 ),
|
|
142
181
|
title : 'Pro',
|
|
143
182
|
conversion : 1.5
|
|
144
183
|
},
|
|
145
184
|
DB00004 : {
|
|
146
185
|
features : all.features([
|
|
186
|
+
connection.sender.key,
|
|
147
187
|
organization.advertisements.key,
|
|
148
188
|
organization.analytics.key,
|
|
149
189
|
organization.members.key,
|
|
@@ -168,12 +208,13 @@ const plans = {
|
|
|
168
208
|
[ 'Storage', '50GB' ]
|
|
169
209
|
]
|
|
170
210
|
},
|
|
171
|
-
|
|
211
|
+
...overage( 1.85 ),
|
|
172
212
|
title : 'Premium',
|
|
173
213
|
conversion : 1
|
|
174
214
|
},
|
|
175
215
|
DB00005 : {
|
|
176
216
|
features : all.features([
|
|
217
|
+
connection.sender.key,
|
|
177
218
|
organization.advertisements.key,
|
|
178
219
|
organization.analytics.key,
|
|
179
220
|
organization.members.key,
|
|
@@ -198,7 +239,7 @@ const plans = {
|
|
|
198
239
|
[ 'Storage', '100GB' ]
|
|
199
240
|
]
|
|
200
241
|
},
|
|
201
|
-
|
|
242
|
+
...overage( 1.5 ),
|
|
202
243
|
title : 'Elite',
|
|
203
244
|
conversion : 0.5
|
|
204
245
|
},
|
|
@@ -230,8 +271,17 @@ const resolvePlan = ( subscription ) => {
|
|
|
230
271
|
// infinite when a deal does not name it.
|
|
231
272
|
conversion : custom.conversion ?? free.conversion,
|
|
232
273
|
custom : true,
|
|
233
|
-
|
|
274
|
+
// A custom plan is a negotiated PAID deal, so it carries the paid-tier
|
|
275
|
+
// baseline whether or not the deal thought to name it. Today that is the
|
|
276
|
+
// sending domain: every catalog paid tier grants it, and a custom plan
|
|
277
|
+
// silently lacking it would be a support ticket, not a pricing decision.
|
|
278
|
+
features : all.features([ connection.sender.key, ...( custom.features?.granted || [] ) ]),
|
|
234
279
|
limits : all.limits( custom.limits?.organization || {} ),
|
|
280
|
+
// A custom plan stores its overage BARE on `custom.overages` — a different
|
|
281
|
+
// shape from the catalog's nested one. Number() so a deal stored as a string
|
|
282
|
+
// still resolves to cents-per-action; an unnamed overage stays undefined
|
|
283
|
+
// (it bills nothing) rather than becoming NaN.
|
|
284
|
+
actionCents : custom.overages == null ? undefined : Number( custom.overages ),
|
|
235
285
|
overages : { actions : custom.overages },
|
|
236
286
|
title : custom.title || 'Custom'
|
|
237
287
|
};
|
|
@@ -250,4 +300,4 @@ const conversionRate = ( subscription ) =>
|
|
|
250
300
|
resolvePlan( subscription )?.conversion
|
|
251
301
|
?? free.conversion;
|
|
252
302
|
|
|
253
|
-
export { conversionRate, free, plans, resolvePlan };
|
|
303
|
+
export { conversionRate, free, plans, resolvePlan, unitAmountDecimal };
|
package/dist/plans.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { organization, page,
|
|
1
|
+
import { connection, organization, page, fields, field } from './features.js';
|
|
2
2
|
import { infinite, gigabyte } from './index.js';
|
|
3
3
|
import 'currency-codes';
|
|
4
4
|
import 'nanoid';
|
|
@@ -31,6 +31,40 @@ const featuresFor = ( array = [] ) => Object.values({
|
|
|
31
31
|
{ denied : {}, granted : {} }
|
|
32
32
|
);
|
|
33
33
|
|
|
34
|
+
// Overage pricing, unit-explicit.
|
|
35
|
+
//
|
|
36
|
+
// `actionCents` is CENTS PER ACTION, as a number: 2.5 = $0.025. Before
|
|
37
|
+
// 2026-05-31 the same rate was encoded as dollars-per-1,000-actions, Stripe read
|
|
38
|
+
// it as per-action, and every paid tier billed 10x. `overages.actions` — the
|
|
39
|
+
// string this used to be the only name for — is kept as a byte-identical alias
|
|
40
|
+
// so consumers can be pin-bumped in any order; drop it once they all read
|
|
41
|
+
// `actionCents`.
|
|
42
|
+
//
|
|
43
|
+
// Sub-cent precision is deliberate: Premium's 1.85¢ does not fit integer cents
|
|
44
|
+
// (2¢ is +8%, 1¢ is −46%).
|
|
45
|
+
const overage = ( actionCents ) => ({
|
|
46
|
+
actionCents,
|
|
47
|
+
overages : { actions : String( actionCents ) }
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Cents → Stripe's `unit_amount_decimal`.
|
|
51
|
+
//
|
|
52
|
+
// The Stripe boundary MUST use `unit_amount_decimal`, not `unit_amount`:
|
|
53
|
+
// standalone invoice items reject a top-level `unit_amount` with "Received
|
|
54
|
+
// unknown parameter", which is what left invoice drafts stuck on 2026-08-06.
|
|
55
|
+
// It is a decimal string of CENTS, so 2.5 → '2.5' — no scaling, no rounding.
|
|
56
|
+
const unitAmountDecimal = ( cents ) => {
|
|
57
|
+
|
|
58
|
+
const value = Number( cents );
|
|
59
|
+
|
|
60
|
+
// `cents == null` is checked separately because Number( null ) is 0 — an
|
|
61
|
+
// unnamed rate must fail loudly, not silently bill nothing.
|
|
62
|
+
if( cents == null || ! Number.isFinite( value ) ) throw new Error( `Invalid overage rate: ${ cents }` );
|
|
63
|
+
|
|
64
|
+
return String( value );
|
|
65
|
+
|
|
66
|
+
};
|
|
67
|
+
|
|
34
68
|
const all = {
|
|
35
69
|
features : ( array = [] ) => featuresFor([
|
|
36
70
|
connection.mailchimp.key,
|
|
@@ -93,7 +127,11 @@ const free = {
|
|
|
93
127
|
|
|
94
128
|
const plans = {
|
|
95
129
|
DB00002 : {
|
|
96
|
-
|
|
130
|
+
// A verified sending domain is a PAID capability: free plans cannot send
|
|
131
|
+
// lead-facing email at all (the send path gates on an active
|
|
132
|
+
// subscription), so granting it there would offer a domain that can
|
|
133
|
+
// never send from.
|
|
134
|
+
features : all.features([ connection.sender.key, organization.members.key ]),
|
|
97
135
|
limits : all.limits({ actions : 5000, members : 3, storage : gigabyte * 10 }),
|
|
98
136
|
marketing : {
|
|
99
137
|
description : 'Tools to fine-tune campaigns and improve lead quality.',
|
|
@@ -108,12 +146,13 @@ const plans = {
|
|
|
108
146
|
[ 'Storage', '10GB' ]
|
|
109
147
|
]
|
|
110
148
|
},
|
|
111
|
-
|
|
149
|
+
...overage( 2.5 ),
|
|
112
150
|
title : 'Starter',
|
|
113
151
|
conversion : 2
|
|
114
152
|
},
|
|
115
153
|
DB00003 : {
|
|
116
154
|
features : all.features([
|
|
155
|
+
connection.sender.key,
|
|
117
156
|
organization.advertisements.key,
|
|
118
157
|
organization.analytics.key,
|
|
119
158
|
organization.members.key,
|
|
@@ -138,12 +177,13 @@ const plans = {
|
|
|
138
177
|
[ 'Storage', '20GB' ]
|
|
139
178
|
]
|
|
140
179
|
},
|
|
141
|
-
|
|
180
|
+
...overage( 2 ),
|
|
142
181
|
title : 'Pro',
|
|
143
182
|
conversion : 1.5
|
|
144
183
|
},
|
|
145
184
|
DB00004 : {
|
|
146
185
|
features : all.features([
|
|
186
|
+
connection.sender.key,
|
|
147
187
|
organization.advertisements.key,
|
|
148
188
|
organization.analytics.key,
|
|
149
189
|
organization.members.key,
|
|
@@ -168,12 +208,13 @@ const plans = {
|
|
|
168
208
|
[ 'Storage', '50GB' ]
|
|
169
209
|
]
|
|
170
210
|
},
|
|
171
|
-
|
|
211
|
+
...overage( 1.85 ),
|
|
172
212
|
title : 'Premium',
|
|
173
213
|
conversion : 1
|
|
174
214
|
},
|
|
175
215
|
DB00005 : {
|
|
176
216
|
features : all.features([
|
|
217
|
+
connection.sender.key,
|
|
177
218
|
organization.advertisements.key,
|
|
178
219
|
organization.analytics.key,
|
|
179
220
|
organization.members.key,
|
|
@@ -198,7 +239,7 @@ const plans = {
|
|
|
198
239
|
[ 'Storage', '100GB' ]
|
|
199
240
|
]
|
|
200
241
|
},
|
|
201
|
-
|
|
242
|
+
...overage( 1.5 ),
|
|
202
243
|
title : 'Elite',
|
|
203
244
|
conversion : 0.5
|
|
204
245
|
},
|
|
@@ -230,8 +271,17 @@ const resolvePlan = ( subscription ) => {
|
|
|
230
271
|
// infinite when a deal does not name it.
|
|
231
272
|
conversion : custom.conversion ?? free.conversion,
|
|
232
273
|
custom : true,
|
|
233
|
-
|
|
274
|
+
// A custom plan is a negotiated PAID deal, so it carries the paid-tier
|
|
275
|
+
// baseline whether or not the deal thought to name it. Today that is the
|
|
276
|
+
// sending domain: every catalog paid tier grants it, and a custom plan
|
|
277
|
+
// silently lacking it would be a support ticket, not a pricing decision.
|
|
278
|
+
features : all.features([ connection.sender.key, ...( custom.features?.granted || [] ) ]),
|
|
234
279
|
limits : all.limits( custom.limits?.organization || {} ),
|
|
280
|
+
// A custom plan stores its overage BARE on `custom.overages` — a different
|
|
281
|
+
// shape from the catalog's nested one. Number() so a deal stored as a string
|
|
282
|
+
// still resolves to cents-per-action; an unnamed overage stays undefined
|
|
283
|
+
// (it bills nothing) rather than becoming NaN.
|
|
284
|
+
actionCents : custom.overages == null ? undefined : Number( custom.overages ),
|
|
235
285
|
overages : { actions : custom.overages },
|
|
236
286
|
title : custom.title || 'Custom'
|
|
237
287
|
};
|
|
@@ -250,4 +300,4 @@ const conversionRate = ( subscription ) =>
|
|
|
250
300
|
resolvePlan( subscription )?.conversion
|
|
251
301
|
?? free.conversion;
|
|
252
302
|
|
|
253
|
-
export { conversionRate, free, plans, resolvePlan };
|
|
303
|
+
export { conversionRate, free, plans, resolvePlan, unitAmountDecimal };
|