@happyvertical/smrt-subscriptions 0.51.21 → 0.51.25
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/AGENTS.md +29 -1
- package/README.md +126 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1067 -94
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1758 -13
- package/dist/models/commercial.d.ts +61 -1
- package/dist/models/commercial.d.ts.map +1 -1
- package/dist/models/index.d.ts +3 -1
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/reseller.d.ts +107 -0
- package/dist/models/reseller.d.ts.map +1 -0
- package/dist/services/commercial.d.ts +147 -6
- package/dist/services/commercial.d.ts.map +1 -1
- package/dist/services/index.d.ts +3 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/reseller-errors.d.ts +7 -0
- package/dist/services/reseller-errors.d.ts.map +1 -0
- package/dist/services/reseller.d.ts +154 -0
- package/dist/services/reseller.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +962 -183
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -1
- package/package.json +6 -6
package/AGENTS.md
CHANGED
|
@@ -20,7 +20,8 @@ that reverted to DECIMAL passes every SQLite suite.
|
|
|
20
20
|
## Money
|
|
21
21
|
|
|
22
22
|
- **Money is integer minor units** (cents) — `$19.99` is `1999`.
|
|
23
|
-
`SubscriptionPlan.priceAmount`, `ClientCharge.amount`,
|
|
23
|
+
`SubscriptionPlan.priceAmount`, `ClientCharge.amount`, `RetailCharge.amount`,
|
|
24
|
+
`CreditGrant.amount`,
|
|
24
25
|
`BillingAdjustment.amount` and `SpendingPolicy.limitAmount` all initialize
|
|
25
26
|
`= 0`, never `= 0.0`: the integer literal is what maps them to INTEGER
|
|
26
27
|
columns (BIGINT on fresh PostgreSQL/DuckDB databases; #2401, #2373). `ClientCharge.quantity` and `TenantUsageMetric.quantity` are
|
|
@@ -63,6 +64,33 @@ that reverted to DECIMAL passes every SQLite suite.
|
|
|
63
64
|
are BIGINT, and hydration rejects values outside JavaScript's safe-integer
|
|
64
65
|
range. Existing PostgreSQL `int4` columns require the explicit widening in #2424.
|
|
65
66
|
|
|
67
|
+
## Reseller billing (#3059)
|
|
68
|
+
|
|
69
|
+
- Billing ownership is consumed from `smrt-tenancy`'s
|
|
70
|
+
`BillingRelationshipService` (`BillingRelationshipReader`); never re-derive
|
|
71
|
+
parentage or owners here.
|
|
72
|
+
- `ClientCharge` stays one row per usage event (`usage_event_id` unique) and
|
|
73
|
+
`tenantId` is always the payer. That uniqueness is the double-billing guard
|
|
74
|
+
across owner changes; do not widen its conflict key. The retail leg lives in
|
|
75
|
+
`RetailCharge` (also one per event), written in the same transaction.
|
|
76
|
+
- Price-book rules are `PricingRule` rows with `priceBookId`; `price()` must
|
|
77
|
+
keep ignoring them, and `rateUsage()` only trusts rules whose tenant owns the
|
|
78
|
+
book.
|
|
79
|
+
- Delegated `SpendingPolicy` rows (`setByTenantId`) are guarded in
|
|
80
|
+
`validateBeforeSave` and a `beforeDelete` hook, by id and by conflict key;
|
|
81
|
+
`CreditGrant` applies the same parent-only rule to delegated balances.
|
|
82
|
+
`PriceBookAssignmentCollection` is deliberately not a root export.
|
|
83
|
+
- `ResellerBillingService` authorizes first, then writes in a system context
|
|
84
|
+
(parent writes land on child-owned rows). The wholesale leg is authorized by
|
|
85
|
+
the book's publisher and may never be published by the payer; whether a
|
|
86
|
+
publisher is a legitimate provider for the reseller is a host obligation
|
|
87
|
+
(the model records no upstream seller). Relationship reads precede
|
|
88
|
+
authorization and stay under smrt-tenancy's own read rules. The only
|
|
89
|
+
cross-tenant read in evaluation is the bounded `wholesale`-basis sum.
|
|
90
|
+
- `balance` policies take their limit from `CreditGrant`s; `limitAmount` must
|
|
91
|
+
be 0, and currency and period are fixed once saved (the ledger is scoped to
|
|
92
|
+
them). Auto top-up is a host hook only — no payment provider calls.
|
|
93
|
+
|
|
66
94
|
## Notes
|
|
67
95
|
|
|
68
96
|
- Keep Stripe-specific fields as provider binding metadata. Runtime Stripe API
|
package/README.md
CHANGED
|
@@ -52,6 +52,9 @@ subscription and plan.
|
|
|
52
52
|
commercial evidence.
|
|
53
53
|
- `SpendingPolicyEvaluator` applies scoped budget behavior without embedding a
|
|
54
54
|
payment provider.
|
|
55
|
+
- `PriceBook`, `PriceBookAssignment`, `RetailCharge`, and `CreditGrant` add
|
|
56
|
+
reseller pricing, the reseller's retail ledger, and prepaid credit (see
|
|
57
|
+
[Reseller billing](#reseller-billing)).
|
|
55
58
|
|
|
56
59
|
Subscribers are polymorphic: tenant subscribers use a tenant ID, while external
|
|
57
60
|
subscribers add a stable external discriminator. Utilities such as
|
|
@@ -68,6 +71,128 @@ Commercial usage records client-facing prices separately from usage evidence.
|
|
|
68
71
|
Spending policy decisions can allow, warn, require approval, or deny based on
|
|
69
72
|
the configured behavior and scope.
|
|
70
73
|
|
|
74
|
+
## Reseller billing
|
|
75
|
+
|
|
76
|
+
Resellers buy a service at wholesale and resell it to their own child tenants.
|
|
77
|
+
Parentage and the billing owner come from `smrt-tenancy`'s
|
|
78
|
+
`BillingRelationshipService`; this package prices usage against it.
|
|
79
|
+
|
|
80
|
+
- **Price books.** A `PriceBook` is published by one seller tenant and is
|
|
81
|
+
either `wholesale` (what a provider charges a reseller) or `retail` (what a
|
|
82
|
+
reseller charges its children). Its prices are ordinary `PricingRule` rows
|
|
83
|
+
carrying the book's `priceBookId`, one row per currency.
|
|
84
|
+
- **Assignment per relationship.** `ResellerBillingService.assignPriceBooks()`
|
|
85
|
+
picks, for one child, the wholesale book and currency and (optionally) the
|
|
86
|
+
reseller's retail book and currency. An assignment made under one reseller
|
|
87
|
+
never applies after the child moves to another.
|
|
88
|
+
- **Rating.** `CommercialUsageService.rateUsage()` resolves the child's
|
|
89
|
+
relationship and returns a `UsageRating`:
|
|
90
|
+
- self-billed or unrelated tenant: one direct `ClientCharge`, priced exactly
|
|
91
|
+
like `price()`;
|
|
92
|
+
- reseller-billed: a wholesale `ClientCharge` payable by the reseller
|
|
93
|
+
(`tenantId` = reseller, `usageTenantId` = child) and, with a retail book,
|
|
94
|
+
a `RetailCharge` the child owes the reseller, committed together.
|
|
95
|
+
|
|
96
|
+
A usage event is rated once. `ClientCharge` keeps exactly one row per usage
|
|
97
|
+
event, so a later billing-owner change returns the original rating instead
|
|
98
|
+
of charging a second payer.
|
|
99
|
+
- **Delegated spending.** `setDelegatedSpendingPolicy()` writes a
|
|
100
|
+
`SpendingPolicy` on the child with `setByTenantId` = the parent. The ordinary
|
|
101
|
+
`SpendingPolicyEvaluator` enforces it (observe, warn, approval, block), and
|
|
102
|
+
the child cannot edit, delete, or overwrite it. Parents change or retire
|
|
103
|
+
(`active: false`) their policies through the same method. A policy's `basis` selects
|
|
104
|
+
what it counts: `billed` (what the tenant pays the provider, the default),
|
|
105
|
+
`retail` (what it owes its reseller), or `wholesale` (what the parent pays
|
|
106
|
+
for this child's usage).
|
|
107
|
+
- **Prepaid credit.** A `period: 'balance'` policy's limit is the sum of its
|
|
108
|
+
`CreditGrant`s; spend accumulates from `balanceFrom`. Its currency and
|
|
109
|
+
period cannot change once saved; create a new balance instead. Grants are append-only (updates and deletes are refused)
|
|
110
|
+
and idempotent per `source`/`sourceId`. Pass `autoTopUp` to
|
|
111
|
+
`SpendingPolicyEvaluator.create()` to be called when a charge would exhaust a
|
|
112
|
+
balance; return a grant once the host has secured the funds. The evaluator
|
|
113
|
+
never calls a payment provider.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { BillingRelationshipService } from '@happyvertical/smrt-tenancy';
|
|
117
|
+
import {
|
|
118
|
+
CommercialUsageService,
|
|
119
|
+
ResellerBillingService,
|
|
120
|
+
} from '@happyvertical/smrt-subscriptions';
|
|
121
|
+
|
|
122
|
+
const relationships = await BillingRelationshipService.create({
|
|
123
|
+
db,
|
|
124
|
+
tenantExists,
|
|
125
|
+
});
|
|
126
|
+
const reseller = await ResellerBillingService.create({
|
|
127
|
+
db,
|
|
128
|
+
billingRelationships: relationships,
|
|
129
|
+
});
|
|
130
|
+
await reseller.definePrice({
|
|
131
|
+
priceBookId: retailBook.id,
|
|
132
|
+
ruleKey: 'tokens',
|
|
133
|
+
metricKey: 'ai.tokens',
|
|
134
|
+
strategy: 'fixed_unit',
|
|
135
|
+
prices: [
|
|
136
|
+
{ currency: 'USD', terms: { unitPrice: 5 } },
|
|
137
|
+
{ currency: 'EUR', terms: { unitPrice: 4.6 } },
|
|
138
|
+
],
|
|
139
|
+
});
|
|
140
|
+
await reseller.assignPriceBooks({
|
|
141
|
+
childTenantId,
|
|
142
|
+
wholesale: { priceBookId: wholesaleBook.id, currency: 'USD' },
|
|
143
|
+
retail: { priceBookId: retailBook.id, currency: 'EUR' },
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const rating = await commercial.rateUsage({
|
|
147
|
+
usageEventId,
|
|
148
|
+
billingRelationships: relationships,
|
|
149
|
+
approved: true,
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Mutations require a system context, a super-admin bypass, or a host
|
|
154
|
+
`authorize` callback, like `BillingRelationshipService`; a seller may also
|
|
155
|
+
define prices in its own book under its own tenant context. The callback
|
|
156
|
+
receives the tenant whose authority the action exercises: the book's publisher
|
|
157
|
+
for `define_prices` and `assign_wholesale_price_book`, the parent for
|
|
158
|
+
`assign_retail_price_book` and `manage_child_spending`. Bind every decision to
|
|
159
|
+
the caller, and never authorize a reseller for the wholesale leg that charges
|
|
160
|
+
it. The package refuses a wholesale book published by the reseller or child it
|
|
161
|
+
would charge, but whether a publisher is a legitimate provider for that
|
|
162
|
+
reseller (`resellerTenantId` in the request) is the host's decision. The
|
|
163
|
+
calls first read the child's relationship through `BillingRelationshipService`
|
|
164
|
+
in the caller's context, so a provider assigning a wholesale leg also needs
|
|
165
|
+
that service's `authorize` to grant it `read` for the child. Once
|
|
166
|
+
authorized, the service writes in a system context, because a parent's writes
|
|
167
|
+
land on rows the child owns.
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
const reseller = await ResellerBillingService.create({
|
|
171
|
+
db,
|
|
172
|
+
billingRelationships: relationships,
|
|
173
|
+
authorize: async ({ action, tenantId, resellerTenantId }) => {
|
|
174
|
+
const caller = currentTenantId();
|
|
175
|
+
if (caller !== tenantId || !(await isBillingAdmin(caller))) return false;
|
|
176
|
+
return action === 'assign_wholesale_price_book'
|
|
177
|
+
? isProviderFor(caller, resellerTenantId)
|
|
178
|
+
: true;
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Rating reads a seller's books across tenants, so run it where the host's
|
|
184
|
+
tenancy rules allow that (typically a system context). Spending evaluation can
|
|
185
|
+
run in the child's own tenant context: a `wholesale`-basis policy reads only
|
|
186
|
+
the sum of the parent's charges for that child's usage. Pass
|
|
187
|
+
`billingRelationships` to `SpendingPolicyEvaluator.create()` so a former
|
|
188
|
+
parent's delegated policies stop applying after the child changes reseller;
|
|
189
|
+
without it they keep applying (fail closed), and the new parent may replace
|
|
190
|
+
them by name. A delegated balance counts only credit granted by its current
|
|
191
|
+
parent. A former parent's prepaid balance cannot be taken over by name; it
|
|
192
|
+
keeps its credit ledger (reversible in a system context), and the new parent
|
|
193
|
+
creates its own balance. A parent's `retail` cap counts only what the child owes that
|
|
194
|
+
parent.
|
|
195
|
+
|
|
71
196
|
## Svelte entry point
|
|
72
197
|
|
|
73
198
|
`@happyvertical/smrt-subscriptions/svelte` exports provider-neutral,
|
|
@@ -84,6 +209,7 @@ Hosts own data loading and mutation actions.
|
|
|
84
209
|
|
|
85
210
|
```bash
|
|
86
211
|
pnpm --filter @happyvertical/smrt-subscriptions test
|
|
212
|
+
pnpm --filter @happyvertical/smrt-subscriptions test:postgres
|
|
87
213
|
pnpm --filter @happyvertical/smrt-subscriptions typecheck
|
|
88
214
|
pnpm --filter @happyvertical/smrt-subscriptions build
|
|
89
215
|
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export { SubscriptionPlanCollection, TenantSubscriptionCollection, TenantUsageMetricCollection, } from './collections/index.js';
|
|
2
2
|
export { migrateSubscriptionsMoneyToMinorUnits, preflightSubscriptionsMoneyMinorUnits, SUBSCRIPTIONS_MONEY_COLUMNS, SUBSCRIPTIONS_MONEY_MINOR_UNITS_BACKFILL, } from './migrations/moneyMinorUnits.js';
|
|
3
|
-
export type { PricingStrategy, SpendingPeriod, SpendingPolicyBehavior, } from './models/index.js';
|
|
4
|
-
export { BillingAdjustment, BillingAdjustmentCollection, ClientCharge, ClientChargeCollection, PricingRule, PricingRuleCollection, SpendingPolicy, SpendingPolicyCollection, SubscriptionPlan, TenantSubscription, TenantUsageMetric, } from './models/index.js';
|
|
5
|
-
export type { CommercialBillingStorage, CommercialUsageServiceOptions, CustomPricingContext, CustomPricingStrategy, PriceUsageOptions, SpendingDecision, } from './services/index.js';
|
|
6
|
-
export { assertCommercialBillingStorageSupported, CommercialBillingStorageConfigurationError, CommercialUsageService, evaluateThreshold, evaluateThresholds, SpendingPolicyEvaluator, type SubscriptionPlanReader, SubscriptionResolver, type SubscriptionResolverReaders, type TenantSubscriptionReader, TenantUsageMeter, UnsupportedCommercialBillingStorageError, type UsageSummaryReader, } from './services/index.js';
|
|
3
|
+
export type { PriceBookKind, PricingStrategy, SpendingBasis, SpendingPeriod, SpendingPolicyBehavior, } from './models/index.js';
|
|
4
|
+
export { BillingAdjustment, BillingAdjustmentCollection, ClientCharge, ClientChargeCollection, CreditGrant, CreditGrantCollection, isCurrencyCode, isPriceBookKind, PriceBook, PriceBookAssignment, PriceBookCollection, PricingRule, PricingRuleCollection, RetailCharge, RetailChargeCollection, SpendingPolicy, SpendingPolicyCollection, SubscriptionPlan, TenantSubscription, TenantUsageMetric, } from './models/index.js';
|
|
5
|
+
export type { AssignPriceBooksInput, AutoTopUpGrant, AutoTopUpHook, AutoTopUpRequest, BillingRelationshipReader, CommercialBillingStorage, CommercialUsageServiceOptions, CustomPricingContext, CustomPricingStrategy, DefinePriceBookPriceInput, DelegatedSpendingPolicyInput, GrantChildCreditInput, GrantCreditInput, PriceBookAssignmentView, PriceBookLeg, PriceUsageOptions, RateUsageOptions, ResellerBillingAction, ResellerBillingErrorCode, ResellerBillingServiceOptions, SpendingDecision, SpendingEvaluationInput, SpendingPolicyEvaluatorExtensions, SpendingPolicyEvaluatorOptions, UsageRating, } from './services/index.js';
|
|
6
|
+
export { assertCommercialBillingStorageSupported, CommercialBillingStorageConfigurationError, CommercialUsageService, evaluateThreshold, evaluateThresholds, ResellerBillingError, ResellerBillingService, SpendingPolicyEvaluator, type SubscriptionPlanReader, SubscriptionResolver, type SubscriptionResolverReaders, type TenantSubscriptionReader, TenantUsageMeter, UnsupportedCommercialBillingStorageError, type UsageSummaryReader, } from './services/index.js';
|
|
7
7
|
export type { AiUsageSummary, BillingInterval, EntitlementResolution, EntitlementResolutionContext, JsonObject, PlanFeatureGrant, PlanThreshold, RecordUsageOptions, Subscriber, SubscriberKind, SubscriptionPlanStatus, SubscriptionResolverOptions, SubscriptionStatus, SummarizeAiUsageOptions, SummarizeUsageBatchOptions, SummarizeUsageOptions, ThresholdEnforcement, ThresholdEvaluation, ThresholdWindow, UsageMeterOptions, UsageMetricRecord, UsageSummary, UsageWindow, } from './types.js';
|
|
8
8
|
export { assertSubscriberInvariant, getWindowForThreshold, getWindowKey, isValidThreshold, normalizeFeatureGrants, normalizeSubscriber, subscriberToColumns, } from './utils.js';
|
|
9
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,wBAAwB,CAAC;AAEhC,OAAO,EACL,0BAA0B,EAC1B,4BAA4B,EAC5B,2BAA2B,GAC5B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,qCAAqC,EACrC,qCAAqC,EACrC,2BAA2B,EAC3B,wCAAwC,GACzC,MAAM,iCAAiC,CAAC;AACzC,YAAY,EACV,eAAe,EACf,cAAc,EACd,sBAAsB,GACvB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,iBAAiB,EACjB,2BAA2B,EAC3B,YAAY,EACZ,sBAAsB,EACtB,WAAW,EACX,qBAAqB,EACrB,cAAc,EACd,wBAAwB,EACxB,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,wBAAwB,EACxB,6BAA6B,EAC7B,oBAAoB,EACpB,qBAAqB,EACrB,iBAAiB,EACjB,gBAAgB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,wBAAwB,CAAC;AAEhC,OAAO,EACL,0BAA0B,EAC1B,4BAA4B,EAC5B,2BAA2B,GAC5B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,qCAAqC,EACrC,qCAAqC,EACrC,2BAA2B,EAC3B,wCAAwC,GACzC,MAAM,iCAAiC,CAAC;AACzC,YAAY,EACV,aAAa,EACb,eAAe,EACf,aAAa,EACb,cAAc,EACd,sBAAsB,GACvB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,iBAAiB,EACjB,2BAA2B,EAC3B,YAAY,EACZ,sBAAsB,EACtB,WAAW,EACX,qBAAqB,EACrB,cAAc,EACd,eAAe,EACf,SAAS,EACT,mBAAmB,EACnB,mBAAmB,EACnB,WAAW,EACX,qBAAqB,EACrB,YAAY,EACZ,sBAAsB,EACtB,cAAc,EACd,wBAAwB,EACxB,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,qBAAqB,EACrB,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,yBAAyB,EACzB,wBAAwB,EACxB,6BAA6B,EAC7B,oBAAoB,EACpB,qBAAqB,EACrB,yBAAyB,EACzB,4BAA4B,EAC5B,qBAAqB,EACrB,gBAAgB,EAChB,uBAAuB,EACvB,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,qBAAqB,EACrB,wBAAwB,EACxB,6BAA6B,EAC7B,gBAAgB,EAChB,uBAAuB,EACvB,iCAAiC,EACjC,8BAA8B,EAC9B,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,uCAAuC,EACvC,0CAA0C,EAC1C,sBAAsB,EACtB,iBAAiB,EACjB,kBAAkB,EAClB,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,KAAK,sBAAsB,EAC3B,oBAAoB,EACpB,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,gBAAgB,EAChB,wCAAwC,EACxC,KAAK,kBAAkB,GACxB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,cAAc,EACd,eAAe,EACf,qBAAqB,EACrB,4BAA4B,EAC5B,UAAU,EACV,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EAClB,UAAU,EACV,cAAc,EACd,sBAAsB,EACtB,2BAA2B,EAC3B,kBAAkB,EAClB,uBAAuB,EACvB,0BAA0B,EAC1B,qBAAqB,EACrB,oBAAoB,EACpB,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,YAAY,EACZ,gBAAgB,EAChB,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
|