payment-kit 1.29.1 → 1.29.2

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 (310) hide show
  1. package/api/dev.ts +41 -2
  2. package/api/hono.d.ts +42 -0
  3. package/api/node-sqlite.d.ts +12 -0
  4. package/api/src/bootstrap.ts +36 -0
  5. package/api/src/crons/base.ts +3 -3
  6. package/api/src/crons/currency.ts +1 -1
  7. package/api/src/crons/index.ts +27 -24
  8. package/api/src/crons/metering-subscription-detection.ts +1 -1
  9. package/api/src/crons/overdue-detection.ts +2 -2
  10. package/api/src/crons/retry-pending-events.ts +6 -0
  11. package/api/src/index.ts +22 -161
  12. package/api/src/integrations/app-store/client.ts +3 -4
  13. package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
  14. package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
  15. package/api/src/integrations/arcblock/token.ts +21 -7
  16. package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
  17. package/api/src/integrations/google-play/handlers/voided.ts +2 -2
  18. package/api/src/integrations/google-play/verify.ts +3 -2
  19. package/api/src/integrations/iap-reconcile.ts +3 -5
  20. package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
  21. package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
  22. package/api/src/libs/archive/query.ts +19 -0
  23. package/api/src/libs/audit.ts +61 -4
  24. package/api/src/libs/auth.ts +99 -38
  25. package/api/src/libs/context.ts +78 -1
  26. package/api/src/libs/currency.ts +2 -2
  27. package/api/src/libs/dayjs.ts +8 -2
  28. package/api/src/libs/drivers/auth-storage.ts +118 -0
  29. package/api/src/libs/drivers/cron.ts +264 -0
  30. package/api/src/libs/drivers/db.ts +170 -0
  31. package/api/src/libs/drivers/identity.ts +81 -0
  32. package/api/src/libs/drivers/index.ts +40 -0
  33. package/api/src/libs/drivers/locks.ts +226 -0
  34. package/api/src/libs/drivers/migrate-runner.ts +70 -0
  35. package/api/src/libs/drivers/queue.ts +104 -0
  36. package/api/src/libs/drivers/secrets.ts +194 -0
  37. package/api/src/libs/env.ts +170 -54
  38. package/api/src/libs/exchange-rate/service.ts +7 -6
  39. package/api/src/libs/http-fetch-adapter.ts +50 -0
  40. package/api/src/libs/invoice.ts +1 -1
  41. package/api/src/libs/lock.ts +51 -47
  42. package/api/src/libs/logger.ts +48 -8
  43. package/api/src/libs/notification/index.ts +1 -1
  44. package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
  45. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
  46. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
  47. package/api/src/libs/overdraft-protection.ts +1 -1
  48. package/api/src/libs/payout.ts +1 -1
  49. package/api/src/libs/queue/index.ts +259 -52
  50. package/api/src/libs/queue/runtime.ts +175 -0
  51. package/api/src/libs/resource.ts +3 -3
  52. package/api/src/libs/secrets.ts +38 -0
  53. package/api/src/libs/session.ts +3 -2
  54. package/api/src/libs/subscription.ts +5 -5
  55. package/api/src/libs/tenant.ts +92 -0
  56. package/api/src/libs/url.ts +3 -3
  57. package/api/src/libs/util.ts +21 -13
  58. package/api/src/middlewares/hono/cdn.ts +63 -0
  59. package/api/src/middlewares/hono/context.ts +73 -0
  60. package/api/src/middlewares/hono/csrf.ts +72 -0
  61. package/api/src/middlewares/hono/fallback.ts +194 -0
  62. package/api/src/middlewares/hono/pipeline.ts +73 -0
  63. package/api/src/middlewares/hono/resource-mount.ts +42 -0
  64. package/api/src/middlewares/hono/resource.ts +63 -0
  65. package/api/src/middlewares/hono/security.ts +214 -0
  66. package/api/src/middlewares/hono/session.ts +114 -0
  67. package/api/src/middlewares/hono/xss.ts +61 -0
  68. package/api/src/queues/auto-recharge.ts +12 -10
  69. package/api/src/queues/checkout-session.ts +17 -12
  70. package/api/src/queues/credit-consume.ts +40 -36
  71. package/api/src/queues/credit-grant.ts +25 -18
  72. package/api/src/queues/credit-reconciliation.ts +7 -5
  73. package/api/src/queues/discount-status.ts +9 -6
  74. package/api/src/queues/event.ts +12 -4
  75. package/api/src/queues/exchange-rate-health.ts +49 -30
  76. package/api/src/queues/invoice.ts +18 -15
  77. package/api/src/queues/notification.ts +14 -7
  78. package/api/src/queues/payment.ts +41 -28
  79. package/api/src/queues/payout.ts +9 -5
  80. package/api/src/queues/refund.ts +18 -12
  81. package/api/src/queues/subscription.ts +83 -53
  82. package/api/src/queues/token-transfer.ts +15 -10
  83. package/api/src/queues/usage-record.ts +8 -5
  84. package/api/src/queues/vendors/commission.ts +7 -5
  85. package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
  86. package/api/src/queues/vendors/fulfillment.ts +4 -2
  87. package/api/src/queues/vendors/return-processor.ts +5 -3
  88. package/api/src/queues/vendors/return-scanner.ts +5 -4
  89. package/api/src/queues/vendors/status-check.ts +10 -7
  90. package/api/src/queues/webhook.ts +60 -32
  91. package/api/src/routes/connect/shared.ts +1 -2
  92. package/api/src/routes/connect/subscribe.ts +3 -3
  93. package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
  94. package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
  95. package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
  96. package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
  97. package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
  98. package/api/src/routes/hono/credit-tokens.ts +43 -0
  99. package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
  100. package/api/src/routes/{customers.ts → hono/customers.ts} +193 -223
  101. package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
  102. package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
  103. package/api/src/routes/{events.ts → hono/events.ts} +107 -71
  104. package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
  105. package/api/src/routes/hono/exchange-rates.ts +77 -0
  106. package/api/src/routes/hono/index.ts +115 -0
  107. package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
  108. package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
  109. package/api/src/routes/hono/integrations/stripe.ts +74 -0
  110. package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
  111. package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
  112. package/api/src/routes/hono/meters.ts +288 -0
  113. package/api/src/routes/hono/passports.ts +73 -0
  114. package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
  115. package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
  116. package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
  117. package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
  118. package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
  119. package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
  120. package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
  121. package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
  122. package/api/src/routes/{products.ts → hono/products.ts} +172 -159
  123. package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
  124. package/api/src/routes/hono/redirect.ts +24 -0
  125. package/api/src/routes/{refunds.ts → hono/refunds.ts} +96 -80
  126. package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
  127. package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
  128. package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
  129. package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
  130. package/api/src/routes/hono/tool.ts +69 -0
  131. package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
  132. package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
  133. package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
  134. package/api/src/routes/hono/webhook-endpoints.ts +126 -0
  135. package/api/src/service.ts +667 -0
  136. package/api/src/store/migrations/20230911-seeding.ts +2 -1
  137. package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
  138. package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
  139. package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
  140. package/api/src/store/models/auto-recharge-config.ts +22 -10
  141. package/api/src/store/models/checkout-session.ts +15 -14
  142. package/api/src/store/models/coupon.ts +29 -20
  143. package/api/src/store/models/credit-grant.ts +38 -29
  144. package/api/src/store/models/credit-transaction.ts +32 -21
  145. package/api/src/store/models/customer.ts +19 -17
  146. package/api/src/store/models/discount.ts +11 -2
  147. package/api/src/store/models/entitlement-grant.ts +21 -9
  148. package/api/src/store/models/entitlement-product.ts +21 -9
  149. package/api/src/store/models/entitlement.ts +19 -10
  150. package/api/src/store/models/event.ts +18 -9
  151. package/api/src/store/models/exchange-rate-provider.ts +17 -4
  152. package/api/src/store/models/invoice-item.ts +18 -9
  153. package/api/src/store/models/invoice.ts +16 -8
  154. package/api/src/store/models/meter-event.ts +27 -9
  155. package/api/src/store/models/meter.ts +31 -22
  156. package/api/src/store/models/payment-currency.ts +25 -8
  157. package/api/src/store/models/payment-intent.ts +15 -6
  158. package/api/src/store/models/payment-link.ts +15 -6
  159. package/api/src/store/models/payment-method.ts +38 -22
  160. package/api/src/store/models/payment-stat.ts +18 -9
  161. package/api/src/store/models/payout.ts +15 -6
  162. package/api/src/store/models/price-quote.ts +17 -8
  163. package/api/src/store/models/price.ts +24 -12
  164. package/api/src/store/models/pricing-table.ts +29 -20
  165. package/api/src/store/models/product-vendor.ts +20 -10
  166. package/api/src/store/models/product.ts +15 -6
  167. package/api/src/store/models/promotion-code.ts +14 -6
  168. package/api/src/store/models/refund.ts +15 -6
  169. package/api/src/store/models/revenue-snapshot.ts +21 -9
  170. package/api/src/store/models/setting.ts +18 -9
  171. package/api/src/store/models/setup-intent.ts +36 -27
  172. package/api/src/store/models/subscription-item.ts +21 -9
  173. package/api/src/store/models/subscription-schedule.ts +21 -9
  174. package/api/src/store/models/subscription.ts +21 -10
  175. package/api/src/store/models/tax-rate.ts +29 -21
  176. package/api/src/store/models/usage-record.ts +11 -2
  177. package/api/src/store/models/webhook-attempt.ts +18 -9
  178. package/api/src/store/models/webhook-endpoint.ts +18 -9
  179. package/api/src/store/scoped-core.ts +55 -0
  180. package/api/src/store/scoped.ts +247 -0
  181. package/api/src/store/sequelize.ts +66 -22
  182. package/api/src/store/sql-migrations.ts +20 -0
  183. package/api/src/store/tenant-backfill.ts +260 -0
  184. package/api/src/store/tenant-model.ts +124 -0
  185. package/api/src/store/tenant-tables.ts +50 -0
  186. package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
  187. package/api/tests/fixtures/bare-query-violation.ts +13 -0
  188. package/api/tests/fixtures/core-env-violation.ts +10 -0
  189. package/api/tests/fixtures/host-read-violation.ts +19 -0
  190. package/api/tests/fixtures/tenants.ts +4 -0
  191. package/api/tests/integrations/iap-tenant.spec.ts +284 -0
  192. package/api/tests/libs/archive-query.spec.ts +26 -0
  193. package/api/tests/libs/audit-tenant.spec.ts +153 -0
  194. package/api/tests/libs/context.spec.ts +204 -0
  195. package/api/tests/libs/core-config.spec.ts +115 -0
  196. package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
  197. package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
  198. package/api/tests/libs/lock-tenant.spec.ts +66 -0
  199. package/api/tests/libs/scoped.spec.ts +222 -0
  200. package/api/tests/libs/secrets-facade.spec.ts +52 -0
  201. package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
  202. package/api/tests/libs/tenant-middleware.spec.ts +42 -0
  203. package/api/tests/libs/tenant-scanner.spec.ts +120 -0
  204. package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
  205. package/api/tests/middlewares/hono/context.spec.ts +113 -0
  206. package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
  207. package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
  208. package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
  209. package/api/tests/middlewares/hono/security.spec.ts +181 -0
  210. package/api/tests/middlewares/hono/session.spec.ts +42 -0
  211. package/api/tests/middlewares/hono/xss.spec.ts +81 -0
  212. package/api/tests/models/tenant-backfill.spec.ts +287 -0
  213. package/api/tests/models/tenant-columns-model.spec.ts +46 -0
  214. package/api/tests/models/tenant-columns.spec.ts +161 -0
  215. package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
  216. package/api/tests/queues/credit-consume.spec.ts +8 -1
  217. package/api/tests/queues/event-tenant.spec.ts +236 -0
  218. package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
  219. package/api/tests/queues/queue-parity.spec.ts +249 -0
  220. package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
  221. package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
  222. package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
  223. package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
  224. package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
  225. package/api/tests/service/collapse.spec.ts +96 -0
  226. package/api/tests/store/tenant-crosscut.spec.ts +202 -0
  227. package/api/tests/store/tenant-model-spike.spec.ts +177 -0
  228. package/api/tests/store/tenant-model.spec.ts +162 -0
  229. package/api/tests/store/tenant-residual.spec.ts +196 -0
  230. package/api/third.d.ts +4 -0
  231. package/blocklet.yml +1 -1
  232. package/cloudflare/README.md +26 -6
  233. package/cloudflare/build.ts +28 -13
  234. package/cloudflare/did-connect-auth.ts +0 -217
  235. package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
  236. package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
  237. package/cloudflare/migrations/0008_schema_parity.sql +16 -0
  238. package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
  239. package/cloudflare/queue-runtime-mode.ts +13 -0
  240. package/cloudflare/run-build.js +10 -56
  241. package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
  242. package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
  243. package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
  244. package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
  245. package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
  246. package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
  247. package/cloudflare/shims/blocklet-sdk/util-csrf.ts +13 -0
  248. package/cloudflare/shims/blocklet-sdk/util-wallet.ts +8 -0
  249. package/cloudflare/shims/cron.ts +38 -158
  250. package/cloudflare/shims/events.ts +124 -0
  251. package/cloudflare/shims/fastq.ts +15 -1
  252. package/cloudflare/shims/nedb-storage.ts +16 -8
  253. package/cloudflare/shims/xss.ts +8 -0
  254. package/cloudflare/tenant-middleware.ts +36 -0
  255. package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
  256. package/cloudflare/tests/worker-handler-gate.spec.ts +44 -0
  257. package/cloudflare/worker.ts +204 -433
  258. package/cloudflare/wrangler.local-e2e.jsonc +26 -0
  259. package/jest.config.js +3 -1
  260. package/package.json +33 -38
  261. package/scripts/core-env-whitelist.json +1 -0
  262. package/scripts/e2e-12b-runtime.ts +149 -0
  263. package/scripts/e2e-core-config.ts +125 -0
  264. package/scripts/e2e-d1-tenancy.ts +116 -0
  265. package/scripts/e2e-d2-cron-queue.ts +139 -0
  266. package/scripts/e2e-d3-embedded-multi.ts +171 -0
  267. package/scripts/e2e-hono-s2.ts +125 -0
  268. package/scripts/e2e-hono-s3e.ts +135 -0
  269. package/scripts/e2e-hono-s4.ts +114 -0
  270. package/scripts/e2e-migration-contract.ts +100 -0
  271. package/scripts/e2e-s0.ts +61 -0
  272. package/scripts/e2e-s1.ts +107 -0
  273. package/scripts/e2e-s2.ts +178 -0
  274. package/scripts/e2e-s3.ts +110 -0
  275. package/scripts/e2e-s4.ts +191 -0
  276. package/scripts/e2e-s5.ts +139 -0
  277. package/scripts/e2e-s6.ts +127 -0
  278. package/scripts/e2e-tenant-model.ts +119 -0
  279. package/scripts/e2e-tenant-worker.ts +199 -0
  280. package/scripts/gen-sql-migrations.js +46 -0
  281. package/scripts/phase8-codemod.js +219 -0
  282. package/scripts/phase9a-env-getters-codemod.js +82 -0
  283. package/scripts/scan-core-env.js +109 -0
  284. package/scripts/scan-tenant-queries.js +235 -0
  285. package/scripts/schema-drift-guard.ts +210 -0
  286. package/scripts/tenant-scan-whitelist.json +1 -0
  287. package/src/env.d.ts +13 -1
  288. package/tsconfig.json +1 -1
  289. package/api/src/libs/did-space.ts +0 -235
  290. package/api/src/libs/middleware.ts +0 -50
  291. package/api/src/libs/security.ts +0 -192
  292. package/api/src/queues/space.ts +0 -662
  293. package/api/src/routes/credit-tokens.ts +0 -38
  294. package/api/src/routes/exchange-rates.ts +0 -87
  295. package/api/src/routes/index.ts +0 -142
  296. package/api/src/routes/integrations/stripe.ts +0 -61
  297. package/api/src/routes/meters.ts +0 -274
  298. package/api/src/routes/passports.ts +0 -68
  299. package/api/src/routes/redirect.ts +0 -20
  300. package/api/src/routes/tool.ts +0 -65
  301. package/api/src/routes/webhook-endpoints.ts +0 -126
  302. package/api/tests/routes/credit-grants.spec.ts +0 -1261
  303. package/cloudflare/shims/did-space-js.ts +0 -17
  304. package/cloudflare/shims/did-space.ts +0 -11
  305. package/cloudflare/shims/express-compat/index.ts +0 -80
  306. package/cloudflare/shims/express-compat/types.ts +0 -41
  307. package/cloudflare/shims/lock.ts +0 -115
  308. package/cloudflare/shims/queue.ts +0 -611
  309. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
  310. package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
@@ -0,0 +1,20 @@
1
+ /* eslint-disable */
2
+ // AUTO-GENERATED — do not edit by hand.
3
+ // Source: cloudflare/migrations/*.sql · Regenerate: node scripts/gen-sql-migrations.js
4
+ //
5
+ // The D1 SQL migration lineage (Path A), inlined so it is bundled into the
6
+ // published @arcblock/payment-service dist. Apply via applyPaymentCoreMigrations()
7
+ // (drivers barrel) or applySqlMigrations(driver, paymentCoreSqlMigrations).
8
+ import type { SqlMigration } from '../libs/drivers/migrate-runner';
9
+
10
+ export const paymentCoreSqlMigrations: SqlMigration[] = [
11
+ { name: "0001_initial_schema.sql", sql: "-- Payment Kit: Complete D1 schema\n-- All statements use IF NOT EXISTS for idempotency.\n\nCREATE TABLE IF NOT EXISTS `archive_locks` (`id` VARCHAR(40) NOT NULL PRIMARY KEY, `locked_by` VARCHAR(64), `locked_at` INTEGER, `expires_at` INTEGER, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL);\n\nCREATE TABLE IF NOT EXISTS `archive_metadata` (`id` VARCHAR(40) NOT NULL PRIMARY KEY, `archive_file` VARCHAR(128) NOT NULL, `date_range_start` INTEGER NOT NULL, `date_range_end` INTEGER NOT NULL, `tables` JSON NOT NULL DEFAULT '{}', `total_records` INTEGER NOT NULL DEFAULT 0, `checksum` VARCHAR(128), `file_size` INTEGER, `duration_ms` INTEGER, `triggered_by` TEXT NOT NULL, `triggered_by_user_id` VARCHAR(64), `status` TEXT NOT NULL, `error` TEXT, `query_count` INTEGER NOT NULL DEFAULT 0, `query_actor_ids` JSON DEFAULT '[]', `last_queried_at` INTEGER, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL);\n\nCREATE TABLE IF NOT EXISTS `auto_recharge_configs` (`id` VARCHAR(30) NOT NULL PRIMARY KEY, `customer_id` VARCHAR(18) NOT NULL REFERENCES `customers` (`id`), `livemode` TINYINT(1) NOT NULL, `enabled` TINYINT(1) NOT NULL DEFAULT 0, `threshold` VARCHAR(32) NOT NULL, `payment_method_id` VARCHAR(15), `currency_id` VARCHAR(15) NOT NULL, `recharge_currency_id` VARCHAR(15), `price_id` VARCHAR(32) NOT NULL, `quantity` INTEGER NOT NULL DEFAULT 1, `payment_settings` JSON DEFAULT '{\"payment_method_types\":[],\"payment_method_options\":{}}', `payment_details` JSON, `daily_limits` JSON DEFAULT '{\"max_attempts\":0,\"max_amount\":\"0\"}', `last_recharge_date` VARCHAR(10), `daily_stats` JSON NOT NULL DEFAULT '{\"attempt_count\":0,\"total_amount\":\"0\"}', `metadata` JSON, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, `slippage_config` JSON);\n\nCREATE TABLE IF NOT EXISTS `checkout_sessions` (`id` VARCHAR(64) NOT NULL PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `client_reference_id` VARCHAR(128), `currency_id` VARCHAR(15) NOT NULL, `invoice_id` VARCHAR(30), `customer_id` VARCHAR(30), `customer_did` VARCHAR(40), `payment_link_id` VARCHAR(30), `subscription_id` VARCHAR(30), `recovered_from` VARCHAR(30), `payment_intent_id` VARCHAR(30), `mode` TEXT NOT NULL, `status` TEXT NOT NULL, `payment_status` TEXT NOT NULL, `line_items` JSON NOT NULL, `amount_subtotal` VARCHAR(32) NOT NULL, `amount_total` VARCHAR(32) NOT NULL, `total_details` JSON NOT NULL, `url` VARCHAR(512), `cancel_url` VARCHAR(512), `success_url` VARCHAR(512), `currency_conversion` JSON, `after_expiration` JSON, `allow_promotion_codes` TINYINT(1) DEFAULT 0, `consent_collection` JSON, `consent` JSON, `custom_fields` JSON DEFAULT '[]', `custom_text` JSON, `customer_creation` TEXT NOT NULL, `customer_update` JSON DEFAULT '{\"address\":\"never\",\"name\":\"never\",\"shipping\":\"never\"}', `expires_at` INTEGER, `invoice_creation` JSON, `payment_method_types` JSON DEFAULT '[]', `phone_number_collection` JSON, `billing_address_collection` TEXT NOT NULL DEFAULT 'auto', `submit_type` TEXT NOT NULL, `subscription_data` JSON, `payment_details` JSON, `metadata` JSON, `created_at` DATETIME NOT NULL, `created_via` TEXT, `updated_at` DATETIME NOT NULL, `nft_mint_settings` JSON, `payment_intent_data` JSON, `nft_mint_details` JSON, `nft_mint_status` TEXT NOT NULL DEFAULT 'disabled', `cross_sell_behavior` TEXT DEFAULT 'auto', `setup_intent_id` VARCHAR(30), `enable_subscription_grouping` TINYINT(1) DEFAULT 0, `subscription_groups` JSON, `success_subscription_count` INTEGER DEFAULT 0, \"discounts\" JSON, `fulfillment_status` VARCHAR(255), `vendor_info` JSON, `slippage_percent` DECIMAL(5,2) NOT NULL DEFAULT 0.5);\n\nCREATE TABLE IF NOT EXISTS `coupons` (`id` VARCHAR(15) NOT NULL UNIQUE PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `amount_off` VARCHAR(32) DEFAULT '0', `percent_off` INTEGER DEFAULT '0', `currency_id` VARCHAR(15) NOT NULL, `duration` TEXT, `duration_in_months` INTEGER NOT NULL, `name` VARCHAR(64) NOT NULL, `applies_to` JSON, `currency_options` JSON DEFAULT '{}', `max_redemptions` INTEGER, `times_redeemed` INTEGER DEFAULT '0', `valid` TINYINT(1) DEFAULT 0, `metadata` JSON, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, `created_via` TEXT, `locked` TINYINT(1) NOT NULL DEFAULT 0, `redeem_by` INTEGER, `redeem_by_new` INTEGER, `description` TEXT);\n\nCREATE TABLE IF NOT EXISTS `credit_grants` (`id` VARCHAR(18) NOT NULL PRIMARY KEY, `object` VARCHAR(32) NOT NULL DEFAULT 'credit_grant', `amount` VARCHAR(32) NOT NULL, `currency_id` VARCHAR(15) NOT NULL, `applicability_config` JSON, `category` TEXT NOT NULL, `customer_id` VARCHAR(18) NOT NULL, `effective_at` INTEGER, `expires_at` INTEGER, `livemode` TINYINT(1) NOT NULL, `metadata` JSON, `name` VARCHAR(255), `priority` INTEGER NOT NULL DEFAULT 50, `test_clock` VARCHAR(32), `voided_at` INTEGER, `status` TEXT NOT NULL DEFAULT 'granted', `remaining_amount` VARCHAR(32) NOT NULL, `created_by` VARCHAR(40), `updated_by` VARCHAR(40), `created_via` TEXT NOT NULL, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, `chain_status` TEXT, `chain_detail` JSON);\n\nCREATE TABLE IF NOT EXISTS `credit_transactions` (`id` VARCHAR(18) NOT NULL PRIMARY KEY, `quantity` VARCHAR(32) NOT NULL, `credit_amount` VARCHAR(32) NOT NULL, `remaining_balance` VARCHAR(32) NOT NULL, `customer_id` VARCHAR(18) NOT NULL, `credit_grant_id` VARCHAR(18) NOT NULL, `meter_id` VARCHAR(18), `subscription_id` VARCHAR(18), `source` VARCHAR(255), `meter_event_name` VARCHAR(128) NOT NULL, `meter_unit` VARCHAR(32) NOT NULL, `description` TEXT, `metadata` JSON, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, `transfer_status` TEXT, `transfer_hash` VARCHAR(255));\n\nCREATE TABLE IF NOT EXISTS `customers` (`id` VARCHAR(18) NOT NULL UNIQUE PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `did` VARCHAR(40) NOT NULL, `address` JSON, `description` VARCHAR(512), `name` VARCHAR(64), `email` VARCHAR(128), `phone` VARCHAR(32), `shipping` JSON, `balance` VARCHAR(32) DEFAULT '0', `currency_id` VARCHAR(15), `delinquent` TINYINT(1) NOT NULL, `discount_id` VARCHAR(32), `metadata` JSON, `invoice_credit_balance` JSON, `invoice_prefix` VARCHAR(30), `invoice_settings` JSON, `next_invoice_sequence` NUMBER DEFAULT '1', `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, `token_balance` JSON DEFAULT '\"{}\"', `last_sync_at` INTEGER, `preference` JSON DEFAULT '\"{\\\"notification\\\":{\\\"frequency\\\":\\\"monthly\\\",\\\"schedule\\\":{\\\"time\\\":\\\"10:00\\\",\\\"date\\\":1}}}\"');\n\nCREATE TABLE IF NOT EXISTS `discounts` (`id` VARCHAR(15) NOT NULL UNIQUE PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `coupon_id` VARCHAR(30) NOT NULL, `customer_id` VARCHAR(30) NOT NULL, `promotion_code_id` VARCHAR(30), `subscription_id` VARCHAR(30), `checkout_session_id` VARCHAR(30), `invoice_id` VARCHAR(30), `invoice_item_id` VARCHAR(30), `metadata` JSON, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, `verification_method` VARCHAR(50), `verification_data` JSON, `start` INTEGER NOT NULL, `end` INTEGER, `confirmed` TINYINT(1) NOT NULL DEFAULT 1);\n\nCREATE TABLE IF NOT EXISTS `events` (`id` VARCHAR(30) NOT NULL PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `type` VARCHAR(128) NOT NULL, `object_type` VARCHAR(64) NOT NULL, `object_id` VARCHAR(64) NOT NULL, `data` JSON NOT NULL, `request` JSON, `pending_webhooks` INTEGER NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL);\n\nCREATE TABLE IF NOT EXISTS `exchange_rate_providers` (`id` VARCHAR(32) NOT NULL PRIMARY KEY, `name` VARCHAR(64) NOT NULL UNIQUE, `enabled` TINYINT(1) NOT NULL DEFAULT 1, `priority` INTEGER NOT NULL DEFAULT 1, `status` TEXT NOT NULL DEFAULT 'active', `paused_reason` VARCHAR(512), `config` JSON, `last_success_at` DATETIME, `last_failure_at` DATETIME, `failure_count` INTEGER NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, `type` VARCHAR(32) NOT NULL DEFAULT 'token-data');\n\nCREATE TABLE IF NOT EXISTS `invoice_items` (`id` VARCHAR(30) NOT NULL PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `amount` VARCHAR(32) NOT NULL, `quantity` INTEGER NOT NULL, `description` VARCHAR(256) NOT NULL, `period` JSON, `customer_id` VARCHAR(18) NOT NULL, `currency_id` VARCHAR(15) NOT NULL, `price_id` VARCHAR(30) NOT NULL, `invoice_id` VARCHAR(30) NOT NULL, `subscription_id` VARCHAR(30), `subscription_item_id` VARCHAR(30), `discountable` TINYINT(1) NOT NULL, `discount_amounts` JSON DEFAULT '[]', `discounts` JSON DEFAULT '[]', `proration` TINYINT(1) DEFAULT 0, `proration_details` JSON, `metadata` JSON DEFAULT '{}', `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, `tax_rate_id` VARCHAR(30));\n\nCREATE TABLE IF NOT EXISTS `invoices` (`id` VARCHAR(30) NOT NULL PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `auto_advance` TINYINT(1) NOT NULL, `account_country` VARCHAR(64), `account_name` VARCHAR(64), `charge_id` VARCHAR(30), `collection_method` TEXT NOT NULL, `currency_id` VARCHAR(15) NOT NULL, `customer_id` VARCHAR(18) NOT NULL, `description` VARCHAR(512), `hosted_invoice_url` VARCHAR(512), `payment_intent_id` VARCHAR(30), `period_end` INTEGER DEFAULT 0, `period_start` INTEGER DEFAULT 0, `status` TEXT NOT NULL, `checkout_session_id` VARCHAR(64), `subscription_id` VARCHAR(30), `subscription_details` JSON DEFAULT '{}', `subscription_proration_date` INTEGER, `total` VARCHAR(32) NOT NULL, `subtotal` VARCHAR(32) NOT NULL, `subtotal_excluding_tax` VARCHAR(32) NOT NULL, `tax` VARCHAR(32) DEFAULT '0', `amount_due` VARCHAR(32) NOT NULL, `amount_paid` VARCHAR(32) DEFAULT '0', `amount_remaining` VARCHAR(32) NOT NULL, `amount_shipping` VARCHAR(32) DEFAULT '0', `attempt_count` INTEGER DEFAULT 0, `attempted` TINYINT(1) DEFAULT 0, `next_payment_attempt` INTEGER, `billing_reason` TEXT, `custom_fields` JSON DEFAULT '[]', `customer_address` JSON, `customer_name` VARCHAR(64), `customer_email` VARCHAR(128), `customer_phone` VARCHAR(32), `customer_shipping` JSON, `default_payment_method_id` VARCHAR(30), `discounts` JSON DEFAULT '[]', `due_date` INTEGER, `effective_at` INTEGER, `ending_balance` VARCHAR(32) DEFAULT '0', `footer` VARCHAR(512), `from_invoice_id` VARCHAR(30), `invoice_pdf` VARCHAR(512), `last_finalization_error` JSON, `latest_revision` VARCHAR(30), `number` VARCHAR(64), `paid` TINYINT(1) DEFAULT 0, `paid_out_of_band` TINYINT(1) DEFAULT 0, `payment_settings` JSON, `post_payment_credit_notes_amount` VARCHAR(32) DEFAULT '0', `pre_payment_credit_notes_amount` VARCHAR(32) DEFAULT '0', `quote_id` VARCHAR(30), `receipt_number` VARCHAR(64), `days_until_due` NUMBER, `rendering_options` JSON, `starting_balance` VARCHAR(32) DEFAULT '0', `statement_descriptor` VARCHAR(64), `status_transitions` JSON DEFAULT '{}', `test_clock_id` VARCHAR(30), `threshold_reason` JSON, `total_discount_amounts` JSON DEFAULT '[]', `webhooks_delivered_at` INTEGER, `metadata` JSON, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, `starting_token_balance` JSON DEFAULT '\"{}\"', `ending_token_balance` JSON DEFAULT '\"{}\"');\n\nCREATE TABLE IF NOT EXISTS `jobs` (`id` VARCHAR(30) NOT NULL PRIMARY KEY, `queue` VARCHAR(32) NOT NULL, `job` JSON NOT NULL, `retry_count` INTEGER DEFAULT 0, `delay` INTEGER DEFAULT -1, `will_run_at` INTEGER DEFAULT -1, `cancelled` TINYINT(1) DEFAULT 0, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL);\n\nCREATE TABLE IF NOT EXISTS `locks` (`id` VARCHAR(30) NOT NULL PRIMARY KEY, `lock_at` INTEGER DEFAULT 0, `release_at` INTEGER DEFAULT -1, `reason` VARCHAR(255), `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL);\n\nCREATE TABLE IF NOT EXISTS `meter_events` (`id` VARCHAR(18) NOT NULL PRIMARY KEY, `event_name` VARCHAR(128) NOT NULL, `timestamp` INTEGER NOT NULL, `payload` JSON NOT NULL, `identifier` VARCHAR(255) NOT NULL UNIQUE, `livemode` TINYINT(1) NOT NULL, `status` TEXT NOT NULL DEFAULT 'pending', `attempt_count` INTEGER NOT NULL DEFAULT 0, `next_attempt` INTEGER, `processed_at` INTEGER, `credit_consumed` VARCHAR(40) NOT NULL DEFAULT '0', `credit_pending` VARCHAR(40) NOT NULL DEFAULT '0', `metadata` JSON, `created_by` VARCHAR(40), `created_via` TEXT NOT NULL, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, `source_data` JSON);\n\nCREATE TABLE IF NOT EXISTS `meters` (`id` VARCHAR(18) NOT NULL PRIMARY KEY, `object` VARCHAR(32) NOT NULL DEFAULT 'meter', `name` VARCHAR(255) NOT NULL, `event_name` VARCHAR(128) NOT NULL UNIQUE, `aggregation_method` TEXT NOT NULL DEFAULT 'sum', `status` TEXT NOT NULL DEFAULT 'active', `unit` VARCHAR(32) NOT NULL, `description` TEXT, `component_did` VARCHAR(40), `currency_id` VARCHAR(40), `livemode` TINYINT(1) NOT NULL, `metadata` JSON, `created_by` VARCHAR(40), `updated_by` VARCHAR(40), `created_via` TEXT NOT NULL, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL);\n\nCREATE TABLE IF NOT EXISTS `payment_currencies` (`id` VARCHAR(15) NOT NULL PRIMARY KEY, `active` TINYINT(1) NOT NULL, `livemode` TINYINT(1) NOT NULL, `locked` TINYINT(1) DEFAULT 0, `is_base_currency` TINYINT(1) DEFAULT 0, `payment_method_id` VARCHAR(30) NOT NULL, `name` VARCHAR(64) NOT NULL, `description` VARCHAR(512), `logo` VARCHAR(512) NOT NULL, `symbol` VARCHAR(16) NOT NULL, `decimal` NUMBER DEFAULT 2, `maximum_precision` NUMBER DEFAULT 6, `minimum_payment_amount` VARCHAR(32) DEFAULT '0', `maximum_payment_amount` VARCHAR(32) DEFAULT '0', `contract` VARCHAR(256), `metadata` JSON, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, `vault_config` JSON, `type` TEXT NOT NULL DEFAULT 'standard', `recharge_config` JSON, `token_config` JSON);\n\nCREATE TABLE IF NOT EXISTS `payment_intents` (`id` VARCHAR(30) NOT NULL PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `amount` VARCHAR(32) NOT NULL, `amount_received` VARCHAR(32), `amount_capturable` VARCHAR(32), `amount_details` JSON, `currency_id` VARCHAR(16) NOT NULL, `customer_id` VARCHAR(30), `description` VARCHAR(512), `last_payment_error` JSON, `last_charge` VARCHAR(30), `metadata` JSON, `invoice_id` VARCHAR(30), `payment_method_id` VARCHAR(30) NOT NULL, `receipt_email` VARCHAR(255), `statement_descriptor` VARCHAR(32), `statement_descriptor_suffix` VARCHAR(32), `status` TEXT NOT NULL, `canceled_at` DATETIME, `cancellation_reason` TEXT, `capture_method` TEXT NOT NULL, `confirmation_method` TEXT NOT NULL, `payment_method_types` JSON DEFAULT '[]', `review` VARCHAR(30), `payment_details` JSON, `setup_future_usage` TEXT, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, `beneficiaries` JSON, `quote_locked_at` DATETIME);\n\nCREATE TABLE IF NOT EXISTS `payment_links` (`id` VARCHAR(40) NOT NULL UNIQUE PRIMARY KEY, `active` TINYINT(1) NOT NULL, `livemode` TINYINT(1) NOT NULL, `line_items` JSON DEFAULT '[]', `name` VARCHAR(255), `currency_id` VARCHAR(16), `after_completion` JSON DEFAULT '{\"type\":\"hosted_confirmation\"}', `allow_promotion_codes` TINYINT(1) DEFAULT 0, `consent_collection` JSON, `custom_fields` JSON DEFAULT '[]', `custom_text` JSON, `customer_creation` TEXT NOT NULL, `invoice_creation` JSON, `payment_method_types` JSON DEFAULT '[]', `phone_number_collection` JSON, `billing_address_collection` TEXT NOT NULL DEFAULT 'auto', `submit_type` TEXT NOT NULL, `subscription_data` JSON, `metadata` JSON, `created_at` DATETIME NOT NULL, `created_via` TEXT, `updated_at` DATETIME NOT NULL, `nft_mint_settings` JSON, `cross_sell_behavior` TEXT DEFAULT 'auto', `donation_settings` JSON, `beneficiaries` JSON, `enable_subscription_grouping` TINYINT(1) DEFAULT 0, `lookup_key` VARCHAR(128));\n\nCREATE TABLE IF NOT EXISTS `payment_methods` (`id` VARCHAR(30) NOT NULL PRIMARY KEY, `active` TINYINT(1) NOT NULL, `livemode` TINYINT(1) NOT NULL, `locked` TINYINT(1) DEFAULT 0, `type` TEXT, `name` VARCHAR(64), `description` VARCHAR(512), `logo` VARCHAR(512), `default_currency_id` VARCHAR(15), `confirmation` JSON NOT NULL, `settings` JSON NOT NULL, `features` JSON NOT NULL, `metadata` JSON, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL);\n\nCREATE TABLE IF NOT EXISTS `payment_stats` (`id` VARCHAR(30) NOT NULL PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `timestamp` INTEGER DEFAULT 0, `currency_id` VARCHAR(16) NOT NULL, `amount_paid` VARCHAR(64) DEFAULT '0', `amount_payout` VARCHAR(64) DEFAULT '0', `amount_refund` VARCHAR(64) DEFAULT '0', `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL);\n\nCREATE TABLE IF NOT EXISTS `payouts` (`id` VARCHAR(30) NOT NULL PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `automatic` TINYINT(1) NOT NULL, `description` VARCHAR(512), `amount` VARCHAR(32) NOT NULL, `destination` VARCHAR(512) NOT NULL, `payment_details` JSON, `currency_id` VARCHAR(16) NOT NULL, `customer_id` VARCHAR(30), `payment_intent_id` VARCHAR(16) NOT NULL, `payment_method_id` VARCHAR(30), `metadata` JSON, `status` TEXT NOT NULL, `failure_message` VARCHAR(256), `failure_code` TEXT, `attempt_count` INTEGER DEFAULT 0, `attempted` TINYINT(1) DEFAULT 0, `next_attempt` INTEGER, `last_attempt_error` JSON, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, `vendor_info` JSON);\n\nCREATE TABLE IF NOT EXISTS `price_quotes` (`id` VARCHAR(32) NOT NULL PRIMARY KEY, `price_id` VARCHAR(32) NOT NULL, `session_id` VARCHAR(32), `invoice_id` VARCHAR(32), `idempotency_key` VARCHAR(128) NOT NULL UNIQUE, `base_currency` VARCHAR(8) NOT NULL DEFAULT 'USD', `base_amount` VARCHAR(32) NOT NULL, `target_currency_id` VARCHAR(16) NOT NULL, `rate_currency_symbol` VARCHAR(16) NOT NULL, `exchange_rate` VARCHAR(32) NOT NULL, `quoted_amount` VARCHAR(64) NOT NULL, `rate_provider_id` VARCHAR(32) NOT NULL, `rate_provider_name` VARCHAR(64) NOT NULL, `rate_timestamp_ms` BIGINT NOT NULL, `expires_at` INTEGER NOT NULL, `status` TEXT NOT NULL DEFAULT 'active', `metadata` JSON, `created_at` DATETIME NOT NULL, `slippage_percent` DECIMAL(5,2), `max_payable_token` VARCHAR(64), `min_acceptable_rate` VARCHAR(32), `slippage_derived_at_ms` BIGINT);\n\nCREATE TABLE IF NOT EXISTS `prices` (`id` VARCHAR(32) NOT NULL PRIMARY KEY, `product_id` VARCHAR(32) NOT NULL, `nickname` VARCHAR(512), `active` TINYINT(1) NOT NULL, `livemode` TINYINT(1) NOT NULL, `locked` TINYINT(1) DEFAULT 0, `type` TEXT NOT NULL, `billing_scheme` TEXT NOT NULL, `unit_amount` VARCHAR(32) NOT NULL, `recurring` JSON, `tiers_mode` TEXT, `tiers` JSON, `custom_unit_amount` JSON, `lookup_key` VARCHAR(128), `metadata` JSON, `transform_quantity` JSON, `currency_id` VARCHAR(16), `currency_options` JSON DEFAULT '[]', `created_at` DATETIME NOT NULL, `created_via` TEXT, `updated_at` DATETIME NOT NULL, `upsell` JSON, `quantity_available` INTEGER NOT NULL DEFAULT 0, `quantity_sold` INTEGER NOT NULL DEFAULT 0, `quantity_limit_per_checkout` INTEGER NOT NULL DEFAULT 0, `tax_behavior` TEXT NOT NULL DEFAULT 'inclusive', `pricing_type` TEXT NOT NULL DEFAULT 'fixed', `base_currency` VARCHAR(8), `base_amount` VARCHAR(32), `dynamic_pricing_config` JSON);\n\nCREATE TABLE IF NOT EXISTS `pricing_tables` (`id` VARCHAR(42) NOT NULL PRIMARY KEY, `active` TINYINT(1) NOT NULL, `livemode` TINYINT(1) NOT NULL, `locked` TINYINT(1) NOT NULL, `name` VARCHAR(255), `items` JSON DEFAULT '[]', `branding_settings` JSON NOT NULL, `metadata` JSON, `created_at` DATETIME NOT NULL, `created_via` TEXT, `updated_at` DATETIME NOT NULL);\n\nCREATE TABLE IF NOT EXISTS `product_vendors` (`id` STRING NOT NULL UNIQUE PRIMARY KEY, `vendor_key` STRING NOT NULL, `name` STRING NOT NULL, `description` TEXT, `app_url` STRING NOT NULL, `app_pid` STRING NOT NULL, `app_logo` STRING, `status` STRING NOT NULL DEFAULT 'active', `metadata` JSON DEFAULT '{}', `created_by` STRING, `created_at` DATE NOT NULL DEFAULT 'CURRENT_TIMESTAMP', `updated_at` DATE NOT NULL DEFAULT 'CURRENT_TIMESTAMP', `vendor_type` STRING NOT NULL DEFAULT 'launcher', `vendor_did` STRING, `extends` JSON);\n\nCREATE TABLE IF NOT EXISTS `products` (`id` VARCHAR(18) NOT NULL PRIMARY KEY, `active` TINYINT(1) NOT NULL, `livemode` TINYINT(1) NOT NULL, `locked` TINYINT(1) DEFAULT 0, `type` TEXT, `name` VARCHAR(512), `description` VARCHAR(2048), `images` JSON DEFAULT '[]', `features` JSON DEFAULT '[]', `unit_label` VARCHAR(32), `default_price_id` VARCHAR(32), `metadata` JSON, `statement_descriptor` VARCHAR(32), `nft_factory` VARCHAR(40), `created_at` DATETIME NOT NULL, `created_via` TEXT, `updated_at` DATETIME NOT NULL, `cross_sell` JSON, `vendor_config` JSON, `tax_code` VARCHAR(20));\n\nCREATE TABLE IF NOT EXISTS `promotion_codes` (`id` VARCHAR(30) NOT NULL UNIQUE PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `active` TINYINT(1) DEFAULT 1, `code` VARCHAR(16) NOT NULL, `coupon_id` VARCHAR(30), `max_redemptions` INTEGER, `restrictions` JSON DEFAULT '{}', `customer_id` VARCHAR(30), `times_redeemed` INTEGER DEFAULT '0', `metadata` JSON, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, `verification_type` TEXT NOT NULL DEFAULT 'code', `nft_config` JSON, `vc_config` JSON, `customer_dids` JSON, `created_via` TEXT, `locked` TINYINT(1) NOT NULL DEFAULT 0, `expires_at` INTEGER, `expires_at_new` INTEGER, `description` TEXT);\n\nCREATE TABLE IF NOT EXISTS `refunds` (`id` VARCHAR(30) NOT NULL PRIMARY KEY, `description` VARCHAR(512), `livemode` TINYINT(1) NOT NULL, `amount` VARCHAR(32) NOT NULL, `payment_details` JSON, `currency_id` VARCHAR(16) NOT NULL, `customer_id` VARCHAR(30), `payment_intent_id` VARCHAR(16) NOT NULL, `invoice_id` VARCHAR(30), `subscription_id` VARCHAR(30), `metadata` JSON, `status` TEXT NOT NULL, `reason` TEXT, `failure_reason` TEXT, `attempt_count` INTEGER DEFAULT 0, `attempted` TINYINT(1) DEFAULT 0, `next_attempt` INTEGER, `last_attempt_error` JSON, `starting_balance` VARCHAR(32) DEFAULT '0', `ending_balance` VARCHAR(32) DEFAULT '0', `starting_token_balance` JSON DEFAULT '{}', `ending_token_balance` JSON DEFAULT '{}', `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, `payment_method_id` VARCHAR(30), `type` TEXT DEFAULT 'refund');\n\nCREATE TABLE IF NOT EXISTS `revenue_snapshots` (`id` VARCHAR(30) NOT NULL PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `currency_id` VARCHAR(16) NOT NULL, `timestamp` INTEGER NOT NULL, `period_type` TEXT NOT NULL DEFAULT 'monthly', `total_revenue` VARCHAR(64) DEFAULT '0', `refund_amount` VARCHAR(64) DEFAULT '0', `promotion_cost` VARCHAR(64) DEFAULT '0', `credit_grant_cost` VARCHAR(64) DEFAULT '0', `vendor_cost` VARCHAR(64) DEFAULT '0', `taxed_revenue` VARCHAR(64) DEFAULT '0', `net_revenue` VARCHAR(64) DEFAULT '0', `archive_metadata_id` VARCHAR(40), `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL);\n\nCREATE TABLE IF NOT EXISTS `settings` (`id` VARCHAR(30) NOT NULL PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `active` TINYINT(1) NOT NULL DEFAULT 1, `type` VARCHAR(64) NOT NULL, `mount_location` VARCHAR(255) NOT NULL, `component_did` VARCHAR(255), `description` VARCHAR(255) NOT NULL, `settings` JSON, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL);\n\nCREATE TABLE IF NOT EXISTS `setup_intents` (`id` VARCHAR(30) NOT NULL PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `currency_id` VARCHAR(16) NOT NULL, `customer_id` VARCHAR(30), `description` VARCHAR(512), `last_setup_error` JSON, `last_attempt` VARCHAR(30), `metadata` JSON, `status` TEXT NOT NULL, `usage` TEXT NOT NULL, `canceled_at` INTEGER, `cancellation_reason` TEXT, `flow_directions` JSON DEFAULT '[\"inbound\",\"outbound\"]', `payment_method_types` JSON DEFAULT '[]', `payment_method_options` JSON, `payment_method_id` VARCHAR(30) NOT NULL, `setup_details` JSON, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL);\n\nCREATE TABLE IF NOT EXISTS `subscription_items` (`id` VARCHAR(18) NOT NULL PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `subscription_id` VARCHAR(30) NOT NULL, `price_id` VARCHAR(30) NOT NULL, `quantity` INTEGER, `metadata` JSON, `billing_threshold` JSON, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL);\n\nCREATE TABLE IF NOT EXISTS `subscription_schedules` (`id` VARCHAR(30) NOT NULL PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `current_phase` JSON NOT NULL, `customer_id` VARCHAR(30) NOT NULL, `subscription_id` VARCHAR(30) NOT NULL, `released_subscription_id` VARCHAR(30), `status` TEXT NOT NULL, `default_settings` JSON, `end_behavior` TEXT NOT NULL, `test_clock_id` VARCHAR(30), `metadata` JSON, `canceled_at` INTEGER, `completed_at` INTEGER, `released_at` INTEGER, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL);\n\nCREATE TABLE IF NOT EXISTS `subscriptions` (`id` VARCHAR(30) NOT NULL PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `currency_id` VARCHAR(15) NOT NULL, `customer_id` VARCHAR(18) NOT NULL, `cancel_at_period_end` TINYINT(1) NOT NULL, `current_period_end` INTEGER NOT NULL, `current_period_start` INTEGER NOT NULL, `default_payment_method_id` VARCHAR(30), `description` VARCHAR(512), `latest_invoice_id` VARCHAR(30), `pending_setup_intent` VARCHAR(30), `pending_update` JSON, `status` TEXT NOT NULL, `cancel_at` INTEGER, `canceled_at` INTEGER, `cancelation_details` JSON, `billing_cycle_anchor` INTEGER NOT NULL, `billing_thresholds` JSON, `collection_method` TEXT NOT NULL, `days_until_due` NUMBER, `discount_id` VARCHAR(30), `next_pending_invoice_item_invoice_id` VARCHAR(30), `pause_collection` JSON, `payment_settings` JSON, `pending_invoice_item_interval` JSON NOT NULL, `schedule_id` VARCHAR(30), `end_at` INTEGER, `start_date` INTEGER NOT NULL, `payment_details` JSON, `metadata` JSON, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, `proration_behavior` TEXT DEFAULT 'none', `payment_behavior` TEXT DEFAULT 'default_incomplete', `days_until_cancel` NUMBER, `service_actions` JSON DEFAULT '\"[]\"', `trial_start` INTEGER, `trial_end` INTEGER, `trial_settings` JSON, `recovered_from` VARCHAR(40), `overdraft_protection` JSON DEFAULT '\"{\\\"enabled\\\":false,\\\"payment_method_id\\\":null,\\\"payment_details\\\":null}\"', `credit_schedule_state` JSON DEFAULT NULL, `slippage_config` JSON);\n\nCREATE TABLE IF NOT EXISTS `tax_rates` (`id` VARCHAR(30) NOT NULL PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `active` TINYINT(1) DEFAULT 1, `country` VARCHAR(2) NOT NULL, `state` VARCHAR(50), `postal_code` VARCHAR(20), `tax_code` VARCHAR(20), `percentage` DECIMAL(5,4) NOT NULL, `display_name` VARCHAR(100) NOT NULL, `description` VARCHAR(500), `metadata` JSON, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL);\n\nCREATE TABLE IF NOT EXISTS `usage_records` (`id` VARCHAR(30) NOT NULL PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `timestamp` INTEGER NOT NULL, `quantity` BIGINT NOT NULL, `subscription_item_id` VARCHAR(30) NOT NULL, `metadata` JSON, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, `billed` TINYINT(1) DEFAULT 0);\n\nCREATE TABLE IF NOT EXISTS `webhook_attempts` (`id` VARCHAR(30) NOT NULL PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `event_id` VARCHAR(30) NOT NULL, `webhook_endpoint_id` VARCHAR(30) NOT NULL, `status` TEXT NOT NULL, `response_status` INTEGER NOT NULL, `response_body` JSON NOT NULL, `retry_count` INTEGER DEFAULT 0, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL);\n\nCREATE TABLE IF NOT EXISTS `webhook_endpoints` (`id` VARCHAR(30) NOT NULL PRIMARY KEY, `livemode` TINYINT(1) NOT NULL, `api_version` VARCHAR(16) NOT NULL, `url` VARCHAR(512) NOT NULL, `description` VARCHAR(512), `enabled_events` JSON NOT NULL, `metadata` JSON, `status` TEXT NOT NULL, `created_at` DATETIME NOT NULL, `created_via` TEXT, `updated_at` DATETIME NOT NULL);\n" },
12
+ { name: "0002_indexes.sql", sql: "-- Payment Kit: All indexes\n-- Matches the indexes from the original Sequelize migrations.\n\nCREATE INDEX IF NOT EXISTS `idx_auto_recharge_configs_currency_id` ON `auto_recharge_configs` (`currency_id`);\nCREATE INDEX IF NOT EXISTS `idx_auto_recharge_configs_customer_id` ON `auto_recharge_configs` (`customer_id`);\nCREATE INDEX IF NOT EXISTS `idx_checkout_sessions_fulfillment_status` ON `checkout_sessions` (`fulfillment_status`);\nCREATE INDEX IF NOT EXISTS `idx_checkout_sessions_payment_intent` ON `checkout_sessions` (`payment_intent_id`);\nCREATE INDEX IF NOT EXISTS `idx_checkout_sessions_payment_link_status_livemode` ON `checkout_sessions` (`payment_link_id`, `status`, `livemode`);\nCREATE INDEX IF NOT EXISTS `idx_checkout_sessions_status` ON `checkout_sessions` (`status`);\nCREATE INDEX IF NOT EXISTS `idx_checkout_sessions_subscription` ON `checkout_sessions` (`subscription_id`);\nCREATE INDEX IF NOT EXISTS idx_credit_grant_stats_by_grantor ON credit_grants(json_extract(metadata, '$.granted_by'), currency_id, created_at) WHERE json_extract(metadata, '$.granted_by') IS NOT NULL;\nCREATE INDEX IF NOT EXISTS `idx_credit_grants_chain_status` ON `credit_grants` (`chain_status`);\nCREATE INDEX IF NOT EXISTS `idx_credit_grants_currency` ON `credit_grants` (`currency_id`);\nCREATE INDEX IF NOT EXISTS `idx_credit_grants_customer` ON `credit_grants` (`customer_id`);\nCREATE INDEX IF NOT EXISTS `idx_credit_grants_customer_currency_status` ON `credit_grants` (`customer_id`, `currency_id`, `status`);\nCREATE INDEX IF NOT EXISTS `idx_credit_grants_status` ON `credit_grants` (`status`);\nCREATE INDEX IF NOT EXISTS `idx_credit_transactions_created` ON `credit_transactions` (`created_at`);\nCREATE INDEX IF NOT EXISTS `idx_credit_transactions_customer_created` ON `credit_transactions` (`customer_id`, `created_at`);\nCREATE INDEX IF NOT EXISTS `idx_credit_transactions_event_created` ON `credit_transactions` (`meter_event_name`, `created_at`);\nCREATE INDEX IF NOT EXISTS `idx_credit_transactions_meter_created` ON `credit_transactions` (`meter_id`, `created_at`);\nCREATE INDEX IF NOT EXISTS `idx_credit_transactions_subscription_created` ON `credit_transactions` (`subscription_id`, `created_at`);\nCREATE INDEX IF NOT EXISTS `idx_credit_transactions_transfer_status` ON `credit_transactions` (`transfer_status`);\nCREATE INDEX IF NOT EXISTS `idx_customers_did` ON `customers` (`did`);\nCREATE INDEX IF NOT EXISTS `idx_erp_enabled_priority` ON `exchange_rate_providers` (`enabled`, `priority`);\nCREATE UNIQUE INDEX IF NOT EXISTS `idx_erp_name` ON `exchange_rate_providers` (`name`);\nCREATE INDEX IF NOT EXISTS `idx_events_object_id_created` ON `events` (`object_id`, `created_at`);\nCREATE INDEX IF NOT EXISTS `idx_events_object_type_id_created` ON `events` (`object_type`, `object_id`, `created_at`);\nCREATE INDEX IF NOT EXISTS `idx_events_type_created` ON `events` (`type`, `created_at`);\nCREATE INDEX IF NOT EXISTS `idx_invoice_currency_id` ON `invoices` (`currency_id`);\nCREATE INDEX IF NOT EXISTS `idx_invoice_customer_id` ON `invoices` (`customer_id`);\nCREATE INDEX IF NOT EXISTS `idx_invoice_customer_total` ON `invoices` (`customer_id`, `total`);\nCREATE INDEX IF NOT EXISTS `idx_invoice_items_invoice_id` ON `invoice_items` (`invoice_id`);\nCREATE INDEX IF NOT EXISTS `idx_invoice_items_tax_rate_id_invoice_id` ON `invoice_items` (`tax_rate_id`, `invoice_id`);\nCREATE INDEX IF NOT EXISTS `idx_invoice_status_collection` ON `invoices` (`status`, `collection_method`);\nCREATE INDEX IF NOT EXISTS `idx_invoice_status_total` ON `invoices` (`status`, `total`);\nCREATE INDEX IF NOT EXISTS `idx_invoice_subscription_id` ON `invoices` (`subscription_id`);\nCREATE INDEX IF NOT EXISTS `idx_invoice_subtotal` ON `invoices` (`subtotal`);\nCREATE INDEX IF NOT EXISTS `idx_invoice_total` ON `invoices` (`total`);\nCREATE INDEX IF NOT EXISTS `idx_invoices_created_at` ON `invoices` (`created_at`);\nCREATE INDEX IF NOT EXISTS `idx_invoices_status` ON `invoices` (`status`);\nCREATE INDEX IF NOT EXISTS `idx_jobs_cancelled_run_at` ON `jobs` (`cancelled`, `will_run_at`);\nCREATE INDEX IF NOT EXISTS `idx_jobs_queue_cancelled_run_at_delay` ON `jobs` (`queue`, `cancelled`, `will_run_at`, `delay`);\nCREATE INDEX IF NOT EXISTS `idx_jobs_queue_id` ON `jobs` (`queue`, `id`);\nCREATE INDEX IF NOT EXISTS `idx_jobs_queue_run_at` ON `jobs` (`queue`, `will_run_at`);\nCREATE INDEX IF NOT EXISTS idx_meter_events_customer_status ON meter_events(json_extract(payload, '$.customer_id'), status, livemode, created_at);\nCREATE INDEX IF NOT EXISTS `idx_meter_events_event_livemode_created` ON `meter_events` (`event_name`, `livemode`, `created_at`);\nCREATE INDEX IF NOT EXISTS `idx_meter_events_livemode_event_timestamp` ON `meter_events` (`livemode`, `event_name`, `timestamp`);\nCREATE INDEX IF NOT EXISTS `idx_meter_events_status_created` ON `meter_events` (`status`, `created_at`);\nCREATE INDEX IF NOT EXISTS idx_meter_events_subscription_status ON meter_events(json_extract(payload, '$.subscription_id'), status, livemode, created_at);\nCREATE INDEX IF NOT EXISTS `idx_meters_livemode_status` ON `meters` (`livemode`, `status`);\nCREATE INDEX IF NOT EXISTS `idx_payment_currencies_base_livemode` ON `payment_currencies` (`is_base_currency`, `livemode`);\nCREATE INDEX IF NOT EXISTS `idx_payment_intent_currency_id` ON `payment_intents` (`currency_id`);\nCREATE INDEX IF NOT EXISTS `idx_payment_intent_customer_id` ON `payment_intents` (`customer_id`);\nCREATE INDEX IF NOT EXISTS `idx_payment_intent_invoice_id` ON `payment_intents` (`invoice_id`);\nCREATE INDEX IF NOT EXISTS `idx_payment_intent_status_updated_at` ON `payment_intents` (`status`, `updated_at`);\nCREATE INDEX IF NOT EXISTS `idx_payment_stats_timestamp_currency_id` ON `payment_stats` (`timestamp`, `currency_id`);\nCREATE INDEX IF NOT EXISTS `idx_payouts_updated_at_status` ON `payouts` (`updated_at`, `status`);\nCREATE INDEX IF NOT EXISTS `idx_pq_created` ON `price_quotes` (`created_at`);\nCREATE INDEX IF NOT EXISTS `idx_pq_currency_created` ON `price_quotes` (`rate_currency_symbol`, `created_at`);\nCREATE UNIQUE INDEX IF NOT EXISTS `idx_pq_idempotency` ON `price_quotes` (`idempotency_key`);\nCREATE INDEX IF NOT EXISTS `idx_pq_invoice_status` ON `price_quotes` (`invoice_id`, `status`);\nCREATE INDEX IF NOT EXISTS `idx_pq_session_status_expires` ON `price_quotes` (`session_id`, `status`, `expires_at`);\nCREATE INDEX IF NOT EXISTS `idx_refunds_payment_intent_id_type` ON `refunds` (`payment_intent_id`, `type`);\nCREATE INDEX IF NOT EXISTS `idx_refunds_status_type_updated_at` ON `refunds` (`status`, `type`, `updated_at`);\nCREATE INDEX IF NOT EXISTS `idx_refunds_subscription_id_type` ON `refunds` (`subscription_id`, `type`);\nCREATE UNIQUE INDEX IF NOT EXISTS `idx_revenue_snapshots_unique` ON `revenue_snapshots` (`timestamp`, `currency_id`, `livemode`, `period_type`);\nCREATE INDEX IF NOT EXISTS `idx_subscription_item_subscription_id_price_id` ON `subscription_items` (`subscription_id`, `price_id`);\nCREATE INDEX IF NOT EXISTS `idx_subscription_period` ON `subscriptions` (`current_period_start`, `current_period_end`);\nCREATE INDEX IF NOT EXISTS `idx_subscription_status` ON `subscriptions` (`status`);\nCREATE INDEX IF NOT EXISTS `idx_subscriptions_customer_id` ON `subscriptions` (`customer_id`);\nCREATE INDEX IF NOT EXISTS `idx_usage_records_subscription_item_id_timestamp` ON `usage_records` (`subscription_item_id`, `timestamp`);\nCREATE INDEX IF NOT EXISTS `idx_webhook_attempts_event_id` ON `webhook_attempts` (`event_id`);\nCREATE INDEX IF NOT EXISTS `idx_webhook_attempts_webhook_endpoint_id` ON `webhook_attempts` (`webhook_endpoint_id`);\nCREATE INDEX IF NOT EXISTS `idx_webhook_endpoint_status_livemode` ON `webhook_endpoints` (`status`, `livemode`);\nCREATE INDEX IF NOT EXISTS `tax_rates_query_index` ON `tax_rates` (`country`, `state`, `postal_code`, `tax_code`);\n" },
13
+ { name: "0003_locks_and_constraints.sql", sql: "-- Distributed locks table for D1-based locking across CF Worker isolates\nCREATE TABLE IF NOT EXISTS _locks (\n name TEXT PRIMARY KEY,\n owner TEXT NOT NULL,\n expires_at INTEGER NOT NULL\n);\n\n-- Unique index on Stripe invoice ID to prevent duplicate mirroring\nCREATE UNIQUE INDEX IF NOT EXISTS idx_invoices_stripe_id\n ON invoices(json_extract(metadata, '$.stripe_id'))\n WHERE json_extract(metadata, '$.stripe_id') IS NOT NULL;\n\n-- DID Connect token storage (D1 for strong consistency instead of KV)\nCREATE TABLE IF NOT EXISTS _did_connect_tokens (\n token TEXT PRIMARY KEY,\n data TEXT NOT NULL,\n expires_at INTEGER NOT NULL\n);\n" },
14
+ { name: "0004_iap_foundation.sql", sql: "-- Payment Kit: IAP foundation\n-- Adds schema needed for App Store + Google Play subscription channels.\n-- Mirrors blocklets/core/api/src/store/migrations/20260526-iap-foundation.ts.\n--\n-- NOTE: SQLite does not support `ALTER TABLE ADD COLUMN IF NOT EXISTS`.\n-- Wrangler tracks applied migrations in the `d1_migrations` table and skips\n-- re-application by filename, so re-running this file via `wrangler d1\n-- migrations apply` is safe. Do not invoke via `wrangler d1 execute` more\n-- than once on the same database.\n\n-- 1. Customer: per-channel UUID for IAP appAccountToken / obfuscatedAccountId mapping (D-004)\nALTER TABLE customers ADD COLUMN app_store_uuid VARCHAR(36);\nALTER TABLE customers ADD COLUMN google_play_uuid VARCHAR(36);\nCREATE UNIQUE INDEX IF NOT EXISTS idx_customers_app_store_uuid\n ON customers(app_store_uuid)\n WHERE app_store_uuid IS NOT NULL;\nCREATE UNIQUE INDEX IF NOT EXISTS idx_customers_google_play_uuid\n ON customers(google_play_uuid)\n WHERE google_play_uuid IS NOT NULL;\n\n-- 2. Subscription: channel + environment (D-005)\nALTER TABLE subscriptions ADD COLUMN channel VARCHAR(20);\nALTER TABLE subscriptions ADD COLUMN environment VARCHAR(20) DEFAULT 'production';\n\n-- 3. Invoice: three-segment amounts (D-001 A)\nALTER TABLE invoices ADD COLUMN gross_amount VARCHAR(32);\nALTER TABLE invoices ADD COLUMN platform_fee VARCHAR(32) DEFAULT '0';\nALTER TABLE invoices ADD COLUMN net_amount VARCHAR(32);\nUPDATE invoices SET gross_amount = total, net_amount = total WHERE gross_amount IS NULL;\n\n-- 4. Refund: origin source (merchant_initiated | platform_initiated)\nALTER TABLE refunds ADD COLUMN source VARCHAR(30) DEFAULT 'merchant_initiated';\n\n-- 5. Entitlement tables (D-003 B)\nCREATE TABLE IF NOT EXISTS `entitlements` (\n `id` VARCHAR(30) NOT NULL PRIMARY KEY,\n `livemode` TINYINT(1) NOT NULL,\n `key` VARCHAR(64) NOT NULL UNIQUE,\n `name` VARCHAR(255),\n `description` TEXT,\n `metadata` JSON,\n `created_at` DATETIME NOT NULL,\n `updated_at` DATETIME NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS `idx_entitlements_key` ON `entitlements` (`key`);\n\nCREATE TABLE IF NOT EXISTS `entitlement_products` (\n `entitlement_id` VARCHAR(30) NOT NULL,\n `product_id` VARCHAR(30) NOT NULL,\n `created_at` DATETIME NOT NULL,\n `updated_at` DATETIME NOT NULL,\n PRIMARY KEY (`entitlement_id`, `product_id`)\n);\n\nCREATE TABLE IF NOT EXISTS `entitlement_grants` (\n `id` VARCHAR(30) NOT NULL PRIMARY KEY,\n `livemode` TINYINT(1) NOT NULL,\n `entitlement_id` VARCHAR(30) NOT NULL,\n `customer_id` VARCHAR(18) NOT NULL,\n `source_subscription_id` VARCHAR(30),\n `source_channel` VARCHAR(20) NOT NULL,\n `active_from` INTEGER NOT NULL,\n `active_until` INTEGER NOT NULL,\n `status` VARCHAR(20) NOT NULL DEFAULT 'active',\n `metadata` JSON,\n `created_at` DATETIME NOT NULL,\n `updated_at` DATETIME NOT NULL\n);\nCREATE INDEX IF NOT EXISTS `idx_entitlement_grants_lookup`\n ON `entitlement_grants` (`customer_id`, `entitlement_id`, `status`);\nCREATE INDEX IF NOT EXISTS `idx_entitlement_grants_source_sub`\n ON `entitlement_grants` (`source_subscription_id`);\n" },
15
+ { name: "0005_iap_tenant_backfill.sql", sql: "-- Payment Kit: IAP multi-tenant backfill\n-- Backfills metadata.bundle_id / metadata.package_name on existing Prices and\n-- payment_details.app_store.bundle_id / payment_details.google_play.package_name\n-- on existing Subscriptions, so Payment Kit can be wired into multiple iOS /\n-- Android apps without same-SKU collisions across App Store / Play Console\n-- namespaces (each store's SKU space is per-app, not global).\n--\n-- Backend code already filters Price.findOne by (sku, bundle_id) / (sku,\n-- package_name); without this backfill, every pre-existing Price would stop\n-- resolving the moment the new lookup ships.\n--\n-- Safety: tenant value is DERIVED from the configured PaymentMethods (which\n-- store bundle_id / package_name as plain text under settings JSON — only\n-- private keys are encrypted). The migration deliberately refuses to guess in\n-- ambiguous setups:\n--\n-- * For Subscriptions we always use the sub's own\n-- `default_payment_method_id` to resolve the tenant, which is a 1:1 map —\n-- never ambiguous as long as the row points at a real PaymentMethod.\n-- * For Prices we update only when EXACTLY ONE active PaymentMethod of the\n-- matching type + livemode exists with a non-null tenant. Multi-tenant\n-- installations (two iOS apps sharing one Payment Kit, etc.) skip the\n-- Price backfill — admin must set bundle_id / package_name explicitly\n-- because the migration can't safely guess which app a Price belongs to.\n--\n-- Idempotent. The IS NULL guards make re-runs a no-op for already-backfilled\n-- rows, and the subquery filters skip rows that can't be resolved safely.\n\n-- 1. Prices with App Store SKU → set bundle_id (only when one active\n-- app_store PaymentMethod for the same livemode unambiguously identifies\n-- the tenant).\nUPDATE prices\nSET metadata = json_set(\n metadata,\n '$.bundle_id',\n (SELECT json_extract(pm.settings, '$.app_store.bundle_id')\n FROM payment_methods pm\n WHERE pm.type = 'app_store'\n AND pm.livemode = prices.livemode\n AND pm.active = 1\n AND json_extract(pm.settings, '$.app_store.bundle_id') IS NOT NULL\n LIMIT 1)\n)\nWHERE json_extract(metadata, '$.app_store_product_id') IS NOT NULL\n AND json_extract(metadata, '$.bundle_id') IS NULL\n AND (\n SELECT COUNT(*) FROM payment_methods pm\n WHERE pm.type = 'app_store'\n AND pm.livemode = prices.livemode\n AND pm.active = 1\n AND json_extract(pm.settings, '$.app_store.bundle_id') IS NOT NULL\n ) = 1;\n\n-- 2. Prices with Google Play SKU → set package_name (same single-tenant guard).\nUPDATE prices\nSET metadata = json_set(\n metadata,\n '$.package_name',\n (SELECT json_extract(pm.settings, '$.google_play.package_name')\n FROM payment_methods pm\n WHERE pm.type = 'google_play'\n AND pm.livemode = prices.livemode\n AND pm.active = 1\n AND json_extract(pm.settings, '$.google_play.package_name') IS NOT NULL\n LIMIT 1)\n)\nWHERE json_extract(metadata, '$.google_play_product_id') IS NOT NULL\n AND json_extract(metadata, '$.package_name') IS NULL\n AND (\n SELECT COUNT(*) FROM payment_methods pm\n WHERE pm.type = 'google_play'\n AND pm.livemode = prices.livemode\n AND pm.active = 1\n AND json_extract(pm.settings, '$.google_play.package_name') IS NOT NULL\n ) = 1;\n\n-- 3. App Store Subscriptions → set payment_details.app_store.bundle_id from\n-- the sub's own default_payment_method (1:1 — always safe).\nUPDATE subscriptions\nSET payment_details = json_set(\n payment_details,\n '$.app_store.bundle_id',\n (SELECT json_extract(pm.settings, '$.app_store.bundle_id')\n FROM payment_methods pm\n WHERE pm.id = subscriptions.default_payment_method_id)\n)\nWHERE channel = 'app_store'\n AND json_extract(payment_details, '$.app_store.bundle_id') IS NULL\n AND default_payment_method_id IS NOT NULL\n AND (\n SELECT json_extract(pm.settings, '$.app_store.bundle_id')\n FROM payment_methods pm\n WHERE pm.id = subscriptions.default_payment_method_id\n ) IS NOT NULL;\n\n-- 4. Google Play Subscriptions → set payment_details.google_play.package_name.\nUPDATE subscriptions\nSET payment_details = json_set(\n payment_details,\n '$.google_play.package_name',\n (SELECT json_extract(pm.settings, '$.google_play.package_name')\n FROM payment_methods pm\n WHERE pm.id = subscriptions.default_payment_method_id)\n)\nWHERE channel = 'google_play'\n AND json_extract(payment_details, '$.google_play.package_name') IS NULL\n AND default_payment_method_id IS NOT NULL\n AND (\n SELECT json_extract(pm.settings, '$.google_play.package_name')\n FROM payment_methods pm\n WHERE pm.id = subscriptions.default_payment_method_id\n ) IS NOT NULL;\n" },
16
+ { name: "0006_tenant_columns.sql", sql: "-- Payment Kit: tenant columns (Phase 1, W1-1a)\n-- Adds a nullable instance_did column to all 38 tenant tables.\n-- Mirrors blocklets/core/api/src/store/migrations/20260610-tenant-columns.ts.\n-- No constraints / indexes / backfill here — those land in 0007 (Phase 2).\n--\n-- NOTE: wrangler tracks applied migrations in `d1_migrations` by filename,\n-- so this file is applied exactly once via `wrangler d1 migrations apply`.\n\nALTER TABLE customers ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE products ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE prices ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE pricing_tables ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE payment_methods ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE checkout_sessions ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE payment_intents ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE payment_links ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE setup_intents ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE price_quotes ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE subscriptions ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE subscription_items ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE subscription_schedules ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE invoices ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE invoice_items ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE refunds ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE credit_grants ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE credit_transactions ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE meters ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE meter_events ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE usage_records ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE coupons ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE promotion_codes ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE discounts ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE entitlements ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE entitlement_grants ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE entitlement_products ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE events ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE webhook_endpoints ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE webhook_attempts ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE payouts ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE payment_stats ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE revenue_snapshots ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE auto_recharge_configs ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE tax_rates ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE settings ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE payment_currencies ADD COLUMN instance_did VARCHAR(64);\nALTER TABLE product_vendors ADD COLUMN instance_did VARCHAR(64);\n" },
17
+ { name: "0007_tenant_backfill_indexes.sql", sql: "-- Payment Kit: tenant unique keys + indexes (Phase 2, W1-1b)\n-- Mirrors the index/unique-key part of api/src/store/migrations/20260611-tenant-backfill.ts.\n--\n-- IMPORTANT: the instance_did BACKFILL is NOT here. Static migration SQL\n-- cannot know the deployment app DID, so the backfill (and the table rebuilds\n-- that drop old inline single-column UNIQUE constraints) runs through the\n-- shared runtime routine `runTenantBackfill()` (api/src/store/tenant-backfill.ts),\n-- invoked idempotently from the worker's scheduled() handler.\n--\n-- Everything below is NULL-safe before that backfill runs: SQLite treats each\n-- NULL as distinct in unique indexes, and existing single-column uniqueness\n-- (identifier / did / key / idempotency_key) guarantees no composite dupes.\n\n-- composite tenant unique keys (W1 §2.1, decisions D1/D3)\nCREATE UNIQUE INDEX IF NOT EXISTS `uq_customers_tenant_did` ON `customers` (`instance_did`, `did`);\nCREATE UNIQUE INDEX IF NOT EXISTS `uq_meter_events_tenant_identifier` ON `meter_events` (`instance_did`, `identifier`);\nCREATE UNIQUE INDEX IF NOT EXISTS `uq_meters_tenant_event_name` ON `meters` (`instance_did`, `event_name`);\nCREATE UNIQUE INDEX IF NOT EXISTS `uq_entitlements_tenant_key` ON `entitlements` (`instance_did`, `key`);\nCREATE UNIQUE INDEX IF NOT EXISTS `uq_price_quotes_tenant_idem` ON `price_quotes` (`instance_did`, `idempotency_key`);\nCREATE UNIQUE INDEX IF NOT EXISTS `uq_promotion_codes_tenant_code` ON `promotion_codes` (`instance_did`, `livemode`, `code`);\nCREATE UNIQUE INDEX IF NOT EXISTS `uq_product_vendors_tenant_key` ON `product_vendors` (`instance_did`, `vendor_key`);\nCREATE UNIQUE INDEX IF NOT EXISTS `uq_revenue_snapshots_tenant` ON `revenue_snapshots` (`instance_did`, `timestamp`, `currency_id`, `livemode`, `period_type`);\nDROP INDEX IF EXISTS `idx_revenue_snapshots_unique`;\nDROP INDEX IF EXISTS `idx_entitlements_key`;\nDROP INDEX IF EXISTS `idx_pq_idempotency`;\n\n-- plain tenant indexes for scoped queries (Phase 3+); meter_events is served\n-- by its composite unique above (high-write table, avoid a second index)\nCREATE INDEX IF NOT EXISTS `idx_customers_instance_did` ON `customers` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_products_instance_did` ON `products` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_prices_instance_did` ON `prices` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_pricing_tables_instance_did` ON `pricing_tables` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_payment_methods_instance_did` ON `payment_methods` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_checkout_sessions_instance_did` ON `checkout_sessions` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_payment_intents_instance_did` ON `payment_intents` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_payment_links_instance_did` ON `payment_links` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_setup_intents_instance_did` ON `setup_intents` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_price_quotes_instance_did` ON `price_quotes` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_subscriptions_instance_did` ON `subscriptions` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_subscription_items_instance_did` ON `subscription_items` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_subscription_schedules_instance_did` ON `subscription_schedules` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_invoices_instance_did` ON `invoices` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_invoice_items_instance_did` ON `invoice_items` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_refunds_instance_did` ON `refunds` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_credit_grants_instance_did` ON `credit_grants` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_credit_transactions_instance_did` ON `credit_transactions` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_meters_instance_did` ON `meters` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_usage_records_instance_did` ON `usage_records` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_coupons_instance_did` ON `coupons` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_promotion_codes_instance_did` ON `promotion_codes` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_discounts_instance_did` ON `discounts` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_entitlements_instance_did` ON `entitlements` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_entitlement_grants_instance_did` ON `entitlement_grants` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_entitlement_products_instance_did` ON `entitlement_products` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_events_instance_did` ON `events` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_webhook_endpoints_instance_did` ON `webhook_endpoints` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_webhook_attempts_instance_did` ON `webhook_attempts` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_payouts_instance_did` ON `payouts` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_payment_stats_instance_did` ON `payment_stats` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_revenue_snapshots_instance_did` ON `revenue_snapshots` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_auto_recharge_configs_instance_did` ON `auto_recharge_configs` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_tax_rates_instance_did` ON `tax_rates` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_settings_instance_did` ON `settings` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_payment_currencies_instance_did` ON `payment_currencies` (`instance_did`);\nCREATE INDEX IF NOT EXISTS `idx_product_vendors_instance_did` ON `product_vendors` (`instance_did`);\n" },
18
+ { name: "0008_schema_parity.sql", sql: "-- Phase 11 (W2′) schema parity: close the D1-lineage gaps the schema-drift guard\n-- (scripts/schema-drift-guard.ts) found versus the Umzug canonical schema.\n-- Additive only — safe to apply on a deployed D1 (no drops, no rewrites).\n\n-- events: the Umzug genesis creates `events` with api_version (NOT NULL) and\n-- metadata (JSON); the D1 0001 events table predates both, so the worker could\n-- not write them. Backfill api_version with the app constant\n-- (api/src/libs/audit.ts API_VERSION = '2023-09-05'); new rows carry the same.\nALTER TABLE `events` ADD COLUMN `api_version` VARCHAR(16) NOT NULL DEFAULT '2023-09-05';\nALTER TABLE `events` ADD COLUMN `metadata` JSON;\n\n-- Indexes present in the canonical but missing in the D1 lineage (perf parity;\n-- names match the Umzug migrations 20250904-discount / 20251007-relate-tax-rate).\nCREATE INDEX IF NOT EXISTS `idx_discounts_customer_id` ON `discounts` (`customer_id`);\nCREATE INDEX IF NOT EXISTS `idx_invoice_items_tax_rate_id` ON `invoice_items` (`tax_rate_id`);\nCREATE INDEX IF NOT EXISTS `idx_promotion_codes_verification_type_coupon_id` ON `promotion_codes` (`verification_type`, `coupon_id`);\n" },
19
+ { name: "0009_remove_did_space_jobs.sql", sql: "-- The DID Space billing-mirror integration was removed (libs/did-space.ts,\n-- queues/space.ts). Rows in `jobs` with queue='did-space' can never be picked\n-- up again (the scheduled scan only matches registered queue names), so they\n-- would sit in the table forever. Delete them. Idempotent.\nDELETE FROM jobs WHERE queue = 'did-space';\n" },
20
+ ];
@@ -0,0 +1,260 @@
1
+ /* eslint-disable no-await-in-loop */
2
+ import type { Sequelize } from 'sequelize';
3
+
4
+ import { getDefaultInstanceDid } from '../libs/tenant';
5
+ import { TENANT_TABLES } from './tenant-tables';
6
+
7
+ // Phase 2 (W1-1b): backfill instance_did + revise business unique keys.
8
+ //
9
+ // This module is the single implementation for BOTH rails:
10
+ // - Node: called from the Umzug migration 20260611-tenant-backfill.ts
11
+ // - CF/D1: called from the worker as an idempotent boot/cron compensation
12
+ // step (static D1 migration SQL cannot know the deployment app DID, so
13
+ // the backfill must run through the driver at runtime; the additive
14
+ // NULL-safe indexes live in cloudflare/migrations/0007_*.sql)
15
+ //
16
+ // The tenant value comes EXCLUSIVELY from getDefaultInstanceDid() — the
17
+ // Phase 0 single source for "this deployment's app DID". Nothing is ever
18
+ // inferred from request data or row contents (except payment_currencies,
19
+ // whose tenant is defined as its payment_method's tenant — decision D1).
20
+
21
+ export const BACKFILL_BATCH_SIZE = 5000;
22
+
23
+ /**
24
+ * Tables whose single-column business unique keys must become
25
+ * (instance_did, ...) composites (W1 §2.1 / decision D3). The inline
26
+ * single-column UNIQUE constraints are removed via table rebuild.
27
+ */
28
+ export const UNIQUE_KEY_REVISIONS: { table: string; index: string; columns: string[]; dropInline?: string }[] = [
29
+ // inline UNIQUE in table DDL -> needs rebuild
30
+ { table: 'customers', index: 'uq_customers_tenant_did', columns: ['instance_did', 'did'], dropInline: 'did' },
31
+ {
32
+ table: 'meter_events',
33
+ index: 'uq_meter_events_tenant_identifier',
34
+ columns: ['instance_did', 'identifier'],
35
+ dropInline: 'identifier',
36
+ },
37
+ {
38
+ table: 'meters',
39
+ index: 'uq_meters_tenant_event_name',
40
+ columns: ['instance_did', 'event_name'],
41
+ dropInline: 'event_name',
42
+ },
43
+ { table: 'entitlements', index: 'uq_entitlements_tenant_key', columns: ['instance_did', 'key'], dropInline: 'key' },
44
+ {
45
+ table: 'price_quotes',
46
+ index: 'uq_price_quotes_tenant_idem',
47
+ columns: ['instance_did', 'idempotency_key'],
48
+ dropInline: 'idempotency_key',
49
+ },
50
+ // no existing DB-level constraint -> plain new composite unique index
51
+ // (livemode included for promotion codes: the app has always allowed the
52
+ // same code in live and test mode, see routes/promotion-codes.ts)
53
+ { table: 'promotion_codes', index: 'uq_promotion_codes_tenant_code', columns: ['instance_did', 'livemode', 'code'] },
54
+ { table: 'product_vendors', index: 'uq_product_vendors_tenant_key', columns: ['instance_did', 'vendor_key'] },
55
+ // explicit unique index gains the tenant column (recreated below); no
56
+ // inline UNIQUE to drop, so no rebuild and no NOT NULL DDL on this table
57
+ {
58
+ table: 'revenue_snapshots',
59
+ index: 'uq_revenue_snapshots_tenant',
60
+ columns: ['instance_did', 'timestamp', 'currency_id', 'livemode', 'period_type'],
61
+ },
62
+ ];
63
+
64
+ /** Explicit single-column unique indexes superseded by the composites above. */
65
+ const SUPERSEDED_INDEXES = ['idx_entitlements_key', 'idx_pq_idempotency', 'idx_revenue_snapshots_unique'];
66
+
67
+ // table/column arguments must be static identifiers from this module
68
+ // (TENANT_TABLES / UNIQUE_KEY_REVISIONS) — never external input. SQLite
69
+ // cannot bind identifiers, hence the (caller-constrained) interpolation.
70
+ async function tableExists(sequelize: Sequelize, table: string): Promise<boolean> {
71
+ const [rows] = await sequelize.query(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = '${table}'`);
72
+ return (rows as any[]).length > 0;
73
+ }
74
+
75
+ async function columnExists(sequelize: Sequelize, table: string, column: string): Promise<boolean> {
76
+ const [rows] = await sequelize.query(`SELECT name FROM pragma_table_info('${table}') WHERE name = '${column}'`);
77
+ return (rows as any[]).length > 0;
78
+ }
79
+
80
+ // D1 exposes meta.changes; node-sqlite3 via sequelize does not, so fall back
81
+ // to SELECT changes() (safe: sequelize reuses one cached sqlite connection)
82
+ async function affectedRows(sequelize: Sequelize, meta: unknown): Promise<number> {
83
+ if (typeof (meta as any)?.changes === 'number') return (meta as any).changes;
84
+ const [rows] = await sequelize.query('SELECT changes() AS n');
85
+ return Number((rows as any[])[0]?.n ?? 0);
86
+ }
87
+
88
+ /**
89
+ * Batched, idempotent backfill of one table. Only NULL rows are touched, so
90
+ * re-running after a crash simply continues where the previous run stopped.
91
+ */
92
+ async function backfillTable(sequelize: Sequelize, table: string, instanceDid: string): Promise<number> {
93
+ if (!(await tableExists(sequelize, table))) return 0;
94
+ if (!(await columnExists(sequelize, table, 'instance_did'))) {
95
+ throw new Error(`tenant-backfill: column instance_did missing on ${table} — run the Phase 1 migration first`);
96
+ }
97
+ let total = 0;
98
+ for (;;) {
99
+ const [, meta] = await sequelize.query(
100
+ `UPDATE ${table} SET instance_did = $did WHERE rowid IN (SELECT rowid FROM ${table} WHERE instance_did IS NULL LIMIT ${BACKFILL_BATCH_SIZE})`,
101
+ { bind: { did: instanceDid } }
102
+ );
103
+ const changed = await affectedRows(sequelize, meta);
104
+ total += changed;
105
+ if (!changed) break;
106
+ }
107
+ return total;
108
+ }
109
+
110
+ /**
111
+ * payment_currencies follows its payment_method's tenant (decision D1).
112
+ * Falls back to the deployment default when the method link is dangling
113
+ * (same value in single mode anyway).
114
+ */
115
+ async function backfillPaymentCurrencies(sequelize: Sequelize, instanceDid: string): Promise<number> {
116
+ if (!(await tableExists(sequelize, 'payment_currencies'))) return 0;
117
+ if (!(await columnExists(sequelize, 'payment_currencies', 'instance_did'))) {
118
+ throw new Error(
119
+ 'tenant-backfill: column instance_did missing on payment_currencies — run the Phase 1 migration first'
120
+ );
121
+ }
122
+ const [, meta] = await sequelize.query(
123
+ `UPDATE payment_currencies SET instance_did = COALESCE(
124
+ (SELECT pm.instance_did FROM payment_methods pm WHERE pm.id = payment_currencies.payment_method_id),
125
+ $did
126
+ ) WHERE instance_did IS NULL`,
127
+ { bind: { did: instanceDid } }
128
+ );
129
+ return affectedRows(sequelize, meta);
130
+ }
131
+
132
+ /**
133
+ * Rebuild a table to drop an inline single-column UNIQUE constraint and mark
134
+ * instance_did NOT NULL. SQLite cannot do either via ALTER, so: copy DDL from
135
+ * sqlite_master, transform, create {t}__rebuild, INSERT SELECT, swap, then
136
+ * recreate the explicit indexes that the drop discarded.
137
+ *
138
+ * Crash-safe: {t}__rebuild leftovers from a dead run are dropped on entry;
139
+ * if a crash hit between DROP and RENAME, the rename is completed on entry.
140
+ */
141
+ async function rebuildTableForUnique(
142
+ sequelize: Sequelize,
143
+ table: string,
144
+ dropInlineUniqueOn: string,
145
+ instanceDid: string
146
+ ) {
147
+ const rebuildName = `${table}__rebuild`;
148
+
149
+ // recover from a crash between DROP old and RENAME new
150
+ if (!(await tableExists(sequelize, table)) && (await tableExists(sequelize, rebuildName))) {
151
+ await sequelize.query(`ALTER TABLE ${rebuildName} RENAME TO ${table}`);
152
+ return;
153
+ }
154
+ if (await tableExists(sequelize, rebuildName)) {
155
+ await sequelize.query(`DROP TABLE ${rebuildName}`);
156
+ }
157
+
158
+ const [ddlRows] = await sequelize.query(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = '${table}'`);
159
+ const ddl: string = (ddlRows as any[])[0]?.sql || '';
160
+ // [^,] (not [^,)]): type names like VARCHAR(128) contain parens, but column
161
+ // definitions never contain commas, so the comma is the safe boundary
162
+ const inlineUnique = new RegExp(`(\`?${dropInlineUniqueOn}\`?\\s+[^,]*?)\\s+UNIQUE`, 'i');
163
+ const needsUniqueDrop = inlineUnique.test(ddl);
164
+ const needsNotNull = !/instance_did[^,]*NOT NULL/i.test(ddl);
165
+ if (!needsUniqueDrop && !needsNotNull) return; // already rebuilt — idempotent
166
+
167
+ let newDdl = ddl.replace(inlineUnique, '$1');
168
+ // NOT NULL with the deployment DID as DDL default: pre-Phase-3 code paths
169
+ // that insert without instance_did keep working and land in the default
170
+ // tenant (single-tenant behavior unchanged); scoped creates (Phase 3) set
171
+ // the value explicitly so the default never fires in multi mode.
172
+ if (!/^[A-Za-z0-9:_-]+$/.test(instanceDid)) {
173
+ throw new Error(`tenant-backfill: refusing to embed suspicious instanceDid in DDL: ${JSON.stringify(instanceDid)}`);
174
+ }
175
+ newDdl = newDdl.replace(
176
+ /instance_did\s+VARCHAR\(64\)/i,
177
+ `instance_did VARCHAR(64) NOT NULL DEFAULT '${instanceDid}'`
178
+ );
179
+ // CREATE TABLE `name` ... -> CREATE TABLE `name__rebuild` ...
180
+ newDdl = newDdl.replace(new RegExp(`CREATE TABLE\\s+(["\`]?)${table}\\1`, 'i'), `CREATE TABLE \`${rebuildName}\``);
181
+
182
+ // capture explicit index DDL before the table (and its indexes) are dropped
183
+ const [indexRows] = await sequelize.query(
184
+ `SELECT name, sql FROM sqlite_master WHERE type = 'index' AND tbl_name = '${table}' AND sql IS NOT NULL`
185
+ );
186
+
187
+ const [cols] = await sequelize.query(`SELECT name FROM pragma_table_info('${table}')`);
188
+ const columnList = (cols as any[]).map((c) => `\`${c.name}\``).join(', ');
189
+
190
+ await sequelize.query(newDdl);
191
+ await sequelize.query(`INSERT INTO ${rebuildName} (${columnList}) SELECT ${columnList} FROM ${table}`);
192
+ await sequelize.query(`DROP TABLE ${table}`);
193
+ await sequelize.query(`ALTER TABLE ${rebuildName} RENAME TO ${table}`);
194
+
195
+ for (const idx of indexRows as { name: string; sql: string }[]) {
196
+ if (!SUPERSEDED_INDEXES.includes(idx.name)) {
197
+ await sequelize.query(idx.sql.replace(/CREATE (UNIQUE )?INDEX/i, 'CREATE $1INDEX IF NOT EXISTS'));
198
+ }
199
+ }
200
+ }
201
+
202
+ async function ensureIndex(sequelize: Sequelize, table: string, name: string, columns: string[], unique: boolean) {
203
+ if (!(await tableExists(sequelize, table))) return;
204
+ await sequelize.query(
205
+ `CREATE ${unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS \`${name}\` ON \`${table}\` (${columns
206
+ .map((c) => `\`${c}\``)
207
+ .join(', ')})`
208
+ );
209
+ }
210
+
211
+ async function dropIndexIfExists(sequelize: Sequelize, name: string) {
212
+ await sequelize.query(`DROP INDEX IF EXISTS \`${name}\``);
213
+ }
214
+
215
+ export interface TenantBackfillResult {
216
+ instanceDid: string;
217
+ backfilled: Record<string, number>;
218
+ }
219
+
220
+ /**
221
+ * The full Phase 2 routine: backfill -> unique key revisions -> indexes.
222
+ * Idempotent and re-runnable; safe to call from worker boot/cron.
223
+ */
224
+ export async function runTenantBackfill(sequelize: Sequelize): Promise<TenantBackfillResult> {
225
+ const instanceDid = getDefaultInstanceDid();
226
+ const backfilled: Record<string, number> = {};
227
+
228
+ // 1. backfill — payment_methods before payment_currencies (D1 join source)
229
+ for (const table of TENANT_TABLES) {
230
+ if (table !== 'payment_currencies') {
231
+ backfilled[table] = await backfillTable(sequelize, table, instanceDid);
232
+ }
233
+ }
234
+ backfilled.payment_currencies = await backfillPaymentCurrencies(sequelize, instanceDid);
235
+
236
+ // 2. unique key revisions — rebuilds drop the old inline single-column
237
+ // uniques; composites enforce per-tenant uniqueness from here on
238
+ for (const revision of UNIQUE_KEY_REVISIONS) {
239
+ if (await tableExists(sequelize, revision.table)) {
240
+ if (revision.dropInline) {
241
+ await rebuildTableForUnique(sequelize, revision.table, revision.dropInline, instanceDid);
242
+ }
243
+ await ensureIndex(sequelize, revision.table, revision.index, revision.columns, true);
244
+ }
245
+ }
246
+ for (const name of SUPERSEDED_INDEXES) {
247
+ await dropIndexIfExists(sequelize, name);
248
+ }
249
+
250
+ // 3. plain tenant indexes for scoped queries (Phase 3+). meter_events is
251
+ // served by the (instance_did, identifier) composite above — high-write
252
+ // table, one extra index is deliberate cost; see W1 §2.1 note.
253
+ for (const table of TENANT_TABLES) {
254
+ if (table !== 'meter_events') {
255
+ await ensureIndex(sequelize, table, `idx_${table}_instance_did`, ['instance_did'], false);
256
+ }
257
+ }
258
+
259
+ return { instanceDid, backfilled };
260
+ }
@@ -0,0 +1,124 @@
1
+ // TenantModel base-class mechanism (W1 tenant-isolation).
2
+ //
3
+ // Inserts one tenant-scoping layer between the 38 tenant models and the
4
+ // per-runtime `Model` (real Sequelize on Node, sequelize-d1 shim on the
5
+ // worker — the `'sequelize'` import is aliased at build time). Overrides the
6
+ // query surface once so `X.findAll(...)` is transparently tenant-scoped; the
7
+ // 480 "bare" call sites become structurally safe with zero per-call changes.
8
+ //
9
+ // `makeTenantModel(Base)` is a factory so the spike can validate the SAME
10
+ // source against BOTH bases in one Node test run. Production uses the
11
+ // `TenantModel` convenience export (Base = the build-aliased `Model`).
12
+ //
13
+ // The scope primitives (scopeWhere/stampTenant/isTenantTable) live in
14
+ // ./scoped-core, shared with scoped.ts so there is exactly one copy.
15
+ //
16
+ // eslint-disable require-await: the query overrides are INTENTIONALLY `async`
17
+ // even though some only `return super.xxx(...)` — a synchronous fail-closed
18
+ // throw from `$scope`/scopeWhere must surface as a promise REJECTION so callers
19
+ // `await`/`.catch` it (tenant-design §12 hard-constraint 2). Dropping `async`
20
+ // would let the throw escape synchronously and bypass `.catch`.
21
+ /* eslint-disable require-await */
22
+ import { Model } from 'sequelize';
23
+
24
+ import { isSystemContext } from '../libs/context';
25
+ import { isTenantTable, scopeWhere, stampTenant } from './scoped-core';
26
+
27
+ export { isTenantTable, scopeWhere, stampTenant } from './scoped-core';
28
+
29
+ export function makeTenantModel(Base: any): any {
30
+ // NOTE: `_scope` is a RESERVED Sequelize internal (init sets an own static
31
+ // `_scope` object = the model's scope state). Helper names MUST avoid
32
+ // Sequelize's `_`-prefixed surface — `$`-prefixed names are collision-free.
33
+ //
34
+ // Query overrides are `async` so a synchronous fail-closed throw from
35
+ // scopeWhere/stampTenant surfaces as a promise rejection (callers
36
+ // await/.catch — mirrors scoped.ts asAsync).
37
+ return class TenantModel extends Base {
38
+ static $isTenantTable(this: any): boolean {
39
+ return isTenantTable(this.tableName);
40
+ }
41
+
42
+ // A row should be scoped unless this is not a tenant table OR we are inside
43
+ // an explicit system operation (legitimate cross-tenant read).
44
+ static $shouldScope(this: any): boolean {
45
+ return this.$isTenantTable() && !isSystemContext();
46
+ }
47
+
48
+ static $scope(this: any, options: any): any {
49
+ return this.$shouldScope() ? { ...options, where: scopeWhere(options?.where) } : options;
50
+ }
51
+
52
+ // reads -----------------------------------------------------------------
53
+ static async findAll(this: any, o?: any) {
54
+ return super.findAll(this.$scope(o));
55
+ }
56
+ static async findOne(this: any, o?: any) {
57
+ return super.findOne(this.$scope(o));
58
+ }
59
+ static async findByPk(this: any, id: any, o?: any) {
60
+ // route through findOne so the scope (and tenant check) applies as a
61
+ // WHERE predicate -> cross-tenant returns null (not-found), never throws.
62
+ return this.findOne({ ...o, where: { ...(o?.where || {}), id } });
63
+ }
64
+ static async findAndCountAll(this: any, o?: any) {
65
+ return super.findAndCountAll(this.$scope(o));
66
+ }
67
+ static async findOrCreate(this: any, o?: any) {
68
+ // scope the lookup AND stamp the defaults so a freshly-created row lands
69
+ // in the active tenant (the scoped where also carries instance_did, but
70
+ // stamping defaults is explicit and survives shim/real differences).
71
+ // Honors $shouldScope so a system-context findOrCreate neither scopes nor
72
+ // stamps — consistent with create/bulkCreate/update.
73
+ if (!this.$shouldScope()) return super.findOrCreate(o);
74
+ const scoped = this.$scope(o);
75
+ return super.findOrCreate({ ...scoped, defaults: stampTenant(scoped?.defaults || {}) });
76
+ }
77
+ static async count(this: any, o?: any) {
78
+ return super.count(this.$scope(o));
79
+ }
80
+ static async sum(this: any, field: any, o?: any) {
81
+ return super.sum(field, this.$scope(o));
82
+ }
83
+ static async max(this: any, field: any, o?: any) {
84
+ return super.max(field, this.$scope(o));
85
+ }
86
+ static async min(this: any, field: any, o?: any) {
87
+ // Node-only surface (real Sequelize). The worker never calls min.
88
+ return super.min(field, this.$scope(o));
89
+ }
90
+ static async aggregate(this: any, field: any, fn: any, o?: any) {
91
+ // Node-only surface. sum/max/min delegate here internally on real
92
+ // Sequelize; the second scope injection is absorbed by scopeWhere's
93
+ // idempotency.
94
+ return super.aggregate(field, fn, this.$scope(o));
95
+ }
96
+
97
+ // writes ----------------------------------------------------------------
98
+ static async create(this: any, values: any, o?: any) {
99
+ return super.create(this.$shouldScope() ? stampTenant(values) : values, o);
100
+ }
101
+ static async bulkCreate(this: any, records: any[], o?: any) {
102
+ const stamped = this.$shouldScope() ? (records || []).map((r: any) => stampTenant(r)) : records;
103
+ return super.bulkCreate(stamped, o);
104
+ }
105
+ static async update(this: any, values: any, o?: any) {
106
+ return super.update(values, this.$scope(o));
107
+ }
108
+ static async increment(this: any, fields: any, o?: any) {
109
+ return super.increment(fields, this.$scope(o));
110
+ }
111
+ static async decrement(this: any, fields: any, o?: any) {
112
+ return super.decrement(fields, this.$scope(o));
113
+ }
114
+ static async destroy(this: any, o?: any) {
115
+ return super.destroy(this.$scope(o));
116
+ }
117
+ };
118
+ }
119
+
120
+ // Production convenience: Base = the build-aliased Model. Cast to `typeof Model`
121
+ // so the 38 tenant models can `extends TenantModel<InferAttributes<X>, ...>`
122
+ // exactly as they did `extends Model<...>` — the generic constructor surface is
123
+ // preserved; the runtime instance is the scoped subclass.
124
+ export const TenantModel = makeTenantModel(Model) as typeof Model;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The 38 tenant-scoped tables (W1 §2.1, decisions D1/D3 included).
3
+ * Single source of truth — consumed by the tenant-columns migration, the
4
+ * scoped query helpers and the CI bare-query scanner. Do not duplicate.
5
+ *
6
+ * Exempt (W1 §2.2): jobs, locks, archive_locks, archive_metadata,
7
+ * exchange_rate_providers.
8
+ */
9
+ export const TENANT_TABLES = [
10
+ 'customers',
11
+ 'products',
12
+ 'prices',
13
+ 'pricing_tables',
14
+ 'payment_methods',
15
+ 'checkout_sessions',
16
+ 'payment_intents',
17
+ 'payment_links',
18
+ 'setup_intents',
19
+ 'price_quotes',
20
+ 'subscriptions',
21
+ 'subscription_items',
22
+ 'subscription_schedules',
23
+ 'invoices',
24
+ 'invoice_items',
25
+ 'refunds',
26
+ 'credit_grants',
27
+ 'credit_transactions',
28
+ 'meters',
29
+ 'meter_events',
30
+ 'usage_records',
31
+ 'coupons',
32
+ 'promotion_codes',
33
+ 'discounts',
34
+ 'entitlements',
35
+ 'entitlement_grants',
36
+ 'entitlement_products',
37
+ 'events',
38
+ 'webhook_endpoints',
39
+ 'webhook_attempts',
40
+ 'payouts',
41
+ 'payment_stats',
42
+ 'revenue_snapshots',
43
+ 'auto_recharge_configs',
44
+ 'tax_rates',
45
+ 'settings',
46
+ 'payment_currencies',
47
+ 'product_vendors',
48
+ ] as const;
49
+
50
+ export type TenantTable = (typeof TENANT_TABLES)[number];