@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,889 @@
1
+ import { and, eq, inArray, isNotNull, isNull, lte, or } from "drizzle-orm";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { nanoid } from "nanoid";
4
+ import { BillingCompositionError } from "../../core/errors.js";
5
+ export function createDrizzleBillingStore(db, options) {
6
+ const schema = options.schema;
7
+ const transactionContext = new AsyncLocalStorage();
8
+ function activeDb() {
9
+ return (transactionContext.getStore() ?? db);
10
+ }
11
+ function t(key) {
12
+ return schema[key];
13
+ }
14
+ async function one(table, where) {
15
+ let query = activeDb().select().from(table).where(where);
16
+ if (transactionContext.getStore())
17
+ query = query.for("update");
18
+ const rows = (await query.limit(1));
19
+ return rows[0];
20
+ }
21
+ async function many(table, where, limit) {
22
+ let q = activeDb().select().from(table);
23
+ if (transactionContext.getStore())
24
+ q = q.for("update");
25
+ if (where)
26
+ q = q.where(where);
27
+ if (limit !== undefined)
28
+ q = q.limit(limit);
29
+ return (await q);
30
+ }
31
+ function mapPrice(row) {
32
+ return {
33
+ id: String(row.id),
34
+ offerKey: String(row.offerKey),
35
+ plan: String(row.plan),
36
+ interval: row.billingInterval,
37
+ currency: String(row.currency),
38
+ amountMinor: Number(row.amountMinor),
39
+ providerTrialDays: Number(row.providerTrialDays ?? 0),
40
+ provider: String(row.provider),
41
+ providerProductId: String(row.providerProductId),
42
+ verifiedAt: row.verifiedAt ?? null,
43
+ };
44
+ }
45
+ function mapRevision(row) {
46
+ return {
47
+ id: String(row.id),
48
+ revision: Number(row.revision),
49
+ checkoutProvider: String(row.checkoutProvider),
50
+ status: row.status,
51
+ verifiedAt: row.verifiedAt ?? null,
52
+ activatedAt: row.activatedAt ?? null,
53
+ retiredAt: row.retiredAt ?? null,
54
+ };
55
+ }
56
+ function mapCustomer(row) {
57
+ return {
58
+ id: String(row.id),
59
+ provider: String(row.provider),
60
+ payerId: String(row.payerId),
61
+ payerEmail: String(row.payerEmail ?? ""),
62
+ providerCustomerId: row.providerCustomerId ?? null,
63
+ idempotencyKey: String(row.idempotencyKey),
64
+ status: row.status,
65
+ lastError: row.lastError ?? null,
66
+ };
67
+ }
68
+ function mapCheckout(row) {
69
+ return {
70
+ id: String(row.id),
71
+ attemptId: String(row.attemptId),
72
+ billableEntityId: String(row.billableEntityId),
73
+ payerId: String(row.payerId),
74
+ payerEmail: String(row.payerEmail ?? ""),
75
+ returnUrl: String(row.returnUrl ?? ""),
76
+ provider: String(row.provider),
77
+ catalogRevision: Number(row.catalogRevision),
78
+ offerKey: String(row.offerKey),
79
+ plan: String(row.requestedPlan ?? row.plan),
80
+ interval: (row.requestedInterval ?? row.interval),
81
+ priceEntryId: String(row.billingPriceEntryId ?? row.priceEntryId),
82
+ quotedAmountMinor: Number(row.quotedAmountMinor),
83
+ quotedCurrency: String(row.quotedCurrency),
84
+ providerCustomerRowId: row.billingCustomerId ?? null,
85
+ providerCheckoutSessionId: row.providerCheckoutSessionId ?? null,
86
+ checkoutUrlEncrypted: row.checkoutUrlEncrypted ?? null,
87
+ idempotencyKey: String(row.idempotencyKey),
88
+ status: row.status,
89
+ expiresAt: row.expiresAt,
90
+ lastError: row.lastError ?? null,
91
+ completedAt: row.completedAt ?? null,
92
+ applicationFields: options.checkoutApplicationFields?.fromRow(row) ?? undefined,
93
+ };
94
+ }
95
+ function checkoutApplicationColumns(fields) {
96
+ if (!fields || Object.keys(fields).length === 0)
97
+ return {};
98
+ if (!options.checkoutApplicationFields) {
99
+ throw new BillingCompositionError("checkout_application_fields_codec_required");
100
+ }
101
+ const encoded = options.checkoutApplicationFields.toColumns(fields);
102
+ if (!encoded || typeof encoded !== "object" || Array.isArray(encoded)) {
103
+ throw new BillingCompositionError("checkout_application_fields_codec_invalid");
104
+ }
105
+ return encoded;
106
+ }
107
+ function mapSubscription(row) {
108
+ return {
109
+ id: String(row.id),
110
+ billableEntityId: String(row.billableEntityId),
111
+ payerId: String(row.payerId),
112
+ provider: String(row.provider),
113
+ providerCustomerId: String(row.billingCustomerId),
114
+ providerSubscriptionId: String(row.providerSubscriptionId),
115
+ providerProductId: String(row.providerProductId),
116
+ catalogRevision: Number(row.catalogRevision),
117
+ offerKey: String(row.offerKey),
118
+ plan: String(row.plan),
119
+ interval: row.billingInterval,
120
+ priceEntryId: String(row.billingPriceEntryId),
121
+ status: row.status,
122
+ currentPeriodStartsAt: row.currentPeriodStartsAt ?? null,
123
+ currentPeriodEndsAt: row.currentPeriodEndsAt ?? null,
124
+ paidThroughAt: row.paidThroughAt ?? null,
125
+ trialEndsAt: row.trialEndsAt ?? null,
126
+ cancelAtPeriodEnd: Boolean(row.cancelAtPeriodEnd),
127
+ isEntitlementSource: Boolean(row.isEntitlementSource),
128
+ originCheckoutAttemptId: row.originCheckoutAttemptId ?? null,
129
+ providerOccurredAt: row.providerOccurredAt ?? null,
130
+ providerVersion: row.providerVersion ?? null,
131
+ lastObservedAt: row.lastObservedAt ?? null,
132
+ lastReconciledAt: row.lastReconciledAt ?? null,
133
+ };
134
+ }
135
+ function mapPlanChange(row) {
136
+ return {
137
+ id: String(row.id),
138
+ changeId: String(row.changeId),
139
+ billableEntityId: String(row.billableEntityId),
140
+ subscriptionId: String(row.subscriptionId),
141
+ actorId: String(row.actorId),
142
+ payerId: String(row.payerId),
143
+ provider: String(row.provider),
144
+ idempotencyKey: String(row.idempotencyKey),
145
+ currentCatalogRevision: Number(row.currentCatalogRevision),
146
+ currentPriceEntryId: String(row.currentBillingPriceEntryId),
147
+ currentPlan: String(row.currentPlan),
148
+ currentInterval: row.currentInterval,
149
+ targetCatalogRevision: Number(row.targetCatalogRevision),
150
+ targetPriceEntryId: String(row.targetBillingPriceEntryId),
151
+ targetPlan: String(row.targetPlan),
152
+ targetInterval: row.targetInterval,
153
+ targetOfferKey: String(row.targetOfferKey),
154
+ effectiveAt: row.effectiveAt,
155
+ prorationMode: row.prorationMode,
156
+ status: row.status,
157
+ lastError: row.lastError ?? null,
158
+ providerPaymentId: row.providerPaymentId ?? null,
159
+ paymentUrlEncrypted: row.paymentUrlEncrypted ?? null,
160
+ completedAt: row.completedAt ?? null,
161
+ };
162
+ }
163
+ function mapWebhook(row) {
164
+ const subscriptionId = row.subscriptionId ?? null;
165
+ const checkoutAttemptId = row.checkoutAttemptId ?? null;
166
+ return {
167
+ id: String(row.id),
168
+ provider: String(row.provider),
169
+ providerEventId: String(row.providerEventId),
170
+ eventType: String(row.eventType),
171
+ occurredAt: row.occurredAt,
172
+ subscriptionId,
173
+ checkoutAttemptId,
174
+ payloadEncrypted: row.payloadEncrypted ?? null,
175
+ payloadKeyVersion: row.payloadKeyVersion ?? null,
176
+ verifiedKeyVersion: row.verifiedKeyVersion ?? null,
177
+ status: row.status,
178
+ processingAttempts: Number(row.processingAttempts ?? 0),
179
+ lastError: row.lastError ?? null,
180
+ availableAt: row.availableAt,
181
+ lockedAt: row.lockedAt ?? null,
182
+ leaseExpiresAt: row.leaseExpiresAt ?? null,
183
+ workerId: row.workerId ?? null,
184
+ processedAt: row.processedAt ?? null,
185
+ };
186
+ }
187
+ function mapJob(row) {
188
+ return {
189
+ id: String(row.id),
190
+ provider: String(row.provider),
191
+ checkoutAttemptId: row.checkoutAttemptId ?? null,
192
+ planChangeAttemptId: row.planChangeAttemptId ?? null,
193
+ subscriptionId: row.subscriptionId ?? null,
194
+ providerCustomerId: row.providerCustomerId ?? null,
195
+ operation: row.operation,
196
+ status: row.status,
197
+ attemptCount: Number(row.attemptCount ?? 0),
198
+ availableAt: row.availableAt,
199
+ lockedAt: row.lockedAt ?? null,
200
+ leaseExpiresAt: row.leaseExpiresAt ?? null,
201
+ workerId: row.workerId ?? null,
202
+ lastError: row.lastError ?? null,
203
+ };
204
+ }
205
+ const store = {
206
+ transactionDepth: 0,
207
+ isInTransaction() {
208
+ return transactionContext.getStore() !== undefined;
209
+ },
210
+ getTransaction() {
211
+ return transactionContext.getStore();
212
+ },
213
+ async withTransaction(fn) {
214
+ store.transactionDepth += 1;
215
+ try {
216
+ return await db.transaction((tx) => transactionContext.run(tx, () => fn.length > 0
217
+ ? fn(tx)
218
+ : fn()));
219
+ }
220
+ finally {
221
+ store.transactionDepth -= 1;
222
+ }
223
+ },
224
+ noteProviderCall() {
225
+ if (transactionContext.getStore()) {
226
+ throw new Error("transaction_open_across_provider_call");
227
+ }
228
+ },
229
+ async getActiveCatalog(providerName) {
230
+ const revision = await one(t("billingCatalogRevisions"), and(eq(t("billingCatalogRevisions").checkoutProvider, providerName), eq(t("billingCatalogRevisions").status, "active")));
231
+ if (!revision)
232
+ return null;
233
+ const items = await many(t("billingCatalogRevisionItems"), eq(t("billingCatalogRevisionItems").catalogRevisionId, revision.id));
234
+ const prices = await many(t("billingPriceEntries"));
235
+ return {
236
+ revision: mapRevision(revision),
237
+ items: items.map((item) => ({
238
+ offerKey: String(item.offerKey),
239
+ price: mapPrice(prices.find((price) => price.id === item.billingPriceEntryId)),
240
+ })),
241
+ };
242
+ },
243
+ async getCatalogRevision(providerName, revisionNumber) {
244
+ const revision = await one(t("billingCatalogRevisions"), and(eq(t("billingCatalogRevisions").checkoutProvider, providerName), eq(t("billingCatalogRevisions").revision, revisionNumber)));
245
+ if (!revision)
246
+ return null;
247
+ const items = await many(t("billingCatalogRevisionItems"), eq(t("billingCatalogRevisionItems").catalogRevisionId, revision.id));
248
+ const prices = await many(t("billingPriceEntries"));
249
+ return {
250
+ revision: mapRevision(revision),
251
+ items: items.map((item) => {
252
+ const price = prices.find((candidate) => candidate.id === item.billingPriceEntryId);
253
+ if (!price)
254
+ throw new Error("catalog_price_missing");
255
+ return {
256
+ offerKey: String(item.offerKey),
257
+ price: mapPrice(price),
258
+ };
259
+ }),
260
+ };
261
+ },
262
+ async insertCatalogRevision(revision) {
263
+ await activeDb().insert(t("billingCatalogRevisions")).values({
264
+ id: revision.id,
265
+ revision: revision.revision,
266
+ checkoutProvider: revision.checkoutProvider,
267
+ status: revision.status,
268
+ verifiedAt: revision.verifiedAt,
269
+ activatedAt: revision.activatedAt,
270
+ retiredAt: revision.retiredAt,
271
+ });
272
+ },
273
+ async saveCatalogRevision(revision) {
274
+ await activeDb()
275
+ .update(t("billingCatalogRevisions"))
276
+ .set({
277
+ status: revision.status,
278
+ verifiedAt: revision.verifiedAt,
279
+ activatedAt: revision.activatedAt,
280
+ retiredAt: revision.retiredAt,
281
+ updatedAt: options.clock.now(),
282
+ })
283
+ .where(eq(t("billingCatalogRevisions").id, revision.id));
284
+ },
285
+ async insertPriceEntry(price) {
286
+ await activeDb().insert(t("billingPriceEntries")).values({
287
+ id: price.id,
288
+ offerKey: price.offerKey,
289
+ plan: price.plan,
290
+ billingInterval: price.interval,
291
+ currency: price.currency,
292
+ amountMinor: price.amountMinor,
293
+ providerTrialDays: price.providerTrialDays,
294
+ provider: price.provider,
295
+ providerProductId: price.providerProductId,
296
+ verifiedAt: price.verifiedAt,
297
+ });
298
+ },
299
+ async insertCatalogRevisionItem(item) {
300
+ await activeDb().insert(t("billingCatalogRevisionItems")).values({
301
+ id: item.id,
302
+ catalogRevisionId: item.revisionId,
303
+ offerKey: item.offerKey,
304
+ billingPriceEntryId: item.priceEntryId,
305
+ });
306
+ },
307
+ async findPriceById(id) {
308
+ const row = await one(t("billingPriceEntries"), eq(t("billingPriceEntries").id, id));
309
+ return row ? mapPrice(row) : undefined;
310
+ },
311
+ async findPriceByProviderProduct(providerName, productId) {
312
+ const row = await one(t("billingPriceEntries"), and(eq(t("billingPriceEntries").provider, providerName), eq(t("billingPriceEntries").providerProductId, productId)));
313
+ return row ? mapPrice(row) : undefined;
314
+ },
315
+ async listRevisions() {
316
+ const rows = await many(t("billingCatalogRevisions"));
317
+ return rows.map(mapRevision);
318
+ },
319
+ newAttemptId() {
320
+ return `bca_${nanoid(12)}`;
321
+ },
322
+ newChangeId() {
323
+ return `bpc_${nanoid(12)}`;
324
+ },
325
+ async ensurePlanState(billableEntityId) {
326
+ const existing = await one(t("billingPlanStates"), eq(t("billingPlanStates").billableEntityId, billableEntityId));
327
+ if (existing) {
328
+ return {
329
+ billableEntityId,
330
+ activeSubscriptionId: existing.activeSubscriptionId ??
331
+ null,
332
+ projectionVersion: Number(existing.projectionVersion ?? 0),
333
+ };
334
+ }
335
+ await activeDb()
336
+ .insert(t("billingPlanStates"))
337
+ .values({
338
+ billableEntityId,
339
+ activeSubscriptionId: null,
340
+ projectionVersion: 0,
341
+ ...(options.planStateDefaults ?? {}),
342
+ });
343
+ return {
344
+ billableEntityId,
345
+ activeSubscriptionId: null,
346
+ projectionVersion: 0,
347
+ };
348
+ },
349
+ async findPlanState(billableEntityId) {
350
+ const existing = await one(t("billingPlanStates"), eq(t("billingPlanStates").billableEntityId, billableEntityId));
351
+ return existing
352
+ ? {
353
+ billableEntityId,
354
+ activeSubscriptionId: existing.activeSubscriptionId ??
355
+ null,
356
+ projectionVersion: Number(existing.projectionVersion ?? 0),
357
+ }
358
+ : undefined;
359
+ },
360
+ async savePlanState(state) {
361
+ await activeDb()
362
+ .update(t("billingPlanStates"))
363
+ .set({
364
+ activeSubscriptionId: state.activeSubscriptionId,
365
+ projectionVersion: state.projectionVersion,
366
+ updatedAt: options.clock.now(),
367
+ })
368
+ .where(eq(t("billingPlanStates").billableEntityId, state.billableEntityId));
369
+ },
370
+ async findCustomerByPayer(providerName, payerId) {
371
+ const row = await one(t("billingProviderCustomers"), and(eq(t("billingProviderCustomers").provider, providerName), eq(t("billingProviderCustomers").payerId, payerId)));
372
+ return row ? mapCustomer(row) : undefined;
373
+ },
374
+ async findCustomerById(id) {
375
+ const row = await one(t("billingProviderCustomers"), eq(t("billingProviderCustomers").id, id));
376
+ return row ? mapCustomer(row) : undefined;
377
+ },
378
+ async findCustomerByProviderCustomerId(providerName, providerCustomerId) {
379
+ const row = await one(t("billingProviderCustomers"), and(eq(t("billingProviderCustomers").provider, providerName), eq(t("billingProviderCustomers")
380
+ .providerCustomerId, providerCustomerId)));
381
+ return row ? mapCustomer(row) : undefined;
382
+ },
383
+ async listCustomersByPayer(payerId) {
384
+ const rows = await many(t("billingProviderCustomers"), eq(t("billingProviderCustomers").payerId, payerId));
385
+ return rows.map(mapCustomer);
386
+ },
387
+ async insertCustomer(customer) {
388
+ await activeDb().insert(t("billingProviderCustomers")).values({
389
+ id: customer.id,
390
+ provider: customer.provider,
391
+ payerId: customer.payerId,
392
+ payerEmail: customer.payerEmail,
393
+ providerCustomerId: customer.providerCustomerId,
394
+ idempotencyKey: customer.idempotencyKey,
395
+ status: customer.status,
396
+ lastError: customer.lastError,
397
+ });
398
+ },
399
+ async saveCustomer(customer) {
400
+ await activeDb()
401
+ .update(t("billingProviderCustomers"))
402
+ .set({
403
+ providerCustomerId: customer.providerCustomerId,
404
+ payerEmail: customer.payerEmail,
405
+ status: customer.status,
406
+ lastError: customer.lastError,
407
+ updatedAt: options.clock.now(),
408
+ })
409
+ .where(eq(t("billingProviderCustomers").id, customer.id));
410
+ },
411
+ async findLiveCheckout(billableEntityId) {
412
+ const row = await one(t("billingCheckoutAttempts"), and(eq(t("billingCheckoutAttempts").billableEntityId, billableEntityId), inArray(t("billingCheckoutAttempts").status, [
413
+ "creating",
414
+ "open",
415
+ ])));
416
+ return row ? mapCheckout(row) : undefined;
417
+ },
418
+ async findCheckoutById(id) {
419
+ const row = await one(t("billingCheckoutAttempts"), eq(t("billingCheckoutAttempts").id, id));
420
+ return row ? mapCheckout(row) : undefined;
421
+ },
422
+ async findCheckoutByAttemptId(attemptId) {
423
+ const row = await one(t("billingCheckoutAttempts"), eq(t("billingCheckoutAttempts").attemptId, attemptId));
424
+ return row ? mapCheckout(row) : undefined;
425
+ },
426
+ async insertCheckout(attempt) {
427
+ await activeDb()
428
+ .insert(t("billingCheckoutAttempts"))
429
+ .values({
430
+ ...checkoutApplicationColumns(attempt.applicationFields),
431
+ id: attempt.id,
432
+ attemptId: attempt.attemptId,
433
+ billableEntityId: attempt.billableEntityId,
434
+ payerId: attempt.payerId,
435
+ payerEmail: attempt.payerEmail,
436
+ returnUrl: attempt.returnUrl,
437
+ provider: attempt.provider,
438
+ catalogRevision: attempt.catalogRevision,
439
+ offerKey: attempt.offerKey,
440
+ requestedPlan: attempt.plan,
441
+ requestedInterval: attempt.interval,
442
+ billingPriceEntryId: attempt.priceEntryId,
443
+ quotedAmountMinor: attempt.quotedAmountMinor,
444
+ quotedCurrency: attempt.quotedCurrency,
445
+ billingCustomerId: attempt.providerCustomerRowId,
446
+ providerCheckoutSessionId: attempt.providerCheckoutSessionId,
447
+ checkoutUrlEncrypted: attempt.checkoutUrlEncrypted,
448
+ idempotencyKey: attempt.idempotencyKey,
449
+ status: attempt.status,
450
+ expiresAt: attempt.expiresAt,
451
+ lastError: attempt.lastError,
452
+ completedAt: attempt.completedAt,
453
+ });
454
+ },
455
+ async saveCheckout(attempt) {
456
+ await activeDb()
457
+ .update(t("billingCheckoutAttempts"))
458
+ .set({
459
+ ...checkoutApplicationColumns(attempt.applicationFields),
460
+ billingCustomerId: attempt.providerCustomerRowId,
461
+ payerEmail: attempt.payerEmail,
462
+ returnUrl: attempt.returnUrl,
463
+ providerCheckoutSessionId: attempt.providerCheckoutSessionId,
464
+ checkoutUrlEncrypted: attempt.checkoutUrlEncrypted,
465
+ status: attempt.status,
466
+ lastError: attempt.lastError,
467
+ expiresAt: attempt.expiresAt,
468
+ completedAt: attempt.completedAt,
469
+ updatedAt: options.clock.now(),
470
+ })
471
+ .where(eq(t("billingCheckoutAttempts").id, attempt.id));
472
+ },
473
+ async countCreatingCheckouts() {
474
+ const rows = await many(t("billingCheckoutAttempts"), eq(t("billingCheckoutAttempts").status, "creating"));
475
+ return rows.length;
476
+ },
477
+ async listExpiredCheckouts(now, limit) {
478
+ const rows = await many(t("billingCheckoutAttempts"), and(inArray(t("billingCheckoutAttempts").status, [
479
+ "creating",
480
+ "open",
481
+ ]), lte(t("billingCheckoutAttempts").expiresAt, now)), limit);
482
+ return rows.map(mapCheckout);
483
+ },
484
+ async listPurgeableCheckouts(before, limit) {
485
+ const rows = await many(t("billingCheckoutAttempts"), and(inArray(t("billingCheckoutAttempts").status, [
486
+ "completed",
487
+ "expired",
488
+ "abandoned",
489
+ "conflicted",
490
+ ]), isNotNull(t("billingCheckoutAttempts")
491
+ .checkoutUrlEncrypted), lte(t("billingCheckoutAttempts").completedAt, before)), limit);
492
+ return rows.map(mapCheckout);
493
+ },
494
+ async findEntitlementSubscription(billableEntityId) {
495
+ const row = await one(t("billingSubscriptions"), and(eq(t("billingSubscriptions").billableEntityId, billableEntityId), eq(t("billingSubscriptions").isEntitlementSource, true)));
496
+ return row ? mapSubscription(row) : undefined;
497
+ },
498
+ async listSubscriptionsByEntity(billableEntityId) {
499
+ const rows = await many(t("billingSubscriptions"), eq(t("billingSubscriptions").billableEntityId, billableEntityId));
500
+ return rows.map(mapSubscription);
501
+ },
502
+ async findSubscriptionById(id) {
503
+ const row = await one(t("billingSubscriptions"), eq(t("billingSubscriptions").id, id));
504
+ return row ? mapSubscription(row) : undefined;
505
+ },
506
+ async findSubscriptionByProviderIds(providerName, providerSubscriptionId) {
507
+ const row = await one(t("billingSubscriptions"), and(eq(t("billingSubscriptions").provider, providerName), eq(t("billingSubscriptions")
508
+ .providerSubscriptionId, providerSubscriptionId)));
509
+ return row ? mapSubscription(row) : undefined;
510
+ },
511
+ async listSubscriptionsByPayer(payerId) {
512
+ const rows = await many(t("billingSubscriptions"), eq(t("billingSubscriptions").payerId, payerId));
513
+ return rows.map(mapSubscription);
514
+ },
515
+ async upsertSubscription(subscription) {
516
+ const existing = await store.findSubscriptionById(subscription.id);
517
+ const values = {
518
+ id: subscription.id,
519
+ billableEntityId: subscription.billableEntityId,
520
+ billingCustomerId: subscription.providerCustomerId,
521
+ payerId: subscription.payerId,
522
+ originCheckoutAttemptId: subscription.originCheckoutAttemptId,
523
+ provider: subscription.provider,
524
+ providerSubscriptionId: subscription.providerSubscriptionId,
525
+ providerProductId: subscription.providerProductId,
526
+ billingPriceEntryId: subscription.priceEntryId,
527
+ catalogRevision: subscription.catalogRevision,
528
+ offerKey: subscription.offerKey,
529
+ plan: subscription.plan,
530
+ billingInterval: subscription.interval,
531
+ status: subscription.status,
532
+ currentPeriodStartsAt: subscription.currentPeriodStartsAt,
533
+ currentPeriodEndsAt: subscription.currentPeriodEndsAt,
534
+ paidThroughAt: subscription.paidThroughAt,
535
+ trialEndsAt: subscription.trialEndsAt,
536
+ cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
537
+ isEntitlementSource: subscription.isEntitlementSource,
538
+ providerOccurredAt: subscription.providerOccurredAt,
539
+ providerVersion: subscription.providerVersion,
540
+ lastObservedAt: subscription.lastObservedAt,
541
+ lastReconciledAt: subscription.lastReconciledAt,
542
+ updatedAt: options.clock.now(),
543
+ };
544
+ if (existing) {
545
+ await activeDb()
546
+ .update(t("billingSubscriptions"))
547
+ .set(values)
548
+ .where(eq(t("billingSubscriptions").id, subscription.id));
549
+ }
550
+ else {
551
+ await activeDb()
552
+ .insert(t("billingSubscriptions"))
553
+ .values(values);
554
+ }
555
+ },
556
+ async listDueSubscriptionDeadlines(now, limit) {
557
+ const rows = await many(t("billingSubscriptions"), and(eq(t("billingSubscriptions").isEntitlementSource, true), eq(t("billingSubscriptions").status, "cancelled"), eq(t("billingSubscriptions").cancelAtPeriodEnd, true), lte(t("billingSubscriptions").paidThroughAt, now)), limit);
558
+ return rows.map(mapSubscription);
559
+ },
560
+ async findLivePlanChange(billableEntityId) {
561
+ const row = await one(t("billingPlanChangeAttempts"), and(eq(t("billingPlanChangeAttempts")
562
+ .billableEntityId, billableEntityId), inArray(t("billingPlanChangeAttempts").status, [
563
+ "creating",
564
+ "pending",
565
+ ])));
566
+ return row ? mapPlanChange(row) : undefined;
567
+ },
568
+ async findPlanChangeBySubscriptionOffer(subscriptionId, offerKey) {
569
+ const row = await one(t("billingPlanChangeAttempts"), and(eq(t("billingPlanChangeAttempts").subscriptionId, subscriptionId), eq(t("billingPlanChangeAttempts").targetOfferKey, offerKey), inArray(t("billingPlanChangeAttempts").status, [
570
+ "creating",
571
+ "pending",
572
+ ])));
573
+ return row ? mapPlanChange(row) : undefined;
574
+ },
575
+ async findPlanChangeById(id) {
576
+ const row = await one(t("billingPlanChangeAttempts"), eq(t("billingPlanChangeAttempts").id, id));
577
+ return row ? mapPlanChange(row) : undefined;
578
+ },
579
+ async insertPlanChange(attempt) {
580
+ await activeDb()
581
+ .insert(t("billingPlanChangeAttempts"))
582
+ .values({
583
+ id: attempt.id,
584
+ changeId: attempt.changeId,
585
+ billableEntityId: attempt.billableEntityId,
586
+ subscriptionId: attempt.subscriptionId,
587
+ actorId: attempt.actorId,
588
+ payerId: attempt.payerId,
589
+ provider: attempt.provider,
590
+ idempotencyKey: attempt.idempotencyKey,
591
+ currentCatalogRevision: attempt.currentCatalogRevision,
592
+ currentBillingPriceEntryId: attempt.currentPriceEntryId,
593
+ currentPlan: attempt.currentPlan,
594
+ currentInterval: attempt.currentInterval,
595
+ targetCatalogRevision: attempt.targetCatalogRevision,
596
+ targetBillingPriceEntryId: attempt.targetPriceEntryId,
597
+ targetPlan: attempt.targetPlan,
598
+ targetInterval: attempt.targetInterval,
599
+ targetOfferKey: attempt.targetOfferKey,
600
+ effectiveAt: attempt.effectiveAt,
601
+ prorationMode: attempt.prorationMode,
602
+ providerPaymentId: attempt.providerPaymentId ?? null,
603
+ paymentUrlEncrypted: attempt.paymentUrlEncrypted ?? null,
604
+ status: attempt.status,
605
+ lastError: attempt.lastError,
606
+ completedAt: attempt.completedAt ?? null,
607
+ });
608
+ },
609
+ async savePlanChange(attempt) {
610
+ await activeDb()
611
+ .update(t("billingPlanChangeAttempts"))
612
+ .set({
613
+ status: attempt.status,
614
+ lastError: attempt.lastError,
615
+ providerPaymentId: attempt.providerPaymentId ?? null,
616
+ paymentUrlEncrypted: attempt.paymentUrlEncrypted ?? null,
617
+ completedAt: attempt.completedAt ?? null,
618
+ updatedAt: options.clock.now(),
619
+ })
620
+ .where(eq(t("billingPlanChangeAttempts").id, attempt.id));
621
+ },
622
+ async listPurgeablePlanChanges(before, limit) {
623
+ const rows = await many(t("billingPlanChangeAttempts"), and(inArray(t("billingPlanChangeAttempts").status, [
624
+ "succeeded",
625
+ "failed",
626
+ "conflicted",
627
+ ]), isNotNull(t("billingPlanChangeAttempts")
628
+ .paymentUrlEncrypted), lte(t("billingPlanChangeAttempts").completedAt, before)), limit);
629
+ return rows.map(mapPlanChange);
630
+ },
631
+ async findWebhook(providerName, providerEventId) {
632
+ const row = await one(t("billingWebhookEvents"), and(eq(t("billingWebhookEvents").provider, providerName), eq(t("billingWebhookEvents").providerEventId, providerEventId)));
633
+ return row ? mapWebhook(row) : undefined;
634
+ },
635
+ async findWebhookByEventId(providerEventId) {
636
+ const row = await one(t("billingWebhookEvents"), eq(t("billingWebhookEvents").providerEventId, providerEventId));
637
+ return row ? mapWebhook(row) : undefined;
638
+ },
639
+ async insertWebhook(record) {
640
+ await activeDb()
641
+ .insert(t("billingWebhookEvents"))
642
+ .values({
643
+ id: record.id,
644
+ provider: record.provider,
645
+ providerEventId: record.providerEventId,
646
+ eventType: record.eventType,
647
+ occurredAt: record.occurredAt,
648
+ subscriptionId: record.subscriptionId,
649
+ checkoutAttemptId: record.checkoutAttemptId,
650
+ payloadEncrypted: record.payloadEncrypted ?? null,
651
+ payloadKeyVersion: record.payloadKeyVersion ?? null,
652
+ verifiedKeyVersion: record.verifiedKeyVersion,
653
+ status: record.status,
654
+ processingAttempts: record.processingAttempts,
655
+ lastError: record.lastError,
656
+ availableAt: record.availableAt,
657
+ lockedAt: record.lockedAt,
658
+ leaseExpiresAt: record.leaseExpiresAt,
659
+ workerId: record.workerId,
660
+ processedAt: record.processedAt ?? null,
661
+ });
662
+ },
663
+ async saveWebhook(record) {
664
+ await activeDb()
665
+ .update(t("billingWebhookEvents"))
666
+ .set({
667
+ status: record.status,
668
+ processingAttempts: record.processingAttempts,
669
+ lastError: record.lastError,
670
+ availableAt: record.availableAt,
671
+ lockedAt: record.lockedAt,
672
+ leaseExpiresAt: record.leaseExpiresAt,
673
+ workerId: record.workerId,
674
+ processedAt: record.processedAt ?? null,
675
+ subscriptionId: record.subscriptionId,
676
+ checkoutAttemptId: record.checkoutAttemptId,
677
+ payloadEncrypted: record.payloadEncrypted ?? null,
678
+ payloadKeyVersion: record.payloadKeyVersion ?? null,
679
+ })
680
+ .where(eq(t("billingWebhookEvents").id, record.id));
681
+ },
682
+ async saveWebhookIfOwned(record, workerId) {
683
+ const rows = (await activeDb()
684
+ .update(t("billingWebhookEvents"))
685
+ .set({
686
+ status: record.status,
687
+ processingAttempts: record.processingAttempts,
688
+ lastError: record.lastError,
689
+ availableAt: record.availableAt,
690
+ lockedAt: record.lockedAt,
691
+ leaseExpiresAt: record.leaseExpiresAt,
692
+ workerId: record.workerId,
693
+ processedAt: record.processedAt ?? null,
694
+ subscriptionId: record.subscriptionId,
695
+ checkoutAttemptId: record.checkoutAttemptId,
696
+ payloadEncrypted: record.payloadEncrypted ?? null,
697
+ payloadKeyVersion: record.payloadKeyVersion ?? null,
698
+ })
699
+ .where(and(eq(t("billingWebhookEvents").id, record.id), eq(t("billingWebhookEvents").workerId, workerId), eq(t("billingWebhookEvents").status, "processing"), eq(t("billingWebhookEvents").lockedAt, record.lockedAt)))
700
+ .returning());
701
+ return rows.length > 0;
702
+ },
703
+ async listPurgeableWebhooks(before, limit) {
704
+ const rows = await many(t("billingWebhookEvents"), and(inArray(t("billingWebhookEvents").status, [
705
+ "processed",
706
+ "ignored",
707
+ ]), isNotNull(t("billingWebhookEvents").payloadEncrypted), lte(t("billingWebhookEvents").processedAt, before)), limit);
708
+ return rows.map(mapWebhook);
709
+ },
710
+ async listDueWebhooks(now, limit) {
711
+ const rows = await many(t("billingWebhookEvents"));
712
+ return rows
713
+ .map(mapWebhook)
714
+ .filter((row) => {
715
+ if (row.status === "processed" ||
716
+ row.status === "ignored" ||
717
+ row.status === "quarantined") {
718
+ return false;
719
+ }
720
+ if (row.status === "processing" &&
721
+ row.leaseExpiresAt &&
722
+ row.leaseExpiresAt > now) {
723
+ return false;
724
+ }
725
+ return row.availableAt <= now;
726
+ })
727
+ .slice(0, limit);
728
+ },
729
+ async claimDueWebhooks(now, limit, workerId) {
730
+ const candidates = await store.listDueWebhooks(now, limit);
731
+ if (candidates.length === 0)
732
+ return [];
733
+ return db.transaction(async (tx) => {
734
+ const claimed = [];
735
+ const table = t("billingWebhookEvents");
736
+ for (const candidate of candidates) {
737
+ const rows = (await tx
738
+ .update(table)
739
+ .set({
740
+ status: "processing",
741
+ workerId,
742
+ lockedAt: now,
743
+ leaseExpiresAt: new Date(now.getTime() + 5 * 60 * 1000),
744
+ processingAttempts: candidate.processingAttempts + 1,
745
+ })
746
+ .where(and(eq(table.id, candidate.id), lte(table.availableAt, now), or(inArray(table.status, [
747
+ "pending",
748
+ "failed",
749
+ ]), and(eq(table.status, "processing"), or(isNull(table.leaseExpiresAt), lte(table.leaseExpiresAt, now))))))
750
+ .returning());
751
+ if (rows[0])
752
+ claimed.push(mapWebhook(rows[0]));
753
+ }
754
+ return claimed;
755
+ });
756
+ },
757
+ async findLiveJob(subject) {
758
+ if (!subject.checkoutAttemptId &&
759
+ !subject.planChangeAttemptId &&
760
+ !subject.subscriptionId &&
761
+ !subject.providerCustomerId) {
762
+ return undefined;
763
+ }
764
+ const rows = await many(t("billingReconciliationJobs"));
765
+ return rows.map(mapJob).find((job) => {
766
+ if (job.status === "completed" || job.status === "quarantined")
767
+ return false;
768
+ if (job.provider !== subject.provider)
769
+ return false;
770
+ if (subject.checkoutAttemptId)
771
+ return job.checkoutAttemptId === subject.checkoutAttemptId;
772
+ if (subject.planChangeAttemptId)
773
+ return (job.planChangeAttemptId === subject.planChangeAttemptId);
774
+ if (subject.subscriptionId)
775
+ return job.subscriptionId === subject.subscriptionId;
776
+ return job.providerCustomerId === subject.providerCustomerId;
777
+ });
778
+ },
779
+ async insertJob(job) {
780
+ await activeDb().insert(t("billingReconciliationJobs")).values(job);
781
+ },
782
+ async saveJob(job) {
783
+ await activeDb()
784
+ .update(t("billingReconciliationJobs"))
785
+ .set({
786
+ status: job.status,
787
+ attemptCount: job.attemptCount,
788
+ availableAt: job.availableAt,
789
+ lockedAt: job.lockedAt,
790
+ leaseExpiresAt: job.leaseExpiresAt,
791
+ workerId: job.workerId,
792
+ lastError: job.lastError,
793
+ operation: job.operation ?? "reconcile",
794
+ updatedAt: options.clock.now(),
795
+ })
796
+ .where(eq(t("billingReconciliationJobs").id, job.id));
797
+ },
798
+ async saveJobIfOwned(job, workerId) {
799
+ const rows = (await activeDb()
800
+ .update(t("billingReconciliationJobs"))
801
+ .set({
802
+ status: job.status,
803
+ attemptCount: job.attemptCount,
804
+ availableAt: job.availableAt,
805
+ lockedAt: job.lockedAt,
806
+ leaseExpiresAt: job.leaseExpiresAt,
807
+ workerId: job.workerId,
808
+ lastError: job.lastError,
809
+ operation: job.operation ?? "reconcile",
810
+ updatedAt: options.clock.now(),
811
+ })
812
+ .where(and(eq(t("billingReconciliationJobs").id, job.id), eq(t("billingReconciliationJobs").workerId, workerId), eq(t("billingReconciliationJobs").status, "processing"), eq(t("billingReconciliationJobs").lockedAt, job.lockedAt)))
813
+ .returning());
814
+ return rows.length > 0;
815
+ },
816
+ async listClaimableJobs(now, limit) {
817
+ const rows = await many(t("billingReconciliationJobs"));
818
+ const claimed = [];
819
+ for (const job of rows.map(mapJob)) {
820
+ if (claimed.length >= limit)
821
+ break;
822
+ const leaseExpired = job.status === "processing" &&
823
+ (!job.leaseExpiresAt ||
824
+ job.leaseExpiresAt.getTime() <= now.getTime());
825
+ const claimable = (job.status === "pending" ||
826
+ job.status === "failed" ||
827
+ leaseExpired) &&
828
+ job.availableAt.getTime() <= now.getTime();
829
+ if (claimable)
830
+ claimed.push(job);
831
+ }
832
+ return claimed;
833
+ },
834
+ async claimClaimableJobs(now, limit, workerId) {
835
+ const candidates = await store.listClaimableJobs(now, limit);
836
+ if (candidates.length === 0)
837
+ return [];
838
+ return db.transaction(async (tx) => {
839
+ const claimed = [];
840
+ const table = t("billingReconciliationJobs");
841
+ for (const candidate of candidates) {
842
+ const rows = (await tx
843
+ .update(table)
844
+ .set({
845
+ status: "processing",
846
+ workerId,
847
+ lockedAt: now,
848
+ leaseExpiresAt: new Date(now.getTime() + 5 * 60 * 1000),
849
+ attemptCount: candidate.attemptCount + 1,
850
+ })
851
+ .where(and(eq(table.id, candidate.id), lte(table.availableAt, now), or(inArray(table.status, [
852
+ "pending",
853
+ "failed",
854
+ ]), and(eq(table.status, "processing"), or(isNull(table.leaseExpiresAt), lte(table.leaseExpiresAt, now))))))
855
+ .returning());
856
+ if (rows[0])
857
+ claimed.push(mapJob(rows[0]));
858
+ }
859
+ return claimed;
860
+ });
861
+ },
862
+ async health(_now, providerName) {
863
+ const webhooks = (await many(t("billingWebhookEvents"))).map(mapWebhook);
864
+ const jobs = (await many(t("billingReconciliationJobs"))).map(mapJob);
865
+ const checkouts = await many(t("billingCheckoutAttempts"));
866
+ const customers = await many(t("billingProviderCustomers"));
867
+ const subscriptions = (await many(t("billingSubscriptions"))).map(mapSubscription);
868
+ const pendingInbox = webhooks.filter((row) => row.status === "pending" || row.status === "failed");
869
+ const oldest = pendingInbox
870
+ .map((row) => row.occurredAt)
871
+ .sort((a, b) => a.getTime() - b.getTime())[0];
872
+ const revisions = await store.listRevisions();
873
+ const activeRevision = revisions.find((row) => row.status === "active" &&
874
+ (providerName === undefined ||
875
+ row.checkoutProvider === providerName));
876
+ return {
877
+ oldestPendingInboxOccurredAt: oldest ?? null,
878
+ quarantinedWebhooks: webhooks.filter((row) => row.status === "quarantined").length,
879
+ quarantinedJobs: jobs.filter((row) => row.status === "quarantined").length,
880
+ stuckCreatingCheckouts: checkouts.filter((row) => row.status === "creating").length,
881
+ stuckCreatingCustomers: customers.filter((row) => row.status === "creating").length,
882
+ unreconciledSubscriptions: subscriptions.filter((row) => !row.lastReconciledAt).length,
883
+ activeRevision: activeRevision?.revision ?? null,
884
+ };
885
+ },
886
+ };
887
+ return store;
888
+ }
889
+ //# sourceMappingURL=store.js.map