alepha 0.20.5 → 0.20.7

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 (367) hide show
  1. package/AGENTS.md +0 -1
  2. package/CLAUDE.md +0 -1
  3. package/assets/agents-template.md +0 -1
  4. package/dist/api/audits/index.browser.js +1 -0
  5. package/dist/api/audits/index.browser.js.map +1 -1
  6. package/dist/api/audits/index.d.ts +701 -654
  7. package/dist/api/audits/index.d.ts.map +1 -1
  8. package/dist/api/audits/index.js +24 -1
  9. package/dist/api/audits/index.js.map +1 -1
  10. package/dist/api/files/index.browser.js +1 -0
  11. package/dist/api/files/index.browser.js.map +1 -1
  12. package/dist/api/files/index.d.ts +193 -166
  13. package/dist/api/files/index.d.ts.map +1 -1
  14. package/dist/api/files/index.js +52 -0
  15. package/dist/api/files/index.js.map +1 -1
  16. package/dist/api/jobs/index.browser.js +40 -14
  17. package/dist/api/jobs/index.browser.js.map +1 -1
  18. package/dist/api/jobs/index.d.ts +639 -333
  19. package/dist/api/jobs/index.d.ts.map +1 -1
  20. package/dist/api/jobs/index.js +495 -162
  21. package/dist/api/jobs/index.js.map +1 -1
  22. package/dist/api/keys/index.d.ts +222 -188
  23. package/dist/api/keys/index.d.ts.map +1 -1
  24. package/dist/api/keys/index.js +54 -0
  25. package/dist/api/keys/index.js.map +1 -1
  26. package/dist/api/notifications/index.d.ts +265 -236
  27. package/dist/api/notifications/index.d.ts.map +1 -1
  28. package/dist/api/notifications/index.js +55 -13
  29. package/dist/api/notifications/index.js.map +1 -1
  30. package/dist/api/organizations/index.d.ts +100 -97
  31. package/dist/api/organizations/index.d.ts.map +1 -1
  32. package/dist/api/organizations/index.js.map +1 -1
  33. package/dist/api/parameters/index.d.ts +332 -314
  34. package/dist/api/parameters/index.d.ts.map +1 -1
  35. package/dist/api/parameters/index.js +37 -0
  36. package/dist/api/parameters/index.js.map +1 -1
  37. package/dist/api/payments/index.d.ts +431 -376
  38. package/dist/api/payments/index.d.ts.map +1 -1
  39. package/dist/api/payments/index.js +202 -87
  40. package/dist/api/payments/index.js.map +1 -1
  41. package/dist/api/subscriptions/index.d.ts +1695 -0
  42. package/dist/api/subscriptions/index.d.ts.map +1 -0
  43. package/dist/api/subscriptions/index.js +1919 -0
  44. package/dist/api/subscriptions/index.js.map +1 -0
  45. package/dist/api/users/index.d.ts +1001 -844
  46. package/dist/api/users/index.d.ts.map +1 -1
  47. package/dist/api/users/index.js +237 -28
  48. package/dist/api/users/index.js.map +1 -1
  49. package/dist/api/verifications/index.d.ts +123 -122
  50. package/dist/api/verifications/index.d.ts.map +1 -1
  51. package/dist/api/verifications/index.js.map +1 -1
  52. package/dist/batch/index.js.map +1 -1
  53. package/dist/bucket/index.d.ts +21 -2
  54. package/dist/bucket/index.d.ts.map +1 -1
  55. package/dist/bucket/index.js +47 -0
  56. package/dist/bucket/index.js.map +1 -1
  57. package/dist/bucket/index.workerd.js +24 -0
  58. package/dist/bucket/index.workerd.js.map +1 -1
  59. package/dist/cache/core/index.d.ts +134 -7
  60. package/dist/cache/core/index.d.ts.map +1 -1
  61. package/dist/cache/core/index.js +181 -15
  62. package/dist/cache/core/index.js.map +1 -1
  63. package/dist/cache/core/index.workerd.js +181 -15
  64. package/dist/cache/core/index.workerd.js.map +1 -1
  65. package/dist/cache/database/index.d.ts +156 -0
  66. package/dist/cache/database/index.d.ts.map +1 -0
  67. package/dist/cache/database/index.js +266 -0
  68. package/dist/cache/database/index.js.map +1 -0
  69. package/dist/cache/redis/index.d.ts +3 -2
  70. package/dist/cache/redis/index.d.ts.map +1 -1
  71. package/dist/cache/redis/index.js.map +1 -1
  72. package/dist/captcha/index.js.map +1 -1
  73. package/dist/cli/config/index.js.map +1 -1
  74. package/dist/cli/core/index.d.ts +142 -128
  75. package/dist/cli/core/index.d.ts.map +1 -1
  76. package/dist/cli/core/index.js +160 -13
  77. package/dist/cli/core/index.js.map +1 -1
  78. package/dist/cli/devtools/index.d.ts +3 -2
  79. package/dist/cli/devtools/index.d.ts.map +1 -1
  80. package/dist/cli/devtools/index.js.map +1 -1
  81. package/dist/cli/platform/index.d.ts +346 -290
  82. package/dist/cli/platform/index.d.ts.map +1 -1
  83. package/dist/cli/platform/index.js +106 -7
  84. package/dist/cli/platform/index.js.map +1 -1
  85. package/dist/cli/vendor/index.d.ts +12 -11
  86. package/dist/cli/vendor/index.d.ts.map +1 -1
  87. package/dist/cli/vendor/index.js.map +1 -1
  88. package/dist/command/index.d.ts +6 -5
  89. package/dist/command/index.d.ts.map +1 -1
  90. package/dist/command/index.js.map +1 -1
  91. package/dist/core/index.browser.js +1 -1
  92. package/dist/core/index.browser.js.map +1 -1
  93. package/dist/core/index.d.ts +119 -118
  94. package/dist/core/index.d.ts.map +1 -1
  95. package/dist/core/index.js +1 -1
  96. package/dist/core/index.js.map +1 -1
  97. package/dist/core/index.native.js +1 -1
  98. package/dist/core/index.native.js.map +1 -1
  99. package/dist/core/index.workerd.js +1 -1
  100. package/dist/core/index.workerd.js.map +1 -1
  101. package/dist/crypto/index.browser.js.map +1 -1
  102. package/dist/crypto/index.d.ts +3 -2
  103. package/dist/crypto/index.d.ts.map +1 -1
  104. package/dist/crypto/index.js.map +1 -1
  105. package/dist/datetime/index.js.map +1 -1
  106. package/dist/email/brevo/index.js.map +1 -1
  107. package/dist/email/core/index.d.ts +3 -2
  108. package/dist/email/core/index.d.ts.map +1 -1
  109. package/dist/email/core/index.js.map +1 -1
  110. package/dist/email/core/index.workerd.js.map +1 -1
  111. package/dist/email/smtp/index.d.ts +7 -6
  112. package/dist/email/smtp/index.d.ts.map +1 -1
  113. package/dist/email/smtp/index.js.map +1 -1
  114. package/dist/fake/index.js.map +1 -1
  115. package/dist/lock/core/index.d.ts +5 -4
  116. package/dist/lock/core/index.d.ts.map +1 -1
  117. package/dist/lock/core/index.js.map +1 -1
  118. package/dist/lock/redis/index.js.map +1 -1
  119. package/dist/logger/index.d.ts +10 -9
  120. package/dist/logger/index.d.ts.map +1 -1
  121. package/dist/logger/index.js.map +1 -1
  122. package/dist/mcp/index.d.ts +9 -8
  123. package/dist/mcp/index.d.ts.map +1 -1
  124. package/dist/mcp/index.js +1 -1
  125. package/dist/mcp/index.js.map +1 -1
  126. package/dist/orm/core/index.browser.js +9 -3
  127. package/dist/orm/core/index.browser.js.map +1 -1
  128. package/dist/orm/core/index.bun.js +31 -10
  129. package/dist/orm/core/index.bun.js.map +1 -1
  130. package/dist/orm/core/index.d.ts +33 -14
  131. package/dist/orm/core/index.d.ts.map +1 -1
  132. package/dist/orm/core/index.js +31 -10
  133. package/dist/orm/core/index.js.map +1 -1
  134. package/dist/orm/postgres/index.bun.js.map +1 -1
  135. package/dist/orm/postgres/index.d.ts +6 -5
  136. package/dist/orm/postgres/index.d.ts.map +1 -1
  137. package/dist/orm/postgres/index.js.map +1 -1
  138. package/dist/queue/core/index.d.ts +5 -4
  139. package/dist/queue/core/index.d.ts.map +1 -1
  140. package/dist/queue/core/index.js.map +1 -1
  141. package/dist/queue/core/index.workerd.js.map +1 -1
  142. package/dist/queue/redis/index.d.ts +3 -2
  143. package/dist/queue/redis/index.d.ts.map +1 -1
  144. package/dist/queue/redis/index.js.map +1 -1
  145. package/dist/react/auth/index.browser.js.map +1 -1
  146. package/dist/react/auth/index.js.map +1 -1
  147. package/dist/react/core/index.js.map +1 -1
  148. package/dist/react/form/index.d.ts +5 -0
  149. package/dist/react/form/index.d.ts.map +1 -1
  150. package/dist/react/form/index.js +8 -4
  151. package/dist/react/form/index.js.map +1 -1
  152. package/dist/react/head/index.browser.js.map +1 -1
  153. package/dist/react/head/index.js.map +1 -1
  154. package/dist/react/i18n/index.d.ts +2 -1
  155. package/dist/react/i18n/index.d.ts.map +1 -1
  156. package/dist/react/i18n/index.js.map +1 -1
  157. package/dist/react/intro/index.js.map +1 -1
  158. package/dist/react/router/index.browser.js.map +1 -1
  159. package/dist/react/router/index.d.ts +206 -205
  160. package/dist/react/router/index.d.ts.map +1 -1
  161. package/dist/react/router/index.js.map +1 -1
  162. package/dist/react/testing/index.js.map +1 -1
  163. package/dist/react/ui/index.d.ts +11 -11
  164. package/dist/react/ui/index.d.ts.map +1 -1
  165. package/dist/react/ui/index.js.map +1 -1
  166. package/dist/redis/index.bun.js.map +1 -1
  167. package/dist/redis/index.js.map +1 -1
  168. package/dist/retry/index.js.map +1 -1
  169. package/dist/router/index.js.map +1 -1
  170. package/dist/scheduler/index.d.ts +25 -2
  171. package/dist/scheduler/index.d.ts.map +1 -1
  172. package/dist/scheduler/index.js +12 -0
  173. package/dist/scheduler/index.js.map +1 -1
  174. package/dist/scheduler/index.workerd.js +12 -0
  175. package/dist/scheduler/index.workerd.js.map +1 -1
  176. package/dist/security/index.browser.js +29 -1
  177. package/dist/security/index.browser.js.map +1 -1
  178. package/dist/security/index.d.ts +82 -35
  179. package/dist/security/index.d.ts.map +1 -1
  180. package/dist/security/index.js +56 -3
  181. package/dist/security/index.js.map +1 -1
  182. package/dist/server/auth/index.d.ts +163 -158
  183. package/dist/server/auth/index.d.ts.map +1 -1
  184. package/dist/server/auth/index.js +16 -4
  185. package/dist/server/auth/index.js.map +1 -1
  186. package/dist/server/cookies/index.browser.js.map +1 -1
  187. package/dist/server/cookies/index.js.map +1 -1
  188. package/dist/server/core/index.browser.js.map +1 -1
  189. package/dist/server/core/index.d.ts +35 -34
  190. package/dist/server/core/index.d.ts.map +1 -1
  191. package/dist/server/core/index.js.map +1 -1
  192. package/dist/server/cors/index.d.ts +7 -6
  193. package/dist/server/cors/index.d.ts.map +1 -1
  194. package/dist/server/cors/index.js.map +1 -1
  195. package/dist/server/etag/index.js.map +1 -1
  196. package/dist/server/health/index.d.ts +16 -15
  197. package/dist/server/health/index.d.ts.map +1 -1
  198. package/dist/server/health/index.js.map +1 -1
  199. package/dist/server/links/index.browser.js.map +1 -1
  200. package/dist/server/links/index.d.ts +51 -50
  201. package/dist/server/links/index.d.ts.map +1 -1
  202. package/dist/server/links/index.js.map +1 -1
  203. package/dist/server/metrics/index.js.map +1 -1
  204. package/dist/server/proxy/index.js.map +1 -1
  205. package/dist/server/rate-limit/index.d.ts +6 -5
  206. package/dist/server/rate-limit/index.d.ts.map +1 -1
  207. package/dist/server/rate-limit/index.js.map +1 -1
  208. package/dist/server/static/index.js.map +1 -1
  209. package/dist/server/swagger/index.d.ts +2 -1
  210. package/dist/server/swagger/index.d.ts.map +1 -1
  211. package/dist/server/swagger/index.js.map +1 -1
  212. package/dist/sms/index.js.map +1 -1
  213. package/dist/system/index.browser.js.map +1 -1
  214. package/dist/system/index.js.map +1 -1
  215. package/dist/system/index.workerd.js.map +1 -1
  216. package/dist/topic/core/index.js.map +1 -1
  217. package/dist/topic/redis/index.d.ts +3 -2
  218. package/dist/topic/redis/index.d.ts.map +1 -1
  219. package/dist/topic/redis/index.js.map +1 -1
  220. package/package.json +33 -39
  221. package/src/api/audits/controllers/AdminAuditController.ts +29 -0
  222. package/src/api/audits/entities/audits.ts +1 -0
  223. package/src/api/files/controllers/FileController.ts +24 -0
  224. package/src/api/files/entities/files.ts +1 -0
  225. package/src/api/files/services/FileService.ts +41 -0
  226. package/src/api/jobs/__tests__/$job.spec.ts +501 -24
  227. package/src/api/jobs/entities/jobExecutionEntity.ts +4 -3
  228. package/src/api/jobs/index.ts +47 -10
  229. package/src/api/jobs/primitives/$job.ts +22 -9
  230. package/src/api/jobs/providers/DirectJobDispatcher.ts +71 -0
  231. package/src/api/jobs/providers/JobDispatcher.ts +49 -0
  232. package/src/api/jobs/providers/JobProvider.ts +385 -147
  233. package/src/api/jobs/providers/JobQueueProvider.ts +43 -18
  234. package/src/api/jobs/schemas/jobConfigAtom.ts +9 -3
  235. package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +11 -0
  236. package/src/api/jobs/schemas/jobRegistrationSchema.ts +4 -2
  237. package/src/api/jobs/services/JobService.ts +21 -11
  238. package/src/api/keys/controllers/AdminApiKeyController.ts +23 -0
  239. package/src/api/keys/entities/apiKeyEntity.ts +1 -0
  240. package/src/api/keys/services/ApiKeyService.ts +42 -0
  241. package/src/api/notifications/__tests__/AlephaApiNotifications.spec.ts +63 -0
  242. package/src/api/notifications/controllers/AdminNotificationController.ts +48 -1
  243. package/src/api/notifications/index.ts +13 -3
  244. package/src/api/notifications/jobs/NotificationJobs.ts +0 -6
  245. package/src/api/parameters/controllers/AdminParameterController.ts +26 -0
  246. package/src/api/parameters/services/ParameterProvider.ts +18 -0
  247. package/src/api/payments/controllers/MockCheckoutController.ts +146 -0
  248. package/src/api/payments/index.ts +3 -0
  249. package/src/api/payments/providers/MemoryPaymentProvider.ts +9 -4
  250. package/src/api/payments/providers/PaymentProvider.ts +25 -9
  251. package/src/api/payments/services/PaymentService.ts +3 -0
  252. package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
  253. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
  254. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
  255. package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
  256. package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
  257. package/src/api/subscriptions/entities/subscriptions.ts +68 -0
  258. package/src/api/subscriptions/index.ts +133 -0
  259. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
  260. package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
  261. package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
  262. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
  263. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
  264. package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
  265. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
  266. package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
  267. package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
  268. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
  269. package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
  270. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
  271. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
  272. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
  273. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
  274. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
  275. package/src/api/subscriptions/services/BillingService.ts +437 -0
  276. package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
  277. package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
  278. package/src/api/subscriptions/services/UsageService.ts +118 -0
  279. package/src/api/users/__tests__/Registration-emailMode.spec.ts +203 -0
  280. package/src/api/users/__tests__/UsernameSlugger.spec.ts +138 -0
  281. package/src/api/users/atoms/realmAuthSettingsAtom.ts +41 -3
  282. package/src/api/users/controllers/AdminSessionController.ts +29 -0
  283. package/src/api/users/controllers/AdminUserController.ts +32 -0
  284. package/src/api/users/index.ts +3 -0
  285. package/src/api/users/services/CredentialService.ts +5 -0
  286. package/src/api/users/services/RegistrationService.ts +49 -1
  287. package/src/api/users/services/SessionCrudService.ts +16 -0
  288. package/src/api/users/services/SessionService.ts +17 -59
  289. package/src/api/users/services/UsernameSlugger.ts +195 -0
  290. package/src/bucket/primitives/$bucket.ts +21 -0
  291. package/src/bucket/providers/CloudflareR2Provider.ts +15 -0
  292. package/src/bucket/providers/FileStorageProvider.ts +9 -0
  293. package/src/bucket/providers/LocalFileStorageProvider.ts +14 -0
  294. package/src/bucket/providers/MemoryFileStorageProvider.ts +9 -0
  295. package/src/bucket/providers/NodeS3BucketProvider.ts +35 -0
  296. package/src/cache/core/__tests__/$cache.memory.spec.ts +450 -0
  297. package/src/cache/core/__tests__/$cache.swr.spec.ts +394 -0
  298. package/src/cache/core/index.ts +16 -0
  299. package/src/cache/core/primitives/$cache.ts +367 -24
  300. package/src/cache/database/__tests__/DatabaseCacheProvider.behavior.spec.ts +203 -0
  301. package/src/cache/database/__tests__/DatabaseCacheProvider.spec.ts +110 -0
  302. package/src/cache/database/entities/cacheEntries.ts +55 -0
  303. package/src/cache/database/index.ts +36 -0
  304. package/src/cache/database/providers/DatabaseCacheProvider.ts +348 -0
  305. package/src/cli/core/services/ProjectScaffolder.ts +0 -2
  306. package/src/cli/core/tasks/BuildCloudflareTask.ts +33 -3
  307. package/src/cli/core/tasks/BuildSitemapTask.ts +7 -0
  308. package/src/cli/core/tasks/BuildVercelTask.ts +82 -3
  309. package/src/cli/core/templates/agentMd.ts +39 -4
  310. package/src/cli/core/templates/biomeJson.ts +25 -1
  311. package/src/cli/core/templates/saasAdminLayoutTsx.ts +2 -2
  312. package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +117 -0
  313. package/src/cli/platform/__tests__/detectResources.spec.ts +96 -0
  314. package/src/cli/platform/adapters/CloudflareAdapter.ts +104 -7
  315. package/src/cli/platform/atoms/platformOptions.ts +13 -0
  316. package/src/cli/platform/commands/platform.ts +7 -1
  317. package/src/cli/platform/schemas/platform.ts +1 -0
  318. package/src/cli/platform/services/CloudflareApi.ts +61 -0
  319. package/src/cli/platform/services/PlatformOrchestrator.ts +9 -4
  320. package/src/core/__tests__/$module.spec.ts +2 -2
  321. package/src/core/primitives/$module.ts +4 -4
  322. package/src/mcp/providers/McpServerProvider.ts +1 -1
  323. package/src/orm/core/providers/DatabaseTypeProvider.ts +9 -3
  324. package/src/orm/core/providers/drivers/DatabaseProvider.ts +1 -1
  325. package/src/orm/core/schemas/insertSchema.ts +10 -2
  326. package/src/orm/core/services/Repository.ts +27 -7
  327. package/src/react/form/hooks/useFormState.ts +8 -1
  328. package/src/react/form/index.ts +10 -1
  329. package/src/react/form/services/FormModel.ts +9 -3
  330. package/src/scheduler/index.ts +14 -0
  331. package/src/scheduler/providers/CronProvider.ts +13 -0
  332. package/src/security/atoms/currentTenantAtom.ts +34 -0
  333. package/src/security/index.browser.ts +1 -0
  334. package/src/security/index.ts +12 -1
  335. package/src/security/primitives/$issuer.ts +17 -1
  336. package/src/security/providers/SecurityProvider.ts +37 -0
  337. package/src/server/auth/__tests__/validateRedirectUri.spec.ts +78 -0
  338. package/src/server/auth/providers/ServerAuthProvider.ts +21 -5
  339. package/tsconfig.base.json +2 -1
  340. package/dist/react/websocket/index.d.ts +0 -117
  341. package/dist/react/websocket/index.d.ts.map +0 -1
  342. package/dist/react/websocket/index.js +0 -108
  343. package/dist/react/websocket/index.js.map +0 -1
  344. package/dist/websocket/index.browser.js +0 -844
  345. package/dist/websocket/index.browser.js.map +0 -1
  346. package/dist/websocket/index.d.ts +0 -876
  347. package/dist/websocket/index.d.ts.map +0 -1
  348. package/dist/websocket/index.js +0 -1175
  349. package/dist/websocket/index.js.map +0 -1
  350. package/src/react/websocket/hooks/useRoom.tsx +0 -251
  351. package/src/react/websocket/index.ts +0 -7
  352. package/src/websocket/__tests__/$channel.spec.ts +0 -30
  353. package/src/websocket/__tests__/$websocket-new.spec.ts +0 -195
  354. package/src/websocket/__tests__/RoomManager.spec.ts +0 -146
  355. package/src/websocket/__tests__/websocket-integration.spec.ts +0 -951
  356. package/src/websocket/errors/WebSocketError.ts +0 -34
  357. package/src/websocket/index.browser.ts +0 -25
  358. package/src/websocket/index.shared.ts +0 -8
  359. package/src/websocket/index.ts +0 -85
  360. package/src/websocket/interfaces/WebSocketInterfaces.ts +0 -252
  361. package/src/websocket/primitives/$channel.ts +0 -131
  362. package/src/websocket/primitives/$websocket.ts +0 -107
  363. package/src/websocket/providers/NodeWebSocketServerProvider.ts +0 -617
  364. package/src/websocket/providers/WebSocketServerProvider.ts +0 -56
  365. package/src/websocket/services/RoomManager.ts +0 -160
  366. package/src/websocket/services/WebSocketClient.ts +0 -642
  367. package/src/websocket/services/WebSocketTopicService.ts +0 -108
@@ -0,0 +1,437 @@
1
+ import { $hook, $inject, Alepha } from "alepha";
2
+ import { PaymentService } from "alepha/api/payments";
3
+ import { DateTimeProvider } from "alepha/datetime";
4
+ import { $logger } from "alepha/logger";
5
+ import { $repository } from "alepha/orm";
6
+ import type { SubscriptionEventEntity } from "../entities/subscriptionEvents.ts";
7
+ import { subscriptionEvents } from "../entities/subscriptionEvents.ts";
8
+ import {
9
+ type SubscriptionEntity,
10
+ subscriptions,
11
+ } from "../entities/subscriptions.ts";
12
+ import { SubscriptionConfig } from "./SubscriptionConfig.ts";
13
+
14
+ // -----------------------------------------------------------------------------------------------------------------
15
+
16
+ interface PaymentEvent {
17
+ intentId: string;
18
+ amount: number;
19
+ currency: string;
20
+ metadata?: unknown;
21
+ }
22
+
23
+ // -----------------------------------------------------------------------------------------------------------------
24
+
25
+ interface EventContext {
26
+ previousStatus?: string;
27
+ newStatus?: string;
28
+ previousPlanId?: string;
29
+ newPlanId?: string;
30
+ paymentIntentId?: string;
31
+ amount?: number;
32
+ currency?: string;
33
+ triggeredBy?: string;
34
+ userId?: string;
35
+ note?: string;
36
+ }
37
+
38
+ // -----------------------------------------------------------------------------------------------------------------
39
+
40
+ export class BillingService {
41
+ protected readonly alepha = $inject(Alepha);
42
+ protected readonly log = $logger();
43
+ protected readonly dateTime = $inject(DateTimeProvider);
44
+ protected readonly subscriptionRepo = $repository(subscriptions);
45
+ protected readonly eventRepo = $repository(subscriptionEvents);
46
+ protected readonly paymentService = $inject(PaymentService);
47
+ protected readonly config = $inject(SubscriptionConfig);
48
+
49
+ // ---------------------------------------------------------------------------------------------------------------
50
+ // Payment hook listeners
51
+ // ---------------------------------------------------------------------------------------------------------------
52
+
53
+ /**
54
+ * React to successful payment capture.
55
+ * Routes to the appropriate handler based on subscription status.
56
+ */
57
+ protected readonly onPaymentCaptured = $hook({
58
+ on: "payments:captured",
59
+ handler: async (event) => {
60
+ const sub = await this.findByPaymentIntent(event.intentId);
61
+ if (!sub) return;
62
+
63
+ if (sub.status === "trialing") {
64
+ await this.activate(sub, event);
65
+ } else if (sub.status === "active") {
66
+ await this.renew(sub, event);
67
+ } else if (sub.status === "past_due") {
68
+ await this.recoverFromDunning(sub, event);
69
+ } else if (sub.status === "suspended") {
70
+ await this.reactivateFromPayment(sub, event);
71
+ }
72
+ },
73
+ });
74
+
75
+ /**
76
+ * React to failed payment.
77
+ * Starts or advances the dunning flow.
78
+ */
79
+ protected readonly onPaymentFailed = $hook({
80
+ on: "payments:failed",
81
+ handler: async (event) => {
82
+ const sub = await this.findByPaymentIntent(event.intentId);
83
+ if (!sub) return;
84
+
85
+ await this.handlePaymentFailure(sub, event);
86
+ },
87
+ });
88
+
89
+ // ---------------------------------------------------------------------------------------------------------------
90
+ // Lookup
91
+ // ---------------------------------------------------------------------------------------------------------------
92
+
93
+ /**
94
+ * Find a subscription by its last payment intent ID.
95
+ * Returns null if no subscription matches.
96
+ */
97
+ protected async findByPaymentIntent(
98
+ intentId: string,
99
+ ): Promise<SubscriptionEntity | null> {
100
+ const sub = await this.subscriptionRepo.findOne({
101
+ where: { lastPaymentIntentId: { eq: intentId } },
102
+ });
103
+ return sub ?? null;
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------------------------------------------
107
+ // Lifecycle transitions
108
+ // ---------------------------------------------------------------------------------------------------------------
109
+
110
+ /**
111
+ * Trial to active transition.
112
+ * Sets the first paid billing period and records activation events.
113
+ */
114
+ protected async activate(
115
+ sub: SubscriptionEntity,
116
+ event: PaymentEvent,
117
+ ): Promise<void> {
118
+ const orgId = sub.organizationId as string;
119
+ const now = this.dateTime.now();
120
+ const nowISO = now.toISOString();
121
+ const periodEnd = this.computeIntervalEnd(nowISO, sub.interval);
122
+
123
+ await this.subscriptionRepo.updateById(sub.id, {
124
+ status: "active",
125
+ lastPaymentAt: nowISO,
126
+ lastPaymentIntentId: event.intentId,
127
+ currentPeriodStart: nowISO,
128
+ currentPeriodEnd: periodEnd,
129
+ nextBillingAt: periodEnd,
130
+ });
131
+
132
+ await this.recordEvent(sub.id, orgId, "trial_ended", {
133
+ previousStatus: "trialing",
134
+ newStatus: "active",
135
+ paymentIntentId: event.intentId,
136
+ amount: event.amount,
137
+ currency: event.currency,
138
+ });
139
+
140
+ await this.recordEvent(sub.id, orgId, "activated", {
141
+ previousStatus: "trialing",
142
+ newStatus: "active",
143
+ paymentIntentId: event.intentId,
144
+ amount: event.amount,
145
+ currency: event.currency,
146
+ });
147
+
148
+ this.log.info("Subscription activated from trial", {
149
+ id: sub.id,
150
+ organizationId: orgId,
151
+ planId: sub.planId,
152
+ });
153
+
154
+ await this.alepha.events.emit("subscription:activated" as any, {
155
+ subscriptionId: sub.id,
156
+ organizationId: orgId,
157
+ planId: sub.planId,
158
+ });
159
+ }
160
+
161
+ // ---------------------------------------------------------------------------------------------------------------
162
+
163
+ /**
164
+ * Active to active cycle renewal.
165
+ * Applies any pending plan change, then advances the billing period.
166
+ */
167
+ protected async renew(
168
+ sub: SubscriptionEntity,
169
+ event: PaymentEvent,
170
+ ): Promise<void> {
171
+ const orgId = sub.organizationId as string;
172
+ let effectivePlanId = sub.planId;
173
+ let effectiveInterval = sub.interval;
174
+
175
+ if (sub.pendingPlanId) {
176
+ effectivePlanId = sub.pendingPlanId;
177
+ effectiveInterval = sub.pendingInterval ?? sub.interval;
178
+
179
+ await this.subscriptionRepo.updateById(sub.id, {
180
+ planId: effectivePlanId,
181
+ interval: effectiveInterval,
182
+ pendingPlanId: undefined,
183
+ pendingInterval: undefined,
184
+ });
185
+
186
+ await this.recordEvent(sub.id, orgId, "plan_changed", {
187
+ previousPlanId: sub.planId,
188
+ newPlanId: effectivePlanId,
189
+ note: `Plan changed on renewal from '${sub.planId}' to '${effectivePlanId}'`,
190
+ });
191
+ }
192
+
193
+ const newPeriodStart = sub.currentPeriodEnd;
194
+ const newPeriodEnd = this.computeIntervalEnd(
195
+ newPeriodStart,
196
+ effectiveInterval,
197
+ );
198
+
199
+ await this.subscriptionRepo.updateById(sub.id, {
200
+ currentPeriodStart: newPeriodStart,
201
+ currentPeriodEnd: newPeriodEnd,
202
+ lastPaymentAt: this.dateTime.now().toISOString(),
203
+ lastPaymentIntentId: event.intentId,
204
+ nextBillingAt: newPeriodEnd,
205
+ });
206
+
207
+ await this.recordEvent(sub.id, orgId, "renewed", {
208
+ paymentIntentId: event.intentId,
209
+ amount: event.amount,
210
+ currency: event.currency,
211
+ });
212
+
213
+ this.log.info("Subscription renewed", {
214
+ id: sub.id,
215
+ organizationId: orgId,
216
+ planId: effectivePlanId,
217
+ periodEnd: newPeriodEnd,
218
+ });
219
+
220
+ await this.alepha.events.emit("subscription:renewed" as any, {
221
+ subscriptionId: sub.id,
222
+ organizationId: orgId,
223
+ planId: effectivePlanId,
224
+ });
225
+ }
226
+
227
+ // ---------------------------------------------------------------------------------------------------------------
228
+
229
+ /**
230
+ * Recover from dunning: past_due to active.
231
+ * Resets all dunning state and records reactivation.
232
+ */
233
+ protected async recoverFromDunning(
234
+ sub: SubscriptionEntity,
235
+ event: PaymentEvent,
236
+ ): Promise<void> {
237
+ const orgId = sub.organizationId as string;
238
+ const nowISO = this.dateTime.now().toISOString();
239
+
240
+ await this.subscriptionRepo.updateById(sub.id, {
241
+ status: "active",
242
+ lastPaymentAt: nowISO,
243
+ lastPaymentIntentId: event.intentId,
244
+ dunningStartedAt: undefined,
245
+ dunningAttempt: 0,
246
+ dunningNextRetryAt: undefined,
247
+ });
248
+
249
+ await this.recordEvent(sub.id, orgId, "reactivated", {
250
+ previousStatus: "past_due",
251
+ newStatus: "active",
252
+ paymentIntentId: event.intentId,
253
+ amount: event.amount,
254
+ currency: event.currency,
255
+ });
256
+
257
+ this.log.info("Subscription recovered from dunning", {
258
+ id: sub.id,
259
+ organizationId: orgId,
260
+ });
261
+
262
+ await this.alepha.events.emit("subscription:reactivated" as any, {
263
+ subscriptionId: sub.id,
264
+ organizationId: orgId,
265
+ planId: sub.planId,
266
+ });
267
+ }
268
+
269
+ // ---------------------------------------------------------------------------------------------------------------
270
+
271
+ /**
272
+ * Reactivate from suspended state after a successful payment.
273
+ * Resets dunning, sets a fresh billing period, and records reactivation.
274
+ */
275
+ protected async reactivateFromPayment(
276
+ sub: SubscriptionEntity,
277
+ event: PaymentEvent,
278
+ ): Promise<void> {
279
+ const orgId = sub.organizationId as string;
280
+ const now = this.dateTime.now();
281
+ const nowISO = now.toISOString();
282
+ const periodEnd = this.computeIntervalEnd(nowISO, sub.interval);
283
+
284
+ await this.subscriptionRepo.updateById(sub.id, {
285
+ status: "active",
286
+ currentPeriodStart: nowISO,
287
+ currentPeriodEnd: periodEnd,
288
+ nextBillingAt: periodEnd,
289
+ lastPaymentAt: nowISO,
290
+ lastPaymentIntentId: event.intentId,
291
+ dunningStartedAt: undefined,
292
+ dunningAttempt: 0,
293
+ dunningNextRetryAt: undefined,
294
+ });
295
+
296
+ await this.recordEvent(sub.id, orgId, "reactivated", {
297
+ previousStatus: "suspended",
298
+ newStatus: "active",
299
+ paymentIntentId: event.intentId,
300
+ amount: event.amount,
301
+ currency: event.currency,
302
+ });
303
+
304
+ this.log.info("Subscription reactivated from suspended", {
305
+ id: sub.id,
306
+ organizationId: orgId,
307
+ planId: sub.planId,
308
+ });
309
+
310
+ await this.alepha.events.emit("subscription:reactivated" as any, {
311
+ subscriptionId: sub.id,
312
+ organizationId: orgId,
313
+ planId: sub.planId,
314
+ });
315
+ }
316
+
317
+ // ---------------------------------------------------------------------------------------------------------------
318
+
319
+ /**
320
+ * Handle a failed payment: start or advance dunning.
321
+ * Updates dunning state and transitions to past_due if needed.
322
+ */
323
+ protected async handlePaymentFailure(
324
+ sub: SubscriptionEntity,
325
+ event: PaymentEvent,
326
+ ): Promise<void> {
327
+ const orgId = sub.organizationId as string;
328
+ const now = this.dateTime.now();
329
+ const nowISO = now.toISOString();
330
+ const settings = await this.config.getSettings();
331
+ const schedule = settings.dunningSchedule;
332
+
333
+ let attempt: number;
334
+
335
+ if (sub.dunningAttempt === 0) {
336
+ attempt = 1;
337
+ await this.subscriptionRepo.updateById(sub.id, {
338
+ dunningStartedAt: nowISO,
339
+ dunningAttempt: attempt,
340
+ });
341
+ } else {
342
+ attempt = sub.dunningAttempt + 1;
343
+ await this.subscriptionRepo.updateById(sub.id, {
344
+ dunningAttempt: attempt,
345
+ });
346
+ }
347
+
348
+ if (sub.status !== "past_due") {
349
+ await this.subscriptionRepo.updateById(sub.id, {
350
+ status: "past_due",
351
+ });
352
+
353
+ await this.recordEvent(sub.id, orgId, "past_due", {
354
+ previousStatus: sub.status,
355
+ newStatus: "past_due",
356
+ paymentIntentId: event.intentId,
357
+ });
358
+ }
359
+
360
+ const scheduleDays = schedule[attempt - 1];
361
+
362
+ if (scheduleDays !== undefined) {
363
+ const nextRetry = now.add(scheduleDays, "days").toISOString();
364
+
365
+ await this.subscriptionRepo.updateById(sub.id, {
366
+ dunningNextRetryAt: nextRetry,
367
+ });
368
+ } else {
369
+ await this.subscriptionRepo.updateById(sub.id, {
370
+ dunningNextRetryAt: undefined,
371
+ });
372
+ }
373
+
374
+ await this.recordEvent(sub.id, orgId, "payment_failed", {
375
+ paymentIntentId: event.intentId,
376
+ amount: event.amount,
377
+ currency: event.currency,
378
+ note: `Dunning attempt ${attempt}/${schedule.length}`,
379
+ });
380
+
381
+ this.log.warn("Subscription payment failed", {
382
+ id: sub.id,
383
+ organizationId: orgId,
384
+ attempt,
385
+ maxAttempts: schedule.length,
386
+ });
387
+
388
+ await this.alepha.events.emit("subscription:payment_failed" as any, {
389
+ subscriptionId: sub.id,
390
+ organizationId: orgId,
391
+ planId: sub.planId,
392
+ attempt,
393
+ });
394
+ }
395
+
396
+ // ---------------------------------------------------------------------------------------------------------------
397
+ // Helpers
398
+ // ---------------------------------------------------------------------------------------------------------------
399
+
400
+ /**
401
+ * Compute the end of a billing interval from a start date.
402
+ */
403
+ protected computeIntervalEnd(
404
+ start: string,
405
+ interval: "monthly" | "yearly",
406
+ ): string {
407
+ const startDate = this.dateTime.of(start);
408
+ const unit = interval === "monthly" ? "months" : "years";
409
+ return startDate.add(1, unit).toISOString();
410
+ }
411
+
412
+ /**
413
+ * Record a subscription event in the event log.
414
+ */
415
+ protected async recordEvent(
416
+ subscriptionId: string,
417
+ organizationId: string,
418
+ type: SubscriptionEventEntity["type"],
419
+ context?: EventContext,
420
+ ): Promise<void> {
421
+ await this.eventRepo.create({
422
+ subscriptionId,
423
+ organizationId,
424
+ type,
425
+ previousStatus: context?.previousStatus,
426
+ newStatus: context?.newStatus,
427
+ previousPlanId: context?.previousPlanId,
428
+ newPlanId: context?.newPlanId,
429
+ paymentIntentId: context?.paymentIntentId,
430
+ amount: context?.amount,
431
+ currency: context?.currency,
432
+ triggeredBy: context?.triggeredBy,
433
+ userId: context?.userId,
434
+ note: context?.note,
435
+ });
436
+ }
437
+ }
@@ -0,0 +1,56 @@
1
+ import { t } from "alepha";
2
+ import { $parameter } from "alepha/api/parameters";
3
+ import { BadRequestError } from "alepha/server";
4
+ import {
5
+ type PlanDefinition,
6
+ planDefinitionSchema,
7
+ } from "../schemas/planDefinitionSchema.ts";
8
+ import {
9
+ type SubscriptionSettings,
10
+ subscriptionSettingsSchema,
11
+ } from "../schemas/subscriptionSettingsSchema.ts";
12
+
13
+ export class SubscriptionConfig {
14
+ protected readonly plans = $parameter({
15
+ name: "subscriptions.plans",
16
+ description: "Subscription plan definitions",
17
+ schema: t.object({ plans: t.array(planDefinitionSchema) }),
18
+ default: { plans: [] },
19
+ });
20
+
21
+ protected readonly settings = $parameter({
22
+ name: "subscriptions.settings",
23
+ description: "Global subscription settings",
24
+ schema: subscriptionSettingsSchema,
25
+ default: {
26
+ trialDays: 14,
27
+ gracePeriodDays: 7,
28
+ dunningSchedule: [1, 3, 5, 7],
29
+ cancelAtPeriodEnd: true,
30
+ prorateOnChange: true,
31
+ },
32
+ });
33
+
34
+ public async getPlans(): Promise<PlanDefinition[]> {
35
+ return (await this.plans.get()).plans;
36
+ }
37
+
38
+ public async getSettings(): Promise<SubscriptionSettings> {
39
+ return this.settings.get();
40
+ }
41
+
42
+ public async getPlan(planId: string): Promise<PlanDefinition> {
43
+ const plans = await this.getPlans();
44
+ const plan = plans.find((p) => p.id === planId);
45
+ if (!plan) throw new BadRequestError(`Plan '${planId}' not found`);
46
+ return plan;
47
+ }
48
+
49
+ public async getPlanPricing(planId: string, interval: "monthly" | "yearly") {
50
+ const plan = await this.getPlan(planId);
51
+ const pricing = plan.pricing.find((p) => p.interval === interval);
52
+ if (!pricing)
53
+ throw new BadRequestError(`No ${interval} pricing for plan '${planId}'`);
54
+ return pricing;
55
+ }
56
+ }