@codelitdev/billing 0.1.0-alpha.1

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.
Files changed (230) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +339 -0
  3. package/dist/adapters/drizzle/index.d.ts +28 -0
  4. package/dist/adapters/drizzle/index.d.ts.map +1 -0
  5. package/dist/adapters/drizzle/index.js +75 -0
  6. package/dist/adapters/drizzle/index.js.map +1 -0
  7. package/dist/adapters/drizzle/store.d.ts +37 -0
  8. package/dist/adapters/drizzle/store.d.ts.map +1 -0
  9. package/dist/adapters/drizzle/store.js +889 -0
  10. package/dist/adapters/drizzle/store.js.map +1 -0
  11. package/dist/catalog/index.d.ts +5 -0
  12. package/dist/catalog/index.d.ts.map +1 -0
  13. package/dist/catalog/index.js +4 -0
  14. package/dist/catalog/index.js.map +1 -0
  15. package/dist/catalog/public-view.d.ts +7 -0
  16. package/dist/catalog/public-view.d.ts.map +1 -0
  17. package/dist/catalog/public-view.js +22 -0
  18. package/dist/catalog/public-view.js.map +1 -0
  19. package/dist/catalog/revisions.d.ts +8 -0
  20. package/dist/catalog/revisions.d.ts.map +1 -0
  21. package/dist/catalog/revisions.js +12 -0
  22. package/dist/catalog/revisions.js.map +1 -0
  23. package/dist/catalog/types.d.ts +28 -0
  24. package/dist/catalog/types.d.ts.map +1 -0
  25. package/dist/catalog/types.js +2 -0
  26. package/dist/catalog/types.js.map +1 -0
  27. package/dist/catalog/validate.d.ts +16 -0
  28. package/dist/catalog/validate.d.ts.map +1 -0
  29. package/dist/catalog/validate.js +66 -0
  30. package/dist/catalog/validate.js.map +1 -0
  31. package/dist/cli/bin.d.ts +3 -0
  32. package/dist/cli/bin.d.ts.map +1 -0
  33. package/dist/cli/bin.js +35 -0
  34. package/dist/cli/bin.js.map +1 -0
  35. package/dist/cli/generate.d.ts +15 -0
  36. package/dist/cli/generate.d.ts.map +1 -0
  37. package/dist/cli/generate.js +68 -0
  38. package/dist/cli/generate.js.map +1 -0
  39. package/dist/config/define.d.ts +3 -0
  40. package/dist/config/define.d.ts.map +1 -0
  41. package/dist/config/define.js +5 -0
  42. package/dist/config/define.js.map +1 -0
  43. package/dist/config/index.d.ts +3 -0
  44. package/dist/config/index.d.ts.map +1 -0
  45. package/dist/config/index.js +3 -0
  46. package/dist/config/index.js.map +1 -0
  47. package/dist/config/validate.d.ts +32 -0
  48. package/dist/config/validate.d.ts.map +1 -0
  49. package/dist/config/validate.js +227 -0
  50. package/dist/config/validate.js.map +1 -0
  51. package/dist/core/checkout-attempt.d.ts +29 -0
  52. package/dist/core/checkout-attempt.d.ts.map +1 -0
  53. package/dist/core/checkout-attempt.js +10 -0
  54. package/dist/core/checkout-attempt.js.map +1 -0
  55. package/dist/core/clock.d.ts +10 -0
  56. package/dist/core/clock.d.ts.map +1 -0
  57. package/dist/core/clock.js +26 -0
  58. package/dist/core/clock.js.map +1 -0
  59. package/dist/core/customer.d.ts +13 -0
  60. package/dist/core/customer.d.ts.map +1 -0
  61. package/dist/core/customer.js +2 -0
  62. package/dist/core/customer.js.map +1 -0
  63. package/dist/core/errors.d.ts +28 -0
  64. package/dist/core/errors.d.ts.map +1 -0
  65. package/dist/core/errors.js +43 -0
  66. package/dist/core/errors.js.map +1 -0
  67. package/dist/core/ids.d.ts +15 -0
  68. package/dist/core/ids.d.ts.map +1 -0
  69. package/dist/core/ids.js +29 -0
  70. package/dist/core/ids.js.map +1 -0
  71. package/dist/core/index.d.ts +14 -0
  72. package/dist/core/index.d.ts.map +1 -0
  73. package/dist/core/index.js +14 -0
  74. package/dist/core/index.js.map +1 -0
  75. package/dist/core/money.d.ts +8 -0
  76. package/dist/core/money.d.ts.map +1 -0
  77. package/dist/core/money.js +23 -0
  78. package/dist/core/money.js.map +1 -0
  79. package/dist/core/plan-change-attempt.d.ts +30 -0
  80. package/dist/core/plan-change-attempt.d.ts.map +1 -0
  81. package/dist/core/plan-change-attempt.js +9 -0
  82. package/dist/core/plan-change-attempt.js.map +1 -0
  83. package/dist/core/projection-diff.d.ts +32 -0
  84. package/dist/core/projection-diff.d.ts.map +1 -0
  85. package/dist/core/projection-diff.js +32 -0
  86. package/dist/core/projection-diff.js.map +1 -0
  87. package/dist/core/reconciliation-job.d.ts +21 -0
  88. package/dist/core/reconciliation-job.d.ts.map +1 -0
  89. package/dist/core/reconciliation-job.js +26 -0
  90. package/dist/core/reconciliation-job.js.map +1 -0
  91. package/dist/core/subscription.d.ts +62 -0
  92. package/dist/core/subscription.d.ts.map +1 -0
  93. package/dist/core/subscription.js +14 -0
  94. package/dist/core/subscription.js.map +1 -0
  95. package/dist/core/transitions.d.ts +25 -0
  96. package/dist/core/transitions.d.ts.map +1 -0
  97. package/dist/core/transitions.js +161 -0
  98. package/dist/core/transitions.js.map +1 -0
  99. package/dist/core/webhook-inbox.d.ts +23 -0
  100. package/dist/core/webhook-inbox.d.ts.map +1 -0
  101. package/dist/core/webhook-inbox.js +9 -0
  102. package/dist/core/webhook-inbox.js.map +1 -0
  103. package/dist/maintenance/batches.d.ts +3 -0
  104. package/dist/maintenance/batches.d.ts.map +1 -0
  105. package/dist/maintenance/batches.js +3 -0
  106. package/dist/maintenance/batches.js.map +1 -0
  107. package/dist/maintenance/claims.d.ts +2 -0
  108. package/dist/maintenance/claims.d.ts.map +1 -0
  109. package/dist/maintenance/claims.js +2 -0
  110. package/dist/maintenance/claims.js.map +1 -0
  111. package/dist/maintenance/health.d.ts +13 -0
  112. package/dist/maintenance/health.d.ts.map +1 -0
  113. package/dist/maintenance/health.js +19 -0
  114. package/dist/maintenance/health.js.map +1 -0
  115. package/dist/maintenance/retry.d.ts +9 -0
  116. package/dist/maintenance/retry.d.ts.map +1 -0
  117. package/dist/maintenance/retry.js +19 -0
  118. package/dist/maintenance/retry.js.map +1 -0
  119. package/dist/operations/index.d.ts +28 -0
  120. package/dist/operations/index.d.ts.map +1 -0
  121. package/dist/operations/index.js +106 -0
  122. package/dist/operations/index.js.map +1 -0
  123. package/dist/persistence/memory.d.ts +104 -0
  124. package/dist/persistence/memory.d.ts.map +1 -0
  125. package/dist/persistence/memory.js +657 -0
  126. package/dist/persistence/memory.js.map +1 -0
  127. package/dist/persistence/store.d.ts +126 -0
  128. package/dist/persistence/store.d.ts.map +1 -0
  129. package/dist/persistence/store.js +2 -0
  130. package/dist/persistence/store.js.map +1 -0
  131. package/dist/ports/audit.d.ts +20 -0
  132. package/dist/ports/audit.d.ts.map +1 -0
  133. package/dist/ports/audit.js +9 -0
  134. package/dist/ports/audit.js.map +1 -0
  135. package/dist/ports/authorization.d.ts +28 -0
  136. package/dist/ports/authorization.d.ts.map +1 -0
  137. package/dist/ports/authorization.js +33 -0
  138. package/dist/ports/authorization.js.map +1 -0
  139. package/dist/ports/lifecycle.d.ts +23 -0
  140. package/dist/ports/lifecycle.d.ts.map +1 -0
  141. package/dist/ports/lifecycle.js +2 -0
  142. package/dist/ports/lifecycle.js.map +1 -0
  143. package/dist/ports/sensitive-values.d.ts +23 -0
  144. package/dist/ports/sensitive-values.d.ts.map +1 -0
  145. package/dist/ports/sensitive-values.js +19 -0
  146. package/dist/ports/sensitive-values.js.map +1 -0
  147. package/dist/ports/telemetry.d.ts +16 -0
  148. package/dist/ports/telemetry.d.ts.map +1 -0
  149. package/dist/ports/telemetry.js +48 -0
  150. package/dist/ports/telemetry.js.map +1 -0
  151. package/dist/providers/contract.d.ts +98 -0
  152. package/dist/providers/contract.d.ts.map +1 -0
  153. package/dist/providers/contract.js +2 -0
  154. package/dist/providers/contract.js.map +1 -0
  155. package/dist/providers/dodo/index.d.ts +25 -0
  156. package/dist/providers/dodo/index.d.ts.map +1 -0
  157. package/dist/providers/dodo/index.js +296 -0
  158. package/dist/providers/dodo/index.js.map +1 -0
  159. package/dist/providers/dodo/normalize.d.ts +7 -0
  160. package/dist/providers/dodo/normalize.d.ts.map +1 -0
  161. package/dist/providers/dodo/normalize.js +77 -0
  162. package/dist/providers/dodo/normalize.js.map +1 -0
  163. package/dist/providers/fake/index.d.ts +59 -0
  164. package/dist/providers/fake/index.d.ts.map +1 -0
  165. package/dist/providers/fake/index.js +312 -0
  166. package/dist/providers/fake/index.js.map +1 -0
  167. package/dist/providers/index.d.ts +4 -0
  168. package/dist/providers/index.d.ts.map +1 -0
  169. package/dist/providers/index.js +3 -0
  170. package/dist/providers/index.js.map +1 -0
  171. package/dist/providers/registry.d.ts +10 -0
  172. package/dist/providers/registry.d.ts.map +1 -0
  173. package/dist/providers/registry.js +32 -0
  174. package/dist/providers/registry.js.map +1 -0
  175. package/dist/schema/canonical.d.ts +7 -0
  176. package/dist/schema/canonical.d.ts.map +1 -0
  177. package/dist/schema/canonical.js +35 -0
  178. package/dist/schema/canonical.js.map +1 -0
  179. package/dist/schema/generator.d.ts +3 -0
  180. package/dist/schema/generator.d.ts.map +1 -0
  181. package/dist/schema/generator.js +496 -0
  182. package/dist/schema/generator.js.map +1 -0
  183. package/dist/schema/pglite-sql.d.ts +3 -0
  184. package/dist/schema/pglite-sql.d.ts.map +1 -0
  185. package/dist/schema/pglite-sql.js +226 -0
  186. package/dist/schema/pglite-sql.js.map +1 -0
  187. package/dist/testing/clocks.d.ts +2 -0
  188. package/dist/testing/clocks.d.ts.map +1 -0
  189. package/dist/testing/clocks.js +2 -0
  190. package/dist/testing/clocks.js.map +1 -0
  191. package/dist/testing/consumer-conformance.d.ts +12 -0
  192. package/dist/testing/consumer-conformance.d.ts.map +1 -0
  193. package/dist/testing/consumer-conformance.js +17 -0
  194. package/dist/testing/consumer-conformance.js.map +1 -0
  195. package/dist/testing/fixtures.d.ts +9 -0
  196. package/dist/testing/fixtures.d.ts.map +1 -0
  197. package/dist/testing/fixtures.js +69 -0
  198. package/dist/testing/fixtures.js.map +1 -0
  199. package/dist/testing/index.d.ts +7 -0
  200. package/dist/testing/index.d.ts.map +1 -0
  201. package/dist/testing/index.js +7 -0
  202. package/dist/testing/index.js.map +1 -0
  203. package/dist/testing/provider-contract.d.ts +14 -0
  204. package/dist/testing/provider-contract.d.ts.map +1 -0
  205. package/dist/testing/provider-contract.js +238 -0
  206. package/dist/testing/provider-contract.js.map +1 -0
  207. package/dist/testing/state-machine.d.ts +2 -0
  208. package/dist/testing/state-machine.d.ts.map +1 -0
  209. package/dist/testing/state-machine.js +2 -0
  210. package/dist/testing/state-machine.js.map +1 -0
  211. package/dist/testing/workflow-harness.d.ts +224 -0
  212. package/dist/testing/workflow-harness.d.ts.map +1 -0
  213. package/dist/testing/workflow-harness.js +65 -0
  214. package/dist/testing/workflow-harness.js.map +1 -0
  215. package/dist/workflows/engine.d.ts +157 -0
  216. package/dist/workflows/engine.d.ts.map +1 -0
  217. package/dist/workflows/engine.js +2015 -0
  218. package/dist/workflows/engine.js.map +1 -0
  219. package/dist/workflows/index.d.ts +10 -0
  220. package/dist/workflows/index.d.ts.map +1 -0
  221. package/dist/workflows/index.js +6 -0
  222. package/dist/workflows/index.js.map +1 -0
  223. package/dist/workflows/lifecycle.d.ts +5 -0
  224. package/dist/workflows/lifecycle.d.ts.map +1 -0
  225. package/dist/workflows/lifecycle.js +7 -0
  226. package/dist/workflows/lifecycle.js.map +1 -0
  227. package/docs/architecture.md +1188 -0
  228. package/docs/decisions/0001-extraction-discrepancies.md +29 -0
  229. package/docs/runbooks.md +140 -0
  230. package/package.json +110 -0
@@ -0,0 +1,29 @@
1
+ # ADR 0001: Resolve architecture §20 PRD vs SendLit discrepancies
2
+
3
+ Status: accepted
4
+ Default: SendLit implemented behavior, except where a correctness defect is named.
5
+
6
+ | # | Topic | Package decision |
7
+ | --- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
8
+ | 1 | Cancellation recovery | `cancelled` may transition to `trialing`, `active`, or `past_due` (provider recovery before paid-through expiry). `expired` is terminal. |
9
+ | 2 | Pending status | Canonical status includes `pending`. |
10
+ | 3 | Envelope vs snapshot | `VerifiedWebhookEnvelope` is wake-up evidence; `SubscriptionSnapshot` is retrieved truth. |
11
+ | 4 | Catalog availability | Pending/invalid requested revision freezes checkout only. Last verified revision stays the active commercial projection. |
12
+ | 5 | Provider cancellation | Adapter requires `cancelSubscription`. |
13
+ | 6 | Metadata | Allowlisted keys are `checkoutAttemptId` and `catalogKey`. No `sendlit*` keys. |
14
+ | 7 | Clock | Decision functions and adapters take an injected `Clock`. Core never reads wall time. |
15
+ | 8 | Reconciliation claims | Durable `billing_reconciliation_jobs` leases. No row lock spans a provider call. |
16
+ | 9 | Catalog failure | Last verified revision remains `active`. Requested pending/invalid/abandoned revision makes `checkoutAvailable=false`. |
17
+ | 10 | Existing-schema mapping | Config may remap table/column names; generator does not invent parallel tables. |
18
+ | 11 | Config loading | `billing.config.ts` is a pure schema description; generate does not connect to a DB or read provider secrets. |
19
+ | 12 | Plan constraints | Configured `planIds` become generated check constraints (schema SemVer). |
20
+ | 13 | Catalog provenance | Subscriptions persist `catalogRevision` + offer key + price entry; lineage updates only from correlated checkout/change attempts. |
21
+ | 14 | Projection idempotency | **Correctness defect vs SendLit.** Equivalent snapshots update freshness only. Material change increments `projectionVersion` once. |
22
+ | 15 | Snapshot time | `providerOccurredAt` / `providerVersion` are distinct from `observedAt`. Retrieval time is not provider event time. |
23
+ | 16 | Payer invariant | Workflows compare persisted payer on customer/attempt/subscription. A fresh grant cannot substitute a different payer. |
24
+ | 17 | Action grant | Opaque single-use `BillingAuthorizationPort.consume`. Webhooks/reconciliation cannot present a human grant. |
25
+ | 18 | Operator audit | Mutating operator APIs require `OperatorContext` and the audit hook. Decrypt is a separate audit. |
26
+ | 19 | Read models | Export `PublicBillingCatalog` and `CommercialBillingState` only. Product entitlements stay outside. |
27
+ | 20 | Mutation recovery | Each mutation declares `idempotency_key` \| `lookup` \| `unsupported`. Contract tests inject side-effect-then-timeout. |
28
+ | 21 | Plan-state | Canonical row is `billableEntityId`, nullable `activeSubscriptionId`, `projectionVersion`. No Free plan column. |
29
+ | 22 | Required audit hook | Cloud composition without an audit hook is a composition error. |
@@ -0,0 +1,140 @@
1
+ # Operational runbooks
2
+
3
+ Status: package-owned procedures for `@codelitdev/billing`
4
+ Audience: SendLit and CourseLit operators composing product-authenticated commands over `createOperations` and maintenance units
5
+ Package version: 0.1.0-alpha.1 (experimental workflows; not a v1 cutover)
6
+
7
+ These runbooks name package units, not HTTP routes. Product CLIs and admin screens must supply `OperatorContext`, persist audit rows, and keep product nouns (organisation, school, trial, availability) outside canonical billing tables.
8
+
9
+ Production canaries remain consumer-owned. Do not declare a v1/stable cutover until SendLit and CourseLit have each run a limited production canary.
10
+
11
+ ## Shared controls
12
+
13
+ Bounded work units (the package does not start timers):
14
+
15
+ - `recordRequestedCatalog` / `verifyRequestedCatalog` / `abandonRequestedCatalog`
16
+ - `runWebhookInboxBatch`
17
+ - `runReconciliationBatch` / `claimReconciliationJobs`
18
+ - `runDeadlineBatch`
19
+ - `purgeExpiredSensitiveValues`
20
+ - operations: `inspectEntity`, `health`, `reconcileSubscription`, `retryWebhook`, `requestCancellation`, `decryptReplay`
21
+
22
+ Multi-instance safety comes from database claims. Never hold an application transaction across a provider call.
23
+
24
+ ---
25
+
26
+ ## Webhook outage and replay
27
+
28
+ 1. Confirm `health().oldestInboxAgeMs`, `quarantinedWebhooks`, and provider status-page/outage.
29
+ 2. Keep ingesting: signature verification and durable insert must still 2xx before projection.
30
+ 3. Replay from the provider dashboard or stored ciphertext only through `decryptReplay` plus provider replay. Do not re-post unverified bodies.
31
+ 4. Drain with `runWebhookInboxBatch`. Duplicate `providerEventId` values must no-op.
32
+ 5. If events quarantine, inspect metadata (not raw SDK text) and either `retryWebhook(..., "preserve_attempts")` or start `new_budget` with an audit reason.
33
+
34
+ ## Catalog mismatch at startup
35
+
36
+ 1. Configuration validation is synchronous. Ordinary readiness must not wait on Dodo.
37
+ 2. `recordRequestedCatalog` writes `pending_verification` without contacting the provider.
38
+ 3. `verifyRequestedCatalog` retrieves products, then atomically inserts/reuses price entries, attaches revision items, retires the prior revision, and activates the new one.
39
+ 4. Pending or invalid requested revisions set `checkoutAvailable=false`. Last verified revision remains the commercial projection.
40
+ 5. Provider outage during verify leaves the revision pending; it does not mark it invalid.
41
+ 6. After reverting env, `abandonRequestedCatalog` with actor/reason restores checkout on the prior verified revision.
42
+ 7. An older instance cannot activate a lower revision over a newer active catalog.
43
+
44
+ ## Stuck checkout/change
45
+
46
+ 1. `health().stuckCreatingCheckouts` and entity `pendingCheckout` / `pendingPlanChange`.
47
+ 2. `runDeadlineBatch` expires open/creating checkouts past `expiresAt` and clears checkout URLs.
48
+ 3. `runReconciliationBatch` resumes `creating` attempts with the original idempotency key and stored payer email.
49
+ 4. Do not delete the attempt. Abandon only through an audited operator path once the provider session is known terminal.
50
+
51
+ ## Remote success with failed local persistence
52
+
53
+ 1. Symptom: provider customer/session exists, local attempt stays `creating`, reconciliation job queued.
54
+ 2. `runReconciliationBatch` must reuse the stored idempotency key and must not open a second provider customer/session identity.
55
+ 3. After success, attempt is `open` (checkout) or the subscription projection is material once. Equivalent snapshot refresh must not increment `projectionVersion`.
56
+
57
+ ## Duplicate provider customer
58
+
59
+ 1. Unique constraint is `(provider, payerId)` plus a partial unique on provider customer id.
60
+ 2. A second create for the same payer must return the stored row.
61
+ 3. If the provider created two remote customers, keep the locally recorded `providerCustomerId`. Quarantine conflicting jobs; do not merge by email.
62
+
63
+ ## Conflicting active subscriptions
64
+
65
+ 1. At most one `isEntitlementSource` subscription per billable entity.
66
+ 2. A second paid snapshot for the same entity quarantines rather than flipping the pointer.
67
+ 3. Immediate cancellation plus projection must clear `activeSubscriptionId`.
68
+ 4. Manual repair uses `reconcileSubscription` and audited projection, never a direct SQL pointer edit.
69
+
70
+ ## Manual reconciliation with immutable correction/audit
71
+
72
+ 1. `reconcileSubscription(context, subscriptionId)` requires actor/reason and writes an operator audit effect before work.
73
+ 2. Reconciliation retrieves current provider truth and applies the same transitions as webhooks.
74
+ 3. Corrections are new audited effects. Do not rewrite previous projection rows in place to hide history.
75
+
76
+ ## Reconciliation lease exhaustion, reclaim, and quarantine
77
+
78
+ 1. Claims use committed leases (`claimReconciliationJobs`). `FOR UPDATE SKIP LOCKED` must not span the provider call.
79
+ 2. A crashed worker’s lease expires; another worker reclaims the same job.
80
+ 3. Exhausted attempts or `operation_quarantined` / misconfigured provider errors move the job to `quarantined`.
81
+ 4. Operator retry must say `preserve_attempts` or `new_budget`. Setting `status=pending` in SQL is not a public API.
82
+
83
+ ## Entity close or payer removal blocked by live billing responsibility
84
+
85
+ 1. Before product close/delete, call `getBillableEntityBillingBlockers` and `getPayerBillingResponsibilities`.
86
+ 2. Live checkout, nonterminal subscription, or future paid-through entitlement must block close.
87
+ 3. Do not cancel at the provider as a side effect of entity delete. Cancel through the cancellation workflow or operator cancel, then wait for terminal projection and elapsed paid-through (`runDeadlineBatch`).
88
+
89
+ ## Sensitive action-token outage/replay and persisted payer mismatch
90
+
91
+ 1. Checkout, portal, plan change, and cancellation consume a single-use grant before mutation.
92
+ 2. A replayed grant returns `grant_consumed`.
93
+ 3. A fresh grant cannot substitute a different payer than the one persisted on the customer/attempt/subscription (`payer_mismatch` / `grant_invalid`).
94
+ 4. Token-issuer outage is an application/Platform incident. Billing must fail closed, not skip `consume`.
95
+
96
+ ## Operator retry-budget reset versus preservation
97
+
98
+ 1. `retryWebhook(context, providerEventId, "preserve_attempts")` requeues without clearing `processingAttempts`.
99
+ 2. `"new_budget"` zeroes attempts and is a distinct audit effect.
100
+ 3. Never expose retry as an unauthenticated script.
101
+
102
+ ## Audited replay-payload inspection and retention purge
103
+
104
+ 1. `decryptReplay` requires `OperatorContext` and a `SensitiveValuePort`. The decrypt is audited even when no row changes.
105
+ 2. Applications choose retention. `purgeExpiredSensitiveValues({ before, limit })` nulls terminal checkout URLs, plan-change payment URLs, and processed/ignored webhook payloads older than `before`.
106
+ 3. `before` must not be in the future. Purge is idempotent and writes one audit effect per cleared secret.
107
+ 4. Immutable catalog, attempt, and projection history remains.
108
+
109
+ ## Provider credential rotation
110
+
111
+ 1. Inject API keys and webhook secrets from the application. The Dodo adapter never reads `process.env`.
112
+ 2. Webhook secrets are a versioned list. Ingest tries unexpired versions; envelopes record `verifiedKeyVersion`.
113
+ 3. Rotate by adding a new version, deploying, then expiring the old secret. Do not change checkout provider in the same change.
114
+ 4. After rotation, run a sandbox retrieve (`BILLING_DODO_SANDBOX_*`) and a webhook ingest canary.
115
+
116
+ ## Package rollback with app-owned migrations
117
+
118
+ 1. The package never runs DDL on import, install, or worker start.
119
+ 2. Roll back application code only to a build compatible with the already-applied Drizzle migration.
120
+ 3. Do not reverse a billing migration that has expanded columns unless a reviewed down-migration exists in the product repo.
121
+ 4. Catalog revisions and price entries are immutable; rollback of application config may `abandonRequestedCatalog` but must not delete historical mappings needed by live subscriptions.
122
+
123
+ ## CourseLit trial/subscription expiry and public-availability verification
124
+
125
+ 1. CourseLit 14-day trials are application state. They must not create a fake provider subscription or a Free plan row.
126
+ 2. Canonical `runDeadlineBatch` clears entitlement when a cancelled subscription’s `paidThroughAt` has elapsed. Public school availability is CourseLit policy composed with `commercialState`.
127
+ 3. Two schools under one payer expire independently. Recovering one school must not re-open the other.
128
+ 4. Verify: unpaid school past trial is unavailable; paid school remains available; admin billing routes still load `commercialState`.
129
+
130
+ ## Canary checklist (consumer-owned)
131
+
132
+ Until both products complete this list, keep `@codelitdev/billing@0.1.0-alpha.1` and treat workflows as experimental:
133
+
134
+ - Feature-flag or percentage canary in SendLit first.
135
+ - `generate --check` clean; reviewed Drizzle migration applied by the product job.
136
+ - Catalog record/verify against Dodo test then live products.
137
+ - One checkout, webhook, reconciliation reclaim, cancellation, and deadline/purge cycle.
138
+ - Action-grant consume-once and payer-mismatch probes.
139
+ - Operator audit visible for reconcile, retry, abandon, and decrypt.
140
+ - CourseLit canary only after SendLit’s limited production canary is green.
package/package.json ADDED
@@ -0,0 +1,110 @@
1
+ {
2
+ "name": "@codelitdev/billing",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Provider-neutral billing engine for CodeLit products",
5
+ "author": "CodeLit",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "bin": {
9
+ "codelit-billing": "./dist/cli/bin.js"
10
+ },
11
+ "exports": {
12
+ "./core": {
13
+ "types": "./dist/core/index.d.ts",
14
+ "import": "./dist/core/index.js",
15
+ "default": "./dist/core/index.js"
16
+ },
17
+ "./catalog": {
18
+ "types": "./dist/catalog/index.d.ts",
19
+ "import": "./dist/catalog/index.js",
20
+ "default": "./dist/catalog/index.js"
21
+ },
22
+ "./config": {
23
+ "types": "./dist/config/index.d.ts",
24
+ "import": "./dist/config/index.js",
25
+ "default": "./dist/config/index.js"
26
+ },
27
+ "./drizzle": {
28
+ "types": "./dist/adapters/drizzle/index.d.ts",
29
+ "import": "./dist/adapters/drizzle/index.js",
30
+ "default": "./dist/adapters/drizzle/index.js"
31
+ },
32
+ "./workflows": {
33
+ "types": "./dist/workflows/index.d.ts",
34
+ "import": "./dist/workflows/index.js",
35
+ "default": "./dist/workflows/index.js"
36
+ },
37
+ "./operations": {
38
+ "types": "./dist/operations/index.d.ts",
39
+ "import": "./dist/operations/index.js",
40
+ "default": "./dist/operations/index.js"
41
+ },
42
+ "./providers": {
43
+ "types": "./dist/providers/index.d.ts",
44
+ "import": "./dist/providers/index.js",
45
+ "default": "./dist/providers/index.js"
46
+ },
47
+ "./providers/dodo": {
48
+ "types": "./dist/providers/dodo/index.d.ts",
49
+ "import": "./dist/providers/dodo/index.js",
50
+ "default": "./dist/providers/dodo/index.js"
51
+ },
52
+ "./testing": {
53
+ "types": "./dist/testing/index.d.ts",
54
+ "import": "./dist/testing/index.js",
55
+ "default": "./dist/testing/index.js"
56
+ }
57
+ },
58
+ "files": [
59
+ "dist",
60
+ "docs/architecture.md",
61
+ "docs/decisions",
62
+ "docs/runbooks.md"
63
+ ],
64
+ "publishConfig": {
65
+ "access": "public",
66
+ "tag": "alpha"
67
+ },
68
+ "dependencies": {
69
+ "drizzle-orm": "^0.45.2",
70
+ "nanoid": "^5.1.6",
71
+ "tsx": "^4.20.6",
72
+ "zod": "^3.25.76"
73
+ },
74
+ "peerDependencies": {
75
+ "dodopayments": "^2.48.0"
76
+ },
77
+ "peerDependenciesMeta": {
78
+ "dodopayments": {
79
+ "optional": true
80
+ }
81
+ },
82
+ "devDependencies": {
83
+ "@electric-sql/pglite": "^0.2.17",
84
+ "@eslint/js": "^10.0.1",
85
+ "@types/node": "^22.14.1",
86
+ "dodopayments": "^2.48.0",
87
+ "drizzle-kit": "^0.28.1",
88
+ "eslint": "^10.9.1",
89
+ "eslint-config-prettier": "^8.5.0",
90
+ "eslint-plugin-unused-imports": "^4.3.0",
91
+ "globals": "^17.11.0",
92
+ "husky": "^9.1.7",
93
+ "lint-staged": "^15.2.2",
94
+ "prettier": "^3.0.2",
95
+ "typescript": "^5.9.3",
96
+ "typescript-eslint": "^8.68.0",
97
+ "vitest": "^4.1.9"
98
+ },
99
+ "scripts": {
100
+ "build": "tsc -p tsconfig.json",
101
+ "test": "vitest run",
102
+ "test:watch": "vitest",
103
+ "test:packed": "node scripts/packed-consumer.mjs",
104
+ "typecheck": "tsc -p tsconfig.test.json --noEmit",
105
+ "lint": "eslint . --quiet",
106
+ "prettier": "prettier --write .",
107
+ "generate": "tsx src/cli/bin.ts generate",
108
+ "release:alpha": "pnpm test && pnpm typecheck && pnpm lint && pnpm build && pnpm test:packed"
109
+ }
110
+ }