alepha 0.20.1 → 0.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/dist/api/files/index.js +2 -1
  2. package/dist/api/files/index.js.map +1 -1
  3. package/dist/api/jobs/index.browser.js +64 -148
  4. package/dist/api/jobs/index.browser.js.map +1 -1
  5. package/dist/api/jobs/index.d.ts +371 -573
  6. package/dist/api/jobs/index.d.ts.map +1 -1
  7. package/dist/api/jobs/index.js +605 -1012
  8. package/dist/api/jobs/index.js.map +1 -1
  9. package/dist/api/notifications/index.d.ts +78 -17
  10. package/dist/api/notifications/index.d.ts.map +1 -1
  11. package/dist/api/notifications/index.js +90 -23
  12. package/dist/api/notifications/index.js.map +1 -1
  13. package/dist/api/payments/index.d.ts +2 -1
  14. package/dist/api/payments/index.d.ts.map +1 -1
  15. package/dist/api/payments/index.js +4 -2
  16. package/dist/api/payments/index.js.map +1 -1
  17. package/dist/api/users/index.d.ts +34 -31
  18. package/dist/api/users/index.d.ts.map +1 -1
  19. package/dist/api/users/index.js +13 -7
  20. package/dist/api/users/index.js.map +1 -1
  21. package/dist/api/verifications/index.js +2 -1
  22. package/dist/api/verifications/index.js.map +1 -1
  23. package/dist/cli/core/index.d.ts +8 -34
  24. package/dist/cli/core/index.d.ts.map +1 -1
  25. package/dist/cli/core/index.js +43 -232
  26. package/dist/cli/core/index.js.map +1 -1
  27. package/dist/cli/platform/index.d.ts +36 -11
  28. package/dist/cli/platform/index.d.ts.map +1 -1
  29. package/dist/cli/platform/index.js +93 -27
  30. package/dist/cli/platform/index.js.map +1 -1
  31. package/dist/command/index.d.ts +1 -1
  32. package/dist/core/index.browser.js +6 -0
  33. package/dist/core/index.browser.js.map +1 -1
  34. package/dist/core/index.d.ts +6 -0
  35. package/dist/core/index.d.ts.map +1 -1
  36. package/dist/core/index.js +6 -0
  37. package/dist/core/index.js.map +1 -1
  38. package/dist/core/index.native.js +6 -0
  39. package/dist/core/index.native.js.map +1 -1
  40. package/dist/core/index.workerd.js +6 -0
  41. package/dist/core/index.workerd.js.map +1 -1
  42. package/dist/react/form/index.d.ts +60 -1
  43. package/dist/react/form/index.d.ts.map +1 -1
  44. package/dist/react/form/index.js +86 -1
  45. package/dist/react/form/index.js.map +1 -1
  46. package/dist/react/head/index.browser.js +16 -1
  47. package/dist/react/head/index.browser.js.map +1 -1
  48. package/dist/react/head/index.d.ts +6 -0
  49. package/dist/react/head/index.d.ts.map +1 -1
  50. package/dist/react/head/index.js +16 -1
  51. package/dist/react/head/index.js.map +1 -1
  52. package/dist/react/router/index.browser.js +0 -10
  53. package/dist/react/router/index.browser.js.map +1 -1
  54. package/dist/react/router/index.d.ts +35 -12
  55. package/dist/react/router/index.d.ts.map +1 -1
  56. package/dist/react/router/index.js +0 -10
  57. package/dist/react/router/index.js.map +1 -1
  58. package/dist/react/ui/index.d.ts +124 -0
  59. package/dist/react/ui/index.d.ts.map +1 -0
  60. package/dist/react/ui/index.js +206 -0
  61. package/dist/react/ui/index.js.map +1 -0
  62. package/dist/router/index.d.ts +13 -13
  63. package/dist/router/index.d.ts.map +1 -1
  64. package/dist/router/index.js +45 -32
  65. package/dist/router/index.js.map +1 -1
  66. package/dist/system/index.d.ts.map +1 -1
  67. package/dist/system/index.js +1 -0
  68. package/dist/system/index.js.map +1 -1
  69. package/dist/topic/core/index.js +1 -1
  70. package/dist/topic/core/index.js.map +1 -1
  71. package/package.json +6 -23
  72. package/src/api/files/jobs/FileJobs.ts +2 -1
  73. package/src/api/jobs/__tests__/$job.spec.ts +316 -2867
  74. package/src/api/jobs/controllers/AdminJobController.ts +29 -138
  75. package/src/api/jobs/entities/jobExecutionEntity.ts +27 -19
  76. package/src/api/jobs/index.browser.ts +5 -7
  77. package/src/api/jobs/index.ts +23 -51
  78. package/src/api/jobs/primitives/$job.ts +66 -58
  79. package/src/api/jobs/providers/JobProvider.ts +561 -566
  80. package/src/api/jobs/providers/JobQueueProvider.ts +18 -19
  81. package/src/api/jobs/schemas/jobConfigAtom.ts +20 -23
  82. package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +3 -27
  83. package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +5 -7
  84. package/src/api/jobs/schemas/jobRegistrationSchema.ts +7 -4
  85. package/src/api/jobs/schemas/triggerJobSchema.ts +0 -1
  86. package/src/api/jobs/services/JobService.ts +90 -483
  87. package/src/api/notifications/controllers/AdminNotificationController.ts +19 -12
  88. package/src/api/notifications/index.ts +7 -4
  89. package/src/api/notifications/jobs/NotificationJobs.ts +83 -12
  90. package/src/api/payments/services/PaymentService.ts +4 -2
  91. package/src/api/users/__tests__/UserJobs.spec.ts +10 -49
  92. package/src/api/users/audits/UserAudits.ts +3 -1
  93. package/src/api/users/buckets/UserBuckets.ts +2 -1
  94. package/src/api/users/index.ts +1 -4
  95. package/src/api/users/jobs/UserJobs.ts +5 -4
  96. package/src/api/verifications/jobs/VerificationJobs.ts +2 -1
  97. package/src/cli/core/__tests__/init.spec.ts +1 -1
  98. package/src/cli/core/commands/init.ts +0 -12
  99. package/src/cli/core/services/PackageManagerUtils.ts +2 -9
  100. package/src/cli/core/services/ProjectScaffolder.ts +17 -65
  101. package/src/cli/core/templates/agentMd.ts +2 -8
  102. package/src/cli/core/templates/apiIndexTs.ts +4 -18
  103. package/src/cli/core/templates/mainCss.ts +1 -36
  104. package/src/cli/core/templates/vitestConfigTs.ts +17 -0
  105. package/src/cli/core/templates/webAppRouterTs.ts +2 -85
  106. package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +22 -71
  107. package/src/cli/platform/adapters/CloudflareAdapter.ts +12 -11
  108. package/src/cli/platform/atoms/platformOptions.ts +9 -0
  109. package/src/cli/platform/schemas/cloudflare.ts +3 -2
  110. package/src/cli/platform/services/CloudflareApi.ts +164 -25
  111. package/src/cli/platform/services/WranglerApi.ts +0 -17
  112. package/src/core/Alepha.ts +9 -0
  113. package/src/react/form/index.ts +2 -0
  114. package/src/react/form/services/parseField.ts +163 -0
  115. package/src/react/form/services/prettyName.ts +19 -0
  116. package/src/react/head/providers/BrowserHeadProvider.ts +31 -10
  117. package/src/react/router/primitives/$page.ts +35 -12
  118. package/src/react/ui/atoms/uiAtom.ts +28 -0
  119. package/src/react/ui/components/ColorScheme.tsx +36 -0
  120. package/src/react/ui/hooks/useColorMode.ts +49 -0
  121. package/src/react/ui/hooks/useSidebarState.ts +26 -0
  122. package/src/react/ui/hooks/useTheme.ts +22 -0
  123. package/src/react/ui/index.ts +35 -0
  124. package/src/react/ui/services/UiPersistence.ts +41 -0
  125. package/src/router/TemplatedPathParser.ts +50 -51
  126. package/src/router/__tests__/RouterProvider.spec.ts +62 -0
  127. package/src/router/__tests__/TemplatedPathParser.spec.ts +18 -0
  128. package/src/router/providers/RouterProvider.ts +10 -5
  129. package/src/system/providers/NodeShellProvider.ts +1 -0
  130. package/src/topic/core/providers/TopicProvider.ts +1 -1
  131. package/dist/api/invitations/index.d.ts +0 -790
  132. package/dist/api/invitations/index.d.ts.map +0 -1
  133. package/dist/api/invitations/index.js +0 -662
  134. package/dist/api/invitations/index.js.map +0 -1
  135. package/dist/api/issues/index.d.ts +0 -810
  136. package/dist/api/issues/index.d.ts.map +0 -1
  137. package/dist/api/issues/index.js +0 -444
  138. package/dist/api/issues/index.js.map +0 -1
  139. package/dist/api/subscriptions/index.d.ts +0 -1692
  140. package/dist/api/subscriptions/index.d.ts.map +0 -1
  141. package/dist/api/subscriptions/index.js +0 -1867
  142. package/dist/api/subscriptions/index.js.map +0 -1
  143. package/dist/api/workflows/index.browser.js +0 -246
  144. package/dist/api/workflows/index.browser.js.map +0 -1
  145. package/dist/api/workflows/index.d.ts +0 -1618
  146. package/dist/api/workflows/index.d.ts.map +0 -1
  147. package/dist/api/workflows/index.js +0 -1495
  148. package/dist/api/workflows/index.js.map +0 -1
  149. package/src/api/invitations/__tests__/InvitationService.spec.ts +0 -439
  150. package/src/api/invitations/controllers/AdminInvitationController.ts +0 -86
  151. package/src/api/invitations/controllers/InvitationController.ts +0 -84
  152. package/src/api/invitations/entities/invitations.ts +0 -33
  153. package/src/api/invitations/index.ts +0 -58
  154. package/src/api/invitations/jobs/InvitationJobs.ts +0 -37
  155. package/src/api/invitations/providers/InvitationProvider.ts +0 -45
  156. package/src/api/invitations/schemas/createInvitationSchema.ts +0 -12
  157. package/src/api/invitations/schemas/invitationConfigAtom.ts +0 -20
  158. package/src/api/invitations/schemas/invitationQuerySchema.ts +0 -15
  159. package/src/api/invitations/schemas/invitationResourceSchema.ts +0 -6
  160. package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +0 -22
  161. package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +0 -10
  162. package/src/api/invitations/services/InvitationService.ts +0 -556
  163. package/src/api/issues/__tests__/IssueService.spec.ts +0 -263
  164. package/src/api/issues/controllers/AdminIssueController.ts +0 -149
  165. package/src/api/issues/controllers/IssueController.ts +0 -44
  166. package/src/api/issues/entities/issues.ts +0 -49
  167. package/src/api/issues/index.ts +0 -50
  168. package/src/api/issues/schemas/createIssueSchema.ts +0 -13
  169. package/src/api/issues/schemas/issueConfigAtom.ts +0 -13
  170. package/src/api/issues/schemas/issueQuerySchema.ts +0 -18
  171. package/src/api/issues/schemas/issueResourceSchema.ts +0 -6
  172. package/src/api/issues/schemas/myIssueQuerySchema.ts +0 -10
  173. package/src/api/issues/schemas/updateIssueSchema.ts +0 -13
  174. package/src/api/issues/services/IssueService.ts +0 -264
  175. package/src/api/jobs/__tests__/$job-middleware.spec.ts +0 -126
  176. package/src/api/jobs/__tests__/JobService.spec.ts +0 -31
  177. package/src/api/jobs/entities/jobExecutionLogEntity.ts +0 -13
  178. package/src/api/jobs/schemas/jobActivitySchema.ts +0 -15
  179. package/src/api/jobs/schemas/jobCronInfoSchema.ts +0 -22
  180. package/src/api/jobs/schemas/jobExecutionDetailResourceSchema.ts +0 -20
  181. package/src/api/jobs/schemas/jobFailureSchema.ts +0 -9
  182. package/src/api/jobs/schemas/jobQueueDepthSchema.ts +0 -14
  183. package/src/api/jobs/schemas/jobStatsSchema.ts +0 -14
  184. package/src/api/jobs/services/JobService-tests.ts +0 -157
  185. package/src/api/subscriptions/__tests__/BillingService.spec.ts +0 -218
  186. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +0 -278
  187. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +0 -212
  188. package/src/api/subscriptions/controllers/SubscriptionController.ts +0 -189
  189. package/src/api/subscriptions/entities/subscriptionEvents.ts +0 -54
  190. package/src/api/subscriptions/entities/subscriptions.ts +0 -68
  191. package/src/api/subscriptions/index.ts +0 -133
  192. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +0 -382
  193. package/src/api/subscriptions/middleware/$requireLimit.ts +0 -50
  194. package/src/api/subscriptions/middleware/$requirePlan.ts +0 -49
  195. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +0 -110
  196. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +0 -8
  197. package/src/api/subscriptions/schemas/changePlanSchema.ts +0 -9
  198. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +0 -11
  199. package/src/api/subscriptions/schemas/entitlementsSchema.ts +0 -21
  200. package/src/api/subscriptions/schemas/mrrSchema.ts +0 -13
  201. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +0 -71
  202. package/src/api/subscriptions/schemas/planResourceSchema.ts +0 -25
  203. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +0 -8
  204. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +0 -19
  205. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +0 -6
  206. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +0 -32
  207. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +0 -23
  208. package/src/api/subscriptions/services/BillingService.ts +0 -437
  209. package/src/api/subscriptions/services/SubscriptionConfig.ts +0 -56
  210. package/src/api/subscriptions/services/SubscriptionService.ts +0 -867
  211. package/src/api/subscriptions/services/UsageService.ts +0 -118
  212. package/src/api/workflows/__tests__/$workflow.spec.ts +0 -616
  213. package/src/api/workflows/controllers/AdminWorkflowController.ts +0 -191
  214. package/src/api/workflows/entities/workflowExecutions.ts +0 -74
  215. package/src/api/workflows/entities/workflowStepExecutions.ts +0 -74
  216. package/src/api/workflows/entities/workflowStepLogs.ts +0 -13
  217. package/src/api/workflows/index.browser.ts +0 -22
  218. package/src/api/workflows/index.ts +0 -115
  219. package/src/api/workflows/jobs/WorkflowJobs.ts +0 -77
  220. package/src/api/workflows/primitives/$workflow.ts +0 -202
  221. package/src/api/workflows/providers/WorkflowProvider.ts +0 -1284
  222. package/src/api/workflows/schemas/workflowActivitySchema.ts +0 -15
  223. package/src/api/workflows/schemas/workflowConfigAtom.ts +0 -51
  224. package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +0 -18
  225. package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +0 -26
  226. package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +0 -30
  227. package/src/api/workflows/schemas/workflowRegistrationSchema.ts +0 -26
  228. package/src/api/workflows/schemas/workflowStatsSchema.ts +0 -16
  229. package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +0 -15
  230. package/src/api/workflows/services/WorkflowService.ts +0 -382
  231. package/src/cli/core/templates/apiAppSecurityTs.ts +0 -43
  232. package/src/cli/core/templates/webAdminDashboardTsx.ts +0 -17
@@ -1,218 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { Alepha } from "alepha";
3
- import { $repository } from "alepha/orm";
4
- import { AlephaOrmPostgres } from "alepha/orm/postgres";
5
- import { describe, it } from "vitest";
6
- import { subscriptions } from "../entities/subscriptions.ts";
7
- import { AlephaApiSubscriptions } from "../index.ts";
8
- import type { PlanDefinition } from "../schemas/planDefinitionSchema.ts";
9
- import { SubscriptionConfig } from "../services/SubscriptionConfig.ts";
10
- import { SubscriptionService } from "../services/SubscriptionService.ts";
11
-
12
- // -----------------------------------------------------------------------------------------------------------------
13
-
14
- class TestSubscriptionConfig extends SubscriptionConfig {
15
- public async seedPlans(plans: PlanDefinition[]): Promise<void> {
16
- await this.plans.set({ plans });
17
- }
18
- }
19
-
20
- /**
21
- * Helper to directly update subscription records for test setup.
22
- */
23
- class TestRepositories {
24
- subscriptionRepo = $repository(subscriptions);
25
- }
26
-
27
- // -----------------------------------------------------------------------------------------------------------------
28
-
29
- const testPlans: PlanDefinition[] = [
30
- {
31
- id: "free",
32
- name: "Free",
33
- available: true,
34
- pricing: [{ interval: "monthly", amount: 0, currency: "usd" }],
35
- features: ["dashboard"],
36
- limits: { seats: 1, "api-calls": 100 },
37
- order: 0,
38
- },
39
- {
40
- id: "pro",
41
- name: "Pro",
42
- available: true,
43
- pricing: [
44
- { interval: "monthly", amount: 2900, currency: "usd" },
45
- { interval: "yearly", amount: 29000, currency: "usd" },
46
- ],
47
- trial: { days: 14, requirePaymentMethod: false },
48
- features: ["dashboard", "analytics", "export"],
49
- limits: { seats: 10, "api-calls": 10000 },
50
- order: 1,
51
- },
52
- ];
53
-
54
- // -----------------------------------------------------------------------------------------------------------------
55
-
56
- const setup = async () => {
57
- const alepha = Alepha.create()
58
- .with(AlephaOrmPostgres)
59
- .with({ provide: SubscriptionConfig, use: TestSubscriptionConfig })
60
- .with(AlephaApiSubscriptions);
61
-
62
- const service = alepha.inject(SubscriptionService);
63
- const config = alepha.inject(
64
- TestSubscriptionConfig,
65
- ) as TestSubscriptionConfig;
66
- const repos = alepha.inject(TestRepositories);
67
- await alepha.start();
68
-
69
- await config.seedPlans(testPlans);
70
-
71
- /**
72
- * Helper: create a subscription and attach a payment intent ID for billing lookup.
73
- */
74
- const createSubscriptionWithIntent = async (
75
- planId: string,
76
- intentId: string,
77
- options?: { skipTrial?: boolean },
78
- ) => {
79
- const orgId = randomUUID();
80
- const sub = await service.subscribe(orgId, planId, "monthly", options);
81
- await repos.subscriptionRepo.updateById(sub.id, {
82
- lastPaymentIntentId: intentId,
83
- });
84
- return service.getSubscription(sub.id);
85
- };
86
-
87
- return { alepha, service, config, repos, createSubscriptionWithIntent };
88
- };
89
-
90
- // -----------------------------------------------------------------------------------------------------------------
91
-
92
- describe("BillingService", () => {
93
- // ---------------------------------------------------------------------------------------------------------------
94
-
95
- describe("payment captured", () => {
96
- it("should activate a trialing subscription after payment", async ({
97
- expect,
98
- }) => {
99
- const { alepha, service, createSubscriptionWithIntent } = await setup();
100
-
101
- const intentId = randomUUID();
102
- const sub = await createSubscriptionWithIntent("pro", intentId);
103
- expect(sub.status).toBe("trialing");
104
-
105
- await alepha.events.emit("payments:captured", {
106
- intentId,
107
- amount: 2900,
108
- currency: "usd",
109
- });
110
-
111
- const updated = await service.getSubscription(sub.id);
112
- expect(updated.status).toBe("active");
113
- expect(updated.lastPaymentIntentId).toBe(intentId);
114
- });
115
-
116
- it("should renew an active subscription and advance period", async ({
117
- expect,
118
- }) => {
119
- const { alepha, service, createSubscriptionWithIntent } = await setup();
120
-
121
- const intentId = randomUUID();
122
- const sub = await createSubscriptionWithIntent("pro", intentId, {
123
- skipTrial: true,
124
- });
125
- expect(sub.status).toBe("active");
126
-
127
- const originalPeriodEnd = sub.currentPeriodEnd;
128
-
129
- await alepha.events.emit("payments:captured", {
130
- intentId,
131
- amount: 2900,
132
- currency: "usd",
133
- });
134
-
135
- const updated = await service.getSubscription(sub.id);
136
- expect(updated.status).toBe("active");
137
- expect(updated.currentPeriodStart).toBe(originalPeriodEnd);
138
- expect(updated.currentPeriodEnd).not.toBe(originalPeriodEnd);
139
- });
140
-
141
- it("should recover from dunning", async ({ expect }) => {
142
- const { alepha, service, repos, createSubscriptionWithIntent } =
143
- await setup();
144
-
145
- const intentId = randomUUID();
146
- const sub = await createSubscriptionWithIntent("pro", intentId, {
147
- skipTrial: true,
148
- });
149
-
150
- await repos.subscriptionRepo.updateById(sub.id, {
151
- status: "past_due",
152
- dunningAttempt: 2,
153
- dunningStartedAt: new Date().toISOString(),
154
- });
155
-
156
- await alepha.events.emit("payments:captured", {
157
- intentId,
158
- amount: 2900,
159
- currency: "usd",
160
- });
161
-
162
- const updated = await service.getSubscription(sub.id);
163
- expect(updated.status).toBe("active");
164
- expect(updated.dunningAttempt).toBe(0);
165
- });
166
- });
167
-
168
- // ---------------------------------------------------------------------------------------------------------------
169
-
170
- describe("payment failed", () => {
171
- it("should start dunning on first failure", async ({ expect }) => {
172
- const { alepha, service, createSubscriptionWithIntent } = await setup();
173
-
174
- const intentId = randomUUID();
175
- const sub = await createSubscriptionWithIntent("pro", intentId, {
176
- skipTrial: true,
177
- });
178
- expect(sub.status).toBe("active");
179
-
180
- await alepha.events.emit("payments:failed", {
181
- intentId,
182
- amount: 2900,
183
- currency: "usd",
184
- });
185
-
186
- const updated = await service.getSubscription(sub.id);
187
- expect(updated.status).toBe("past_due");
188
- expect(updated.dunningAttempt).toBe(1);
189
- expect(updated.dunningStartedAt).toBeDefined();
190
- });
191
-
192
- it("should increment dunning on subsequent failure", async ({ expect }) => {
193
- const { alepha, service, repos, createSubscriptionWithIntent } =
194
- await setup();
195
-
196
- const intentId = randomUUID();
197
- const sub = await createSubscriptionWithIntent("pro", intentId, {
198
- skipTrial: true,
199
- });
200
-
201
- await repos.subscriptionRepo.updateById(sub.id, {
202
- status: "past_due",
203
- dunningAttempt: 1,
204
- dunningStartedAt: new Date().toISOString(),
205
- });
206
-
207
- await alepha.events.emit("payments:failed", {
208
- intentId,
209
- amount: 2900,
210
- currency: "usd",
211
- });
212
-
213
- const updated = await service.getSubscription(sub.id);
214
- expect(updated.status).toBe("past_due");
215
- expect(updated.dunningAttempt).toBe(2);
216
- });
217
- });
218
- });
@@ -1,278 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { Alepha } from "alepha";
3
- import { AlephaOrmPostgres } from "alepha/orm/postgres";
4
- import { BadRequestError } from "alepha/server";
5
- import { describe, it } from "vitest";
6
- import { AlephaApiSubscriptions } from "../index.ts";
7
- import type { PlanDefinition } from "../schemas/planDefinitionSchema.ts";
8
- import { SubscriptionConfig } from "../services/SubscriptionConfig.ts";
9
- import { SubscriptionService } from "../services/SubscriptionService.ts";
10
-
11
- // -----------------------------------------------------------------------------------------------------------------
12
-
13
- class TestSubscriptionConfig extends SubscriptionConfig {
14
- public async seedPlans(plans: PlanDefinition[]): Promise<void> {
15
- await this.plans.set({ plans });
16
- }
17
- }
18
-
19
- // -----------------------------------------------------------------------------------------------------------------
20
-
21
- const testPlans: PlanDefinition[] = [
22
- {
23
- id: "free",
24
- name: "Free",
25
- available: true,
26
- pricing: [{ interval: "monthly", amount: 0, currency: "usd" }],
27
- features: ["dashboard"],
28
- limits: { seats: 1, "api-calls": 100 },
29
- order: 0,
30
- },
31
- {
32
- id: "pro",
33
- name: "Pro",
34
- available: true,
35
- pricing: [
36
- { interval: "monthly", amount: 2900, currency: "usd" },
37
- { interval: "yearly", amount: 29000, currency: "usd" },
38
- ],
39
- trial: { days: 14, requirePaymentMethod: false },
40
- features: ["dashboard", "analytics", "export"],
41
- limits: { seats: 10, "api-calls": 10000 },
42
- order: 1,
43
- },
44
- ];
45
-
46
- // -----------------------------------------------------------------------------------------------------------------
47
-
48
- const setup = async () => {
49
- const alepha = Alepha.create()
50
- .with(AlephaOrmPostgres)
51
- .with({ provide: SubscriptionConfig, use: TestSubscriptionConfig })
52
- .with(AlephaApiSubscriptions);
53
-
54
- const service = alepha.inject(SubscriptionService);
55
- const config = alepha.inject(
56
- TestSubscriptionConfig,
57
- ) as TestSubscriptionConfig;
58
- await alepha.start();
59
-
60
- await config.seedPlans(testPlans);
61
-
62
- return { alepha, service, config };
63
- };
64
-
65
- // -----------------------------------------------------------------------------------------------------------------
66
-
67
- describe("SubscriptionService", () => {
68
- // ---------------------------------------------------------------------------------------------------------------
69
-
70
- describe("subscribe", () => {
71
- it("should create a trialing subscription with trial days", async ({
72
- expect,
73
- }) => {
74
- const { service } = await setup();
75
- const orgId = randomUUID();
76
-
77
- const sub = await service.subscribe(orgId, "pro", "monthly");
78
-
79
- expect(sub.status).toBe("trialing");
80
- expect(sub.planId).toBe("pro");
81
- expect(sub.interval).toBe("monthly");
82
- expect(sub.organizationId).toBe(orgId);
83
- expect(sub.trialStart).toBeDefined();
84
- expect(sub.trialEnd).toBeDefined();
85
- });
86
-
87
- it("should create an active subscription when skipTrial", async ({
88
- expect,
89
- }) => {
90
- const { service } = await setup();
91
- const orgId = randomUUID();
92
-
93
- const sub = await service.subscribe(orgId, "pro", "monthly", {
94
- skipTrial: true,
95
- });
96
-
97
- expect(sub.status).toBe("active");
98
- expect(sub.planId).toBe("pro");
99
- expect(sub.trialStart).toBeUndefined();
100
- expect(sub.trialEnd).toBeUndefined();
101
- });
102
-
103
- it("should reject when plan not found", async ({ expect }) => {
104
- const { service } = await setup();
105
- const orgId = randomUUID();
106
-
107
- await expect(
108
- service.subscribe(orgId, "nonexistent", "monthly"),
109
- ).rejects.toThrow(BadRequestError);
110
- });
111
-
112
- it("should reject duplicate subscription for same org", async ({
113
- expect,
114
- }) => {
115
- const { service } = await setup();
116
- const orgId = randomUUID();
117
-
118
- await service.subscribe(orgId, "pro", "monthly", {
119
- skipTrial: true,
120
- });
121
-
122
- await expect(service.subscribe(orgId, "free", "monthly")).rejects.toThrow(
123
- BadRequestError,
124
- );
125
- });
126
- });
127
-
128
- // ---------------------------------------------------------------------------------------------------------------
129
-
130
- describe("cancel", () => {
131
- it("should cancel at period end", async ({ expect }) => {
132
- const { service } = await setup();
133
- const orgId = randomUUID();
134
-
135
- const sub = await service.subscribe(orgId, "pro", "monthly", {
136
- skipTrial: true,
137
- });
138
-
139
- await service.cancel(sub.id, { immediate: false });
140
-
141
- const updated = await service.getSubscription(sub.id);
142
- expect(updated.status).toBe("cancelled");
143
- expect(updated.cancelAtPeriodEnd).toBe(true);
144
- expect(updated.cancelledAt).toBeDefined();
145
- });
146
-
147
- it("should cancel immediately with expired status", async ({ expect }) => {
148
- const { service } = await setup();
149
- const orgId = randomUUID();
150
-
151
- const sub = await service.subscribe(orgId, "pro", "monthly", {
152
- skipTrial: true,
153
- });
154
-
155
- await service.cancel(sub.id, { immediate: true });
156
-
157
- const updated = await service.getSubscription(sub.id);
158
- expect(updated.status).toBe("expired");
159
- expect(updated.cancelAtPeriodEnd).toBe(false);
160
- expect(updated.cancelledAt).toBeDefined();
161
- });
162
- });
163
-
164
- // ---------------------------------------------------------------------------------------------------------------
165
-
166
- describe("resume", () => {
167
- it("should resume a cancelled subscription back to active", async ({
168
- expect,
169
- }) => {
170
- const { service } = await setup();
171
- const orgId = randomUUID();
172
-
173
- const sub = await service.subscribe(orgId, "pro", "monthly", {
174
- skipTrial: true,
175
- });
176
-
177
- await service.cancel(sub.id, { immediate: false });
178
-
179
- const cancelled = await service.getSubscription(sub.id);
180
- expect(cancelled.status).toBe("cancelled");
181
-
182
- await service.resume(sub.id);
183
-
184
- const resumed = await service.getSubscription(sub.id);
185
- expect(resumed.status).toBe("active");
186
- expect(resumed.cancelAtPeriodEnd).toBe(false);
187
- });
188
- });
189
-
190
- // ---------------------------------------------------------------------------------------------------------------
191
-
192
- describe("changePlan", () => {
193
- it("should schedule plan change at period end", async ({ expect }) => {
194
- const { service } = await setup();
195
- const orgId = randomUUID();
196
-
197
- const sub = await service.subscribe(orgId, "free", "monthly", {
198
- skipTrial: true,
199
- });
200
-
201
- await service.changePlan(sub.id, "pro", "monthly", {
202
- immediate: false,
203
- });
204
-
205
- const updated = await service.getSubscription(sub.id);
206
- expect(updated.pendingPlanId).toBe("pro");
207
- expect(updated.planId).toBe("free");
208
- });
209
-
210
- it("should apply immediate plan change", async ({ expect }) => {
211
- const { service } = await setup();
212
- const orgId = randomUUID();
213
-
214
- const sub = await service.subscribe(orgId, "free", "monthly", {
215
- skipTrial: true,
216
- });
217
-
218
- await service.changePlan(sub.id, "pro", "monthly", {
219
- immediate: true,
220
- });
221
-
222
- const updated = await service.getSubscription(sub.id);
223
- expect(updated.planId).toBe("pro");
224
- expect(updated.pendingPlanId).toBeUndefined();
225
- });
226
- });
227
-
228
- // ---------------------------------------------------------------------------------------------------------------
229
-
230
- describe("entitlements", () => {
231
- it("should return true for included feature", async ({ expect }) => {
232
- const { service } = await setup();
233
- const orgId = randomUUID();
234
-
235
- await service.subscribe(orgId, "pro", "monthly", {
236
- skipTrial: true,
237
- });
238
-
239
- const canAnalytics = await service.can(orgId, "analytics");
240
- expect(canAnalytics).toBe(true);
241
- });
242
-
243
- it("should return false for excluded feature", async ({ expect }) => {
244
- const { service } = await setup();
245
- const orgId = randomUUID();
246
-
247
- await service.subscribe(orgId, "free", "monthly", {
248
- skipTrial: true,
249
- });
250
-
251
- const canAnalytics = await service.can(orgId, "analytics");
252
- expect(canAnalytics).toBe(false);
253
- });
254
-
255
- it("should return correct limit value", async ({ expect }) => {
256
- const { service } = await setup();
257
- const orgId = randomUUID();
258
-
259
- await service.subscribe(orgId, "pro", "monthly", {
260
- skipTrial: true,
261
- });
262
-
263
- const seats = await service.limit(orgId, "seats");
264
- expect(seats).toBe(10);
265
-
266
- const apiCalls = await service.limit(orgId, "api-calls");
267
- expect(apiCalls).toBe(10000);
268
- });
269
-
270
- it("should return 0 limit for no subscription", async ({ expect }) => {
271
- const { service } = await setup();
272
- const orgId = randomUUID();
273
-
274
- const seats = await service.limit(orgId, "seats");
275
- expect(seats).toBe(0);
276
- });
277
- });
278
- });
@@ -1,212 +0,0 @@
1
- import { $inject, t } from "alepha";
2
- import { $secure } from "alepha/security";
3
- import { $action, okSchema } from "alepha/server";
4
- import { cancelSubscriptionSchema } from "../schemas/cancelSubscriptionSchema.ts";
5
- import { changePlanSchema } from "../schemas/changePlanSchema.ts";
6
- import { mrrSchema } from "../schemas/mrrSchema.ts";
7
- import { subscriptionQuerySchema } from "../schemas/subscriptionQuerySchema.ts";
8
- import { subscriptionResourceSchema } from "../schemas/subscriptionResourceSchema.ts";
9
- import { subscriptionStatsSchema } from "../schemas/subscriptionStatsSchema.ts";
10
- import { SubscriptionConfig } from "../services/SubscriptionConfig.ts";
11
- import { SubscriptionService } from "../services/SubscriptionService.ts";
12
-
13
- export class AdminSubscriptionController {
14
- protected readonly url = "/subscriptions";
15
- protected readonly group = "admin:subscriptions";
16
- protected readonly service = $inject(SubscriptionService);
17
- protected readonly config = $inject(SubscriptionConfig);
18
-
19
- /**
20
- * Find subscriptions with pagination and filtering.
21
- */
22
- public readonly findSubscriptions = $action({
23
- path: this.url,
24
- group: this.group,
25
- use: [$secure({ permissions: ["admin:subscription:read"] })],
26
- description: "Find subscriptions with pagination and filtering",
27
- schema: {
28
- query: subscriptionQuerySchema,
29
- response: t.page(subscriptionResourceSchema),
30
- },
31
- handler: ({ query }) => this.service.findSubscriptions(query),
32
- });
33
-
34
- /**
35
- * Get a subscription by ID.
36
- */
37
- public readonly getSubscription = $action({
38
- path: `${this.url}/:id`,
39
- group: this.group,
40
- use: [$secure({ permissions: ["admin:subscription:read"] })],
41
- description: "Get a subscription by ID",
42
- schema: {
43
- params: t.object({ id: t.uuid() }),
44
- response: subscriptionResourceSchema,
45
- },
46
- handler: ({ params }) => this.service.getSubscription(params.id),
47
- });
48
-
49
- /**
50
- * Get aggregated subscription statistics.
51
- */
52
- public readonly getStats = $action({
53
- path: `${this.url}/stats`,
54
- group: this.group,
55
- use: [$secure({ permissions: ["admin:subscription:read"] })],
56
- description: "Get aggregated subscription statistics",
57
- schema: {
58
- response: subscriptionStatsSchema,
59
- },
60
- handler: () => this.service.getStats(),
61
- });
62
-
63
- /**
64
- * Get revenue data from recent subscription events.
65
- */
66
- public readonly getRevenue = $action({
67
- path: `${this.url}/revenue`,
68
- group: this.group,
69
- use: [$secure({ permissions: ["admin:subscription:read"] })],
70
- description: "Get revenue data from recent subscription events",
71
- schema: {
72
- query: t.object({
73
- days: t.optional(t.integer({ minimum: 1, maximum: 365 })),
74
- }),
75
- response: t.object({ total: t.integer(), count: t.integer() }),
76
- },
77
- handler: ({ query }) => this.service.getRevenue(query.days),
78
- });
79
-
80
- /**
81
- * Get Monthly Recurring Revenue breakdown.
82
- */
83
- public readonly getMrr = $action({
84
- path: `${this.url}/mrr`,
85
- group: this.group,
86
- use: [$secure({ permissions: ["admin:subscription:read"] })],
87
- description: "Get Monthly Recurring Revenue breakdown",
88
- schema: {
89
- response: mrrSchema,
90
- },
91
- handler: async () => {
92
- const activeSubs = await this.service.findSubscriptions({
93
- status: "active",
94
- size: 1000,
95
- });
96
-
97
- const plans = await this.config.getPlans();
98
- const byPlan: Record<string, number> = {};
99
- let total = 0;
100
-
101
- for (const sub of activeSubs.content) {
102
- const plan = plans.find((p) => p.id === sub.planId);
103
- if (!plan) continue;
104
-
105
- const pricing = plan.pricing.find((p) => p.interval === sub.interval);
106
- if (!pricing) continue;
107
-
108
- const monthlyAmount =
109
- sub.interval === "yearly"
110
- ? Math.round(pricing.amount / 12)
111
- : pricing.amount;
112
-
113
- byPlan[sub.planId] = (byPlan[sub.planId] ?? 0) + monthlyAmount;
114
- total += monthlyAmount;
115
- }
116
-
117
- return {
118
- total,
119
- byPlan,
120
- growth: 0,
121
- newMrr: 0,
122
- expansionMrr: 0,
123
- contractionMrr: 0,
124
- churnMrr: 0,
125
- };
126
- },
127
- });
128
-
129
- /**
130
- * Force a plan change for a subscription (admin action).
131
- */
132
- public readonly adminChangePlan = $action({
133
- method: "POST",
134
- path: `${this.url}/:id/change-plan`,
135
- group: this.group,
136
- use: [$secure({ permissions: ["admin:subscription:update"] })],
137
- description: "Force a plan change for a subscription",
138
- schema: {
139
- params: t.object({ id: t.uuid() }),
140
- body: changePlanSchema,
141
- response: subscriptionResourceSchema,
142
- },
143
- handler: async ({ params, body }) => {
144
- await this.service.changePlan(params.id, body.planId, body.interval, {
145
- immediate: body.immediate,
146
- });
147
- return this.service.getSubscription(params.id);
148
- },
149
- });
150
-
151
- /**
152
- * Force cancel a subscription (admin action).
153
- */
154
- public readonly adminCancel = $action({
155
- method: "POST",
156
- path: `${this.url}/:id/cancel`,
157
- group: this.group,
158
- use: [$secure({ permissions: ["admin:subscription:update"] })],
159
- description: "Force cancel a subscription",
160
- schema: {
161
- params: t.object({ id: t.uuid() }),
162
- body: cancelSubscriptionSchema,
163
- response: okSchema,
164
- },
165
- handler: async ({ params, body }) => {
166
- await this.service.cancel(params.id, {
167
- reason: body.reason,
168
- immediate: body.immediate,
169
- });
170
- return { ok: true };
171
- },
172
- });
173
-
174
- /**
175
- * Reactivate a suspended subscription (admin action).
176
- */
177
- public readonly adminReactivate = $action({
178
- method: "POST",
179
- path: `${this.url}/:id/reactivate`,
180
- group: this.group,
181
- use: [$secure({ permissions: ["admin:subscription:update"] })],
182
- description: "Reactivate a suspended subscription",
183
- schema: {
184
- params: t.object({ id: t.uuid() }),
185
- response: okSchema,
186
- },
187
- handler: async ({ params }) => {
188
- await this.service.reactivate(params.id);
189
- return { ok: true };
190
- },
191
- });
192
-
193
- /**
194
- * Extend the trial period for a trialing subscription (admin action).
195
- */
196
- public readonly adminExtendTrial = $action({
197
- method: "POST",
198
- path: `${this.url}/:id/extend-trial`,
199
- group: this.group,
200
- use: [$secure({ permissions: ["admin:subscription:update"] })],
201
- description: "Extend the trial period for a subscription",
202
- schema: {
203
- params: t.object({ id: t.uuid() }),
204
- body: t.object({ days: t.integer({ minimum: 1, maximum: 365 }) }),
205
- response: okSchema,
206
- },
207
- handler: async ({ params, body }) => {
208
- await this.service.extendTrial(params.id, body.days);
209
- return { ok: true };
210
- },
211
- });
212
- }