@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,226 @@
1
+ /** Canonical DDL used by adapter conformance on PGlite. Mirrors the generator. */
2
+ export const CANONICAL_PGLITE_SQL = `
3
+ CREATE TABLE accounts (
4
+ id text PRIMARY KEY,
5
+ email text NOT NULL
6
+ );
7
+ CREATE TABLE workspaces (
8
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
9
+ name text NOT NULL
10
+ );
11
+ CREATE TABLE billing_price_entries (
12
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
13
+ offer_key text NOT NULL,
14
+ plan text NOT NULL,
15
+ billing_interval text NOT NULL,
16
+ currency text NOT NULL,
17
+ amount_minor integer NOT NULL,
18
+ provider_trial_days integer NOT NULL DEFAULT 0,
19
+ provider text NOT NULL,
20
+ provider_product_id text NOT NULL,
21
+ verified_at timestamptz,
22
+ created_at timestamptz NOT NULL DEFAULT now(),
23
+ updated_at timestamptz NOT NULL DEFAULT now(),
24
+ CONSTRAINT billing_price_entries_amount_check CHECK (amount_minor > 0),
25
+ CONSTRAINT billing_price_entries_trial_days_check CHECK (provider_trial_days >= 0),
26
+ CONSTRAINT billing_price_entries_currency_check CHECK (currency ~ '^[A-Z]{3}$'),
27
+ CONSTRAINT billing_price_entries_plan_check CHECK (plan IN ('pro', 'business')),
28
+ CONSTRAINT billing_price_entries_interval_check CHECK (billing_interval IN ('month', 'year'))
29
+ );
30
+ CREATE UNIQUE INDEX billing_price_entries_provider_product_uidx
31
+ ON billing_price_entries (provider, provider_product_id);
32
+ CREATE TABLE billing_catalog_revisions (
33
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
34
+ revision integer NOT NULL UNIQUE,
35
+ checkout_provider text NOT NULL,
36
+ status text NOT NULL DEFAULT 'pending_verification',
37
+ verified_at timestamptz,
38
+ activated_at timestamptz,
39
+ retired_at timestamptz,
40
+ created_at timestamptz NOT NULL DEFAULT now(),
41
+ updated_at timestamptz NOT NULL DEFAULT now(),
42
+ CONSTRAINT billing_catalog_revisions_status_check CHECK (status IN ('pending_verification', 'active', 'retired', 'invalid', 'abandoned')),
43
+ CONSTRAINT billing_catalog_revisions_revision_check CHECK (revision > 0)
44
+ );
45
+ CREATE UNIQUE INDEX billing_catalog_revisions_active_provider_uidx
46
+ ON billing_catalog_revisions (checkout_provider) WHERE status = 'active';
47
+ CREATE TABLE billing_catalog_revision_items (
48
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
49
+ catalog_revision_id uuid NOT NULL REFERENCES billing_catalog_revisions(id) ON DELETE CASCADE,
50
+ offer_key text NOT NULL,
51
+ billing_price_entry_id uuid NOT NULL REFERENCES billing_price_entries(id) ON DELETE RESTRICT
52
+ );
53
+ CREATE UNIQUE INDEX billing_catalog_revision_items_revision_key_uidx
54
+ ON billing_catalog_revision_items (catalog_revision_id, offer_key);
55
+ CREATE TABLE billing_provider_customers (
56
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
57
+ provider text NOT NULL,
58
+ payer_id text NOT NULL REFERENCES accounts(id) ON DELETE RESTRICT,
59
+ payer_email text NOT NULL DEFAULT '',
60
+ provider_customer_id text,
61
+ idempotency_key text NOT NULL,
62
+ status text NOT NULL DEFAULT 'creating',
63
+ last_error text,
64
+ created_at timestamptz NOT NULL DEFAULT now(),
65
+ updated_at timestamptz NOT NULL DEFAULT now(),
66
+ CONSTRAINT billing_provider_customers_status_check CHECK (status IN ('creating', 'active', 'conflicted'))
67
+ );
68
+ CREATE UNIQUE INDEX billing_provider_customers_provider_payer_uidx
69
+ ON billing_provider_customers (provider, payer_id);
70
+ CREATE UNIQUE INDEX billing_provider_customers_idempotency_uidx
71
+ ON billing_provider_customers (idempotency_key);
72
+ CREATE TABLE billing_checkout_attempts (
73
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
74
+ attempt_id text NOT NULL UNIQUE,
75
+ billable_entity_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE RESTRICT,
76
+ payer_id text NOT NULL REFERENCES accounts(id) ON DELETE RESTRICT,
77
+ payer_email text NOT NULL DEFAULT '',
78
+ return_url text NOT NULL DEFAULT '',
79
+ provider text NOT NULL,
80
+ catalog_revision integer NOT NULL,
81
+ offer_key text NOT NULL,
82
+ requested_plan text NOT NULL,
83
+ requested_interval text NOT NULL,
84
+ billing_price_entry_id uuid NOT NULL REFERENCES billing_price_entries(id) ON DELETE RESTRICT,
85
+ quoted_amount_minor integer NOT NULL,
86
+ quoted_currency text NOT NULL,
87
+ billing_customer_id uuid REFERENCES billing_provider_customers(id) ON DELETE RESTRICT,
88
+ provider_checkout_session_id text,
89
+ checkout_url_encrypted text,
90
+ idempotency_key text NOT NULL,
91
+ status text NOT NULL DEFAULT 'creating',
92
+ expires_at timestamptz NOT NULL,
93
+ last_error text,
94
+ completed_at timestamptz,
95
+ created_at timestamptz NOT NULL DEFAULT now(),
96
+ updated_at timestamptz NOT NULL DEFAULT now(),
97
+ CONSTRAINT billing_checkout_attempts_status_check CHECK (status IN ('creating', 'open', 'completed', 'expired', 'abandoned', 'conflicted')),
98
+ CONSTRAINT billing_checkout_attempts_amount_check CHECK (quoted_amount_minor > 0)
99
+ );
100
+ CREATE UNIQUE INDEX billing_checkout_attempts_idempotency_uidx
101
+ ON billing_checkout_attempts (idempotency_key);
102
+ CREATE UNIQUE INDEX billing_checkout_attempts_entity_nonterminal_uidx
103
+ ON billing_checkout_attempts (billable_entity_id) WHERE status IN ('creating', 'open');
104
+ CREATE TABLE billing_webhook_events (
105
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
106
+ provider text NOT NULL,
107
+ provider_event_id text NOT NULL,
108
+ event_type text NOT NULL,
109
+ occurred_at timestamptz NOT NULL,
110
+ subscription_id text,
111
+ checkout_attempt_id text,
112
+ payload_encrypted text,
113
+ payload_key_version text,
114
+ verified_key_version text,
115
+ status text NOT NULL DEFAULT 'pending',
116
+ processing_attempts integer NOT NULL DEFAULT 0,
117
+ last_error text,
118
+ available_at timestamptz NOT NULL DEFAULT now(),
119
+ locked_at timestamptz,
120
+ lease_expires_at timestamptz,
121
+ worker_id text,
122
+ received_at timestamptz NOT NULL DEFAULT now(),
123
+ processed_at timestamptz,
124
+ CONSTRAINT billing_webhook_events_status_check CHECK (status IN ('pending', 'processing', 'processed', 'ignored', 'quarantined', 'failed'))
125
+ );
126
+ CREATE UNIQUE INDEX billing_webhook_events_provider_event_uidx
127
+ ON billing_webhook_events (provider, provider_event_id);
128
+ CREATE TABLE billing_subscriptions (
129
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
130
+ billable_entity_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE RESTRICT,
131
+ billing_customer_id uuid NOT NULL REFERENCES billing_provider_customers(id) ON DELETE RESTRICT,
132
+ payer_id text NOT NULL REFERENCES accounts(id) ON DELETE RESTRICT,
133
+ origin_checkout_attempt_id uuid REFERENCES billing_checkout_attempts(id) ON DELETE RESTRICT,
134
+ provider text NOT NULL,
135
+ provider_subscription_id text NOT NULL,
136
+ provider_product_id text NOT NULL,
137
+ billing_price_entry_id uuid NOT NULL REFERENCES billing_price_entries(id) ON DELETE RESTRICT,
138
+ catalog_revision integer NOT NULL,
139
+ offer_key text NOT NULL,
140
+ plan text NOT NULL,
141
+ billing_interval text NOT NULL,
142
+ status text NOT NULL DEFAULT 'pending',
143
+ current_period_starts_at timestamptz,
144
+ current_period_ends_at timestamptz,
145
+ paid_through_at timestamptz,
146
+ trial_ends_at timestamptz,
147
+ cancel_at_period_end boolean NOT NULL DEFAULT false,
148
+ is_entitlement_source boolean NOT NULL DEFAULT false,
149
+ provider_occurred_at timestamptz,
150
+ provider_version text,
151
+ last_observed_at timestamptz,
152
+ last_reconciled_at timestamptz,
153
+ created_at timestamptz NOT NULL DEFAULT now(),
154
+ updated_at timestamptz NOT NULL DEFAULT now()
155
+ );
156
+ CREATE UNIQUE INDEX billing_subscriptions_provider_subscription_uidx
157
+ ON billing_subscriptions (provider, provider_subscription_id);
158
+ CREATE UNIQUE INDEX billing_subscriptions_entity_source_uidx
159
+ ON billing_subscriptions (billable_entity_id) WHERE is_entitlement_source = true;
160
+ CREATE TABLE billing_plan_states (
161
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
162
+ billable_entity_id uuid NOT NULL UNIQUE REFERENCES workspaces(id) ON DELETE RESTRICT,
163
+ active_subscription_id uuid REFERENCES billing_subscriptions(id) ON DELETE RESTRICT,
164
+ projection_version integer NOT NULL DEFAULT 0,
165
+ created_at timestamptz NOT NULL DEFAULT now(),
166
+ updated_at timestamptz NOT NULL DEFAULT now()
167
+ );
168
+ CREATE TABLE billing_plan_change_attempts (
169
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
170
+ change_id text NOT NULL UNIQUE,
171
+ billable_entity_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE RESTRICT,
172
+ subscription_id uuid NOT NULL REFERENCES billing_subscriptions(id) ON DELETE RESTRICT,
173
+ actor_id text NOT NULL,
174
+ payer_id text NOT NULL REFERENCES accounts(id) ON DELETE RESTRICT,
175
+ provider text NOT NULL,
176
+ idempotency_key text NOT NULL,
177
+ current_catalog_revision integer NOT NULL,
178
+ current_billing_price_entry_id uuid NOT NULL REFERENCES billing_price_entries(id) ON DELETE RESTRICT,
179
+ current_plan text NOT NULL,
180
+ current_interval text NOT NULL,
181
+ target_catalog_revision integer NOT NULL,
182
+ target_billing_price_entry_id uuid NOT NULL REFERENCES billing_price_entries(id) ON DELETE RESTRICT,
183
+ target_plan text NOT NULL,
184
+ target_interval text NOT NULL,
185
+ target_offer_key text NOT NULL,
186
+ effective_at text NOT NULL,
187
+ proration_mode text NOT NULL,
188
+ provider_payment_id text,
189
+ payment_url_encrypted text,
190
+ status text NOT NULL DEFAULT 'creating',
191
+ last_error text,
192
+ completed_at timestamptz,
193
+ created_at timestamptz NOT NULL DEFAULT now(),
194
+ updated_at timestamptz NOT NULL DEFAULT now()
195
+ );
196
+ CREATE UNIQUE INDEX billing_plan_change_attempts_idempotency_uidx
197
+ ON billing_plan_change_attempts (idempotency_key);
198
+ CREATE TABLE billing_reconciliation_jobs (
199
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
200
+ provider text NOT NULL,
201
+ checkout_attempt_id uuid REFERENCES billing_checkout_attempts(id) ON DELETE RESTRICT,
202
+ plan_change_attempt_id uuid REFERENCES billing_plan_change_attempts(id) ON DELETE RESTRICT,
203
+ subscription_id uuid REFERENCES billing_subscriptions(id) ON DELETE RESTRICT,
204
+ provider_customer_id uuid REFERENCES billing_provider_customers(id) ON DELETE RESTRICT,
205
+ operation text NOT NULL DEFAULT 'reconcile',
206
+ status text NOT NULL DEFAULT 'pending',
207
+ attempt_count integer NOT NULL DEFAULT 0,
208
+ available_at timestamptz NOT NULL DEFAULT now(),
209
+ locked_at timestamptz,
210
+ lease_expires_at timestamptz,
211
+ worker_id text,
212
+ last_error text,
213
+ created_at timestamptz NOT NULL DEFAULT now(),
214
+ updated_at timestamptz NOT NULL DEFAULT now(),
215
+ CONSTRAINT billing_reconciliation_jobs_exactly_one_subject CHECK (
216
+ ((CASE WHEN checkout_attempt_id IS NOT NULL THEN 1 ELSE 0 END)
217
+ + (CASE WHEN plan_change_attempt_id IS NOT NULL THEN 1 ELSE 0 END)
218
+ + (CASE WHEN subscription_id IS NOT NULL THEN 1 ELSE 0 END)
219
+ + (CASE WHEN provider_customer_id IS NOT NULL THEN 1 ELSE 0 END)) = 1
220
+ ),
221
+ CONSTRAINT billing_reconciliation_jobs_operation_check CHECK (
222
+ operation = 'reconcile' OR (operation = 'cancellation' AND subscription_id IS NOT NULL)
223
+ )
224
+ );
225
+ `;
226
+ //# sourceMappingURL=pglite-sql.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pglite-sql.js","sourceRoot":"","sources":["../../src/schema/pglite-sql.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+NnC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { frozenClock, advancingClock, systemClock } from "../core/clock.js";
2
+ //# sourceMappingURL=clocks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clocks.d.ts","sourceRoot":"","sources":["../../src/testing/clocks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { frozenClock, advancingClock, systemClock } from "../core/clock.js";
2
+ //# sourceMappingURL=clocks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clocks.js","sourceRoot":"","sources":["../../src/testing/clocks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,12 @@
1
+ /** SendLit-shaped: paid catalog plus an application Free plan that never enters package types. */
2
+ export declare function sendlitShapedCatalog(): {
3
+ applicationFreePlan: "free";
4
+ paidOffers: import("../catalog/types.js").BillingOffer[];
5
+ };
6
+ /** CourseLit-shaped: no Free plan; two independently billed entities per payer. */
7
+ export declare function courselitShapedCatalog(): {
8
+ applicationFreePlan: null;
9
+ paidOffers: import("../catalog/types.js").BillingOffer[];
10
+ entitiesPerPayer: number;
11
+ };
12
+ //# sourceMappingURL=consumer-conformance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consumer-conformance.d.ts","sourceRoot":"","sources":["../../src/testing/consumer-conformance.ts"],"names":[],"mappings":"AAEA,kGAAkG;AAClG,wBAAgB,oBAAoB;;;EAKnC;AAED,mFAAmF;AACnF,wBAAgB,sBAAsB;;;;EAMrC"}
@@ -0,0 +1,17 @@
1
+ import { REFERENCE_OFFERS } from "./fixtures.js";
2
+ /** SendLit-shaped: paid catalog plus an application Free plan that never enters package types. */
3
+ export function sendlitShapedCatalog() {
4
+ return {
5
+ applicationFreePlan: "free",
6
+ paidOffers: REFERENCE_OFFERS,
7
+ };
8
+ }
9
+ /** CourseLit-shaped: no Free plan; two independently billed entities per payer. */
10
+ export function courselitShapedCatalog() {
11
+ return {
12
+ applicationFreePlan: null,
13
+ paidOffers: REFERENCE_OFFERS,
14
+ entitiesPerPayer: 2,
15
+ };
16
+ }
17
+ //# sourceMappingURL=consumer-conformance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consumer-conformance.js","sourceRoot":"","sources":["../../src/testing/consumer-conformance.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjD,kGAAkG;AAClG,MAAM,UAAU,oBAAoB;IAChC,OAAO;QACH,mBAAmB,EAAE,MAAe;QACpC,UAAU,EAAE,gBAAgB;KAC/B,CAAC;AACN,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,sBAAsB;IAClC,OAAO;QACH,mBAAmB,EAAE,IAAI;QACzB,UAAU,EAAE,gBAAgB;QAC5B,gBAAgB,EAAE,CAAC;KACtB,CAAC;AACN,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { BillingOffer } from "../catalog/types.js";
2
+ import type { BillingActionGrant } from "../ports/authorization.js";
3
+ import type { BillableEntityRef, PayerRef } from "../core/ids.js";
4
+ export declare const REFERENCE_OFFERS: BillingOffer[];
5
+ export declare const COURSELIT_OFFER_KEYS: readonly ["pro_month", "pro_year", "business_month", "business_year"];
6
+ export declare function payer(id?: string): PayerRef;
7
+ export declare function entity(id: string, kind?: string): BillableEntityRef;
8
+ export declare function grant(action: BillingActionGrant["action"], targetId: string, actorId?: string, now?: Date): BillingActionGrant;
9
+ //# sourceMappingURL=fixtures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixtures.d.ts","sourceRoot":"","sources":["../../src/testing/fixtures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAElE,eAAO,MAAM,gBAAgB,EAAE,YAAY,EA6C1C,CAAC;AAEF,eAAO,MAAM,oBAAoB,uEAKvB,CAAC;AAEX,wBAAgB,KAAK,CAAC,EAAE,SAAe,GAAG,QAAQ,CAEjD;AAED,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,SAAc,GAAG,iBAAiB,CAExE;AAED,wBAAgB,KAAK,CACjB,MAAM,EAAE,kBAAkB,CAAC,QAAQ,CAAC,EACpC,QAAQ,EAAE,MAAM,EAChB,OAAO,SAAe,EACtB,GAAG,OAAuC,GAC3C,kBAAkB,CASpB"}
@@ -0,0 +1,69 @@
1
+ export const REFERENCE_OFFERS = [
2
+ {
3
+ key: "pro_month",
4
+ revision: 1,
5
+ plan: "pro",
6
+ interval: "month",
7
+ currency: "USD",
8
+ amountMinor: 4900,
9
+ provider: "fake",
10
+ providerProductId: "pdt_pro_month",
11
+ providerTrialDays: 14,
12
+ },
13
+ {
14
+ key: "pro_year",
15
+ revision: 1,
16
+ plan: "pro",
17
+ interval: "year",
18
+ currency: "USD",
19
+ amountMinor: 49000,
20
+ provider: "fake",
21
+ providerProductId: "pdt_pro_year",
22
+ providerTrialDays: 0,
23
+ },
24
+ {
25
+ key: "business_month",
26
+ revision: 1,
27
+ plan: "business",
28
+ interval: "month",
29
+ currency: "USD",
30
+ amountMinor: 19900,
31
+ provider: "fake",
32
+ providerProductId: "pdt_business_month",
33
+ providerTrialDays: 0,
34
+ },
35
+ {
36
+ key: "business_year",
37
+ revision: 1,
38
+ plan: "business",
39
+ interval: "year",
40
+ currency: "USD",
41
+ amountMinor: 199000,
42
+ provider: "fake",
43
+ providerProductId: "pdt_business_year",
44
+ providerTrialDays: 0,
45
+ },
46
+ ];
47
+ export const COURSELIT_OFFER_KEYS = [
48
+ "pro_month",
49
+ "pro_year",
50
+ "business_month",
51
+ "business_year",
52
+ ];
53
+ export function payer(id = "acct_payer") {
54
+ return { id, email: "payer@example.com", name: "Payer" };
55
+ }
56
+ export function entity(id, kind = "workspace") {
57
+ return { kind, id };
58
+ }
59
+ export function grant(action, targetId, actorId = "acct_payer", now = new Date("2026-01-01T00:00:00.000Z")) {
60
+ return {
61
+ grantId: `grant_${action}_${targetId}_${now.getTime()}`,
62
+ actorId,
63
+ action,
64
+ target: { kind: "workspace", id: targetId },
65
+ issuedAt: now,
66
+ expiresAt: new Date(now.getTime() + 5 * 60 * 1000),
67
+ };
68
+ }
69
+ //# sourceMappingURL=fixtures.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixtures.js","sourceRoot":"","sources":["../../src/testing/fixtures.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,MAAM,gBAAgB,GAAmB;IAC5C;QACI,GAAG,EAAE,WAAW;QAChB,QAAQ,EAAE,CAAC;QACX,IAAI,EAAE,KAAK;QACX,QAAQ,EAAE,OAAO;QACjB,QAAQ,EAAE,KAAK;QACf,WAAW,EAAE,IAAI;QACjB,QAAQ,EAAE,MAAM;QAChB,iBAAiB,EAAE,eAAe;QAClC,iBAAiB,EAAE,EAAE;KACxB;IACD;QACI,GAAG,EAAE,UAAU;QACf,QAAQ,EAAE,CAAC;QACX,IAAI,EAAE,KAAK;QACX,QAAQ,EAAE,MAAM;QAChB,QAAQ,EAAE,KAAK;QACf,WAAW,EAAE,KAAK;QAClB,QAAQ,EAAE,MAAM;QAChB,iBAAiB,EAAE,cAAc;QACjC,iBAAiB,EAAE,CAAC;KACvB;IACD;QACI,GAAG,EAAE,gBAAgB;QACrB,QAAQ,EAAE,CAAC;QACX,IAAI,EAAE,UAAU;QAChB,QAAQ,EAAE,OAAO;QACjB,QAAQ,EAAE,KAAK;QACf,WAAW,EAAE,KAAK;QAClB,QAAQ,EAAE,MAAM;QAChB,iBAAiB,EAAE,oBAAoB;QACvC,iBAAiB,EAAE,CAAC;KACvB;IACD;QACI,GAAG,EAAE,eAAe;QACpB,QAAQ,EAAE,CAAC;QACX,IAAI,EAAE,UAAU;QAChB,QAAQ,EAAE,MAAM;QAChB,QAAQ,EAAE,KAAK;QACf,WAAW,EAAE,MAAM;QACnB,QAAQ,EAAE,MAAM;QAChB,iBAAiB,EAAE,mBAAmB;QACtC,iBAAiB,EAAE,CAAC;KACvB;CACJ,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAChC,WAAW;IACX,UAAU;IACV,gBAAgB;IAChB,eAAe;CACT,CAAC;AAEX,MAAM,UAAU,KAAK,CAAC,EAAE,GAAG,YAAY;IACnC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,mBAAmB,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,EAAU,EAAE,IAAI,GAAG,WAAW;IACjD,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,KAAK,CACjB,MAAoC,EACpC,QAAgB,EAChB,OAAO,GAAG,YAAY,EACtB,GAAG,GAAG,IAAI,IAAI,CAAC,0BAA0B,CAAC;IAE1C,OAAO;QACH,OAAO,EAAE,SAAS,MAAM,IAAI,QAAQ,IAAI,GAAG,CAAC,OAAO,EAAE,EAAE;QACvD,OAAO;QACP,MAAM;QACN,MAAM,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,QAAQ,EAAE;QAC3C,QAAQ,EAAE,GAAG;QACb,SAAS,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;KACrD,CAAC;AACN,CAAC"}
@@ -0,0 +1,7 @@
1
+ export { runBillingProviderContract, createContractFake, } from "./provider-contract.js";
2
+ export { createBillingFrom, createWorkflowHarness, } from "./workflow-harness.js";
3
+ export { REFERENCE_OFFERS, COURSELIT_OFFER_KEYS, payer, entity, grant, } from "./fixtures.js";
4
+ export { sendlitShapedCatalog, courselitShapedCatalog, } from "./consumer-conformance.js";
5
+ export { frozenClock, advancingClock, systemClock } from "./clocks.js";
6
+ export { decideCheckoutTransition, decideCustomerTransition, decidePlanChangeTransition, decideReconciliationTransition, decideSubscriptionTransition, decideWebhookInboxTransition, } from "./state-machine.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,0BAA0B,EAC1B,kBAAkB,GACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACH,iBAAiB,EACjB,qBAAqB,GACxB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACH,gBAAgB,EAChB,oBAAoB,EACpB,KAAK,EACL,MAAM,EACN,KAAK,GACR,MAAM,eAAe,CAAC;AACvB,OAAO,EACH,oBAAoB,EACpB,sBAAsB,GACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EACH,wBAAwB,EACxB,wBAAwB,EACxB,0BAA0B,EAC1B,8BAA8B,EAC9B,4BAA4B,EAC5B,4BAA4B,GAC/B,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,7 @@
1
+ export { runBillingProviderContract, createContractFake, } from "./provider-contract.js";
2
+ export { createBillingFrom, createWorkflowHarness, } from "./workflow-harness.js";
3
+ export { REFERENCE_OFFERS, COURSELIT_OFFER_KEYS, payer, entity, grant, } from "./fixtures.js";
4
+ export { sendlitShapedCatalog, courselitShapedCatalog, } from "./consumer-conformance.js";
5
+ export { frozenClock, advancingClock, systemClock } from "./clocks.js";
6
+ export { decideCheckoutTransition, decideCustomerTransition, decidePlanChangeTransition, decideReconciliationTransition, decideSubscriptionTransition, decideWebhookInboxTransition, } from "./state-machine.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,0BAA0B,EAC1B,kBAAkB,GACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACH,iBAAiB,EACjB,qBAAqB,GACxB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACH,gBAAgB,EAChB,oBAAoB,EACpB,KAAK,EACL,MAAM,EACN,KAAK,GACR,MAAM,eAAe,CAAC;AACvB,OAAO,EACH,oBAAoB,EACpB,sBAAsB,GACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EACH,wBAAwB,EACxB,wBAAwB,EACxB,0BAA0B,EAC1B,8BAA8B,EAC9B,4BAA4B,EAC5B,4BAA4B,GAC/B,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { BillingProviderAdapter } from "../providers/contract.js";
2
+ import { FakeBillingProvider } from "../providers/fake/index.js";
3
+ export declare function runBillingProviderContract(adapter: BillingProviderAdapter, helpers: {
4
+ signWebhook: FakeBillingProvider["signWebhook"];
5
+ simulatePayment: FakeBillingProvider["simulatePayment"];
6
+ productId: string;
7
+ otherProductId: string;
8
+ injectTimeoutAfter?: (mutation: "createCustomer" | "createCheckout" | "planChange" | "cancellation") => void;
9
+ }): Promise<void>;
10
+ export declare function createContractFake(): {
11
+ adapter: FakeBillingProvider;
12
+ helpers: Parameters<typeof runBillingProviderContract>[1];
13
+ };
14
+ //# sourceMappingURL=provider-contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-contract.d.ts","sourceRoot":"","sources":["../../src/testing/provider-contract.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AACvE,OAAO,EACH,mBAAmB,EAEtB,MAAM,4BAA4B,CAAC;AAoBpC,wBAAsB,0BAA0B,CAC5C,OAAO,EAAE,sBAAsB,EAC/B,OAAO,EAAE;IACL,WAAW,EAAE,mBAAmB,CAAC,aAAa,CAAC,CAAC;IAChD,eAAe,EAAE,mBAAmB,CAAC,iBAAiB,CAAC,CAAC;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,CACjB,QAAQ,EACF,gBAAgB,GAChB,gBAAgB,GAChB,YAAY,GACZ,cAAc,KACnB,IAAI,CAAC;CACb,GACF,OAAO,CAAC,IAAI,CAAC,CAyUf;AAED,wBAAgB,kBAAkB,IAAI;IAClC,OAAO,EAAE,mBAAmB,CAAC;IAC7B,OAAO,EAAE,UAAU,CAAC,OAAO,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAAC;CAC7D,CAiBA"}
@@ -0,0 +1,238 @@
1
+ import { BillingProviderError } from "../core/errors.js";
2
+ import { FakeBillingProvider, FAKE_BILLING_WEBHOOK_KEY, } from "../providers/fake/index.js";
3
+ function assert(condition, message) {
4
+ if (!condition)
5
+ throw new Error(message);
6
+ }
7
+ async function assertRejects(fn, test, message) {
8
+ try {
9
+ await fn();
10
+ }
11
+ catch (error) {
12
+ assert(test(error), message);
13
+ return;
14
+ }
15
+ throw new Error(`${message}: did not reject`);
16
+ }
17
+ export async function runBillingProviderContract(adapter, helpers) {
18
+ assert(adapter.capabilities.portalPlanChanges === false, "portalPlanChanges");
19
+ assert(adapter.capabilities.portalIntervalChanges === false, "portalIntervalChanges");
20
+ assert(adapter.capabilities.mutationRecovery.createCustomer !== "unsupported", "createCustomer recovery");
21
+ assert(adapter.capabilities.mutationRecovery.createCheckout !== "unsupported", "createCheckout recovery");
22
+ const product = await adapter.retrieveProduct(helpers.productId);
23
+ assert(product.provider === adapter.provider, "product provider");
24
+ assert(product.providerProductId === helpers.productId, "product id");
25
+ assert(product.amountMinor > 0, "amount");
26
+ assert(product.interval === "month" || product.interval === "year", "interval");
27
+ await assertRejects(() => adapter.retrieveProduct("pdt_unknown"), (error) => error instanceof BillingProviderError, "unknown product");
28
+ const customer = await adapter.createCustomer({
29
+ email: "payer@example.com",
30
+ name: "Payer",
31
+ idempotencyKey: "customer:contract:1",
32
+ });
33
+ const customerAgain = await adapter.createCustomer({
34
+ email: "payer@example.com",
35
+ name: "Payer",
36
+ idempotencyKey: "customer:contract:1",
37
+ });
38
+ assert(customerAgain.providerCustomerId === customer.providerCustomerId, "customer idempotent");
39
+ if (helpers.injectTimeoutAfter) {
40
+ helpers.injectTimeoutAfter("createCustomer");
41
+ const timeoutCustomerKey = "customer:contract:timeout";
42
+ await assertRejects(() => adapter.createCustomer({
43
+ email: "timeout@example.com",
44
+ name: "Timeout",
45
+ idempotencyKey: timeoutCustomerKey,
46
+ }), (error) => error instanceof BillingProviderError &&
47
+ error.code === "unavailable", "customer timeout");
48
+ const recoveredCustomer = await adapter.createCustomer({
49
+ email: "timeout@example.com",
50
+ name: "Timeout",
51
+ idempotencyKey: timeoutCustomerKey,
52
+ });
53
+ assert(/^cus_/.test(recoveredCustomer.providerCustomerId), "recovered customer id");
54
+ const recoveredAgain = await adapter.createCustomer({
55
+ email: "timeout@example.com",
56
+ name: "Timeout",
57
+ idempotencyKey: timeoutCustomerKey,
58
+ });
59
+ assert(recoveredAgain.providerCustomerId ===
60
+ recoveredCustomer.providerCustomerId, "customer timeout retry same id");
61
+ }
62
+ const checkout = await adapter.createCheckout({
63
+ productId: helpers.productId,
64
+ currency: product.currency,
65
+ customerId: customer.providerCustomerId,
66
+ payerEmail: "payer@example.com",
67
+ returnUrl: "https://app.test/billing",
68
+ attemptId: "bca_attempt_1",
69
+ catalogKey: "pro_month",
70
+ trialDays: 14,
71
+ idempotencyKey: "checkout:contract:1",
72
+ });
73
+ assert(checkout.checkoutUrl.startsWith("http"), "checkout url");
74
+ const checkoutAgain = await adapter.createCheckout({
75
+ productId: helpers.productId,
76
+ currency: product.currency,
77
+ customerId: customer.providerCustomerId,
78
+ payerEmail: "payer@example.com",
79
+ returnUrl: "https://app.test/billing",
80
+ attemptId: "bca_attempt_1",
81
+ catalogKey: "pro_month",
82
+ trialDays: 14,
83
+ idempotencyKey: "checkout:contract:1",
84
+ });
85
+ assert(checkoutAgain.providerCheckoutSessionId ===
86
+ checkout.providerCheckoutSessionId, "checkout idempotent");
87
+ if (helpers.injectTimeoutAfter) {
88
+ helpers.injectTimeoutAfter("createCheckout");
89
+ const timeoutCheckoutKey = "checkout:contract:timeout";
90
+ await assertRejects(() => adapter.createCheckout({
91
+ productId: helpers.productId,
92
+ currency: product.currency,
93
+ customerId: customer.providerCustomerId,
94
+ payerEmail: "payer@example.com",
95
+ returnUrl: "https://app.test/billing",
96
+ attemptId: "bca_attempt_timeout",
97
+ catalogKey: "pro_month",
98
+ trialDays: 0,
99
+ idempotencyKey: timeoutCheckoutKey,
100
+ }), (error) => error instanceof BillingProviderError &&
101
+ error.code === "unavailable", "checkout timeout");
102
+ const recoveredCheckout = await adapter.createCheckout({
103
+ productId: helpers.productId,
104
+ currency: product.currency,
105
+ customerId: customer.providerCustomerId,
106
+ payerEmail: "payer@example.com",
107
+ returnUrl: "https://app.test/billing",
108
+ attemptId: "bca_attempt_timeout",
109
+ catalogKey: "pro_month",
110
+ trialDays: 0,
111
+ idempotencyKey: timeoutCheckoutKey,
112
+ });
113
+ const recoveredCheckoutAgain = await adapter.createCheckout({
114
+ productId: helpers.productId,
115
+ currency: product.currency,
116
+ customerId: customer.providerCustomerId,
117
+ payerEmail: "payer@example.com",
118
+ returnUrl: "https://app.test/billing",
119
+ attemptId: "bca_attempt_timeout",
120
+ catalogKey: "pro_month",
121
+ trialDays: 0,
122
+ idempotencyKey: timeoutCheckoutKey,
123
+ });
124
+ assert(recoveredCheckoutAgain.providerCheckoutSessionId ===
125
+ recoveredCheckout.providerCheckoutSessionId, "checkout timeout retry same id");
126
+ }
127
+ const paid = await helpers.simulatePayment(checkout.providerCheckoutSessionId);
128
+ assert(paid.status === "trialing" || paid.status === "active", "paid status");
129
+ assert(paid.metadata.checkoutAttemptId === "bca_attempt_1", "attempt metadata");
130
+ assert(!/sendlit/.test(JSON.stringify(paid.metadata)), "no sendlit keys");
131
+ const retrieved = await adapter.retrieveSubscription(paid.providerSubscriptionId);
132
+ assert(retrieved.providerProductId === helpers.productId, "retrieved product");
133
+ assert(retrieved.providerCustomerId === customer.providerCustomerId, "retrieved customer");
134
+ assert(retrieved.observedAt instanceof Date, "observedAt");
135
+ if (adapter.capabilities.planChanges) {
136
+ if (helpers.injectTimeoutAfter) {
137
+ helpers.injectTimeoutAfter("planChange");
138
+ await assertRejects(() => adapter.changeSubscriptionPlan({
139
+ providerSubscriptionId: paid.providerSubscriptionId,
140
+ targetProviderProductId: helpers.otherProductId,
141
+ effectiveAt: "immediately",
142
+ prorationMode: "prorated_immediately",
143
+ idempotencyKey: "plan-change:contract:timeout",
144
+ }), (error) => error instanceof BillingProviderError &&
145
+ error.code === "unavailable", "plan change timeout");
146
+ const recoveredChange = await adapter.changeSubscriptionPlan({
147
+ providerSubscriptionId: paid.providerSubscriptionId,
148
+ targetProviderProductId: helpers.otherProductId,
149
+ effectiveAt: "immediately",
150
+ prorationMode: "prorated_immediately",
151
+ idempotencyKey: "plan-change:contract:timeout",
152
+ });
153
+ const recoveredChangeAgain = await adapter.changeSubscriptionPlan({
154
+ providerSubscriptionId: paid.providerSubscriptionId,
155
+ targetProviderProductId: helpers.otherProductId,
156
+ effectiveAt: "immediately",
157
+ prorationMode: "prorated_immediately",
158
+ idempotencyKey: "plan-change:contract:timeout",
159
+ });
160
+ assert(recoveredChangeAgain.providerPaymentId ===
161
+ recoveredChange.providerPaymentId, "plan change timeout retry same payment");
162
+ }
163
+ else {
164
+ const changed = await adapter.changeSubscriptionPlan({
165
+ providerSubscriptionId: paid.providerSubscriptionId,
166
+ targetProviderProductId: helpers.otherProductId,
167
+ effectiveAt: "immediately",
168
+ prorationMode: "prorated_immediately",
169
+ idempotencyKey: "plan-change:contract:1",
170
+ });
171
+ assert(changed.provider === adapter.provider, "plan change provider");
172
+ }
173
+ const afterChange = await adapter.retrieveSubscription(paid.providerSubscriptionId);
174
+ assert(afterChange.providerProductId === helpers.otherProductId, "product after change");
175
+ }
176
+ const portal = await adapter.createPortalSession({
177
+ customerId: customer.providerCustomerId,
178
+ returnUrl: "https://app.test/billing",
179
+ });
180
+ assert(portal.portalUrl.startsWith("http"), "portal url");
181
+ if (helpers.injectTimeoutAfter) {
182
+ helpers.injectTimeoutAfter("cancellation");
183
+ await assertRejects(() => adapter.cancelSubscription(paid.providerSubscriptionId, "cancel:contract:timeout"), (error) => error instanceof BillingProviderError &&
184
+ error.code === "unavailable", "cancel timeout");
185
+ await adapter.cancelSubscription(paid.providerSubscriptionId, "cancel:contract:timeout");
186
+ }
187
+ else {
188
+ await adapter.cancelSubscription(paid.providerSubscriptionId, "cancel:contract:1");
189
+ }
190
+ const cancelled = await adapter.retrieveSubscription(paid.providerSubscriptionId);
191
+ assert(cancelled.status === "cancelled", "cancelled");
192
+ const signed = helpers.signWebhook(JSON.stringify({
193
+ type: "subscription.updated",
194
+ data: {
195
+ subscription_id: paid.providerSubscriptionId,
196
+ customer_id: customer.providerCustomerId,
197
+ product_id: helpers.otherProductId,
198
+ status: "cancelled",
199
+ metadata: { checkoutAttemptId: "bca_attempt_1" },
200
+ },
201
+ }));
202
+ const event = await adapter.parseWebhook(signed);
203
+ assert(event.provider === adapter.provider, "envelope provider");
204
+ assert(event.subscriptionId === paid.providerSubscriptionId, "envelope sub");
205
+ assert(event.eventType === "subscription.updated", "envelope type");
206
+ assert(!("snapshot" in event), "envelope vs snapshot");
207
+ assert(Boolean(event.verifiedKeyVersion), "verified key");
208
+ await assertRejects(() => adapter.parseWebhook({
209
+ body: signed.body,
210
+ headers: {
211
+ ...signed.headers,
212
+ "webhook-signature": "v1,deadbeef",
213
+ },
214
+ }), (error) => error instanceof Error &&
215
+ /webhook_signature_invalid/.test(error.message), "bad signature");
216
+ const stale = helpers.signWebhook(JSON.stringify({ type: "subscription.updated", data: {} }), "evt_stale", new Date(Date.now() - 10 * 60 * 1000));
217
+ await assertRejects(() => adapter.parseWebhook(stale), (error) => error instanceof Error &&
218
+ /webhook_timestamp_stale/.test(error.message), "stale webhook");
219
+ }
220
+ export function createContractFake() {
221
+ const adapter = new FakeBillingProvider({
222
+ webhookKey: FAKE_BILLING_WEBHOOK_KEY,
223
+ });
224
+ adapter.seedDefaultCatalog();
225
+ return {
226
+ adapter,
227
+ helpers: {
228
+ signWebhook: adapter.signWebhook.bind(adapter),
229
+ simulatePayment: adapter.simulatePayment.bind(adapter),
230
+ productId: "pdt_pro_month",
231
+ otherProductId: "pdt_business_month",
232
+ injectTimeoutAfter: (mutation) => {
233
+ adapter.controls.timeoutAfter = { [mutation]: true };
234
+ },
235
+ },
236
+ };
237
+ }
238
+ //# sourceMappingURL=provider-contract.js.map