@wopr-network/platform-core 0.1.0

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 (694) hide show
  1. package/biome.json +61 -0
  2. package/dist/admin/admin-audit-log-repository.d.ts +33 -0
  3. package/dist/admin/admin-audit-log-repository.js +102 -0
  4. package/dist/admin/audit-log.d.ts +49 -0
  5. package/dist/admin/audit-log.js +63 -0
  6. package/dist/admin/index.d.ts +6 -0
  7. package/dist/admin/index.js +3 -0
  8. package/dist/admin/role-store.d.ts +37 -0
  9. package/dist/admin/role-store.js +106 -0
  10. package/dist/auth/api-key-repository.d.ts +11 -0
  11. package/dist/auth/api-key-repository.js +33 -0
  12. package/dist/auth/api-key-repository.test.d.ts +1 -0
  13. package/dist/auth/api-key-repository.test.js +46 -0
  14. package/dist/auth/auth.test.d.ts +1 -0
  15. package/dist/auth/auth.test.js +140 -0
  16. package/dist/auth/better-auth.d.ts +42 -0
  17. package/dist/auth/better-auth.js +196 -0
  18. package/dist/auth/index.d.ts +186 -0
  19. package/dist/auth/index.js +422 -0
  20. package/dist/auth/login-history-repository.d.ts +14 -0
  21. package/dist/auth/login-history-repository.js +15 -0
  22. package/dist/auth/login-history-repository.test.d.ts +1 -0
  23. package/dist/auth/login-history-repository.test.js +47 -0
  24. package/dist/auth/middleware.d.ts +55 -0
  25. package/dist/auth/middleware.js +101 -0
  26. package/dist/auth/middleware.test.d.ts +1 -0
  27. package/dist/auth/middleware.test.js +213 -0
  28. package/dist/auth/scoped-tokens.test.d.ts +1 -0
  29. package/dist/auth/scoped-tokens.test.js +306 -0
  30. package/dist/auth/tenant-access.test.d.ts +1 -0
  31. package/dist/auth/tenant-access.test.js +62 -0
  32. package/dist/auth/user-creator.d.ts +9 -0
  33. package/dist/auth/user-creator.js +47 -0
  34. package/dist/auth/user-creator.test.d.ts +1 -0
  35. package/dist/auth/user-creator.test.js +78 -0
  36. package/dist/auth/user-role-repository.d.ts +31 -0
  37. package/dist/auth/user-role-repository.js +53 -0
  38. package/dist/auth/user-role-repository.test.d.ts +1 -0
  39. package/dist/auth/user-role-repository.test.js +122 -0
  40. package/dist/billing/drizzle-webhook-seen-repository.d.ts +10 -0
  41. package/dist/billing/drizzle-webhook-seen-repository.js +28 -0
  42. package/dist/billing/index.d.ts +7 -0
  43. package/dist/billing/index.js +7 -0
  44. package/dist/billing/payment-processor.d.ts +127 -0
  45. package/dist/billing/payment-processor.js +8 -0
  46. package/dist/billing/payment-processor.test.d.ts +1 -0
  47. package/dist/billing/payment-processor.test.js +71 -0
  48. package/dist/billing/payram/cents-credits-boundary.test.d.ts +1 -0
  49. package/dist/billing/payram/cents-credits-boundary.test.js +75 -0
  50. package/dist/billing/payram/charge-store.d.ts +41 -0
  51. package/dist/billing/payram/charge-store.js +72 -0
  52. package/dist/billing/payram/charge-store.test.d.ts +1 -0
  53. package/dist/billing/payram/charge-store.test.js +64 -0
  54. package/dist/billing/payram/checkout.d.ts +15 -0
  55. package/dist/billing/payram/checkout.js +24 -0
  56. package/dist/billing/payram/checkout.test.d.ts +1 -0
  57. package/dist/billing/payram/checkout.test.js +74 -0
  58. package/dist/billing/payram/client.d.ts +7 -0
  59. package/dist/billing/payram/client.js +15 -0
  60. package/dist/billing/payram/client.test.d.ts +1 -0
  61. package/dist/billing/payram/client.test.js +52 -0
  62. package/dist/billing/payram/index.d.ts +8 -0
  63. package/dist/billing/payram/index.js +4 -0
  64. package/dist/billing/payram/types.d.ts +40 -0
  65. package/dist/billing/payram/types.js +1 -0
  66. package/dist/billing/payram/webhook.d.ts +19 -0
  67. package/dist/billing/payram/webhook.js +67 -0
  68. package/dist/billing/payram/webhook.test.d.ts +7 -0
  69. package/dist/billing/payram/webhook.test.js +248 -0
  70. package/dist/billing/stripe/cents-credits-boundary.test.d.ts +1 -0
  71. package/dist/billing/stripe/cents-credits-boundary.test.js +62 -0
  72. package/dist/billing/stripe/checkout.d.ts +20 -0
  73. package/dist/billing/stripe/checkout.js +63 -0
  74. package/dist/billing/stripe/checkout.test.d.ts +1 -0
  75. package/dist/billing/stripe/checkout.test.js +148 -0
  76. package/dist/billing/stripe/client.d.ts +14 -0
  77. package/dist/billing/stripe/client.js +33 -0
  78. package/dist/billing/stripe/client.test.d.ts +1 -0
  79. package/dist/billing/stripe/client.test.js +58 -0
  80. package/dist/billing/stripe/credit-prices.d.ts +63 -0
  81. package/dist/billing/stripe/credit-prices.js +81 -0
  82. package/dist/billing/stripe/credit-prices.test.d.ts +1 -0
  83. package/dist/billing/stripe/credit-prices.test.js +87 -0
  84. package/dist/billing/stripe/index.d.ts +14 -0
  85. package/dist/billing/stripe/index.js +8 -0
  86. package/dist/billing/stripe/payment-methods-detach-all.test.d.ts +1 -0
  87. package/dist/billing/stripe/payment-methods-detach-all.test.js +40 -0
  88. package/dist/billing/stripe/payment-methods.d.ts +25 -0
  89. package/dist/billing/stripe/payment-methods.js +53 -0
  90. package/dist/billing/stripe/payment-methods.test.d.ts +1 -0
  91. package/dist/billing/stripe/payment-methods.test.js +122 -0
  92. package/dist/billing/stripe/portal.d.ts +10 -0
  93. package/dist/billing/stripe/portal.js +16 -0
  94. package/dist/billing/stripe/portal.test.d.ts +1 -0
  95. package/dist/billing/stripe/portal.test.js +48 -0
  96. package/dist/billing/stripe/setup-intent.d.ts +16 -0
  97. package/dist/billing/stripe/setup-intent.js +22 -0
  98. package/dist/billing/stripe/setup-intent.test.d.ts +1 -0
  99. package/dist/billing/stripe/setup-intent.test.js +58 -0
  100. package/dist/billing/stripe/stripe-payment-processor.d.ts +49 -0
  101. package/dist/billing/stripe/stripe-payment-processor.js +166 -0
  102. package/dist/billing/stripe/stripe-payment-processor.test.d.ts +1 -0
  103. package/dist/billing/stripe/stripe-payment-processor.test.js +413 -0
  104. package/dist/billing/stripe/tenant-store.d.ts +56 -0
  105. package/dist/billing/stripe/tenant-store.js +119 -0
  106. package/dist/billing/stripe/tenant-store.test.d.ts +1 -0
  107. package/dist/billing/stripe/tenant-store.test.js +97 -0
  108. package/dist/billing/stripe/types.d.ts +49 -0
  109. package/dist/billing/stripe/types.js +1 -0
  110. package/dist/billing/webhook-seen-repository.d.ts +14 -0
  111. package/dist/billing/webhook-seen-repository.js +13 -0
  112. package/dist/config/billing-env.test.d.ts +1 -0
  113. package/dist/config/billing-env.test.js +48 -0
  114. package/dist/config/index.d.ts +46 -0
  115. package/dist/config/index.js +38 -0
  116. package/dist/config/logger.d.ts +2 -0
  117. package/dist/config/logger.js +11 -0
  118. package/dist/config/provider-endpoints.d.ts +6 -0
  119. package/dist/config/provider-endpoints.js +12 -0
  120. package/dist/credits/auto-topup-charge.d.ts +27 -0
  121. package/dist/credits/auto-topup-charge.js +139 -0
  122. package/dist/credits/auto-topup-charge.test.d.ts +1 -0
  123. package/dist/credits/auto-topup-charge.test.js +242 -0
  124. package/dist/credits/auto-topup-event-log-repository.d.ts +16 -0
  125. package/dist/credits/auto-topup-event-log-repository.js +18 -0
  126. package/dist/credits/auto-topup-event-log-repository.test.d.ts +1 -0
  127. package/dist/credits/auto-topup-event-log-repository.test.js +83 -0
  128. package/dist/credits/auto-topup-schedule.d.ts +27 -0
  129. package/dist/credits/auto-topup-schedule.js +66 -0
  130. package/dist/credits/auto-topup-schedule.test.d.ts +1 -0
  131. package/dist/credits/auto-topup-schedule.test.js +145 -0
  132. package/dist/credits/auto-topup-settings-repository.d.ts +54 -0
  133. package/dist/credits/auto-topup-settings-repository.js +184 -0
  134. package/dist/credits/auto-topup-settings-repository.test.d.ts +1 -0
  135. package/dist/credits/auto-topup-settings-repository.test.js +104 -0
  136. package/dist/credits/auto-topup-usage.d.ts +22 -0
  137. package/dist/credits/auto-topup-usage.js +56 -0
  138. package/dist/credits/auto-topup-usage.test.d.ts +1 -0
  139. package/dist/credits/auto-topup-usage.test.js +181 -0
  140. package/dist/credits/credit-expiry-cron.d.ts +19 -0
  141. package/dist/credits/credit-expiry-cron.js +50 -0
  142. package/dist/credits/credit-expiry-cron.test.d.ts +1 -0
  143. package/dist/credits/credit-expiry-cron.test.js +67 -0
  144. package/dist/credits/credit-ledger-extra.test.d.ts +1 -0
  145. package/dist/credits/credit-ledger-extra.test.js +40 -0
  146. package/dist/credits/credit-ledger.bench.d.ts +1 -0
  147. package/dist/credits/credit-ledger.bench.js +33 -0
  148. package/dist/credits/credit-ledger.d.ts +130 -0
  149. package/dist/credits/credit-ledger.js +293 -0
  150. package/dist/credits/credit-ledger.test.d.ts +4 -0
  151. package/dist/credits/credit-ledger.test.js +203 -0
  152. package/dist/credits/credit-transaction-repository.d.ts +17 -0
  153. package/dist/credits/credit-transaction-repository.js +35 -0
  154. package/dist/credits/credit-transaction-repository.test.d.ts +1 -0
  155. package/dist/credits/credit-transaction-repository.test.js +232 -0
  156. package/dist/credits/credit.d.ts +75 -0
  157. package/dist/credits/credit.js +139 -0
  158. package/dist/credits/credit.test.d.ts +1 -0
  159. package/dist/credits/credit.test.js +196 -0
  160. package/dist/credits/dividend-cron.d.ts +29 -0
  161. package/dist/credits/dividend-cron.js +88 -0
  162. package/dist/credits/dividend-cron.test.d.ts +1 -0
  163. package/dist/credits/dividend-cron.test.js +128 -0
  164. package/dist/credits/dividend-repository.d.ts +29 -0
  165. package/dist/credits/dividend-repository.js +126 -0
  166. package/dist/credits/dividend-repository.test.d.ts +1 -0
  167. package/dist/credits/dividend-repository.test.js +176 -0
  168. package/dist/credits/index.d.ts +9 -0
  169. package/dist/credits/index.js +5 -0
  170. package/dist/credits/repository-types.d.ts +29 -0
  171. package/dist/credits/repository-types.js +1 -0
  172. package/dist/credits/signup-grant.d.ts +12 -0
  173. package/dist/credits/signup-grant.js +35 -0
  174. package/dist/credits/signup-grant.test.d.ts +1 -0
  175. package/dist/credits/signup-grant.test.js +51 -0
  176. package/dist/credits/tenant-customer-repository.d.ts +30 -0
  177. package/dist/credits/tenant-customer-repository.js +5 -0
  178. package/dist/db/auth-user-repository.d.ts +46 -0
  179. package/dist/db/auth-user-repository.js +90 -0
  180. package/dist/db/credit-column.d.ts +27 -0
  181. package/dist/db/credit-column.js +13 -0
  182. package/dist/db/index.d.ts +14 -0
  183. package/dist/db/index.js +8 -0
  184. package/dist/db/schema/account-deletion-requests.d.ts +203 -0
  185. package/dist/db/schema/account-deletion-requests.js +36 -0
  186. package/dist/db/schema/account-export-requests.d.ts +148 -0
  187. package/dist/db/schema/account-export-requests.js +19 -0
  188. package/dist/db/schema/admin-audit.d.ts +194 -0
  189. package/dist/db/schema/admin-audit.js +21 -0
  190. package/dist/db/schema/admin-users.d.ts +177 -0
  191. package/dist/db/schema/admin-users.js +23 -0
  192. package/dist/db/schema/affiliate-fraud.d.ts +160 -0
  193. package/dist/db/schema/affiliate-fraud.js +18 -0
  194. package/dist/db/schema/affiliate.d.ts +277 -0
  195. package/dist/db/schema/affiliate.js +32 -0
  196. package/dist/db/schema/coupon-codes.d.ts +143 -0
  197. package/dist/db/schema/coupon-codes.js +17 -0
  198. package/dist/db/schema/credit-auto-topup-settings.d.ts +232 -0
  199. package/dist/db/schema/credit-auto-topup-settings.js +27 -0
  200. package/dist/db/schema/credit-auto-topup.d.ts +130 -0
  201. package/dist/db/schema/credit-auto-topup.js +21 -0
  202. package/dist/db/schema/credits.d.ts +283 -0
  203. package/dist/db/schema/credits.js +38 -0
  204. package/dist/db/schema/dividend-distributions.d.ts +130 -0
  205. package/dist/db/schema/dividend-distributions.js +19 -0
  206. package/dist/db/schema/email-notifications.d.ts +99 -0
  207. package/dist/db/schema/email-notifications.js +21 -0
  208. package/dist/db/schema/index.d.ts +33 -0
  209. package/dist/db/schema/index.js +33 -0
  210. package/dist/db/schema/meter-events.d.ts +599 -0
  211. package/dist/db/schema/meter-events.js +55 -0
  212. package/dist/db/schema/notification-preferences.d.ts +165 -0
  213. package/dist/db/schema/notification-preferences.js +18 -0
  214. package/dist/db/schema/notification-queue.d.ts +236 -0
  215. package/dist/db/schema/notification-queue.js +40 -0
  216. package/dist/db/schema/org-memberships.d.ts +63 -0
  217. package/dist/db/schema/org-memberships.js +15 -0
  218. package/dist/db/schema/organization-members.d.ts +235 -0
  219. package/dist/db/schema/organization-members.js +27 -0
  220. package/dist/db/schema/payram.d.ts +164 -0
  221. package/dist/db/schema/payram.js +21 -0
  222. package/dist/db/schema/platform-api-keys.d.ts +143 -0
  223. package/dist/db/schema/platform-api-keys.js +20 -0
  224. package/dist/db/schema/promotion-redemptions.d.ts +143 -0
  225. package/dist/db/schema/promotion-redemptions.js +18 -0
  226. package/dist/db/schema/promotions.d.ts +445 -0
  227. package/dist/db/schema/promotions.js +48 -0
  228. package/dist/db/schema/provider-credentials.d.ts +201 -0
  229. package/dist/db/schema/provider-credentials.js +36 -0
  230. package/dist/db/schema/rate-limit-entries.d.ts +75 -0
  231. package/dist/db/schema/rate-limit-entries.js +7 -0
  232. package/dist/db/schema/secret-audit-log.d.ts +109 -0
  233. package/dist/db/schema/secret-audit-log.js +15 -0
  234. package/dist/db/schema/session-usage.d.ts +194 -0
  235. package/dist/db/schema/session-usage.js +19 -0
  236. package/dist/db/schema/spending-limits.d.ts +92 -0
  237. package/dist/db/schema/spending-limits.js +8 -0
  238. package/dist/db/schema/tenant-addons.d.ts +58 -0
  239. package/dist/db/schema/tenant-addons.js +9 -0
  240. package/dist/db/schema/tenant-api-keys.d.ts +131 -0
  241. package/dist/db/schema/tenant-api-keys.js +21 -0
  242. package/dist/db/schema/tenant-capability-settings.d.ts +79 -0
  243. package/dist/db/schema/tenant-capability-settings.js +12 -0
  244. package/dist/db/schema/tenant-customers.d.ts +303 -0
  245. package/dist/db/schema/tenant-customers.js +25 -0
  246. package/dist/db/schema/tenants.d.ts +126 -0
  247. package/dist/db/schema/tenants.js +18 -0
  248. package/dist/db/schema/user-roles.d.ts +98 -0
  249. package/dist/db/schema/user-roles.js +18 -0
  250. package/dist/db/schema/webhook-seen-events.d.ts +58 -0
  251. package/dist/db/schema/webhook-seen-events.js +9 -0
  252. package/dist/email/billing-emails.d.ts +51 -0
  253. package/dist/email/billing-emails.js +163 -0
  254. package/dist/email/billing-emails.test.d.ts +1 -0
  255. package/dist/email/billing-emails.test.js +162 -0
  256. package/dist/email/client.d.ts +51 -0
  257. package/dist/email/client.js +102 -0
  258. package/dist/email/client.test.d.ts +1 -0
  259. package/dist/email/client.test.js +120 -0
  260. package/dist/email/drizzle-billing-email-repository.d.ts +21 -0
  261. package/dist/email/drizzle-billing-email-repository.js +36 -0
  262. package/dist/email/drizzle-billing-email-repository.test.d.ts +1 -0
  263. package/dist/email/drizzle-billing-email-repository.test.js +42 -0
  264. package/dist/email/index.d.ts +33 -0
  265. package/dist/email/index.js +22 -0
  266. package/dist/email/notification-preferences-store.d.ts +12 -0
  267. package/dist/email/notification-preferences-store.js +82 -0
  268. package/dist/email/notification-preferences-store.test.d.ts +1 -0
  269. package/dist/email/notification-preferences-store.test.js +86 -0
  270. package/dist/email/notification-queue-store.d.ts +25 -0
  271. package/dist/email/notification-queue-store.js +97 -0
  272. package/dist/email/notification-queue-store.test.d.ts +1 -0
  273. package/dist/email/notification-queue-store.test.js +177 -0
  274. package/dist/email/notification-repository-types.d.ts +70 -0
  275. package/dist/email/notification-repository-types.js +6 -0
  276. package/dist/email/notification-service.d.ts +41 -0
  277. package/dist/email/notification-service.js +196 -0
  278. package/dist/email/notification-service.test.d.ts +1 -0
  279. package/dist/email/notification-service.test.js +160 -0
  280. package/dist/email/notification-templates.d.ts +18 -0
  281. package/dist/email/notification-templates.js +574 -0
  282. package/dist/email/notification-templates.test.d.ts +1 -0
  283. package/dist/email/notification-templates.test.js +238 -0
  284. package/dist/email/notification-worker.d.ts +24 -0
  285. package/dist/email/notification-worker.js +109 -0
  286. package/dist/email/notification-worker.test.d.ts +1 -0
  287. package/dist/email/notification-worker.test.js +153 -0
  288. package/dist/email/require-verified.d.ts +25 -0
  289. package/dist/email/require-verified.js +52 -0
  290. package/dist/email/require-verified.test.d.ts +1 -0
  291. package/dist/email/require-verified.test.js +62 -0
  292. package/dist/email/resend-adapter.d.ts +47 -0
  293. package/dist/email/resend-adapter.js +137 -0
  294. package/dist/email/resend-adapter.test.d.ts +1 -0
  295. package/dist/email/resend-adapter.test.js +190 -0
  296. package/dist/email/templates.d.ts +22 -0
  297. package/dist/email/templates.js +359 -0
  298. package/dist/email/templates.test.d.ts +1 -0
  299. package/dist/email/templates.test.js +170 -0
  300. package/dist/email/verification.d.ts +42 -0
  301. package/dist/email/verification.js +83 -0
  302. package/dist/email/verification.test.d.ts +1 -0
  303. package/dist/email/verification.test.js +141 -0
  304. package/dist/index.d.ts +13 -0
  305. package/dist/index.js +23 -0
  306. package/dist/metering/aggregator.d.ts +54 -0
  307. package/dist/metering/aggregator.js +123 -0
  308. package/dist/metering/aggregator.test.d.ts +1 -0
  309. package/dist/metering/aggregator.test.js +179 -0
  310. package/dist/metering/dlq.d.ts +31 -0
  311. package/dist/metering/dlq.js +82 -0
  312. package/dist/metering/dlq.test.d.ts +1 -0
  313. package/dist/metering/dlq.test.js +117 -0
  314. package/dist/metering/drizzle-usage-summary-repository.d.ts +67 -0
  315. package/dist/metering/drizzle-usage-summary-repository.js +98 -0
  316. package/dist/metering/emitter.d.ts +66 -0
  317. package/dist/metering/emitter.js +185 -0
  318. package/dist/metering/emitter.test.d.ts +1 -0
  319. package/dist/metering/emitter.test.js +171 -0
  320. package/dist/metering/index.d.ts +11 -0
  321. package/dist/metering/index.js +5 -0
  322. package/dist/metering/load-test.bench.d.ts +1 -0
  323. package/dist/metering/load-test.bench.js +103 -0
  324. package/dist/metering/meter-event-repository.d.ts +33 -0
  325. package/dist/metering/meter-event-repository.js +58 -0
  326. package/dist/metering/meter-repositories.test.d.ts +1 -0
  327. package/dist/metering/meter-repositories.test.js +419 -0
  328. package/dist/metering/metering.test.d.ts +1 -0
  329. package/dist/metering/metering.test.js +1046 -0
  330. package/dist/metering/reconciliation-cron.d.ts +37 -0
  331. package/dist/metering/reconciliation-cron.js +85 -0
  332. package/dist/metering/reconciliation-cron.test.d.ts +1 -0
  333. package/dist/metering/reconciliation-cron.test.js +162 -0
  334. package/dist/metering/reconciliation-repository.d.ts +27 -0
  335. package/dist/metering/reconciliation-repository.js +43 -0
  336. package/dist/metering/reconciliation-repository.test.d.ts +1 -0
  337. package/dist/metering/reconciliation-repository.test.js +160 -0
  338. package/dist/metering/types.d.ts +88 -0
  339. package/dist/metering/types.js +1 -0
  340. package/dist/metering/wal.d.ts +49 -0
  341. package/dist/metering/wal.js +124 -0
  342. package/dist/metering/wal.test.d.ts +1 -0
  343. package/dist/metering/wal.test.js +175 -0
  344. package/dist/middleware/csrf.d.ts +24 -0
  345. package/dist/middleware/csrf.js +80 -0
  346. package/dist/middleware/csrf.test.d.ts +1 -0
  347. package/dist/middleware/csrf.test.js +152 -0
  348. package/dist/middleware/drizzle-rate-limit-repository.d.ts +9 -0
  349. package/dist/middleware/drizzle-rate-limit-repository.js +52 -0
  350. package/dist/middleware/drizzle-rate-limit-repository.test.d.ts +1 -0
  351. package/dist/middleware/drizzle-rate-limit-repository.test.js +74 -0
  352. package/dist/middleware/get-client-ip.d.ts +22 -0
  353. package/dist/middleware/get-client-ip.js +51 -0
  354. package/dist/middleware/get-client-ip.test.d.ts +1 -0
  355. package/dist/middleware/get-client-ip.test.js +40 -0
  356. package/dist/middleware/index.d.ts +5 -0
  357. package/dist/middleware/index.js +4 -0
  358. package/dist/middleware/rate-limit-repository.d.ts +19 -0
  359. package/dist/middleware/rate-limit-repository.js +1 -0
  360. package/dist/middleware/rate-limit.d.ts +57 -0
  361. package/dist/middleware/rate-limit.js +109 -0
  362. package/dist/middleware/rate-limit.test.d.ts +1 -0
  363. package/dist/middleware/rate-limit.test.js +247 -0
  364. package/dist/security/credential-vault/audit-repository.d.ts +27 -0
  365. package/dist/security/credential-vault/audit-repository.js +42 -0
  366. package/dist/security/credential-vault/audit-repository.test.d.ts +1 -0
  367. package/dist/security/credential-vault/audit-repository.test.js +78 -0
  368. package/dist/security/credential-vault/credential-repository.d.ts +94 -0
  369. package/dist/security/credential-vault/credential-repository.js +145 -0
  370. package/dist/security/credential-vault/credential-repository.test.d.ts +1 -0
  371. package/dist/security/credential-vault/credential-repository.test.js +206 -0
  372. package/dist/security/credential-vault/index.d.ts +12 -0
  373. package/dist/security/credential-vault/index.js +6 -0
  374. package/dist/security/credential-vault/key-rotation.d.ts +18 -0
  375. package/dist/security/credential-vault/key-rotation.js +52 -0
  376. package/dist/security/credential-vault/key-rotation.test.d.ts +1 -0
  377. package/dist/security/credential-vault/key-rotation.test.js +95 -0
  378. package/dist/security/credential-vault/migrate-plaintext.d.ts +15 -0
  379. package/dist/security/credential-vault/migrate-plaintext.js +80 -0
  380. package/dist/security/credential-vault/migrate-plaintext.test.d.ts +1 -0
  381. package/dist/security/credential-vault/migrate-plaintext.test.js +111 -0
  382. package/dist/security/credential-vault/migration-check.d.ts +15 -0
  383. package/dist/security/credential-vault/migration-check.js +71 -0
  384. package/dist/security/credential-vault/migration-check.test.d.ts +1 -0
  385. package/dist/security/credential-vault/migration-check.test.js +457 -0
  386. package/dist/security/credential-vault/store.d.ts +106 -0
  387. package/dist/security/credential-vault/store.js +181 -0
  388. package/dist/security/credential-vault/store.test.d.ts +1 -0
  389. package/dist/security/credential-vault/store.test.js +482 -0
  390. package/dist/security/encryption.d.ts +22 -0
  391. package/dist/security/encryption.js +53 -0
  392. package/dist/security/encryption.test.d.ts +1 -0
  393. package/dist/security/encryption.test.js +95 -0
  394. package/dist/security/host-validation.d.ts +11 -0
  395. package/dist/security/host-validation.js +108 -0
  396. package/dist/security/host-validation.test.d.ts +1 -0
  397. package/dist/security/host-validation.test.js +106 -0
  398. package/dist/security/index.d.ts +11 -0
  399. package/dist/security/index.js +11 -0
  400. package/dist/security/key-audit.d.ts +16 -0
  401. package/dist/security/key-audit.js +35 -0
  402. package/dist/security/key-audit.test.d.ts +1 -0
  403. package/dist/security/key-audit.test.js +50 -0
  404. package/dist/security/key-injection.d.ts +28 -0
  405. package/dist/security/key-injection.js +57 -0
  406. package/dist/security/key-injection.test.d.ts +1 -0
  407. package/dist/security/key-injection.test.js +97 -0
  408. package/dist/security/key-validation.d.ts +16 -0
  409. package/dist/security/key-validation.js +78 -0
  410. package/dist/security/key-validation.test.d.ts +1 -0
  411. package/dist/security/key-validation.test.js +87 -0
  412. package/dist/security/redirect-allowlist.d.ts +6 -0
  413. package/dist/security/redirect-allowlist.js +36 -0
  414. package/dist/security/redirect-allowlist.test.d.ts +1 -0
  415. package/dist/security/redirect-allowlist.test.js +55 -0
  416. package/dist/security/tenant-keys/capability-settings-store.d.ts +22 -0
  417. package/dist/security/tenant-keys/capability-settings-store.js +33 -0
  418. package/dist/security/tenant-keys/capability-settings-store.test.d.ts +1 -0
  419. package/dist/security/tenant-keys/capability-settings-store.test.js +77 -0
  420. package/dist/security/tenant-keys/index.d.ts +10 -0
  421. package/dist/security/tenant-keys/index.js +5 -0
  422. package/dist/security/tenant-keys/key-resolution-repository.d.ts +15 -0
  423. package/dist/security/tenant-keys/key-resolution-repository.js +18 -0
  424. package/dist/security/tenant-keys/key-resolution-repository.test.d.ts +1 -0
  425. package/dist/security/tenant-keys/key-resolution-repository.test.js +72 -0
  426. package/dist/security/tenant-keys/key-resolution.d.ts +39 -0
  427. package/dist/security/tenant-keys/key-resolution.js +59 -0
  428. package/dist/security/tenant-keys/key-resolution.test.d.ts +1 -0
  429. package/dist/security/tenant-keys/key-resolution.test.js +97 -0
  430. package/dist/security/tenant-keys/org-key-resolution.d.ts +30 -0
  431. package/dist/security/tenant-keys/org-key-resolution.js +50 -0
  432. package/dist/security/tenant-keys/org-key-resolution.test.d.ts +1 -0
  433. package/dist/security/tenant-keys/org-key-resolution.test.js +103 -0
  434. package/dist/security/tenant-keys/tenant-key-repository.d.ts +36 -0
  435. package/dist/security/tenant-keys/tenant-key-repository.js +96 -0
  436. package/dist/security/tenant-keys/tenant-key-repository.test.d.ts +1 -0
  437. package/dist/security/tenant-keys/tenant-key-repository.test.js +114 -0
  438. package/dist/security/types.d.ts +35 -0
  439. package/dist/security/types.js +15 -0
  440. package/dist/tenancy/drizzle-org-repository.d.ts +40 -0
  441. package/dist/tenancy/drizzle-org-repository.js +126 -0
  442. package/dist/tenancy/index.d.ts +6 -0
  443. package/dist/tenancy/index.js +3 -0
  444. package/dist/tenancy/org-member-repository.d.ts +57 -0
  445. package/dist/tenancy/org-member-repository.js +99 -0
  446. package/dist/tenancy/org-repository.test.d.ts +1 -0
  447. package/dist/tenancy/org-repository.test.js +143 -0
  448. package/dist/tenancy/org-service.d.ts +70 -0
  449. package/dist/tenancy/org-service.js +223 -0
  450. package/dist/tenancy/org-service.test.d.ts +1 -0
  451. package/dist/tenancy/org-service.test.js +550 -0
  452. package/dist/test/db.d.ts +33 -0
  453. package/dist/test/db.js +65 -0
  454. package/dist/trpc/index.d.ts +1 -0
  455. package/dist/trpc/index.js +1 -0
  456. package/dist/trpc/init.d.ts +49 -0
  457. package/dist/trpc/init.js +108 -0
  458. package/dist/trpc/init.test.d.ts +1 -0
  459. package/dist/trpc/init.test.js +154 -0
  460. package/drizzle/migrations/0000_slippery_mandrill.sql +559 -0
  461. package/drizzle/migrations/meta/0000_snapshot.json +4374 -0
  462. package/drizzle/migrations/meta/_journal.json +13 -0
  463. package/drizzle.config.ts +41 -0
  464. package/package.json +64 -0
  465. package/src/admin/admin-audit-log-repository.ts +135 -0
  466. package/src/admin/audit-log.ts +111 -0
  467. package/src/admin/index.ts +6 -0
  468. package/src/admin/role-store.ts +134 -0
  469. package/src/auth/api-key-repository.test.ts +63 -0
  470. package/src/auth/api-key-repository.ts +46 -0
  471. package/src/auth/auth.test.ts +166 -0
  472. package/src/auth/better-auth.ts +216 -0
  473. package/src/auth/index.ts +520 -0
  474. package/src/auth/login-history-repository.test.ts +54 -0
  475. package/src/auth/login-history-repository.ts +28 -0
  476. package/src/auth/middleware.test.ts +264 -0
  477. package/src/auth/middleware.ts +117 -0
  478. package/src/auth/scoped-tokens.test.ts +362 -0
  479. package/src/auth/tenant-access.test.ts +69 -0
  480. package/src/auth/user-creator.test.ts +98 -0
  481. package/src/auth/user-creator.ts +54 -0
  482. package/src/auth/user-role-repository.test.ts +149 -0
  483. package/src/auth/user-role-repository.ts +67 -0
  484. package/src/billing/drizzle-webhook-seen-repository.ts +34 -0
  485. package/src/billing/index.ts +22 -0
  486. package/src/billing/payment-processor.test.ts +93 -0
  487. package/src/billing/payment-processor.ts +150 -0
  488. package/src/billing/payram/cents-credits-boundary.test.ts +84 -0
  489. package/src/billing/payram/charge-store.test.ts +84 -0
  490. package/src/billing/payram/charge-store.ts +109 -0
  491. package/src/billing/payram/checkout.test.ts +99 -0
  492. package/src/billing/payram/checkout.ts +40 -0
  493. package/src/billing/payram/client.test.ts +62 -0
  494. package/src/billing/payram/client.ts +21 -0
  495. package/src/billing/payram/index.ts +14 -0
  496. package/src/billing/payram/types.ts +44 -0
  497. package/src/billing/payram/webhook.test.ts +318 -0
  498. package/src/billing/payram/webhook.ts +97 -0
  499. package/src/billing/stripe/cents-credits-boundary.test.ts +70 -0
  500. package/src/billing/stripe/checkout.test.ts +186 -0
  501. package/src/billing/stripe/checkout.ts +82 -0
  502. package/src/billing/stripe/client.test.ts +64 -0
  503. package/src/billing/stripe/client.ts +39 -0
  504. package/src/billing/stripe/credit-prices.test.ts +114 -0
  505. package/src/billing/stripe/credit-prices.ts +113 -0
  506. package/src/billing/stripe/index.ts +14 -0
  507. package/src/billing/stripe/payment-methods-detach-all.test.ts +53 -0
  508. package/src/billing/stripe/payment-methods.test.ts +157 -0
  509. package/src/billing/stripe/payment-methods.ts +76 -0
  510. package/src/billing/stripe/portal.test.ts +63 -0
  511. package/src/billing/stripe/portal.ts +25 -0
  512. package/src/billing/stripe/setup-intent.test.ts +78 -0
  513. package/src/billing/stripe/setup-intent.ts +34 -0
  514. package/src/billing/stripe/stripe-payment-processor.test.ts +517 -0
  515. package/src/billing/stripe/stripe-payment-processor.ts +255 -0
  516. package/src/billing/stripe/tenant-store.test.ts +124 -0
  517. package/src/billing/stripe/tenant-store.ts +151 -0
  518. package/src/billing/stripe/types.ts +53 -0
  519. package/src/billing/webhook-seen-repository.ts +24 -0
  520. package/src/config/billing-env.test.ts +54 -0
  521. package/src/config/index.ts +44 -0
  522. package/src/config/logger.ts +12 -0
  523. package/src/config/provider-endpoints.ts +14 -0
  524. package/src/credits/auto-topup-charge.test.ts +292 -0
  525. package/src/credits/auto-topup-charge.ts +171 -0
  526. package/src/credits/auto-topup-event-log-repository.test.ts +99 -0
  527. package/src/credits/auto-topup-event-log-repository.ts +30 -0
  528. package/src/credits/auto-topup-schedule.test.ts +179 -0
  529. package/src/credits/auto-topup-schedule.ts +93 -0
  530. package/src/credits/auto-topup-settings-repository.test.ts +123 -0
  531. package/src/credits/auto-topup-settings-repository.ts +245 -0
  532. package/src/credits/auto-topup-usage.test.ts +220 -0
  533. package/src/credits/auto-topup-usage.ts +68 -0
  534. package/src/credits/credit-expiry-cron.test.ts +125 -0
  535. package/src/credits/credit-expiry-cron.ts +76 -0
  536. package/src/credits/credit-ledger-extra.test.ts +57 -0
  537. package/src/credits/credit-ledger.bench.ts +56 -0
  538. package/src/credits/credit-ledger.test.ts +276 -0
  539. package/src/credits/credit-ledger.ts +450 -0
  540. package/src/credits/credit-transaction-repository.test.ts +274 -0
  541. package/src/credits/credit-transaction-repository.ts +62 -0
  542. package/src/credits/credit.test.ts +234 -0
  543. package/src/credits/credit.ts +160 -0
  544. package/src/credits/dividend-cron.test.ts +158 -0
  545. package/src/credits/dividend-cron.ts +127 -0
  546. package/src/credits/dividend-repository.test.ts +223 -0
  547. package/src/credits/dividend-repository.ts +182 -0
  548. package/src/credits/index.ts +25 -0
  549. package/src/credits/repository-types.ts +33 -0
  550. package/src/credits/signup-grant.test.ts +63 -0
  551. package/src/credits/signup-grant.ts +44 -0
  552. package/src/credits/tenant-customer-repository.ts +28 -0
  553. package/src/db/auth-user-repository.ts +124 -0
  554. package/src/db/credit-column.ts +17 -0
  555. package/src/db/index.ts +21 -0
  556. package/src/db/schema/account-deletion-requests.ts +41 -0
  557. package/src/db/schema/account-export-requests.ts +24 -0
  558. package/src/db/schema/admin-audit.ts +26 -0
  559. package/src/db/schema/admin-users.ts +31 -0
  560. package/src/db/schema/affiliate-fraud.ts +23 -0
  561. package/src/db/schema/affiliate.ts +38 -0
  562. package/src/db/schema/coupon-codes.ts +22 -0
  563. package/src/db/schema/credit-auto-topup-settings.ts +32 -0
  564. package/src/db/schema/credit-auto-topup.ts +26 -0
  565. package/src/db/schema/credits.ts +44 -0
  566. package/src/db/schema/dividend-distributions.ts +24 -0
  567. package/src/db/schema/email-notifications.ts +26 -0
  568. package/src/db/schema/index.ts +33 -0
  569. package/src/db/schema/meter-events.ts +70 -0
  570. package/src/db/schema/notification-preferences.ts +19 -0
  571. package/src/db/schema/notification-queue.ts +45 -0
  572. package/src/db/schema/org-memberships.ts +20 -0
  573. package/src/db/schema/organization-members.ts +37 -0
  574. package/src/db/schema/payram.ts +26 -0
  575. package/src/db/schema/platform-api-keys.ts +25 -0
  576. package/src/db/schema/promotion-redemptions.ts +23 -0
  577. package/src/db/schema/promotions.ts +57 -0
  578. package/src/db/schema/provider-credentials.ts +41 -0
  579. package/src/db/schema/rate-limit-entries.ts +12 -0
  580. package/src/db/schema/secret-audit-log.ts +20 -0
  581. package/src/db/schema/session-usage.ts +24 -0
  582. package/src/db/schema/spending-limits.ts +9 -0
  583. package/src/db/schema/tenant-addons.ts +14 -0
  584. package/src/db/schema/tenant-api-keys.ts +26 -0
  585. package/src/db/schema/tenant-capability-settings.ts +17 -0
  586. package/src/db/schema/tenant-customers.ts +35 -0
  587. package/src/db/schema/tenants.ts +23 -0
  588. package/src/db/schema/user-roles.ts +23 -0
  589. package/src/db/schema/webhook-seen-events.ts +14 -0
  590. package/src/email/billing-emails.test.ts +198 -0
  591. package/src/email/billing-emails.ts +211 -0
  592. package/src/email/client.test.ts +149 -0
  593. package/src/email/client.ts +137 -0
  594. package/src/email/drizzle-billing-email-repository.test.ts +52 -0
  595. package/src/email/drizzle-billing-email-repository.ts +59 -0
  596. package/src/email/index.ts +57 -0
  597. package/src/email/notification-preferences-store.test.ts +102 -0
  598. package/src/email/notification-preferences-store.ts +90 -0
  599. package/src/email/notification-queue-store.test.ts +215 -0
  600. package/src/email/notification-queue-store.ts +127 -0
  601. package/src/email/notification-repository-types.ts +101 -0
  602. package/src/email/notification-service.test.ts +178 -0
  603. package/src/email/notification-service.ts +265 -0
  604. package/src/email/notification-templates.test.ts +261 -0
  605. package/src/email/notification-templates.ts +727 -0
  606. package/src/email/notification-worker.test.ts +189 -0
  607. package/src/email/notification-worker.ts +133 -0
  608. package/src/email/require-verified.ts +65 -0
  609. package/src/email/resend-adapter.test.ts +253 -0
  610. package/src/email/resend-adapter.ts +157 -0
  611. package/src/email/templates.test.ts +217 -0
  612. package/src/email/templates.ts +469 -0
  613. package/src/email/verification.test.ts +185 -0
  614. package/src/email/verification.ts +110 -0
  615. package/src/index.ts +51 -0
  616. package/src/metering/aggregator.test.ts +239 -0
  617. package/src/metering/aggregator.ts +160 -0
  618. package/src/metering/dlq.test.ts +134 -0
  619. package/src/metering/dlq.ts +102 -0
  620. package/src/metering/drizzle-usage-summary-repository.ts +167 -0
  621. package/src/metering/emitter.test.ts +202 -0
  622. package/src/metering/emitter.ts +227 -0
  623. package/src/metering/index.ts +21 -0
  624. package/src/metering/load-test.bench.ts +130 -0
  625. package/src/metering/meter-event-repository.ts +87 -0
  626. package/src/metering/meter-repositories.test.ts +491 -0
  627. package/src/metering/metering.test.ts +1317 -0
  628. package/src/metering/reconciliation-cron.test.ts +202 -0
  629. package/src/metering/reconciliation-cron.ts +134 -0
  630. package/src/metering/reconciliation-repository.test.ts +196 -0
  631. package/src/metering/reconciliation-repository.ts +83 -0
  632. package/src/metering/types.ts +93 -0
  633. package/src/metering/wal.test.ts +222 -0
  634. package/src/metering/wal.ts +139 -0
  635. package/src/middleware/csrf.test.ts +178 -0
  636. package/src/middleware/csrf.ts +101 -0
  637. package/src/middleware/drizzle-rate-limit-repository.test.ts +97 -0
  638. package/src/middleware/drizzle-rate-limit-repository.ts +57 -0
  639. package/src/middleware/get-client-ip.test.ts +49 -0
  640. package/src/middleware/get-client-ip.ts +62 -0
  641. package/src/middleware/index.ts +12 -0
  642. package/src/middleware/rate-limit-repository.ts +22 -0
  643. package/src/middleware/rate-limit.test.ts +338 -0
  644. package/src/middleware/rate-limit.ts +169 -0
  645. package/src/security/credential-vault/audit-repository.test.ts +91 -0
  646. package/src/security/credential-vault/audit-repository.ts +64 -0
  647. package/src/security/credential-vault/credential-repository.test.ts +264 -0
  648. package/src/security/credential-vault/credential-repository.ts +233 -0
  649. package/src/security/credential-vault/index.ts +26 -0
  650. package/src/security/credential-vault/key-rotation.test.ts +139 -0
  651. package/src/security/credential-vault/key-rotation.ts +70 -0
  652. package/src/security/credential-vault/migrate-plaintext.test.ts +138 -0
  653. package/src/security/credential-vault/migrate-plaintext.ts +101 -0
  654. package/src/security/credential-vault/migration-check.test.ts +533 -0
  655. package/src/security/credential-vault/migration-check.ts +88 -0
  656. package/src/security/credential-vault/store.test.ts +569 -0
  657. package/src/security/credential-vault/store.ts +284 -0
  658. package/src/security/encryption.test.ts +114 -0
  659. package/src/security/encryption.ts +65 -0
  660. package/src/security/host-validation.test.ts +136 -0
  661. package/src/security/host-validation.ts +116 -0
  662. package/src/security/index.ts +59 -0
  663. package/src/security/key-audit.test.ts +57 -0
  664. package/src/security/key-audit.ts +45 -0
  665. package/src/security/key-injection.test.ts +131 -0
  666. package/src/security/key-injection.ts +71 -0
  667. package/src/security/key-validation.test.ts +111 -0
  668. package/src/security/key-validation.ts +84 -0
  669. package/src/security/redirect-allowlist.test.ts +70 -0
  670. package/src/security/redirect-allowlist.ts +35 -0
  671. package/src/security/tenant-keys/capability-settings-store.test.ts +98 -0
  672. package/src/security/tenant-keys/capability-settings-store.ts +53 -0
  673. package/src/security/tenant-keys/index.ts +10 -0
  674. package/src/security/tenant-keys/key-resolution-repository.test.ts +95 -0
  675. package/src/security/tenant-keys/key-resolution-repository.ts +31 -0
  676. package/src/security/tenant-keys/key-resolution.test.ts +173 -0
  677. package/src/security/tenant-keys/key-resolution.ts +87 -0
  678. package/src/security/tenant-keys/org-key-resolution.test.ts +217 -0
  679. package/src/security/tenant-keys/org-key-resolution.ts +76 -0
  680. package/src/security/tenant-keys/tenant-key-repository.test.ts +143 -0
  681. package/src/security/tenant-keys/tenant-key-repository.ts +130 -0
  682. package/src/security/types.ts +43 -0
  683. package/src/tenancy/drizzle-org-repository.ts +169 -0
  684. package/src/tenancy/index.ts +6 -0
  685. package/src/tenancy/org-member-repository.ts +159 -0
  686. package/src/tenancy/org-repository.test.ts +172 -0
  687. package/src/tenancy/org-service.test.ts +634 -0
  688. package/src/tenancy/org-service.ts +290 -0
  689. package/src/test/db.ts +97 -0
  690. package/src/trpc/index.ts +11 -0
  691. package/src/trpc/init.test.ts +196 -0
  692. package/src/trpc/init.ts +138 -0
  693. package/tsconfig.json +20 -0
  694. package/vitest.config.ts +8 -0
@@ -0,0 +1,242 @@
1
+ import crypto from "node:crypto";
2
+ import Stripe from "stripe";
3
+ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { creditAutoTopup } from "../db/schema/credit-auto-topup.js";
5
+ import { createTestDb, truncateAllTables } from "../test/db.js";
6
+ import { Credit } from "./credit.js";
7
+ import { chargeAutoTopup, MAX_CONSECUTIVE_FAILURES } from "./auto-topup-charge.js";
8
+ import { DrizzleAutoTopupEventLogRepository } from "./auto-topup-event-log-repository.js";
9
+ import { CreditLedger } from "./credit-ledger.js";
10
+ function mockStripe(overrides) {
11
+ const piId = overrides?.paymentIntentId ?? `pi_${crypto.randomUUID()}`;
12
+ return {
13
+ paymentIntents: {
14
+ create: vi.fn().mockImplementation(async () => {
15
+ if (overrides?.shouldFail) {
16
+ if (overrides.failWithStripeError !== false) {
17
+ throw new Stripe.errors.StripeCardError({
18
+ message: overrides.failMessage ?? "card_declined",
19
+ type: "card_error",
20
+ code: overrides.failMessage ?? "card_declined",
21
+ });
22
+ }
23
+ throw new Error(overrides.failMessage ?? "card_declined");
24
+ }
25
+ return { id: piId, status: "succeeded" };
26
+ }),
27
+ },
28
+ customers: {
29
+ listPaymentMethods: vi.fn().mockResolvedValue({ data: [{ id: "pm_123" }] }),
30
+ },
31
+ };
32
+ }
33
+ function mockTenantStore(stripeCustomerId = "cus_123") {
34
+ return {
35
+ getByTenant: vi.fn().mockResolvedValue({ tenant: "t1", processor_customer_id: stripeCustomerId }),
36
+ };
37
+ }
38
+ describe("chargeAutoTopup", () => {
39
+ let pool;
40
+ let db;
41
+ let ledger;
42
+ beforeAll(async () => {
43
+ ({ db, pool } = await createTestDb());
44
+ });
45
+ afterAll(async () => {
46
+ await pool.close();
47
+ });
48
+ beforeEach(async () => {
49
+ await truncateAllTables(pool);
50
+ ledger = new CreditLedger(db);
51
+ });
52
+ it("charges Stripe and credits ledger on success", async () => {
53
+ const stripe = mockStripe();
54
+ const tenantRepo = mockTenantStore();
55
+ const deps = {
56
+ stripe: stripe,
57
+ tenantRepo: tenantRepo,
58
+ creditLedger: ledger,
59
+ eventLogRepo: new DrizzleAutoTopupEventLogRepository(db),
60
+ };
61
+ const result = await chargeAutoTopup(deps, "t1", Credit.fromCents(500), "auto_topup_usage");
62
+ expect(result.success).toBe(true);
63
+ expect(result.paymentReference).toEqual(expect.any(String));
64
+ expect((await ledger.balance("t1")).toCents()).toBe(500);
65
+ const history = await ledger.history("t1");
66
+ expect(history[0].type).toBe("purchase");
67
+ expect(history[0].fundingSource).toBe("stripe");
68
+ });
69
+ it("writes success event to credit_auto_topup log", async () => {
70
+ const stripe = mockStripe();
71
+ const tenantRepo = mockTenantStore();
72
+ const deps = {
73
+ stripe: stripe,
74
+ tenantRepo: tenantRepo,
75
+ creditLedger: ledger,
76
+ eventLogRepo: new DrizzleAutoTopupEventLogRepository(db),
77
+ };
78
+ await chargeAutoTopup(deps, "t1", Credit.fromCents(500), "auto_topup_usage");
79
+ const events = await db
80
+ .select()
81
+ .from(creditAutoTopup)
82
+ .where((await import("drizzle-orm")).eq(creditAutoTopup.tenantId, "t1"));
83
+ expect(events).toHaveLength(1);
84
+ expect(events[0].status).toBe("success");
85
+ expect(events[0].amountCents).toBe(500);
86
+ });
87
+ it("returns failure result and writes failure event on Stripe error", async () => {
88
+ const stripe = mockStripe({ shouldFail: true, failMessage: "card_declined" });
89
+ const tenantRepo = mockTenantStore();
90
+ const deps = {
91
+ stripe: stripe,
92
+ tenantRepo: tenantRepo,
93
+ creditLedger: ledger,
94
+ eventLogRepo: new DrizzleAutoTopupEventLogRepository(db),
95
+ };
96
+ const result = await chargeAutoTopup(deps, "t1", Credit.fromCents(500), "auto_topup_usage");
97
+ expect(result.success).toBe(false);
98
+ expect(result.error).toContain("Card declined");
99
+ expect(result.error).toContain("card_declined");
100
+ expect((await ledger.balance("t1")).toCents()).toBe(0);
101
+ const events = await db
102
+ .select()
103
+ .from(creditAutoTopup)
104
+ .where((await import("drizzle-orm")).eq(creditAutoTopup.tenantId, "t1"));
105
+ expect(events).toHaveLength(1);
106
+ expect(events[0].status).toBe("failed");
107
+ });
108
+ it("returns failure when tenant has no Stripe customer", async () => {
109
+ const stripe = mockStripe();
110
+ const tenantRepo = { getByTenant: vi.fn().mockResolvedValue(null) };
111
+ const deps = {
112
+ stripe: stripe,
113
+ tenantRepo: tenantRepo,
114
+ creditLedger: ledger,
115
+ eventLogRepo: new DrizzleAutoTopupEventLogRepository(db),
116
+ };
117
+ const result = await chargeAutoTopup(deps, "t1", Credit.fromCents(500), "auto_topup_usage");
118
+ expect(result.success).toBe(false);
119
+ expect(result.error).toContain("No Stripe customer");
120
+ });
121
+ it("returns failure when tenant has no payment methods", async () => {
122
+ const stripe = mockStripe();
123
+ stripe.customers.listPaymentMethods = vi.fn().mockResolvedValue({ data: [] });
124
+ const tenantRepo = mockTenantStore();
125
+ const deps = {
126
+ stripe: stripe,
127
+ tenantRepo: tenantRepo,
128
+ creditLedger: ledger,
129
+ eventLogRepo: new DrizzleAutoTopupEventLogRepository(db),
130
+ };
131
+ const result = await chargeAutoTopup(deps, "t1", Credit.fromCents(500), "auto_topup_usage");
132
+ expect(result.success).toBe(false);
133
+ expect(result.error).toContain("No payment method");
134
+ });
135
+ it("is idempotent -- referenceId already credited means hasReferenceId returns true", async () => {
136
+ const piId = `pi_${crypto.randomUUID()}`;
137
+ const stripe = mockStripe({ paymentIntentId: piId });
138
+ const tenantRepo = mockTenantStore();
139
+ const deps = {
140
+ stripe: stripe,
141
+ tenantRepo: tenantRepo,
142
+ creditLedger: ledger,
143
+ eventLogRepo: new DrizzleAutoTopupEventLogRepository(db),
144
+ };
145
+ await chargeAutoTopup(deps, "t1", Credit.fromCents(500), "auto_topup_usage");
146
+ expect((await ledger.balance("t1")).toCents()).toBe(500);
147
+ expect(await ledger.hasReferenceId(piId)).toBe(true);
148
+ });
149
+ it("exports MAX_CONSECUTIVE_FAILURES as 3", () => {
150
+ expect(MAX_CONSECUTIVE_FAILURES).toBe(3);
151
+ });
152
+ it("logs structured card-decline info when Stripe throws StripeCardError on payment", async () => {
153
+ const cardError = new Stripe.errors.StripeCardError({
154
+ message: "Your card was declined.",
155
+ type: "card_error",
156
+ code: "card_declined",
157
+ decline_code: "insufficient_funds",
158
+ });
159
+ const stripe = mockStripe();
160
+ stripe.paymentIntents.create = vi.fn().mockRejectedValue(cardError);
161
+ const tenantRepo = mockTenantStore();
162
+ const deps = {
163
+ stripe: stripe,
164
+ tenantRepo: tenantRepo,
165
+ creditLedger: ledger,
166
+ eventLogRepo: new DrizzleAutoTopupEventLogRepository(db),
167
+ };
168
+ const result = await chargeAutoTopup(deps, "t1", Credit.fromCents(500), "auto_topup_usage");
169
+ expect(result.success).toBe(false);
170
+ expect(result.error).toContain("Card declined");
171
+ expect(result.error).toContain("card_declined");
172
+ });
173
+ it("logs structured Stripe error info when Stripe throws StripeAPIError on payment", async () => {
174
+ const apiError = new Stripe.errors.StripeAPIError({
175
+ message: "Internal Stripe error",
176
+ type: "api_error",
177
+ });
178
+ const stripe = mockStripe();
179
+ stripe.paymentIntents.create = vi.fn().mockRejectedValue(apiError);
180
+ const tenantRepo = mockTenantStore();
181
+ const deps = {
182
+ stripe: stripe,
183
+ tenantRepo: tenantRepo,
184
+ creditLedger: ledger,
185
+ eventLogRepo: new DrizzleAutoTopupEventLogRepository(db),
186
+ };
187
+ const result = await chargeAutoTopup(deps, "t1", Credit.fromCents(500), "auto_topup_usage");
188
+ expect(result.success).toBe(false);
189
+ expect(result.error).toContain("Stripe error");
190
+ });
191
+ it("re-throws non-Stripe errors from paymentIntents.create", async () => {
192
+ const stripe = mockStripe();
193
+ stripe.paymentIntents.create = vi.fn().mockRejectedValue(new TypeError("Cannot read properties of undefined"));
194
+ const tenantRepo = mockTenantStore();
195
+ const deps = {
196
+ stripe: stripe,
197
+ tenantRepo: tenantRepo,
198
+ creditLedger: ledger,
199
+ eventLogRepo: new DrizzleAutoTopupEventLogRepository(db),
200
+ };
201
+ await expect(chargeAutoTopup(deps, "t1", Credit.fromCents(500), "auto_topup_usage")).rejects.toThrow(TypeError);
202
+ });
203
+ it("returns structured error when StripeCardError thrown by listPaymentMethods", async () => {
204
+ const cardError = new Stripe.errors.StripeCardError({
205
+ message: "Card error on list",
206
+ type: "card_error",
207
+ code: "card_declined",
208
+ });
209
+ const stripe = mockStripe();
210
+ stripe.customers.listPaymentMethods = vi.fn().mockRejectedValue(cardError);
211
+ const tenantRepo = mockTenantStore();
212
+ const deps = {
213
+ stripe: stripe,
214
+ tenantRepo: tenantRepo,
215
+ creditLedger: ledger,
216
+ eventLogRepo: new DrizzleAutoTopupEventLogRepository(db),
217
+ };
218
+ const result = await chargeAutoTopup(deps, "t1", Credit.fromCents(500), "auto_topup_usage");
219
+ expect(result.success).toBe(false);
220
+ expect(result.error).toContain("Card declined");
221
+ });
222
+ it("propagates error with PaymentIntent ID when credit grant fails after Stripe charge", async () => {
223
+ const piId = "pi_ledger_fail_test";
224
+ const stripe = mockStripe({ paymentIntentId: piId });
225
+ const tenantRepo = mockTenantStore();
226
+ const deps = {
227
+ stripe: stripe,
228
+ tenantRepo: tenantRepo,
229
+ creditLedger: ledger,
230
+ eventLogRepo: new DrizzleAutoTopupEventLogRepository(db),
231
+ };
232
+ // Make credit() fail after Stripe succeeds
233
+ vi.spyOn(ledger, "credit").mockRejectedValueOnce(new Error("DB connection lost"));
234
+ // Error propagates and contains the PI ID for manual reconciliation
235
+ await expect(chargeAutoTopup(deps, "t1", Credit.fromCents(500), "auto_topup_usage")).rejects.toThrow(piId);
236
+ // Also verify the original error message is preserved
237
+ vi.spyOn(ledger, "credit").mockRejectedValueOnce(new Error("DB connection lost"));
238
+ await expect(chargeAutoTopup(deps, "t1", Credit.fromCents(500), "auto_topup_usage")).rejects.toThrow("DB connection lost");
239
+ // No credits were silently granted
240
+ expect((await ledger.balance("t1")).toCents()).toBe(0);
241
+ });
242
+ });
@@ -0,0 +1,16 @@
1
+ import type { PlatformDb } from "../db/index.js";
2
+ export interface AutoTopupEventLogEntry {
3
+ tenantId: string;
4
+ amountCents: number;
5
+ status: "success" | "failed";
6
+ failureReason?: string | null;
7
+ paymentReference?: string | null;
8
+ }
9
+ export interface IAutoTopupEventLogRepository {
10
+ writeEvent(entry: AutoTopupEventLogEntry): Promise<void>;
11
+ }
12
+ export declare class DrizzleAutoTopupEventLogRepository implements IAutoTopupEventLogRepository {
13
+ private readonly db;
14
+ constructor(db: PlatformDb);
15
+ writeEvent(entry: AutoTopupEventLogEntry): Promise<void>;
16
+ }
@@ -0,0 +1,18 @@
1
+ import crypto from "node:crypto";
2
+ import { creditAutoTopup } from "../db/schema/credit-auto-topup.js";
3
+ export class DrizzleAutoTopupEventLogRepository {
4
+ db;
5
+ constructor(db) {
6
+ this.db = db;
7
+ }
8
+ async writeEvent(entry) {
9
+ await this.db.insert(creditAutoTopup).values({
10
+ id: crypto.randomUUID(),
11
+ tenantId: entry.tenantId,
12
+ amountCents: entry.amountCents,
13
+ status: entry.status,
14
+ failureReason: entry.failureReason ?? null,
15
+ paymentReference: entry.paymentReference ?? null,
16
+ });
17
+ }
18
+ }
@@ -0,0 +1,83 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
3
+ import { creditAutoTopup } from "../db/schema/credit-auto-topup.js";
4
+ import { beginTestTransaction, createTestDb, endTestTransaction, rollbackTestTransaction } from "../test/db.js";
5
+ import { DrizzleAutoTopupEventLogRepository } from "./auto-topup-event-log-repository.js";
6
+ describe("DrizzleAutoTopupEventLogRepository", () => {
7
+ let pool;
8
+ let db;
9
+ let repo;
10
+ beforeAll(async () => {
11
+ ({ db, pool } = await createTestDb());
12
+ await beginTestTransaction(pool);
13
+ });
14
+ afterAll(async () => {
15
+ await endTestTransaction(pool);
16
+ await pool.close();
17
+ });
18
+ beforeEach(async () => {
19
+ await rollbackTestTransaction(pool);
20
+ repo = new DrizzleAutoTopupEventLogRepository(db);
21
+ });
22
+ it("writes a success event with all fields", async () => {
23
+ await repo.writeEvent({
24
+ tenantId: "t1",
25
+ amountCents: 500,
26
+ status: "success",
27
+ failureReason: null,
28
+ paymentReference: "pi_abc123",
29
+ });
30
+ const rows = await db.select().from(creditAutoTopup).where(eq(creditAutoTopup.tenantId, "t1"));
31
+ expect(rows).toHaveLength(1);
32
+ expect(rows[0].tenantId).toBe("t1");
33
+ expect(rows[0].amountCents).toBe(500);
34
+ expect(rows[0].status).toBe("success");
35
+ expect(rows[0].failureReason).toBeNull();
36
+ expect(rows[0].paymentReference).toBe("pi_abc123");
37
+ expect(rows[0].id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
38
+ expect(rows[0].createdAt).toMatch(/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}/);
39
+ });
40
+ it("writes a failed event with failure reason", async () => {
41
+ await repo.writeEvent({
42
+ tenantId: "t1",
43
+ amountCents: 1000,
44
+ status: "failed",
45
+ failureReason: "card_declined",
46
+ paymentReference: null,
47
+ });
48
+ const rows = await db.select().from(creditAutoTopup).where(eq(creditAutoTopup.tenantId, "t1"));
49
+ expect(rows).toHaveLength(1);
50
+ expect(rows[0].status).toBe("failed");
51
+ expect(rows[0].failureReason).toBe("card_declined");
52
+ expect(rows[0].paymentReference).toBeNull();
53
+ });
54
+ it("defaults optional fields to null when omitted", async () => {
55
+ await repo.writeEvent({
56
+ tenantId: "t1",
57
+ amountCents: 200,
58
+ status: "success",
59
+ });
60
+ const rows = await db.select().from(creditAutoTopup).where(eq(creditAutoTopup.tenantId, "t1"));
61
+ expect(rows).toHaveLength(1);
62
+ expect(rows[0].failureReason).toBeNull();
63
+ expect(rows[0].paymentReference).toBeNull();
64
+ });
65
+ it("generates unique ids for each event", async () => {
66
+ await repo.writeEvent({ tenantId: "t1", amountCents: 100, status: "success" });
67
+ await repo.writeEvent({ tenantId: "t1", amountCents: 200, status: "success" });
68
+ const rows = await db.select().from(creditAutoTopup).where(eq(creditAutoTopup.tenantId, "t1"));
69
+ expect(rows).toHaveLength(2);
70
+ expect(rows[0].id).not.toBe(rows[1].id);
71
+ });
72
+ it("isolates events by tenant", async () => {
73
+ await repo.writeEvent({ tenantId: "t1", amountCents: 100, status: "success" });
74
+ await repo.writeEvent({ tenantId: "t2", amountCents: 200, status: "failed", failureReason: "insufficient_funds" });
75
+ const t1Rows = await db.select().from(creditAutoTopup).where(eq(creditAutoTopup.tenantId, "t1"));
76
+ const t2Rows = await db.select().from(creditAutoTopup).where(eq(creditAutoTopup.tenantId, "t2"));
77
+ expect(t1Rows).toHaveLength(1);
78
+ expect(t1Rows[0].amountCents).toBe(100);
79
+ expect(t2Rows).toHaveLength(1);
80
+ expect(t2Rows[0].amountCents).toBe(200);
81
+ expect(t2Rows[0].failureReason).toBe("insufficient_funds");
82
+ });
83
+ });
@@ -0,0 +1,27 @@
1
+ import type { Credit } from "./credit.js";
2
+ import type { AutoTopupChargeResult } from "./auto-topup-charge.js";
3
+ import type { IAutoTopupSettingsRepository } from "./auto-topup-settings-repository.js";
4
+ export interface ScheduleTopupDeps {
5
+ settingsRepo: IAutoTopupSettingsRepository;
6
+ chargeAutoTopup: (tenantId: string, amount: Credit, source: string) => Promise<AutoTopupChargeResult>;
7
+ /** Optional tenant status check. If provided and returns non-null, skip the charge. */
8
+ checkTenantStatus?: (tenantId: string) => Promise<{
9
+ error: string;
10
+ message: string;
11
+ } | null>;
12
+ }
13
+ export interface ScheduleTopupResult {
14
+ processed: number;
15
+ succeeded: string[];
16
+ failed: string[];
17
+ errors: string[];
18
+ }
19
+ /**
20
+ * Run scheduled auto-topup charges for all due tenants.
21
+ *
22
+ * Called by an hourly cron job. For each tenant with schedule_next_at <= now:
23
+ * 1. Charge Stripe
24
+ * 2. Advance schedule_next_at by interval_hours (even on failure)
25
+ * 3. Track failures and disable after MAX_CONSECUTIVE_FAILURES
26
+ */
27
+ export declare function runScheduledTopups(deps: ScheduleTopupDeps): Promise<ScheduleTopupResult>;
@@ -0,0 +1,66 @@
1
+ import { logger } from "../config/logger.js";
2
+ import { MAX_CONSECUTIVE_FAILURES } from "./auto-topup-charge.js";
3
+ /**
4
+ * Run scheduled auto-topup charges for all due tenants.
5
+ *
6
+ * Called by an hourly cron job. For each tenant with schedule_next_at <= now:
7
+ * 1. Charge Stripe
8
+ * 2. Advance schedule_next_at by interval_hours (even on failure)
9
+ * 3. Track failures and disable after MAX_CONSECUTIVE_FAILURES
10
+ */
11
+ export async function runScheduledTopups(deps) {
12
+ const now = new Date().toISOString();
13
+ const due = await deps.settingsRepo.listDueScheduled(now);
14
+ const result = {
15
+ processed: 0,
16
+ succeeded: [],
17
+ failed: [],
18
+ errors: [],
19
+ };
20
+ for (const settings of due) {
21
+ result.processed++;
22
+ // Skip banned/suspended tenants
23
+ if (deps.checkTenantStatus) {
24
+ const statusErr = await deps.checkTenantStatus(settings.tenantId);
25
+ if (statusErr) {
26
+ // Still advance schedule to prevent hammer-retry on reactivation
27
+ await deps.settingsRepo.advanceScheduleNextAt(settings.tenantId);
28
+ result.failed.push(settings.tenantId);
29
+ continue;
30
+ }
31
+ }
32
+ try {
33
+ const chargeResult = await deps.chargeAutoTopup(settings.tenantId, settings.scheduleAmount, "auto_topup_schedule");
34
+ // Always advance schedule_next_at (even on failure, to prevent hammer-retry)
35
+ await deps.settingsRepo.advanceScheduleNextAt(settings.tenantId);
36
+ if (chargeResult.success) {
37
+ await deps.settingsRepo.resetScheduleFailures(settings.tenantId);
38
+ result.succeeded.push(settings.tenantId);
39
+ }
40
+ else {
41
+ const failureCount = await deps.settingsRepo.incrementScheduleFailures(settings.tenantId);
42
+ if (failureCount >= MAX_CONSECUTIVE_FAILURES) {
43
+ await deps.settingsRepo.disableSchedule(settings.tenantId);
44
+ logger.warn("Schedule auto-topup disabled after consecutive failures", {
45
+ tenantId: settings.tenantId,
46
+ failureCount,
47
+ });
48
+ }
49
+ result.failed.push(settings.tenantId);
50
+ }
51
+ }
52
+ catch (err) {
53
+ // Advance even on unexpected error
54
+ await deps.settingsRepo.advanceScheduleNextAt(settings.tenantId);
55
+ const failureCount = await deps.settingsRepo.incrementScheduleFailures(settings.tenantId);
56
+ if (failureCount >= MAX_CONSECUTIVE_FAILURES) {
57
+ await deps.settingsRepo.disableSchedule(settings.tenantId);
58
+ }
59
+ const msg = err instanceof Error ? err.message : String(err);
60
+ logger.error("Schedule auto-topup unexpected error", { tenantId: settings.tenantId, error: msg });
61
+ result.errors.push(`${settings.tenantId}: ${msg}`);
62
+ result.failed.push(settings.tenantId);
63
+ }
64
+ }
65
+ return result;
66
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,145 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { beginTestTransaction, createTestDb, endTestTransaction, rollbackTestTransaction } from "../test/db.js";
3
+ import { Credit } from "./credit.js";
4
+ import { runScheduledTopups } from "./auto-topup-schedule.js";
5
+ import { DrizzleAutoTopupSettingsRepository } from "./auto-topup-settings-repository.js";
6
+ describe("runScheduledTopups", () => {
7
+ let pool;
8
+ let db;
9
+ let settingsRepo;
10
+ beforeAll(async () => {
11
+ ({ db, pool } = await createTestDb());
12
+ await beginTestTransaction(pool);
13
+ });
14
+ afterAll(async () => {
15
+ await endTestTransaction(pool);
16
+ await pool.close();
17
+ });
18
+ beforeEach(async () => {
19
+ await rollbackTestTransaction(pool);
20
+ settingsRepo = new DrizzleAutoTopupSettingsRepository(db);
21
+ });
22
+ it("processes no tenants when none are due", async () => {
23
+ const mockCharge = vi.fn();
24
+ const deps = { settingsRepo, chargeAutoTopup: mockCharge };
25
+ const result = await runScheduledTopups(deps);
26
+ expect(result.processed).toBe(0);
27
+ expect(result.succeeded).toHaveLength(0);
28
+ expect(result.failed).toHaveLength(0);
29
+ expect(mockCharge).not.toHaveBeenCalled();
30
+ });
31
+ it("charges tenant whose schedule_next_at is in the past", async () => {
32
+ const past = "2026-02-20T00:00:00.000Z";
33
+ await settingsRepo.upsert("t1", {
34
+ scheduleEnabled: true,
35
+ scheduleAmount: Credit.fromCents(1000),
36
+ scheduleNextAt: past,
37
+ });
38
+ const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_abc" });
39
+ const deps = { settingsRepo, chargeAutoTopup: mockCharge };
40
+ const result = await runScheduledTopups(deps);
41
+ expect(result.processed).toBe(1);
42
+ expect(result.succeeded).toEqual(["t1"]);
43
+ expect(mockCharge).toHaveBeenCalledWith("t1", Credit.fromCents(1000), "auto_topup_schedule");
44
+ });
45
+ it("resets failure counter on success", async () => {
46
+ const past = "2026-02-20T00:00:00.000Z";
47
+ await settingsRepo.upsert("t1", {
48
+ scheduleEnabled: true,
49
+ scheduleAmount: Credit.fromCents(500),
50
+ scheduleNextAt: past,
51
+ });
52
+ await settingsRepo.incrementScheduleFailures("t1");
53
+ await settingsRepo.incrementScheduleFailures("t1");
54
+ const mockCharge = vi.fn().mockResolvedValue({ success: true });
55
+ const deps = { settingsRepo, chargeAutoTopup: mockCharge };
56
+ await runScheduledTopups(deps);
57
+ expect((await settingsRepo.getByTenant("t1"))?.scheduleConsecutiveFailures).toBe(0);
58
+ });
59
+ it("advances scheduleNextAt after successful charge", async () => {
60
+ const past = "2026-02-20T00:00:00.000Z";
61
+ await settingsRepo.upsert("t1", {
62
+ scheduleEnabled: true,
63
+ scheduleAmount: Credit.fromCents(1000),
64
+ scheduleIntervalHours: 24,
65
+ scheduleNextAt: past,
66
+ });
67
+ const before = (await settingsRepo.getByTenant("t1"))?.scheduleNextAt;
68
+ const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_ok" });
69
+ const deps = { settingsRepo, chargeAutoTopup: mockCharge };
70
+ await runScheduledTopups(deps);
71
+ const after = (await settingsRepo.getByTenant("t1"))?.scheduleNextAt;
72
+ expect(new Date(after ?? "").getTime()).toBeGreaterThan(new Date(before ?? "").getTime());
73
+ });
74
+ it("advances scheduleNextAt after charge throws an error", async () => {
75
+ const past = "2026-02-20T00:00:00.000Z";
76
+ await settingsRepo.upsert("t1", {
77
+ scheduleEnabled: true,
78
+ scheduleAmount: Credit.fromCents(1000),
79
+ scheduleIntervalHours: 24,
80
+ scheduleNextAt: past,
81
+ });
82
+ const before = (await settingsRepo.getByTenant("t1"))?.scheduleNextAt;
83
+ const mockCharge = vi.fn().mockRejectedValue(new Error("Stripe network error"));
84
+ const deps = { settingsRepo, chargeAutoTopup: mockCharge };
85
+ await runScheduledTopups(deps);
86
+ const after = (await settingsRepo.getByTenant("t1"))?.scheduleNextAt;
87
+ expect(new Date(after ?? "").getTime()).toBeGreaterThan(new Date(before ?? "").getTime());
88
+ });
89
+ it("disables schedule after 3 consecutive failures", async () => {
90
+ const past = "2026-02-20T00:00:00.000Z";
91
+ await settingsRepo.upsert("t1", {
92
+ scheduleEnabled: true,
93
+ scheduleAmount: Credit.fromCents(500),
94
+ scheduleNextAt: past,
95
+ });
96
+ await settingsRepo.incrementScheduleFailures("t1");
97
+ await settingsRepo.incrementScheduleFailures("t1");
98
+ const mockCharge = vi.fn().mockResolvedValue({ success: false, error: "declined" });
99
+ const deps = { settingsRepo, chargeAutoTopup: mockCharge };
100
+ await runScheduledTopups(deps);
101
+ expect((await settingsRepo.getByTenant("t1"))?.scheduleEnabled).toBe(false);
102
+ });
103
+ it("skips tenants where checkTenantStatus returns non-null", async () => {
104
+ // Setup: one due tenant that is banned
105
+ await settingsRepo.upsert("t-banned", {
106
+ scheduleEnabled: true,
107
+ scheduleAmount: Credit.fromCents(1000),
108
+ scheduleIntervalHours: 24,
109
+ scheduleNextAt: new Date(Date.now() - 60_000).toISOString(),
110
+ });
111
+ const chargeAutoTopup = vi.fn();
112
+ const checkTenantStatus = vi.fn().mockResolvedValue({ error: "account_banned", message: "banned" });
113
+ const result = await runScheduledTopups({
114
+ settingsRepo,
115
+ chargeAutoTopup,
116
+ checkTenantStatus,
117
+ });
118
+ expect(chargeAutoTopup).not.toHaveBeenCalled();
119
+ // The tenant should still be processed (counter incremented) but skipped
120
+ expect(result.processed).toBe(1);
121
+ expect(result.failed).toContain("t-banned");
122
+ });
123
+ it("processes multiple tenants independently", async () => {
124
+ const past = "2026-02-20T00:00:00.000Z";
125
+ await settingsRepo.upsert("t1", {
126
+ scheduleEnabled: true,
127
+ scheduleAmount: Credit.fromCents(500),
128
+ scheduleNextAt: past,
129
+ });
130
+ await settingsRepo.upsert("t2", {
131
+ scheduleEnabled: true,
132
+ scheduleAmount: Credit.fromCents(1000),
133
+ scheduleNextAt: past,
134
+ });
135
+ const mockCharge = vi
136
+ .fn()
137
+ .mockResolvedValueOnce({ success: true })
138
+ .mockResolvedValueOnce({ success: false, error: "declined" });
139
+ const deps = { settingsRepo, chargeAutoTopup: mockCharge };
140
+ const result = await runScheduledTopups(deps);
141
+ expect(result.processed).toBe(2);
142
+ expect(result.succeeded).toContain("t1");
143
+ expect(result.failed).toContain("t2");
144
+ });
145
+ });
@@ -0,0 +1,54 @@
1
+ import type { PlatformDb } from "../db/index.js";
2
+ import { Credit } from "./credit.js";
3
+ export declare const ALLOWED_TOPUP_AMOUNTS: readonly [500, 1000, 2000, 5000, 10000, 20000, 50000];
4
+ export declare const ALLOWED_THRESHOLDS: readonly [200, 500, 1000];
5
+ export declare const ALLOWED_SCHEDULE_INTERVALS: readonly ["daily", "weekly", "monthly"];
6
+ export declare function computeNextScheduleAt(interval: "daily" | "weekly" | "monthly" | null, now?: Date): string | null;
7
+ export interface AutoTopupSettings {
8
+ tenantId: string;
9
+ usageEnabled: boolean;
10
+ usageThreshold: Credit;
11
+ usageTopup: Credit;
12
+ usageConsecutiveFailures: number;
13
+ usageChargeInFlight: boolean;
14
+ scheduleEnabled: boolean;
15
+ scheduleAmount: Credit;
16
+ scheduleIntervalHours: number;
17
+ scheduleNextAt: string | null;
18
+ scheduleConsecutiveFailures: number;
19
+ createdAt: string;
20
+ updatedAt: string;
21
+ }
22
+ export interface IAutoTopupSettingsRepository {
23
+ getByTenant(tenantId: string): Promise<AutoTopupSettings | null>;
24
+ upsert(tenantId: string, settings: Partial<Omit<AutoTopupSettings, "tenantId" | "createdAt" | "updatedAt">>): Promise<void>;
25
+ setUsageChargeInFlight(tenantId: string, inFlight: boolean): Promise<void>;
26
+ /** Atomically set usage_charge_in_flight = true IFF it is currently false. Returns true if acquired. */
27
+ tryAcquireUsageInFlight(tenantId: string): Promise<boolean>;
28
+ incrementUsageFailures(tenantId: string): Promise<number>;
29
+ resetUsageFailures(tenantId: string): Promise<void>;
30
+ disableUsage(tenantId: string): Promise<void>;
31
+ incrementScheduleFailures(tenantId: string): Promise<number>;
32
+ resetScheduleFailures(tenantId: string): Promise<void>;
33
+ disableSchedule(tenantId: string): Promise<void>;
34
+ advanceScheduleNextAt(tenantId: string): Promise<void>;
35
+ listDueScheduled(now: string): Promise<AutoTopupSettings[]>;
36
+ getMaxConsecutiveFailures(): Promise<number>;
37
+ }
38
+ export declare class DrizzleAutoTopupSettingsRepository implements IAutoTopupSettingsRepository {
39
+ private readonly db;
40
+ constructor(db: PlatformDb);
41
+ getByTenant(tenantId: string): Promise<AutoTopupSettings | null>;
42
+ upsert(tenantId: string, settings: Partial<Omit<AutoTopupSettings, "tenantId" | "createdAt" | "updatedAt">>): Promise<void>;
43
+ setUsageChargeInFlight(tenantId: string, inFlight: boolean): Promise<void>;
44
+ tryAcquireUsageInFlight(tenantId: string): Promise<boolean>;
45
+ incrementUsageFailures(tenantId: string): Promise<number>;
46
+ resetUsageFailures(tenantId: string): Promise<void>;
47
+ disableUsage(tenantId: string): Promise<void>;
48
+ incrementScheduleFailures(tenantId: string): Promise<number>;
49
+ resetScheduleFailures(tenantId: string): Promise<void>;
50
+ disableSchedule(tenantId: string): Promise<void>;
51
+ advanceScheduleNextAt(tenantId: string): Promise<void>;
52
+ getMaxConsecutiveFailures(): Promise<number>;
53
+ listDueScheduled(now: string): Promise<AutoTopupSettings[]>;
54
+ }