alepha 0.19.3 → 0.19.4

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 (215) hide show
  1. package/assets/swagger-ui/swagger-ui-bundle.js +1 -1
  2. package/dist/api/audits/index.d.ts +8 -8
  3. package/dist/api/invitations/index.d.ts +790 -0
  4. package/dist/api/invitations/index.d.ts.map +1 -0
  5. package/dist/api/invitations/index.js +665 -0
  6. package/dist/api/invitations/index.js.map +1 -0
  7. package/dist/api/jobs/index.browser.js +8 -9
  8. package/dist/api/jobs/index.browser.js.map +1 -1
  9. package/dist/api/jobs/index.d.ts +99 -43
  10. package/dist/api/jobs/index.d.ts.map +1 -1
  11. package/dist/api/jobs/index.js +257 -40
  12. package/dist/api/jobs/index.js.map +1 -1
  13. package/dist/api/keys/index.d.ts +5 -5
  14. package/dist/api/notifications/index.browser.js +0 -1
  15. package/dist/api/notifications/index.browser.js.map +1 -1
  16. package/dist/api/notifications/index.d.ts +3 -3
  17. package/dist/api/notifications/index.d.ts.map +1 -1
  18. package/dist/api/notifications/index.js +0 -1
  19. package/dist/api/notifications/index.js.map +1 -1
  20. package/dist/api/parameters/index.browser.js +112 -1
  21. package/dist/api/parameters/index.browser.js.map +1 -1
  22. package/dist/api/parameters/index.d.ts +90 -3
  23. package/dist/api/parameters/index.d.ts.map +1 -1
  24. package/dist/api/parameters/index.js +79 -12
  25. package/dist/api/parameters/index.js.map +1 -1
  26. package/dist/{billing → api/payments}/index.d.ts +67 -49
  27. package/dist/api/payments/index.d.ts.map +1 -0
  28. package/dist/{billing → api/payments}/index.js +108 -74
  29. package/dist/api/payments/index.js.map +1 -0
  30. package/dist/api/subscriptions/index.d.ts +1692 -0
  31. package/dist/api/subscriptions/index.d.ts.map +1 -0
  32. package/dist/api/subscriptions/index.js +1870 -0
  33. package/dist/api/subscriptions/index.js.map +1 -0
  34. package/dist/api/users/index.d.ts +18 -2
  35. package/dist/api/users/index.d.ts.map +1 -1
  36. package/dist/api/users/index.js +167 -34
  37. package/dist/api/users/index.js.map +1 -1
  38. package/dist/api/verifications/index.d.ts +13 -13
  39. package/dist/api/workflows/index.browser.js +246 -0
  40. package/dist/api/workflows/index.browser.js.map +1 -0
  41. package/dist/api/workflows/index.d.ts +1618 -0
  42. package/dist/api/workflows/index.d.ts.map +1 -0
  43. package/dist/api/workflows/index.js +1504 -0
  44. package/dist/api/workflows/index.js.map +1 -0
  45. package/dist/cli/core/index.d.ts +44 -28
  46. package/dist/cli/core/index.d.ts.map +1 -1
  47. package/dist/cli/core/index.js +16 -61
  48. package/dist/cli/core/index.js.map +1 -1
  49. package/dist/cli/vendor/index.d.ts +31 -8
  50. package/dist/cli/vendor/index.d.ts.map +1 -1
  51. package/dist/cli/vendor/index.js +79 -24
  52. package/dist/cli/vendor/index.js.map +1 -1
  53. package/dist/core/index.browser.js +21 -2
  54. package/dist/core/index.browser.js.map +1 -1
  55. package/dist/core/index.d.ts +33 -2
  56. package/dist/core/index.d.ts.map +1 -1
  57. package/dist/core/index.js +21 -2
  58. package/dist/core/index.js.map +1 -1
  59. package/dist/core/index.native.js +21 -2
  60. package/dist/core/index.native.js.map +1 -1
  61. package/dist/core/index.workerd.js +21 -2
  62. package/dist/core/index.workerd.js.map +1 -1
  63. package/dist/email/smtp/index.js +24 -8
  64. package/dist/email/smtp/index.js.map +1 -1
  65. package/dist/orm/core/index.browser.js +0 -18
  66. package/dist/orm/core/index.browser.js.map +1 -1
  67. package/dist/orm/core/index.bun.js +0 -17
  68. package/dist/orm/core/index.bun.js.map +1 -1
  69. package/dist/orm/core/index.d.ts +1 -13
  70. package/dist/orm/core/index.d.ts.map +1 -1
  71. package/dist/orm/core/index.js +0 -17
  72. package/dist/orm/core/index.js.map +1 -1
  73. package/dist/orm/postgres/index.bun.js +3 -3
  74. package/dist/orm/postgres/index.bun.js.map +1 -1
  75. package/dist/orm/postgres/index.d.ts.map +1 -1
  76. package/dist/orm/postgres/index.js +3 -3
  77. package/dist/orm/postgres/index.js.map +1 -1
  78. package/dist/react/router/index.browser.js +25 -3
  79. package/dist/react/router/index.browser.js.map +1 -1
  80. package/dist/react/router/index.d.ts +16 -1
  81. package/dist/react/router/index.d.ts.map +1 -1
  82. package/dist/react/router/index.js +25 -3
  83. package/dist/react/router/index.js.map +1 -1
  84. package/dist/security/index.d.ts +28 -0
  85. package/dist/security/index.d.ts.map +1 -1
  86. package/dist/security/index.js +28 -0
  87. package/dist/security/index.js.map +1 -1
  88. package/package.json +37 -20
  89. package/src/api/invitations/__tests__/InvitationService.spec.ts +439 -0
  90. package/src/api/invitations/controllers/AdminInvitationController.ts +86 -0
  91. package/src/api/invitations/controllers/InvitationController.ts +84 -0
  92. package/src/api/invitations/entities/invitations.ts +33 -0
  93. package/src/api/invitations/index.ts +65 -0
  94. package/src/api/invitations/jobs/InvitationJobs.ts +37 -0
  95. package/src/api/invitations/providers/InvitationProvider.ts +45 -0
  96. package/src/api/invitations/schemas/createInvitationSchema.ts +12 -0
  97. package/src/api/invitations/schemas/invitationConfigAtom.ts +20 -0
  98. package/src/api/invitations/schemas/invitationQuerySchema.ts +15 -0
  99. package/src/api/invitations/schemas/invitationResourceSchema.ts +6 -0
  100. package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +22 -0
  101. package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +10 -0
  102. package/src/api/invitations/services/InvitationService.ts +556 -0
  103. package/src/api/jobs/__tests__/$job.spec.ts +876 -0
  104. package/src/api/jobs/controllers/AdminJobController.ts +44 -0
  105. package/src/api/jobs/entities/jobExecutionEntity.ts +0 -2
  106. package/src/api/jobs/index.ts +0 -3
  107. package/src/api/jobs/primitives/$job.ts +22 -11
  108. package/src/api/jobs/providers/JobProvider.ts +229 -19
  109. package/src/api/jobs/schemas/jobConfigAtom.ts +4 -0
  110. package/src/api/jobs/schemas/jobCronInfoSchema.ts +1 -0
  111. package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +0 -1
  112. package/src/api/jobs/schemas/jobQueueDepthSchema.ts +1 -0
  113. package/src/api/jobs/schemas/jobRegistrationSchema.ts +1 -6
  114. package/src/api/jobs/services/JobService.ts +51 -12
  115. package/src/api/notifications/schemas/notificationQuerySchema.ts +0 -1
  116. package/src/api/parameters/__tests__/$parameter.spec.ts +327 -0
  117. package/src/api/parameters/controllers/AdminParameterController.ts +29 -3
  118. package/src/api/parameters/index.browser.ts +12 -0
  119. package/src/api/parameters/primitives/$parameter.ts +20 -3
  120. package/src/api/parameters/services/ParameterProvider.ts +48 -7
  121. package/src/{billing → api/payments}/__tests__/PaymentMethodService.spec.ts +32 -6
  122. package/src/api/payments/__tests__/PaymentService.spec.ts +279 -0
  123. package/src/{billing/controllers/AdminBillingController.ts → api/payments/controllers/AdminPaymentController.ts} +26 -21
  124. package/src/{billing/controllers/BillingController.ts → api/payments/controllers/PaymentController.ts} +23 -11
  125. package/src/{billing → api/payments}/entities/paymentIntents.ts +1 -0
  126. package/src/{billing/errors/BillingError.ts → api/payments/errors/PaymentError.ts} +1 -1
  127. package/src/{billing → api/payments}/index.ts +31 -25
  128. package/src/{billing/providers/MemoryBillingProvider.ts → api/payments/providers/MemoryPaymentProvider.ts} +4 -4
  129. package/src/{billing/providers/BillingProvider.ts → api/payments/providers/PaymentProvider.ts} +9 -2
  130. package/src/{billing → api/payments}/services/PaymentMethodService.ts +5 -5
  131. package/src/{billing/services/BillingService.ts → api/payments/services/PaymentService.ts} +94 -18
  132. package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
  133. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
  134. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
  135. package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
  136. package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
  137. package/src/api/subscriptions/entities/subscriptions.ts +68 -0
  138. package/src/api/subscriptions/index.ts +144 -0
  139. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
  140. package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
  141. package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
  142. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
  143. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
  144. package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
  145. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
  146. package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
  147. package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
  148. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
  149. package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
  150. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
  151. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
  152. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
  153. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
  154. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
  155. package/src/api/subscriptions/services/BillingService.ts +437 -0
  156. package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
  157. package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
  158. package/src/api/subscriptions/services/UsageService.ts +118 -0
  159. package/src/api/users/__tests__/AdminUserController.spec.ts +80 -1
  160. package/src/api/users/__tests__/CredentialService.spec.ts +177 -0
  161. package/src/api/users/__tests__/EmailVerification.spec.ts +29 -18
  162. package/src/api/users/__tests__/PasswordReset.spec.ts +3 -0
  163. package/src/api/users/__tests__/RegistrationService.spec.ts +148 -1
  164. package/src/api/users/__tests__/SessionService.spec.ts +142 -1
  165. package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -1
  166. package/src/api/users/controllers/UserController.ts +3 -8
  167. package/src/api/users/notifications/UserNotifications.ts +23 -0
  168. package/src/api/users/schemas/loginSchema.ts +1 -1
  169. package/src/api/users/services/CredentialService.ts +51 -4
  170. package/src/api/users/services/RegistrationService.ts +38 -9
  171. package/src/api/users/services/SessionService.ts +62 -9
  172. package/src/api/users/services/UserService.ts +21 -12
  173. package/src/api/workflows/__tests__/$workflow.spec.ts +616 -0
  174. package/src/api/workflows/controllers/AdminWorkflowController.ts +191 -0
  175. package/src/api/workflows/entities/workflowExecutions.ts +74 -0
  176. package/src/api/workflows/entities/workflowStepExecutions.ts +74 -0
  177. package/src/api/workflows/entities/workflowStepLogs.ts +13 -0
  178. package/src/api/workflows/index.browser.ts +22 -0
  179. package/src/api/workflows/index.ts +124 -0
  180. package/src/api/workflows/jobs/WorkflowJobs.ts +77 -0
  181. package/src/api/workflows/primitives/$workflow.ts +202 -0
  182. package/src/api/workflows/providers/WorkflowProvider.ts +1284 -0
  183. package/src/api/workflows/schemas/workflowActivitySchema.ts +15 -0
  184. package/src/api/workflows/schemas/workflowConfigAtom.ts +51 -0
  185. package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +18 -0
  186. package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +26 -0
  187. package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +30 -0
  188. package/src/api/workflows/schemas/workflowRegistrationSchema.ts +26 -0
  189. package/src/api/workflows/schemas/workflowStatsSchema.ts +16 -0
  190. package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +15 -0
  191. package/src/api/workflows/services/WorkflowService.ts +382 -0
  192. package/src/cli/core/templates/webAppRouterTs.ts +5 -58
  193. package/src/cli/vendor/__tests__/VendorService.spec.ts +283 -178
  194. package/src/cli/vendor/services/VendorService.ts +126 -27
  195. package/src/core/__tests__/TypeProvider.spec.ts +4 -2
  196. package/src/core/providers/SchemaValidator.ts +1 -1
  197. package/src/core/providers/TypeProvider.ts +46 -3
  198. package/src/orm/__tests__/enums.spec.ts +22 -29
  199. package/src/orm/__tests__/orm-showcase-tests.ts +430 -0
  200. package/src/orm/__tests__/orm-showcase.spec.ts +167 -0
  201. package/src/orm/core/providers/DatabaseTypeProvider.ts +0 -29
  202. package/src/orm/postgres/services/PostgresModelBuilder.ts +3 -6
  203. package/src/react/router/__tests__/$page.browser.spec.tsx +157 -0
  204. package/src/react/router/providers/ReactBrowserProvider.ts +39 -0
  205. package/src/react/router/providers/ReactBrowserRouterProvider.ts +22 -0
  206. package/src/security/__tests__/$secure-combinations.spec.ts +945 -0
  207. package/src/security/primitives/$secure.ts +28 -0
  208. package/dist/billing/index.d.ts.map +0 -1
  209. package/dist/billing/index.js.map +0 -1
  210. package/src/billing/__tests__/BillingService.spec.ts +0 -136
  211. /package/src/{billing → api/payments}/entities/paymentMethods.ts +0 -0
  212. /package/src/{billing → api/payments}/entities/refunds.ts +0 -0
  213. /package/src/{billing → api/payments}/schemas/intentSchemas.ts +0 -0
  214. /package/src/{billing → api/payments}/schemas/paymentMethodSchemas.ts +0 -0
  215. /package/src/{billing → api/payments}/schemas/refundSchemas.ts +0 -0
@@ -0,0 +1,54 @@
1
+ import { type Static, t } from "alepha";
2
+ import { $entity, db } from "alepha/orm";
3
+ import { subscriptions } from "./subscriptions.ts";
4
+
5
+ export const subscriptionEvents = $entity({
6
+ name: "subscription_events",
7
+ schema: t.object({
8
+ id: db.primaryKey(t.uuid()),
9
+ createdAt: db.createdAt(),
10
+ subscriptionId: db.ref(t.uuid(), () => subscriptions.cols.id, {
11
+ onDelete: "cascade",
12
+ }),
13
+ organizationId: db.organization(),
14
+
15
+ type: t.enum([
16
+ "created",
17
+ "trial_started",
18
+ "trial_ended",
19
+ "activated",
20
+ "renewed",
21
+ "payment_failed",
22
+ "payment_retried",
23
+ "past_due",
24
+ "suspended",
25
+ "reactivated",
26
+ "plan_changed",
27
+ "plan_change_scheduled",
28
+ "cancelled",
29
+ "expired",
30
+ "resumed",
31
+ ]),
32
+
33
+ // Context
34
+ previousStatus: t.optional(t.string()),
35
+ newStatus: t.optional(t.string()),
36
+ previousPlanId: t.optional(t.string()),
37
+ newPlanId: t.optional(t.string()),
38
+ paymentIntentId: t.optional(t.uuid()),
39
+ amount: t.optional(t.integer()),
40
+ currency: t.optional(t.string()),
41
+
42
+ // Who / why
43
+ triggeredBy: t.optional(t.string()),
44
+ userId: t.optional(t.uuid()),
45
+ note: t.optional(t.string()),
46
+ }),
47
+ indexes: [
48
+ { columns: ["subscriptionId", "createdAt"] },
49
+ { columns: ["organizationId", "createdAt"] },
50
+ { columns: ["type"] },
51
+ ],
52
+ });
53
+
54
+ export type SubscriptionEventEntity = Static<typeof subscriptionEvents.schema>;
@@ -0,0 +1,68 @@
1
+ import { type Static, t } from "alepha";
2
+ import { $entity, db } from "alepha/orm";
3
+
4
+ export const subscriptions = $entity({
5
+ name: "subscriptions",
6
+ schema: t.object({
7
+ id: db.primaryKey(t.uuid()),
8
+ version: db.version(),
9
+ createdAt: db.createdAt(),
10
+ updatedAt: db.updatedAt(),
11
+ organizationId: db.organization(),
12
+
13
+ // Plan
14
+ planId: t.string(),
15
+ interval: t.enum(["monthly", "yearly"]),
16
+
17
+ // Status
18
+ status: t.enum([
19
+ "trialing",
20
+ "active",
21
+ "past_due",
22
+ "suspended",
23
+ "cancelled",
24
+ "expired",
25
+ ]),
26
+
27
+ // Billing cycle
28
+ currentPeriodStart: t.datetime(),
29
+ currentPeriodEnd: t.datetime(),
30
+
31
+ // Trial
32
+ trialStart: t.optional(t.datetime()),
33
+ trialEnd: t.optional(t.datetime()),
34
+
35
+ // Cancellation
36
+ cancelledAt: t.optional(t.datetime()),
37
+ cancelReason: t.optional(t.string()),
38
+ cancelAtPeriodEnd: t.boolean({ default: false }),
39
+
40
+ // Payment tracking
41
+ lastPaymentIntentId: t.optional(t.uuid()),
42
+ lastPaymentAt: t.optional(t.datetime()),
43
+ nextBillingAt: t.optional(t.datetime()),
44
+
45
+ // Dunning state
46
+ dunningStartedAt: t.optional(t.datetime()),
47
+ dunningAttempt: t.integer({ default: 0 }),
48
+ dunningNextRetryAt: t.optional(t.datetime()),
49
+
50
+ // Plan change (pending)
51
+ pendingPlanId: t.optional(t.string()),
52
+ pendingInterval: t.optional(t.enum(["monthly", "yearly"])),
53
+
54
+ // Metadata
55
+ metadata: t.optional(t.record(t.text(), t.any())),
56
+ }),
57
+ indexes: [
58
+ { columns: ["organizationId"], unique: true },
59
+ { columns: ["status"] },
60
+ { columns: ["planId", "status"] },
61
+ { columns: ["nextBillingAt"] },
62
+ { columns: ["trialEnd"] },
63
+ { columns: ["dunningNextRetryAt"] },
64
+ { columns: ["currentPeriodEnd"] },
65
+ ],
66
+ });
67
+
68
+ export type SubscriptionEntity = Static<typeof subscriptions.schema>;
@@ -0,0 +1,144 @@
1
+ import { $module } from "alepha";
2
+ import { AdminSubscriptionController } from "./controllers/AdminSubscriptionController.ts";
3
+ import { SubscriptionController } from "./controllers/SubscriptionController.ts";
4
+ import { SubscriptionJobs } from "./jobs/SubscriptionJobs.ts";
5
+ import { SubscriptionNotifications } from "./notifications/SubscriptionNotifications.ts";
6
+ import { BillingService } from "./services/BillingService.ts";
7
+ import { SubscriptionConfig } from "./services/SubscriptionConfig.ts";
8
+ import { SubscriptionService } from "./services/SubscriptionService.ts";
9
+ import { UsageService } from "./services/UsageService.ts";
10
+
11
+ // Controllers
12
+ export * from "./controllers/AdminSubscriptionController.ts";
13
+ export * from "./controllers/SubscriptionController.ts";
14
+ // Entities
15
+ export * from "./entities/subscriptionEvents.ts";
16
+ export * from "./entities/subscriptions.ts";
17
+ // Jobs
18
+ export * from "./jobs/SubscriptionJobs.ts";
19
+ // Middleware
20
+ export * from "./middleware/$requireLimit.ts";
21
+ export * from "./middleware/$requirePlan.ts";
22
+ // Notifications
23
+ export * from "./notifications/SubscriptionNotifications.ts";
24
+ // Schemas
25
+ export * from "./schemas/cancelSubscriptionSchema.ts";
26
+ export * from "./schemas/changePlanSchema.ts";
27
+ export * from "./schemas/createSubscriptionSchema.ts";
28
+ export * from "./schemas/entitlementsSchema.ts";
29
+ export * from "./schemas/mrrSchema.ts";
30
+ export * from "./schemas/planDefinitionSchema.ts";
31
+ export * from "./schemas/planResourceSchema.ts";
32
+ export * from "./schemas/subscriptionEventResourceSchema.ts";
33
+ export * from "./schemas/subscriptionQuerySchema.ts";
34
+ export * from "./schemas/subscriptionResourceSchema.ts";
35
+ export * from "./schemas/subscriptionSettingsSchema.ts";
36
+ export * from "./schemas/subscriptionStatsSchema.ts";
37
+ // Services
38
+ export * from "./services/BillingService.ts";
39
+ export * from "./services/SubscriptionConfig.ts";
40
+ export * from "./services/SubscriptionService.ts";
41
+ export * from "./services/UsageService.ts";
42
+
43
+ declare module "alepha" {
44
+ interface Hooks {
45
+ "subscription:created": {
46
+ subscriptionId: string;
47
+ organizationId: string;
48
+ planId: string;
49
+ trial: boolean;
50
+ };
51
+ "subscription:activated": {
52
+ subscriptionId: string;
53
+ organizationId: string;
54
+ planId: string;
55
+ };
56
+ "subscription:renewed": {
57
+ subscriptionId: string;
58
+ organizationId: string;
59
+ planId: string;
60
+ amount: number;
61
+ currency: string;
62
+ };
63
+ "subscription:cancelled": {
64
+ subscriptionId: string;
65
+ organizationId: string;
66
+ planId: string;
67
+ reason?: string;
68
+ immediate: boolean;
69
+ };
70
+ "subscription:expired": {
71
+ subscriptionId: string;
72
+ organizationId: string;
73
+ planId: string;
74
+ };
75
+ "subscription:resumed": {
76
+ subscriptionId: string;
77
+ organizationId: string;
78
+ planId: string;
79
+ };
80
+ "subscription:plan_changed": {
81
+ subscriptionId: string;
82
+ organizationId: string;
83
+ oldPlanId: string;
84
+ newPlanId: string;
85
+ immediate: boolean;
86
+ };
87
+ "subscription:payment_failed": {
88
+ subscriptionId: string;
89
+ organizationId: string;
90
+ planId: string;
91
+ attempt: number;
92
+ };
93
+ "subscription:suspended": {
94
+ subscriptionId: string;
95
+ organizationId: string;
96
+ planId: string;
97
+ };
98
+ "subscription:reactivated": {
99
+ subscriptionId: string;
100
+ organizationId: string;
101
+ planId: string;
102
+ };
103
+ "subscription:trial_ending": {
104
+ subscriptionId: string;
105
+ organizationId: string;
106
+ planId: string;
107
+ endsAt: string;
108
+ };
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Subscription management module — plan-based access control, billing integration,
114
+ * usage limits, and lifecycle events (trial, renewal, cancellation, suspension).
115
+ *
116
+ * Depends on `AlephaPayments` for payment processing — register it in your app
117
+ * alongside this module. Use `SubscriptionConfig` to declare your plans and limits.
118
+ *
119
+ * @module alepha.api.subscriptions
120
+ */
121
+ export const AlephaApiSubscriptions = $module({
122
+ name: "alepha.api.subscriptions",
123
+ services: [
124
+ SubscriptionConfig,
125
+ SubscriptionService,
126
+ BillingService,
127
+ UsageService,
128
+ SubscriptionJobs,
129
+ SubscriptionNotifications,
130
+ SubscriptionController,
131
+ AdminSubscriptionController,
132
+ ],
133
+ register: (alepha) => {
134
+ alepha
135
+ .with(SubscriptionConfig)
136
+ .with(SubscriptionService)
137
+ .with(BillingService)
138
+ .with(UsageService)
139
+ .with(SubscriptionJobs)
140
+ .with(SubscriptionNotifications)
141
+ .with(SubscriptionController)
142
+ .with(AdminSubscriptionController);
143
+ },
144
+ });
@@ -0,0 +1,382 @@
1
+ import { $inject } from "alepha";
2
+ import { $job } from "alepha/api/jobs";
3
+ import { PaymentService } from "alepha/api/payments";
4
+ import { DateTimeProvider } from "alepha/datetime";
5
+ import { $logger } from "alepha/logger";
6
+ import { $repository } from "alepha/orm";
7
+ import type { SubscriptionEventEntity } from "../entities/subscriptionEvents.ts";
8
+ import { subscriptionEvents } from "../entities/subscriptionEvents.ts";
9
+ import { subscriptions } from "../entities/subscriptions.ts";
10
+ import { SubscriptionConfig } from "../services/SubscriptionConfig.ts";
11
+
12
+ // -----------------------------------------------------------------------------------------------------------------
13
+
14
+ interface EventContext {
15
+ previousStatus?: string;
16
+ newStatus?: string;
17
+ paymentIntentId?: string;
18
+ amount?: number;
19
+ currency?: string;
20
+ triggeredBy?: string;
21
+ note?: string;
22
+ }
23
+
24
+ // -----------------------------------------------------------------------------------------------------------------
25
+
26
+ export class SubscriptionJobs {
27
+ protected readonly log = $logger();
28
+ protected readonly dateTime = $inject(DateTimeProvider);
29
+ protected readonly paymentService = $inject(PaymentService);
30
+ protected readonly config = $inject(SubscriptionConfig);
31
+ protected readonly subscriptionRepo = $repository(subscriptions);
32
+ protected readonly eventRepo = $repository(subscriptionEvents);
33
+
34
+ // ---------------------------------------------------------------------------------------------------------------
35
+ // Helpers
36
+ // ---------------------------------------------------------------------------------------------------------------
37
+
38
+ /**
39
+ * Record a subscription event in the event log.
40
+ */
41
+ protected async recordEvent(
42
+ subscriptionId: string,
43
+ organizationId: string,
44
+ type: SubscriptionEventEntity["type"],
45
+ context?: EventContext,
46
+ ): Promise<void> {
47
+ await this.eventRepo.create({
48
+ subscriptionId,
49
+ organizationId,
50
+ type,
51
+ previousStatus: context?.previousStatus,
52
+ newStatus: context?.newStatus,
53
+ paymentIntentId: context?.paymentIntentId,
54
+ amount: context?.amount,
55
+ currency: context?.currency,
56
+ triggeredBy: context?.triggeredBy,
57
+ note: context?.note,
58
+ });
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------------------------------------------
62
+ // Jobs
63
+ // ---------------------------------------------------------------------------------------------------------------
64
+
65
+ /**
66
+ * Creates payment intents for subscriptions due for renewal.
67
+ * Runs hourly.
68
+ */
69
+ public readonly billingCycle = $job({
70
+ cron: "0 * * * *",
71
+ lock: true,
72
+ timeout: [10, "minute"],
73
+ handler: async ({ now }) => {
74
+ const nowISO = now.toISOString();
75
+
76
+ const due = await this.subscriptionRepo.findMany({
77
+ where: {
78
+ nextBillingAt: { lte: nowISO },
79
+ status: { inArray: ["active", "trialing"] },
80
+ },
81
+ });
82
+
83
+ this.log.info(`Billing cycle: processing ${due.length} subscription(s)`);
84
+
85
+ for (const sub of due) {
86
+ try {
87
+ const pricing = await this.config.getPlanPricing(
88
+ sub.planId,
89
+ sub.interval,
90
+ );
91
+
92
+ const intent = await this.paymentService.createIntent(
93
+ pricing.amount,
94
+ pricing.currency,
95
+ { subscriptionId: sub.id },
96
+ );
97
+
98
+ await this.subscriptionRepo.updateById(sub.id, {
99
+ lastPaymentIntentId: intent.id,
100
+ });
101
+
102
+ this.log.debug("Created payment intent for subscription", {
103
+ subscriptionId: sub.id,
104
+ intentId: intent.id,
105
+ });
106
+ } catch (err) {
107
+ this.log.error("Failed to create payment intent for subscription", {
108
+ subscriptionId: sub.id,
109
+ error: err,
110
+ });
111
+ }
112
+ }
113
+ },
114
+ });
115
+
116
+ // ---------------------------------------------------------------------------------------------------------------
117
+
118
+ /**
119
+ * Retries failed payments on the dunning schedule.
120
+ * Runs hourly.
121
+ */
122
+ public readonly dunningRetry = $job({
123
+ cron: "0 * * * *",
124
+ lock: true,
125
+ timeout: [10, "minute"],
126
+ handler: async ({ now }) => {
127
+ const nowISO = now.toISOString();
128
+
129
+ const pastDue = await this.subscriptionRepo.findMany({
130
+ where: {
131
+ dunningNextRetryAt: { lte: nowISO },
132
+ status: { eq: "past_due" },
133
+ },
134
+ });
135
+
136
+ this.log.info(
137
+ `Dunning retry: processing ${pastDue.length} subscription(s)`,
138
+ );
139
+
140
+ const settings = await this.config.getSettings();
141
+
142
+ for (const sub of pastDue) {
143
+ try {
144
+ const pricing = await this.config.getPlanPricing(
145
+ sub.planId,
146
+ sub.interval,
147
+ );
148
+
149
+ const intent = await this.paymentService.createIntent(
150
+ pricing.amount,
151
+ pricing.currency,
152
+ { subscriptionId: sub.id },
153
+ );
154
+
155
+ const newAttempt = sub.dunningAttempt + 1;
156
+ const scheduleDays = settings.dunningSchedule[newAttempt - 1];
157
+ const nextRetry =
158
+ scheduleDays !== undefined
159
+ ? now.add(scheduleDays, "days").toISOString()
160
+ : undefined;
161
+
162
+ await this.subscriptionRepo.updateById(sub.id, {
163
+ lastPaymentIntentId: intent.id,
164
+ dunningAttempt: newAttempt,
165
+ dunningNextRetryAt: nextRetry,
166
+ });
167
+
168
+ await this.recordEvent(
169
+ sub.id,
170
+ sub.organizationId as string,
171
+ "payment_retried",
172
+ {
173
+ paymentIntentId: intent.id,
174
+ note: `Dunning retry attempt ${newAttempt}`,
175
+ },
176
+ );
177
+
178
+ this.log.debug("Dunning retry payment intent created", {
179
+ subscriptionId: sub.id,
180
+ attempt: newAttempt,
181
+ });
182
+ } catch (err) {
183
+ this.log.error("Failed to create dunning retry intent", {
184
+ subscriptionId: sub.id,
185
+ error: err,
186
+ });
187
+ }
188
+ }
189
+ },
190
+ });
191
+
192
+ // ---------------------------------------------------------------------------------------------------------------
193
+
194
+ /**
195
+ * Handles trial expirations.
196
+ * Runs hourly.
197
+ */
198
+ public readonly trialExpiry = $job({
199
+ cron: "0 * * * *",
200
+ lock: true,
201
+ handler: async ({ now }) => {
202
+ const nowISO = now.toISOString();
203
+
204
+ const expired = await this.subscriptionRepo.findMany({
205
+ where: {
206
+ trialEnd: { lte: nowISO },
207
+ status: { eq: "trialing" },
208
+ },
209
+ });
210
+
211
+ this.log.info(
212
+ `Trial expiry: processing ${expired.length} subscription(s)`,
213
+ );
214
+
215
+ for (const sub of expired) {
216
+ try {
217
+ const pricing = await this.config.getPlanPricing(
218
+ sub.planId,
219
+ sub.interval,
220
+ );
221
+
222
+ const intent = await this.paymentService.createIntent(
223
+ pricing.amount,
224
+ pricing.currency,
225
+ { subscriptionId: sub.id },
226
+ );
227
+
228
+ await this.subscriptionRepo.updateById(sub.id, {
229
+ lastPaymentIntentId: intent.id,
230
+ });
231
+
232
+ this.log.debug("Created payment intent for trial expiry", {
233
+ subscriptionId: sub.id,
234
+ intentId: intent.id,
235
+ });
236
+ } catch (err) {
237
+ this.log.error("Failed to process trial expiry", {
238
+ subscriptionId: sub.id,
239
+ error: err,
240
+ });
241
+ }
242
+ }
243
+ },
244
+ });
245
+
246
+ // ---------------------------------------------------------------------------------------------------------------
247
+
248
+ /**
249
+ * Expires cancelled subscriptions that reached period end.
250
+ * Runs hourly.
251
+ */
252
+ public readonly expirationSweep = $job({
253
+ cron: "0 * * * *",
254
+ lock: true,
255
+ handler: async ({ now }) => {
256
+ const nowISO = now.toISOString();
257
+
258
+ const toExpire = await this.subscriptionRepo.findMany({
259
+ where: {
260
+ currentPeriodEnd: { lte: nowISO },
261
+ status: { eq: "cancelled" },
262
+ cancelAtPeriodEnd: { eq: true },
263
+ },
264
+ });
265
+
266
+ this.log.info(
267
+ `Expiration sweep: expiring ${toExpire.length} subscription(s)`,
268
+ );
269
+
270
+ for (const sub of toExpire) {
271
+ try {
272
+ await this.subscriptionRepo.updateById(sub.id, {
273
+ status: "expired",
274
+ });
275
+
276
+ await this.recordEvent(
277
+ sub.id,
278
+ sub.organizationId as string,
279
+ "expired",
280
+ {
281
+ previousStatus: "cancelled",
282
+ newStatus: "expired",
283
+ },
284
+ );
285
+
286
+ this.log.debug("Subscription expired", { subscriptionId: sub.id });
287
+ } catch (err) {
288
+ this.log.error("Failed to expire subscription", {
289
+ subscriptionId: sub.id,
290
+ error: err,
291
+ });
292
+ }
293
+ }
294
+ },
295
+ });
296
+
297
+ // ---------------------------------------------------------------------------------------------------------------
298
+
299
+ /**
300
+ * Suspends past_due subscriptions where grace period has elapsed.
301
+ * Runs daily at 2 AM.
302
+ */
303
+ public readonly gracePeriodSweep = $job({
304
+ cron: "0 2 * * *",
305
+ lock: true,
306
+ handler: async ({ now }) => {
307
+ const settings = await this.config.getSettings();
308
+ const gracePeriodDays = settings.gracePeriodDays;
309
+
310
+ const pastDueSubs = await this.subscriptionRepo.findMany({
311
+ where: {
312
+ status: { eq: "past_due" },
313
+ },
314
+ });
315
+
316
+ const toSuspend = pastDueSubs.filter((sub) => {
317
+ if (!sub.dunningStartedAt) return false;
318
+ const graceEnd = this.dateTime
319
+ .of(sub.dunningStartedAt)
320
+ .add(gracePeriodDays, "days");
321
+ return !now.isBefore(graceEnd.toISOString());
322
+ });
323
+
324
+ this.log.info(
325
+ `Grace period sweep: suspending ${toSuspend.length} subscription(s)`,
326
+ );
327
+
328
+ for (const sub of toSuspend) {
329
+ try {
330
+ await this.subscriptionRepo.updateById(sub.id, {
331
+ status: "suspended",
332
+ });
333
+
334
+ await this.recordEvent(
335
+ sub.id,
336
+ sub.organizationId as string,
337
+ "suspended",
338
+ {
339
+ previousStatus: "past_due",
340
+ newStatus: "suspended",
341
+ },
342
+ );
343
+
344
+ this.log.debug("Subscription suspended after grace period", {
345
+ subscriptionId: sub.id,
346
+ });
347
+ } catch (err) {
348
+ this.log.error("Failed to suspend subscription", {
349
+ subscriptionId: sub.id,
350
+ error: err,
351
+ });
352
+ }
353
+ }
354
+ },
355
+ });
356
+
357
+ // ---------------------------------------------------------------------------------------------------------------
358
+
359
+ /**
360
+ * Purges old subscription events older than 365 days.
361
+ * Runs daily at 3 AM.
362
+ */
363
+ public readonly purgeEvents = $job({
364
+ cron: "0 3 * * *",
365
+ lock: true,
366
+ handler: async ({ now }) => {
367
+ const cutoff = now.subtract(365, "days").toISOString();
368
+
369
+ const old = await this.eventRepo.findMany({
370
+ where: {
371
+ createdAt: { lt: cutoff },
372
+ },
373
+ });
374
+
375
+ this.log.info(`Purge events: removing ${old.length} old event(s)`);
376
+
377
+ for (const event of old) {
378
+ await this.eventRepo.deleteById(event.id);
379
+ }
380
+ },
381
+ });
382
+ }
@@ -0,0 +1,50 @@
1
+ import { $context, createMiddleware, type Middleware } from "alepha";
2
+ import { ForbiddenError } from "alepha/server";
3
+ import { UsageService } from "../services/UsageService.ts";
4
+
5
+ /**
6
+ * Middleware that enforces a per-organization usage limit for a resource.
7
+ *
8
+ * Resolves the organization from `args[0].user.organization`, increments the
9
+ * usage counter for the given resource, and throws `ForbiddenError` if the
10
+ * plan limit has been reached.
11
+ * Throws `ForbiddenError` if no organization is present or the limit is exceeded.
12
+ *
13
+ * ```typescript
14
+ * class ApiController {
15
+ * search = $action({
16
+ * use: [$requireLimit("api_calls")],
17
+ * handler: async ({ query }) => { ... },
18
+ * });
19
+ * }
20
+ * ```
21
+ *
22
+ * @param resource The resource identifier to track (e.g., "api_calls", "exports").
23
+ */
24
+ export const $requireLimit = (resource: string): Middleware => {
25
+ const { alepha } = $context();
26
+ const usageService = alepha.inject(UsageService);
27
+
28
+ return createMiddleware({
29
+ name: "$requireLimit",
30
+ options: { resource } as Record<string, unknown>,
31
+ handler: ({ next }) => {
32
+ return async (...args: any[]) => {
33
+ const user = args[0]?.user;
34
+ if (!user?.organization) {
35
+ throw new ForbiddenError("Organization required");
36
+ }
37
+ const result = await usageService.increment(
38
+ user.organization,
39
+ resource,
40
+ );
41
+ if (!result.allowed) {
42
+ throw new ForbiddenError(
43
+ `Usage limit for '${resource}' has been reached (${result.current}/${result.limit})`,
44
+ );
45
+ }
46
+ return next(...args);
47
+ };
48
+ },
49
+ });
50
+ };