@happyvertical/smrt-subscriptions 0.57.36 → 0.57.38

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.
@@ -3,13 +3,13 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-subscriptions",
6
- "packageVersion": "0.57.36",
6
+ "packageVersion": "0.57.38",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "7017ffb293160b193e4224168747801f931d153ef8bc26f5482f2ad645132018",
11
- "packageJson": "5ed31b2d6d6c34a704a9e83489532b29e67214e0dafc3e89db406bf9c7453e92",
12
- "agents": "8d03287e49f241cfe3f1dac0a3bd32ffc51d67709c1e2af93e0368cd0cadaf02"
10
+ "manifest": "94e7379bed761a703b6586c351b3ec454c91ac9d5d73dc1815ebfe37199a7c65",
11
+ "packageJson": "6160c32f10c513b7f93f7d0a67b76a7fd0c45dbd44dcc7f2eec6e55f4de11321",
12
+ "agents": "5fcd3fec51594afb1d675ef3ea52aa239683416f0169fef8a96dfacaabb5580b"
13
13
  },
14
14
  "exports": [
15
15
  ".",
@@ -487,77 +487,88 @@
487
487
  "type": "text",
488
488
  "required": false,
489
489
  "columnType": "TEXT",
490
- "default": ""
490
+ "default": "",
491
+ "description": "A short code that identifies the plan, such as starter or pro."
491
492
  },
492
493
  {
493
494
  "name": "name",
494
495
  "type": "text",
495
496
  "required": false,
496
497
  "columnType": "TEXT",
497
- "default": ""
498
+ "default": "",
499
+ "description": "What the plan is called, as customers see it."
498
500
  },
499
501
  {
500
502
  "name": "description",
501
503
  "type": "text",
502
504
  "required": false,
503
505
  "columnType": "TEXT",
504
- "default": ""
506
+ "default": "",
507
+ "description": "A sentence or two about what the plan offers."
505
508
  },
506
509
  {
507
510
  "name": "status",
508
511
  "type": "text",
509
512
  "required": false,
510
513
  "columnType": "TEXT",
511
- "default": "active"
514
+ "default": "active",
515
+ "description": "Whether the plan is a draft, on sale (active) or archived."
512
516
  },
513
517
  {
514
518
  "name": "sortOrder",
515
519
  "type": "integer",
516
520
  "required": false,
517
521
  "columnType": "INTEGER",
518
- "default": 0
522
+ "default": 0,
523
+ "description": "Where the plan sits when plans are listed; lower numbers come first."
519
524
  },
520
525
  {
521
526
  "name": "priceAmount",
522
527
  "type": "integer",
523
528
  "required": false,
524
529
  "columnType": "INTEGER",
525
- "default": 0
530
+ "default": 0,
531
+ "description": "What the plan costs each billing period, in cents, as a whole number."
526
532
  },
527
533
  {
528
534
  "name": "currency",
529
535
  "type": "text",
530
536
  "required": false,
531
537
  "columnType": "TEXT",
532
- "default": "USD"
538
+ "default": "USD",
539
+ "description": "The currency the price is in, such as USD."
533
540
  },
534
541
  {
535
542
  "name": "billingInterval",
536
543
  "type": "text",
537
544
  "required": false,
538
545
  "columnType": "TEXT",
539
- "default": "month"
546
+ "default": "month",
547
+ "description": "How often the subscriber is billed: every day, week, month or year."
540
548
  },
541
549
  {
542
550
  "name": "pricingMode",
543
551
  "type": "text",
544
552
  "required": false,
545
553
  "columnType": "TEXT",
546
- "default": "fixed"
554
+ "default": "fixed",
555
+ "description": "Whether the plan is free, has a fixed price, or lets the subscriber choose what to pay. It cannot change once the plan is saved."
547
556
  },
548
557
  {
549
558
  "name": "minimumAmount",
550
559
  "type": "integer",
551
560
  "required": false,
552
561
  "columnType": "INTEGER",
553
- "default": 0
562
+ "default": 0,
563
+ "description": "For a pay-what-you-can plan, the least a subscriber may pay, in cents."
554
564
  },
555
565
  {
556
566
  "name": "suggestedAmount",
557
567
  "type": "integer",
558
568
  "required": false,
559
569
  "columnType": "INTEGER",
560
- "default": 0
570
+ "default": 0,
571
+ "description": "For a pay-what-you-can plan, the amount suggested to the subscriber, in cents."
561
572
  },
562
573
  {
563
574
  "name": "externalProvider",
@@ -961,83 +972,95 @@
961
972
  "type": "foreignKey",
962
973
  "required": false,
963
974
  "related": "SubscriptionPlan",
964
- "columnType": "UUID"
975
+ "columnType": "UUID",
976
+ "description": "The plan this version belongs to."
965
977
  },
966
978
  {
967
979
  "name": "planKey",
968
980
  "type": "text",
969
981
  "required": false,
970
982
  "columnType": "TEXT",
971
- "default": ""
983
+ "default": "",
984
+ "description": "The plan code at the time this version was published."
972
985
  },
973
986
  {
974
987
  "name": "version",
975
988
  "type": "integer",
976
989
  "required": false,
977
990
  "columnType": "INTEGER",
978
- "default": 0
991
+ "default": 0,
992
+ "description": "Which version of the plan this is, counting from 1."
979
993
  },
980
994
  {
981
995
  "name": "effectiveFrom",
982
996
  "type": "datetime",
983
997
  "required": false,
984
- "columnType": "TIMESTAMP"
998
+ "columnType": "TIMESTAMP",
999
+ "description": "The date and time from which these terms apply."
985
1000
  },
986
1001
  {
987
1002
  "name": "name",
988
1003
  "type": "text",
989
1004
  "required": false,
990
1005
  "columnType": "TEXT",
991
- "default": ""
1006
+ "default": "",
1007
+ "description": "What the plan was called when this version was published."
992
1008
  },
993
1009
  {
994
1010
  "name": "status",
995
1011
  "type": "text",
996
1012
  "required": false,
997
1013
  "columnType": "TEXT",
998
- "default": "active"
1014
+ "default": "active",
1015
+ "description": "Whether the plan was a draft, on sale (active) or archived under this version."
999
1016
  },
1000
1017
  {
1001
1018
  "name": "priceAmount",
1002
1019
  "type": "integer",
1003
1020
  "required": false,
1004
1021
  "columnType": "INTEGER",
1005
- "default": 0
1022
+ "default": 0,
1023
+ "description": "What the plan cost each billing period under this version, in cents."
1006
1024
  },
1007
1025
  {
1008
1026
  "name": "currency",
1009
1027
  "type": "text",
1010
1028
  "required": false,
1011
1029
  "columnType": "TEXT",
1012
- "default": "USD"
1030
+ "default": "USD",
1031
+ "description": "The currency the price is in, such as USD."
1013
1032
  },
1014
1033
  {
1015
1034
  "name": "billingInterval",
1016
1035
  "type": "text",
1017
1036
  "required": false,
1018
1037
  "columnType": "TEXT",
1019
- "default": "month"
1038
+ "default": "month",
1039
+ "description": "How often the subscriber was billed: every day, week, month or year."
1020
1040
  },
1021
1041
  {
1022
1042
  "name": "pricingMode",
1023
1043
  "type": "text",
1024
1044
  "required": false,
1025
1045
  "columnType": "TEXT",
1026
- "default": "fixed"
1046
+ "default": "fixed",
1047
+ "description": "Whether the plan was free, had a fixed price, or let the subscriber choose what to pay."
1027
1048
  },
1028
1049
  {
1029
1050
  "name": "minimumAmount",
1030
1051
  "type": "integer",
1031
1052
  "required": false,
1032
1053
  "columnType": "INTEGER",
1033
- "default": 0
1054
+ "default": 0,
1055
+ "description": "For a pay-what-you-can plan, the least a subscriber could pay, in cents."
1034
1056
  },
1035
1057
  {
1036
1058
  "name": "suggestedAmount",
1037
1059
  "type": "integer",
1038
1060
  "required": false,
1039
1061
  "columnType": "INTEGER",
1040
- "default": 0
1062
+ "default": 0,
1063
+ "description": "For a pay-what-you-can plan, the amount suggested to the subscriber, in cents."
1041
1064
  },
1042
1065
  {
1043
1066
  "name": "features",
@@ -1206,78 +1229,90 @@
1206
1229
  "type": "text",
1207
1230
  "required": false,
1208
1231
  "columnType": "TEXT",
1209
- "default": "tenant"
1232
+ "default": "tenant",
1233
+ "description": "Whether the subscriber is your own workspace (tenant) or an outside customer (external)."
1210
1234
  },
1211
1235
  {
1212
1236
  "name": "subscriberExternalId",
1213
1237
  "type": "text",
1214
1238
  "required": false,
1215
1239
  "columnType": "TEXT",
1216
- "default": ""
1240
+ "default": "",
1241
+ "description": "Who the outside subscriber is, when the subscriber type is external."
1217
1242
  },
1218
1243
  {
1219
1244
  "name": "planId",
1220
1245
  "type": "foreignKey",
1221
1246
  "required": false,
1222
1247
  "related": "SubscriptionPlan",
1223
- "columnType": "UUID"
1248
+ "columnType": "UUID",
1249
+ "description": "The plan this subscription is for."
1224
1250
  },
1225
1251
  {
1226
1252
  "name": "status",
1227
1253
  "type": "text",
1228
1254
  "required": false,
1229
1255
  "columnType": "TEXT",
1230
- "default": "incomplete"
1256
+ "default": "incomplete",
1257
+ "description": "Where the subscription stands: incomplete, trialing, active, past due, unpaid or cancelled."
1231
1258
  },
1232
1259
  {
1233
1260
  "name": "startedAt",
1234
1261
  "type": "datetime",
1235
1262
  "required": false,
1236
- "columnType": "TIMESTAMP"
1263
+ "columnType": "TIMESTAMP",
1264
+ "description": "The day the subscription began."
1237
1265
  },
1238
1266
  {
1239
1267
  "name": "currentPeriodStart",
1240
1268
  "type": "datetime",
1241
1269
  "required": false,
1242
- "columnType": "TIMESTAMP"
1270
+ "columnType": "TIMESTAMP",
1271
+ "description": "When the current billing period began."
1243
1272
  },
1244
1273
  {
1245
1274
  "name": "currentPeriodEnd",
1246
1275
  "type": "datetime",
1247
1276
  "required": false,
1248
- "columnType": "TIMESTAMP"
1277
+ "columnType": "TIMESTAMP",
1278
+ "description": "When the current billing period ends and the next one is due."
1249
1279
  },
1250
1280
  {
1251
1281
  "name": "trialEndsAt",
1252
1282
  "type": "datetime",
1253
1283
  "required": false,
1254
- "columnType": "TIMESTAMP"
1284
+ "columnType": "TIMESTAMP",
1285
+ "description": "When the free trial ends, if there is one."
1255
1286
  },
1256
1287
  {
1257
1288
  "name": "cancelAtPeriodEnd",
1258
1289
  "type": "boolean",
1259
1290
  "required": false,
1260
1291
  "columnType": "BOOLEAN",
1261
- "default": false
1292
+ "default": false,
1293
+ "description": "Tick to let the subscription run out at the end of the current period instead of renewing."
1262
1294
  },
1263
1295
  {
1264
1296
  "name": "canceledAt",
1265
1297
  "type": "datetime",
1266
1298
  "required": false,
1267
- "columnType": "TIMESTAMP"
1299
+ "columnType": "TIMESTAMP",
1300
+ "description": "When the subscription was cancelled."
1268
1301
  },
1269
1302
  {
1270
1303
  "name": "agreedAmount",
1271
1304
  "type": "integer",
1272
1305
  "required": false,
1273
- "columnType": "INTEGER"
1306
+ "columnType": "INTEGER",
1307
+ "description": "For a pay-what-you-can plan, the amount the subscriber chose to pay, in cents."
1274
1308
  },
1275
1309
  {
1276
1310
  "name": "planVersionId",
1277
1311
  "type": "foreignKey",
1278
1312
  "required": false,
1279
1313
  "related": "SubscriptionPlanVersion",
1280
- "columnType": "UUID"
1314
+ "columnType": "UUID",
1315
+ "description": "The version of the plan terms this subscription is held to for the current period. It is kept up to date automatically."
1281
1316
  },
1282
1317
  {
1283
1318
  "name": "planVersionPinnedAt",
@@ -2120,42 +2155,48 @@
2120
2155
  "type": "text",
2121
2156
  "required": false,
2122
2157
  "columnType": "TEXT",
2123
- "default": ""
2158
+ "default": "",
2159
+ "description": "Who supplied the service that was used."
2124
2160
  },
2125
2161
  {
2126
2162
  "name": "serviceKey",
2127
2163
  "type": "text",
2128
2164
  "required": false,
2129
2165
  "columnType": "TEXT",
2130
- "default": ""
2166
+ "default": "",
2167
+ "description": "The service that was used."
2131
2168
  },
2132
2169
  {
2133
2170
  "name": "metricKey",
2134
2171
  "type": "text",
2135
2172
  "required": false,
2136
2173
  "columnType": "TEXT",
2137
- "default": ""
2174
+ "default": "",
2175
+ "description": "What was used, such as messages sent or minutes of audio."
2138
2176
  },
2139
2177
  {
2140
2178
  "name": "quantity",
2141
2179
  "type": "decimal",
2142
2180
  "required": false,
2143
2181
  "columnType": "REAL",
2144
- "default": 0
2182
+ "default": 0,
2183
+ "description": "How much of it was used."
2145
2184
  },
2146
2185
  {
2147
2186
  "name": "amount",
2148
2187
  "type": "integer",
2149
2188
  "required": false,
2150
2189
  "columnType": "INTEGER",
2151
- "default": 0
2190
+ "default": 0,
2191
+ "description": "What it cost, in cents."
2152
2192
  },
2153
2193
  {
2154
2194
  "name": "currency",
2155
2195
  "type": "text",
2156
2196
  "required": false,
2157
2197
  "columnType": "TEXT",
2158
- "default": "USD"
2198
+ "default": "USD",
2199
+ "description": "The currency the amount is in, such as USD."
2159
2200
  },
2160
2201
  {
2161
2202
  "name": "pricingRuleId",
@@ -2183,13 +2224,15 @@
2183
2224
  "type": "text",
2184
2225
  "required": false,
2185
2226
  "columnType": "TEXT",
2186
- "default": "draft"
2227
+ "default": "draft",
2228
+ "description": "Where the charge stands: draft, approved or adjusted."
2187
2229
  },
2188
2230
  {
2189
2231
  "name": "approvedAt",
2190
2232
  "type": "datetime",
2191
2233
  "required": false,
2192
- "columnType": "TIMESTAMP"
2234
+ "columnType": "TIMESTAMP",
2235
+ "description": "When the charge was approved."
2193
2236
  }
2194
2237
  ],
2195
2238
  "relationships": [
@@ -4187,7 +4230,380 @@
4187
4230
  "polymorphicAssociations": 0,
4188
4231
  "uuidColumns": 88
4189
4232
  },
4190
- "agentDoc": "# @happyvertical/smrt-subscriptions\n\nTenant subscriptions, plan features, immutable/idempotent usage evidence,\neffective-dated client pricing, scoped spending policies, and entitlement\nresolution for SMRT applications.\n\n## Validation\n\n```bash\npnpm --filter @happyvertical/smrt-subscriptions test\npnpm --filter @happyvertical/smrt-subscriptions test:postgres\npnpm --filter @happyvertical/smrt-subscriptions typecheck\npnpm --filter @happyvertical/smrt-subscriptions build\n```\n\n`test:postgres` is the lane that holds the money-column line — SQLite's type\naffinity stores a REAL in an INTEGER column without complaint, so a money field\nthat reverted to DECIMAL passes every SQLite suite.\n\n## Money\n\n- **Money is integer minor units** (cents) — `$19.99` is `1999`.\n `SubscriptionPlan.priceAmount`, `ClientCharge.amount`, `RetailCharge.amount`,\n `CreditGrant.amount`,\n `BillingAdjustment.amount` and `SpendingPolicy.limitAmount` all initialize\n `= 0`, never `= 0.0`: the integer literal is what maps them to INTEGER\n columns (BIGINT on fresh PostgreSQL/DuckDB databases; #2401, #2373). `ClientCharge.quantity` and `TenantUsageMetric.quantity` are\n the opposite case — metered quantities are genuinely fractional and stay\n DECIMAL.\n- **`PricingRule.terms` are minor units *per unit of usage*, and stay\n fractional.** A per-token rate is routinely a fraction of a cent, so an\n integer `unitPrice` would truncate it to zero. `calculateAmount()` rounds the\n computed result to a whole minor unit — that is the single boundary in the\n package where a rate meets money, and it is what lets `evaluateSpending()`\n compare charge sums against `limitAmount` exactly.\n- **`adjust()` rejects a fractional amount.** Negative amounts are legitimate\n (a credit); fractional ones almost always mean the caller passed major units.\n- **Billing rejects immediate JSON write-back.** The JSON adapter's default\n `writeStrategy: 'immediate'` and DuckDB with that write-back strategy export\n files before transaction commit, while adapter rollback restores only the\n database transaction. `CommercialUsageService` therefore fails before setup\n on those configurations. PostgreSQL, SQLite, ordinary DuckDB, and explicit\n non-immediate write strategies remain supported. When supplying an already\n resolved database handle, also pass `billingStorage` so this capability\n decision is explicit rather than inferred from private adapter state.\n- **Embedded sourced adjustments serialize per database.** SQLite, DuckDB, and\n supported non-immediate JSON handles multiplex local writes; the complete\n deterministic adjustment flow, including recovery reads, runs under one\n database-scoped lock. Every supported embedded adjustment transaction,\n sourced or unsourced, also holds the core embedded write queue for its full\n lifetime; transaction-bound model saves re-enter that hold so adjustments\n cannot absorb, roll back, or deadlock with unrelated ordinary writes.\n- **UI formats by dividing, never by `toFixed`.** `PlanPicker` and\n `CommercialOverview` scale back to major units using the currency's own\n minor-unit exponent, so zero-decimal currencies (JPY, KRW) are not divided.\n- **Migrating an existing database**: `preflightSubscriptionsMoneyMinorUnits(db)`\n reports which columns still hold major units and which rows would be rounded\n or exceed JavaScript's safe-integer range; `migrateSubscriptionsMoneyToMinorUnits(db)` converts them\n (idempotent via `_smrt_backfills`). On SQLite the values are rescaled but the\n declared type needs the table-rebuild path (#2370). Existing\n `PricingRule.terms` are **not** migrated automatically — only the operator\n knows which keys in a given rule are prices rather than quantities or ratios.\n- **Range and existing deployments**: fresh PostgreSQL/DuckDB INTEGER columns\n are BIGINT, and hydration rejects values outside JavaScript's safe-integer\n range. Existing PostgreSQL `int4` columns require the explicit widening in #2424.\n\n## Plan versions (#4032)\n\n- `SubscriptionPlan.save()` publishes the next immutable\n `SubscriptionPlanVersion` when its terms (`PLAN_TERM_FIELDS`) change; the\n logic lives in `src/plan-versioning.ts`. Versions are append-only,\n contiguous, and monotonic in `effectiveFrom`.\n- `TenantSubscription.validateBeforeSave()` maintains `planVersionId` via\n `pinSubscriptionPlanVersion()`: pin at period start, re-pin when\n `currentPeriodStart` moves, and refuse in-period changes that are not an\n `isEntitlementUpgrade()` unless `replacePlanVersion({ policy: 'authorized',\n reason })`. Never relax this to \"latest version wins\": a paid period must\n not lose entitlements.\n- The resolver reads the pinned version; a missing or inactive pinned version\n fails closed. Only unpinned rows on plans without versions fall back to the\n live plan. `migrateSubscriptionPlanVersions(db)` backfills both.\n- Renewal means the new `currentPeriodStart` is at or after the stored\n `currentPeriodEnd`; creation and renewal pin the boundary's version, and a\n caller-named other version needs an authorized replacement.\n- `canceled` subscriptions entitle until `currentPeriodEnd` only with\n `retainsPaidPeriod`, derived on save from an entitled pre-cancel row.\n\n## Reseller billing (#3059)\n\n- Billing ownership is consumed from `smrt-tenancy`'s\n `BillingRelationshipService` (`BillingRelationshipReader`); never re-derive\n parentage or owners here.\n- `ClientCharge` stays one row per usage event (`usage_event_id` unique) and\n `tenantId` is always the payer. That uniqueness is the double-billing guard\n across owner changes; do not widen its conflict key. The retail leg lives in\n `RetailCharge` (also one per event), written in the same transaction.\n- Price-book rules are `PricingRule` rows with `priceBookId`; `price()` must\n keep ignoring them, and `rateUsage()` only trusts rules whose tenant owns the\n book.\n- Delegated `SpendingPolicy` rows (`setByTenantId`) are guarded in\n `validateBeforeSave` and a `beforeDelete` hook, by id and by conflict key;\n `CreditGrant` applies the same parent-only rule to delegated balances.\n `PriceBookAssignmentCollection` is deliberately not a root export.\n- `ResellerBillingService` authorizes first, then writes in a system context\n (parent writes land on child-owned rows). The wholesale leg is authorized by\n the book's publisher and may never be published by the payer; whether a\n publisher is a legitimate provider for the reseller is a host obligation\n (the model records no upstream seller). Relationship reads precede\n authorization and stay under smrt-tenancy's own read rules. The only\n cross-tenant read in evaluation is the bounded `wholesale`-basis sum.\n- `balance` policies take their limit from `CreditGrant`s; `limitAmount` must\n be 0, and currency and period are fixed once saved (the ledger is scoped to\n them). Auto top-up is a host hook only — no payment provider calls. The\n evaluator re-reads credit after the hook returns (grant or not) and hands\n it `currentShortfall()`, so a hook that charges asynchronously can see a\n concurrent top-up (smrt-commerce's `autoTopUpHook()`, #3139).\n\n## Notes\n\n- Keep Stripe-specific fields as provider binding metadata. Runtime Stripe API\n calls belong in the HappyVertical SDK accounting provider.\n- Thresholds are evaluated from tenant usage metrics and optional AI usage\n summaries. Resolve multiple thresholds with the batch usage path where\n available; do not bypass tenant context in application code.\n- Use `SubscriptionResolver.create()` or an explicitly injected\n `SubscriptionResolver` once per request when resolving entitlements multiple\n times. Load and pass `EntitlementResolutionContext` to avoid re-reading the\n current subscription and plan.\n- Subscription UI components should stay provider-neutral and receive actions\n from the host app.\n",
4233
+ "agentDoc": "# @happyvertical/smrt-subscriptions\n\nTenant subscriptions, plan features, immutable/idempotent usage evidence,\neffective-dated client pricing, scoped spending policies, and entitlement\nresolution for SMRT applications.\n\n## Validation\n\n```bash\npnpm --filter @happyvertical/smrt-subscriptions test\npnpm --filter @happyvertical/smrt-subscriptions test:postgres\npnpm --filter @happyvertical/smrt-subscriptions typecheck\npnpm --filter @happyvertical/smrt-subscriptions build\n```\n\n`test:postgres` is the lane that holds the money-column line — SQLite's type\naffinity stores a REAL in an INTEGER column without complaint, so a money field\nthat reverted to DECIMAL passes every SQLite suite.\n\n## Money\n\n- **Money is integer minor units** (cents) — `$19.99` is `1999`.\n `SubscriptionPlan.priceAmount`, `ClientCharge.amount`, `RetailCharge.amount`,\n `CreditGrant.amount`,\n `BillingAdjustment.amount` and `SpendingPolicy.limitAmount` all initialize\n `= 0`, never `= 0.0`: the integer literal is what maps them to INTEGER\n columns (BIGINT on fresh PostgreSQL/DuckDB databases; #2401, #2373). `ClientCharge.quantity` and `TenantUsageMetric.quantity` are\n the opposite case — metered quantities are genuinely fractional and stay\n DECIMAL.\n- **`PricingRule.terms` are minor units *per unit of usage*, and stay\n fractional.** A per-token rate is routinely a fraction of a cent, so an\n integer `unitPrice` would truncate it to zero. `calculateAmount()` rounds the\n computed result to a whole minor unit — that is the single boundary in the\n package where a rate meets money, and it is what lets `evaluateSpending()`\n compare charge sums against `limitAmount` exactly.\n- **`adjust()` rejects a fractional amount.** Negative amounts are legitimate\n (a credit); fractional ones almost always mean the caller passed major units.\n- **Billing rejects immediate JSON write-back.** The JSON adapter's default\n `writeStrategy: 'immediate'` and DuckDB with that write-back strategy export\n files before transaction commit, while adapter rollback restores only the\n database transaction. `CommercialUsageService` therefore fails before setup\n on those configurations. PostgreSQL, SQLite, ordinary DuckDB, and explicit\n non-immediate write strategies remain supported. When supplying an already\n resolved database handle, also pass `billingStorage` so this capability\n decision is explicit rather than inferred from private adapter state.\n- **Embedded sourced adjustments serialize per database.** SQLite, DuckDB, and\n supported non-immediate JSON handles multiplex local writes; the complete\n deterministic adjustment flow, including recovery reads, runs under one\n database-scoped lock. Every supported embedded adjustment transaction,\n sourced or unsourced, also holds the core embedded write queue for its full\n lifetime; transaction-bound model saves re-enter that hold so adjustments\n cannot absorb, roll back, or deadlock with unrelated ordinary writes.\n- **UI formats by dividing, never by `toFixed`.** `PlanPicker` and\n `CommercialOverview` scale back to major units using the currency's own\n minor-unit exponent, so zero-decimal currencies (JPY, KRW) are not divided.\n- **Migrating an existing database**: `preflightSubscriptionsMoneyMinorUnits(db)`\n reports which columns still hold major units and which rows would be rounded\n or exceed JavaScript's safe-integer range; `migrateSubscriptionsMoneyToMinorUnits(db)` converts them\n (idempotent via `_smrt_backfills`). On SQLite the values are rescaled but the\n declared type needs the table-rebuild path (#2370). Existing\n `PricingRule.terms` are **not** migrated automatically — only the operator\n knows which keys in a given rule are prices rather than quantities or ratios.\n- **Range and existing deployments**: fresh PostgreSQL/DuckDB INTEGER columns\n are BIGINT, and hydration rejects values outside JavaScript's safe-integer\n range. Existing PostgreSQL `int4` columns require the explicit widening in #2424.\n\n## Plan versions (#4032)\n\n- `SubscriptionPlan.save()` publishes the next immutable\n `SubscriptionPlanVersion` when its terms (`PLAN_TERM_FIELDS`) change; the\n logic lives in `src/plan-versioning.ts`. Versions are append-only,\n contiguous, and monotonic in `effectiveFrom`.\n- `TenantSubscription.validateBeforeSave()` maintains `planVersionId` via\n `pinSubscriptionPlanVersion()`: pin at period start, re-pin when\n `currentPeriodStart` moves, and refuse in-period changes that are not an\n `isEntitlementUpgrade()` unless `replacePlanVersion({ policy: 'authorized',\n reason })`. Never relax this to \"latest version wins\": a paid period must\n not lose entitlements.\n- The resolver reads the pinned version; a missing or inactive pinned version\n fails closed. Only unpinned rows on plans without versions fall back to the\n live plan. `migrateSubscriptionPlanVersions(db)` backfills both.\n- Renewal means the new `currentPeriodStart` is at or after the stored\n `currentPeriodEnd`; creation and renewal pin the boundary's version, and a\n caller-named other version needs an authorized replacement.\n- `canceled` subscriptions entitle until `currentPeriodEnd` only with\n `retainsPaidPeriod`, derived on save from an entitled pre-cancel row.\n- **Recipe `subscriptions.plans`** (`src/recipes.ts`, help in `src/plans.recipe.md`): lists plans, subscriptions, plan versions (read-only, written by `plan-versioning.ts`) and charges (read-only, written by the usage service) for the planner and other catalog hosts. Fields it shows carry `@field({ description })` in plain words, money fields saying \"in cents\"; keep them when editing the models. Reseller models and pricing rules stay out of it.\n\n## Reseller billing (#3059)\n\n- Billing ownership is consumed from `smrt-tenancy`'s\n `BillingRelationshipService` (`BillingRelationshipReader`); never re-derive\n parentage or owners here.\n- `ClientCharge` stays one row per usage event (`usage_event_id` unique) and\n `tenantId` is always the payer. That uniqueness is the double-billing guard\n across owner changes; do not widen its conflict key. The retail leg lives in\n `RetailCharge` (also one per event), written in the same transaction.\n- Price-book rules are `PricingRule` rows with `priceBookId`; `price()` must\n keep ignoring them, and `rateUsage()` only trusts rules whose tenant owns the\n book.\n- Delegated `SpendingPolicy` rows (`setByTenantId`) are guarded in\n `validateBeforeSave` and a `beforeDelete` hook, by id and by conflict key;\n `CreditGrant` applies the same parent-only rule to delegated balances.\n `PriceBookAssignmentCollection` is deliberately not a root export.\n- `ResellerBillingService` authorizes first, then writes in a system context\n (parent writes land on child-owned rows). The wholesale leg is authorized by\n the book's publisher and may never be published by the payer; whether a\n publisher is a legitimate provider for the reseller is a host obligation\n (the model records no upstream seller). Relationship reads precede\n authorization and stay under smrt-tenancy's own read rules. The only\n cross-tenant read in evaluation is the bounded `wholesale`-basis sum.\n- `balance` policies take their limit from `CreditGrant`s; `limitAmount` must\n be 0, and currency and period are fixed once saved (the ledger is scoped to\n them). Auto top-up is a host hook only — no payment provider calls. The\n evaluator re-reads credit after the hook returns (grant or not) and hands\n it `currentShortfall()`, so a hook that charges asynchronously can see a\n concurrent top-up (smrt-commerce's `autoTopUpHook()`, #3139).\n\n## Notes\n\n- Keep Stripe-specific fields as provider binding metadata. Runtime Stripe API\n calls belong in the HappyVertical SDK accounting provider.\n- Thresholds are evaluated from tenant usage metrics and optional AI usage\n summaries. Resolve multiple thresholds with the batch usage path where\n available; do not bypass tenant context in application code.\n- Use `SubscriptionResolver.create()` or an explicitly injected\n `SubscriptionResolver` once per request when resolving entitlements multiple\n times. Load and pass `EntitlementResolutionContext` to avoid re-reading the\n current subscription and plan.\n- Subscription UI components should stay provider-neutral and receive actions\n from the host app.\n",
4234
+ "recipes": [
4235
+ {
4236
+ "id": "subscriptions.plans",
4237
+ "className": "SubscriptionPlansRecipe",
4238
+ "label": "Subscriptions",
4239
+ "summary": "Sell plans that renew, see who is subscribed to which, and what their usage cost.",
4240
+ "synonyms": [
4241
+ "subscriptions",
4242
+ "subscription plans",
4243
+ "plans",
4244
+ "memberships",
4245
+ "recurring billing",
4246
+ "pricing tiers"
4247
+ ],
4248
+ "models": [
4249
+ "@happyvertical/smrt-subscriptions:SubscriptionPlan",
4250
+ "@happyvertical/smrt-subscriptions:SubscriptionPlanVersion",
4251
+ "@happyvertical/smrt-subscriptions:TenantSubscription",
4252
+ "@happyvertical/smrt-subscriptions:ClientCharge"
4253
+ ],
4254
+ "nav": [
4255
+ {
4256
+ "label": "Plans",
4257
+ "model": "@happyvertical/smrt-subscriptions:SubscriptionPlan",
4258
+ "icon": "tag",
4259
+ "description": "The plans you sell, with their price and how often they bill.",
4260
+ "noun": "plan"
4261
+ },
4262
+ {
4263
+ "label": "Subscriptions",
4264
+ "model": "@happyvertical/smrt-subscriptions:TenantSubscription",
4265
+ "icon": "repeat",
4266
+ "description": "Who is subscribed to which plan, and where each one stands.",
4267
+ "noun": "subscription"
4268
+ },
4269
+ {
4270
+ "label": "Plan versions",
4271
+ "model": "@happyvertical/smrt-subscriptions:SubscriptionPlanVersion",
4272
+ "icon": "fileText",
4273
+ "description": "Every set of terms a plan has had, and from when each applied.",
4274
+ "noun": "plan version"
4275
+ },
4276
+ {
4277
+ "label": "Charges",
4278
+ "model": "@happyvertical/smrt-subscriptions:ClientCharge",
4279
+ "icon": "receipt",
4280
+ "description": "What each subscriber used and what it cost them.",
4281
+ "noun": "charge"
4282
+ }
4283
+ ],
4284
+ "requires": [],
4285
+ "section": {
4286
+ "id": "sales",
4287
+ "label": "Sales",
4288
+ "icon": "shoppingBag",
4289
+ "description": "Customers, orders and quotes: everything you sell."
4290
+ },
4291
+ "options": {
4292
+ "@happyvertical/smrt-subscriptions:SubscriptionPlan": {
4293
+ "fields": {
4294
+ "name": {
4295
+ "order": 1
4296
+ },
4297
+ "planKey": {
4298
+ "label": "Plan code",
4299
+ "order": 2
4300
+ },
4301
+ "description": {
4302
+ "order": 3
4303
+ },
4304
+ "status": {
4305
+ "order": 4
4306
+ },
4307
+ "priceAmount": {
4308
+ "label": "Price (in cents)",
4309
+ "order": 5
4310
+ },
4311
+ "currency": {
4312
+ "order": 6
4313
+ },
4314
+ "billingInterval": {
4315
+ "label": "Billed every",
4316
+ "order": 7
4317
+ },
4318
+ "pricingMode": {
4319
+ "label": "How it is priced",
4320
+ "order": 8
4321
+ },
4322
+ "minimumAmount": {
4323
+ "label": "Minimum (in cents)",
4324
+ "visibility": "advanced"
4325
+ },
4326
+ "suggestedAmount": {
4327
+ "label": "Suggested (in cents)",
4328
+ "visibility": "advanced"
4329
+ },
4330
+ "sortOrder": {
4331
+ "label": "Position in list",
4332
+ "visibility": "advanced"
4333
+ },
4334
+ "tenantId": {
4335
+ "visibility": "hidden"
4336
+ },
4337
+ "features": {
4338
+ "visibility": "hidden"
4339
+ },
4340
+ "thresholds": {
4341
+ "visibility": "hidden"
4342
+ },
4343
+ "externalProvider": {
4344
+ "visibility": "hidden"
4345
+ },
4346
+ "providerProductId": {
4347
+ "visibility": "hidden"
4348
+ },
4349
+ "providerPriceId": {
4350
+ "visibility": "hidden"
4351
+ },
4352
+ "stripeProductId": {
4353
+ "visibility": "hidden"
4354
+ },
4355
+ "stripePriceId": {
4356
+ "visibility": "hidden"
4357
+ },
4358
+ "metadata": {
4359
+ "visibility": "hidden"
4360
+ }
4361
+ }
4362
+ },
4363
+ "@happyvertical/smrt-subscriptions:SubscriptionPlanVersion": {
4364
+ "fields": {
4365
+ "planId": {
4366
+ "label": "Plan",
4367
+ "order": 1
4368
+ },
4369
+ "version": {
4370
+ "label": "Version number",
4371
+ "order": 2
4372
+ },
4373
+ "effectiveFrom": {
4374
+ "label": "In force from",
4375
+ "order": 3
4376
+ },
4377
+ "name": {
4378
+ "order": 4
4379
+ },
4380
+ "status": {
4381
+ "order": 5
4382
+ },
4383
+ "priceAmount": {
4384
+ "label": "Price (in cents)",
4385
+ "order": 6
4386
+ },
4387
+ "currency": {
4388
+ "order": 7
4389
+ },
4390
+ "billingInterval": {
4391
+ "label": "Billed every",
4392
+ "order": 8
4393
+ },
4394
+ "planKey": {
4395
+ "label": "Plan code",
4396
+ "visibility": "advanced"
4397
+ },
4398
+ "pricingMode": {
4399
+ "label": "How it is priced",
4400
+ "visibility": "advanced"
4401
+ },
4402
+ "minimumAmount": {
4403
+ "label": "Minimum (in cents)",
4404
+ "visibility": "advanced"
4405
+ },
4406
+ "suggestedAmount": {
4407
+ "label": "Suggested (in cents)",
4408
+ "visibility": "advanced"
4409
+ },
4410
+ "tenantId": {
4411
+ "visibility": "hidden"
4412
+ },
4413
+ "features": {
4414
+ "visibility": "hidden"
4415
+ },
4416
+ "thresholds": {
4417
+ "visibility": "hidden"
4418
+ }
4419
+ }
4420
+ },
4421
+ "@happyvertical/smrt-subscriptions:TenantSubscription": {
4422
+ "fields": {
4423
+ "planId": {
4424
+ "label": "Plan",
4425
+ "order": 1
4426
+ },
4427
+ "status": {
4428
+ "order": 2
4429
+ },
4430
+ "startedAt": {
4431
+ "label": "Started on",
4432
+ "order": 3
4433
+ },
4434
+ "currentPeriodStart": {
4435
+ "label": "Current period starts",
4436
+ "order": 4
4437
+ },
4438
+ "currentPeriodEnd": {
4439
+ "label": "Current period ends",
4440
+ "order": 5
4441
+ },
4442
+ "trialEndsAt": {
4443
+ "label": "Trial ends",
4444
+ "order": 6
4445
+ },
4446
+ "cancelAtPeriodEnd": {
4447
+ "label": "Cancel at end of period",
4448
+ "order": 7
4449
+ },
4450
+ "canceledAt": {
4451
+ "label": "Cancelled on",
4452
+ "visibility": "advanced"
4453
+ },
4454
+ "agreedAmount": {
4455
+ "label": "Agreed amount (in cents)",
4456
+ "visibility": "advanced"
4457
+ },
4458
+ "subscriberKind": {
4459
+ "label": "Subscriber type",
4460
+ "visibility": "advanced"
4461
+ },
4462
+ "subscriberExternalId": {
4463
+ "label": "Outside subscriber",
4464
+ "visibility": "advanced"
4465
+ },
4466
+ "planVersionId": {
4467
+ "label": "Plan version",
4468
+ "visibility": "advanced",
4469
+ "locked": true
4470
+ },
4471
+ "tenantId": {
4472
+ "visibility": "hidden"
4473
+ },
4474
+ "planVersionPinnedAt": {
4475
+ "visibility": "hidden"
4476
+ },
4477
+ "planVersionPinReason": {
4478
+ "visibility": "hidden"
4479
+ },
4480
+ "planVersionPinNote": {
4481
+ "visibility": "hidden"
4482
+ },
4483
+ "retainsPaidPeriod": {
4484
+ "visibility": "hidden"
4485
+ },
4486
+ "externalProvider": {
4487
+ "visibility": "hidden"
4488
+ },
4489
+ "stripeCustomerId": {
4490
+ "visibility": "hidden"
4491
+ },
4492
+ "stripeSubscriptionId": {
4493
+ "visibility": "hidden"
4494
+ },
4495
+ "stripeCheckoutSessionId": {
4496
+ "visibility": "hidden"
4497
+ },
4498
+ "metadata": {
4499
+ "visibility": "hidden"
4500
+ }
4501
+ }
4502
+ },
4503
+ "@happyvertical/smrt-subscriptions:ClientCharge": {
4504
+ "fields": {
4505
+ "metricKey": {
4506
+ "label": "What was used",
4507
+ "order": 1
4508
+ },
4509
+ "quantity": {
4510
+ "label": "How much",
4511
+ "order": 2
4512
+ },
4513
+ "amount": {
4514
+ "label": "Amount (in cents)",
4515
+ "order": 3
4516
+ },
4517
+ "currency": {
4518
+ "order": 4
4519
+ },
4520
+ "status": {
4521
+ "order": 5
4522
+ },
4523
+ "approvedAt": {
4524
+ "label": "Approved on",
4525
+ "order": 6
4526
+ },
4527
+ "serviceKey": {
4528
+ "label": "Service",
4529
+ "visibility": "advanced"
4530
+ },
4531
+ "provider": {
4532
+ "visibility": "advanced"
4533
+ },
4534
+ "tenantId": {
4535
+ "visibility": "hidden"
4536
+ },
4537
+ "usageEventId": {
4538
+ "visibility": "hidden"
4539
+ },
4540
+ "usageTenantId": {
4541
+ "visibility": "hidden"
4542
+ },
4543
+ "subscriberKind": {
4544
+ "visibility": "hidden"
4545
+ },
4546
+ "subscriberExternalId": {
4547
+ "visibility": "hidden"
4548
+ },
4549
+ "projectId": {
4550
+ "visibility": "hidden"
4551
+ },
4552
+ "workRefType": {
4553
+ "visibility": "hidden"
4554
+ },
4555
+ "workRefId": {
4556
+ "visibility": "hidden"
4557
+ },
4558
+ "pricingRuleId": {
4559
+ "visibility": "hidden"
4560
+ },
4561
+ "priceBookId": {
4562
+ "visibility": "hidden"
4563
+ },
4564
+ "pricingSnapshot": {
4565
+ "visibility": "hidden"
4566
+ }
4567
+ }
4568
+ }
4569
+ },
4570
+ "help": {
4571
+ "markdown": "## Overview\n\nSubscriptions is where you sell plans that renew. A plan says what it costs,\nhow often it bills and whether it is free, a fixed price or pay what you can.\nA subscription joins a subscriber to a plan and shows where it stands, from\ntrial to active to cancelled. Every time you change a plan's terms, the\nearlier terms are kept as a plan version, so a subscriber who already paid for\na period keeps what they paid for until it renews. Charges show what each\nsubscriber used and what it cost them.\n\nAll amounts are whole numbers of cents, with no decimal point.\n\n## Tasks\n\n### Set up a plan\n\n1. Open the plans and choose **New**.\n2. Type the **{field:SubscriptionPlan.name}** customers will see and a short **{field:SubscriptionPlan.planKey}** such as \"starter\".\n3. Choose **{field:SubscriptionPlan.pricingMode}**: free, a fixed price, or pay what you can. This cannot change once the plan is saved; make a new plan instead.\n4. For a fixed price, enter the **{field:SubscriptionPlan.priceAmount}** in cents and the **{field:SubscriptionPlan.currency}**.\n5. Set how often it bills in **{field:SubscriptionPlan.billingInterval}**.\n6. Set the **{field:SubscriptionPlan.status}** to active when it is ready to sell.\n7. Save.\n\n### Change a plan's price or terms\n\n1. Open the plan and edit the **{field:SubscriptionPlan.priceAmount}**, **{field:SubscriptionPlan.billingInterval}** or **{field:SubscriptionPlan.status}**.\n2. Save. The new terms apply to new subscribers and to existing ones from their next renewal; a period already paid for keeps its old terms.\n\n### Retire a plan\n\n1. Open the plan and set its **{field:SubscriptionPlan.status}** to archived.\n2. Save. No one new can subscribe to it; existing subscribers keep going until they cancel.\n\n### Subscribe someone to a plan\n\n1. Open the subscriptions and choose **New**.\n2. Pick the **{field:TenantSubscription.planId}**.\n3. Enter the day it **{field:TenantSubscription.startedAt}** and, if there is a free trial, when the **{field:TenantSubscription.trialEndsAt}**.\n4. Save. The **{field:TenantSubscription.status}** shows where the subscription stands once it has been paid for.\n\n### See where a subscription stands\n\n1. Open the subscriptions and find the one you want.\n2. The **{field:TenantSubscription.status}** shows whether it is still incomplete, on trial, active, past due, unpaid or cancelled.\n3. **{field:TenantSubscription.currentPeriodStart}** and **{field:TenantSubscription.currentPeriodEnd}** show the period that has been paid for.\n\n### Cancel a subscription\n\n1. Open the subscription.\n2. Tick **{field:TenantSubscription.cancelAtPeriodEnd}** so it runs out when the current period ends instead of renewing.\n3. Save.\n\n### Look up the terms a subscriber was sold\n\nPlan versions are written automatically whenever a plan is saved with changed\nterms; you cannot add or edit them here.\n\n1. Open the plan versions and find the **{field:SubscriptionPlanVersion.planId}** you are interested in.\n2. Each row shows the **{field:SubscriptionPlanVersion.version}** and the date it was **{field:SubscriptionPlanVersion.effectiveFrom}**.\n3. Read the **{field:SubscriptionPlanVersion.priceAmount}**, **{field:SubscriptionPlanVersion.currency}** and **{field:SubscriptionPlanVersion.billingInterval}** that applied under it.\n\n### See what a subscriber was charged\n\nCharges are created by the usage and billing service as usage is recorded;\nyou cannot add or edit them here.\n\n1. Open the charges.\n2. Each row shows **{field:ClientCharge.metricKey}**, **{field:ClientCharge.quantity}** and the **{field:ClientCharge.amount}** in cents.\n3. The **{field:ClientCharge.status}** shows whether the charge is still a draft, approved or adjusted.\n",
4572
+ "fieldRefs": [
4573
+ "ClientCharge.amount",
4574
+ "ClientCharge.metricKey",
4575
+ "ClientCharge.quantity",
4576
+ "ClientCharge.status",
4577
+ "SubscriptionPlan.billingInterval",
4578
+ "SubscriptionPlan.currency",
4579
+ "SubscriptionPlan.name",
4580
+ "SubscriptionPlan.planKey",
4581
+ "SubscriptionPlan.priceAmount",
4582
+ "SubscriptionPlan.pricingMode",
4583
+ "SubscriptionPlan.status",
4584
+ "SubscriptionPlanVersion.billingInterval",
4585
+ "SubscriptionPlanVersion.currency",
4586
+ "SubscriptionPlanVersion.effectiveFrom",
4587
+ "SubscriptionPlanVersion.planId",
4588
+ "SubscriptionPlanVersion.priceAmount",
4589
+ "SubscriptionPlanVersion.version",
4590
+ "TenantSubscription.cancelAtPeriodEnd",
4591
+ "TenantSubscription.currentPeriodEnd",
4592
+ "TenantSubscription.currentPeriodStart",
4593
+ "TenantSubscription.planId",
4594
+ "TenantSubscription.startedAt",
4595
+ "TenantSubscription.status",
4596
+ "TenantSubscription.trialEndsAt"
4597
+ ]
4598
+ },
4599
+ "demo": {
4600
+ "mode": "server",
4601
+ "reasons": [
4602
+ "Needs a server because its package does not build for a browser: core migrations (node:crypto, node:os); and its package depends on @happyvertical/smrt-tenancy, which do not build for a browser (#3624, #3626)."
4603
+ ]
4604
+ }
4605
+ }
4606
+ ],
4191
4607
  "browser": {
4192
4608
  "status": "server-only",
4193
4609
  "issues": [