@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 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
@@ -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,GACjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,uCAAuC,EACvC,0CAA0C,EAC1C,sBAAsB,EACtB,iBAAiB,EACjB,kBAAkB,EAClB,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"}
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"}