alepha 0.19.2 → 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 (241) 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 +90 -34
  10. package/dist/api/jobs/index.d.ts.map +1 -1
  11. package/dist/api/jobs/index.js +267 -44
  12. package/dist/api/jobs/index.js.map +1 -1
  13. package/dist/api/notifications/index.browser.js +0 -1
  14. package/dist/api/notifications/index.browser.js.map +1 -1
  15. package/dist/api/notifications/index.d.ts +3 -3
  16. package/dist/api/notifications/index.d.ts.map +1 -1
  17. package/dist/api/notifications/index.js +0 -1
  18. package/dist/api/notifications/index.js.map +1 -1
  19. package/dist/api/parameters/index.browser.js +112 -1
  20. package/dist/api/parameters/index.browser.js.map +1 -1
  21. package/dist/api/parameters/index.d.ts +90 -3
  22. package/dist/api/parameters/index.d.ts.map +1 -1
  23. package/dist/api/parameters/index.js +79 -12
  24. package/dist/api/parameters/index.js.map +1 -1
  25. package/dist/{billing → api/payments}/index.d.ts +67 -49
  26. package/dist/api/payments/index.d.ts.map +1 -0
  27. package/dist/{billing → api/payments}/index.js +108 -74
  28. package/dist/api/payments/index.js.map +1 -0
  29. package/dist/api/subscriptions/index.d.ts +1692 -0
  30. package/dist/api/subscriptions/index.d.ts.map +1 -0
  31. package/dist/api/subscriptions/index.js +1870 -0
  32. package/dist/api/subscriptions/index.js.map +1 -0
  33. package/dist/api/users/index.d.ts +27 -21
  34. package/dist/api/users/index.d.ts.map +1 -1
  35. package/dist/api/users/index.js +167 -34
  36. package/dist/api/users/index.js.map +1 -1
  37. package/dist/api/workflows/index.browser.js +246 -0
  38. package/dist/api/workflows/index.browser.js.map +1 -0
  39. package/dist/api/workflows/index.d.ts +1618 -0
  40. package/dist/api/workflows/index.d.ts.map +1 -0
  41. package/dist/api/workflows/index.js +1504 -0
  42. package/dist/api/workflows/index.js.map +1 -0
  43. package/dist/cli/config/index.d.ts +6 -28
  44. package/dist/cli/config/index.d.ts.map +1 -1
  45. package/dist/cli/config/index.js +5 -10
  46. package/dist/cli/config/index.js.map +1 -1
  47. package/dist/cli/core/index.d.ts +11669 -208
  48. package/dist/cli/core/index.d.ts.map +1 -1
  49. package/dist/cli/core/index.js +60 -69
  50. package/dist/cli/core/index.js.map +1 -1
  51. package/dist/cli/devtools/index.d.ts +5 -0
  52. package/dist/cli/devtools/index.d.ts.map +1 -1
  53. package/dist/cli/devtools/index.js +4 -0
  54. package/dist/cli/devtools/index.js.map +1 -1
  55. package/dist/cli/platform/index.d.ts +69 -64
  56. package/dist/cli/platform/index.d.ts.map +1 -1
  57. package/dist/cli/platform/index.js +6 -2
  58. package/dist/cli/platform/index.js.map +1 -1
  59. package/dist/cli/vendor/index.d.ts +38 -10
  60. package/dist/cli/vendor/index.d.ts.map +1 -1
  61. package/dist/cli/vendor/index.js +85 -26
  62. package/dist/cli/vendor/index.js.map +1 -1
  63. package/dist/core/index.browser.js +21 -2
  64. package/dist/core/index.browser.js.map +1 -1
  65. package/dist/core/index.d.ts +33 -2
  66. package/dist/core/index.d.ts.map +1 -1
  67. package/dist/core/index.js +25 -2
  68. package/dist/core/index.js.map +1 -1
  69. package/dist/core/index.native.js +25 -2
  70. package/dist/core/index.native.js.map +1 -1
  71. package/dist/core/index.workerd.js +25 -2
  72. package/dist/core/index.workerd.js.map +1 -1
  73. package/dist/email/smtp/index.js +24 -8
  74. package/dist/email/smtp/index.js.map +1 -1
  75. package/dist/logger/index.d.ts.map +1 -1
  76. package/dist/logger/index.js +1 -1
  77. package/dist/logger/index.js.map +1 -1
  78. package/dist/orm/core/index.browser.js +0 -18
  79. package/dist/orm/core/index.browser.js.map +1 -1
  80. package/dist/orm/core/index.bun.js +25 -73
  81. package/dist/orm/core/index.bun.js.map +1 -1
  82. package/dist/orm/core/index.d.ts +10 -32
  83. package/dist/orm/core/index.d.ts.map +1 -1
  84. package/dist/orm/core/index.js +25 -73
  85. package/dist/orm/core/index.js.map +1 -1
  86. package/dist/orm/postgres/index.bun.js +3 -3
  87. package/dist/orm/postgres/index.bun.js.map +1 -1
  88. package/dist/orm/postgres/index.d.ts +2 -1
  89. package/dist/orm/postgres/index.d.ts.map +1 -1
  90. package/dist/orm/postgres/index.js +3 -3
  91. package/dist/orm/postgres/index.js.map +1 -1
  92. package/dist/react/router/index.browser.js +25 -3
  93. package/dist/react/router/index.browser.js.map +1 -1
  94. package/dist/react/router/index.d.ts +16 -1
  95. package/dist/react/router/index.d.ts.map +1 -1
  96. package/dist/react/router/index.js +25 -3
  97. package/dist/react/router/index.js.map +1 -1
  98. package/dist/security/index.d.ts +28 -0
  99. package/dist/security/index.d.ts.map +1 -1
  100. package/dist/security/index.js +28 -0
  101. package/dist/security/index.js.map +1 -1
  102. package/package.json +37 -20
  103. package/src/api/invitations/__tests__/InvitationService.spec.ts +439 -0
  104. package/src/api/invitations/controllers/AdminInvitationController.ts +86 -0
  105. package/src/api/invitations/controllers/InvitationController.ts +84 -0
  106. package/src/api/invitations/entities/invitations.ts +33 -0
  107. package/src/api/invitations/index.ts +65 -0
  108. package/src/api/invitations/jobs/InvitationJobs.ts +37 -0
  109. package/src/api/invitations/providers/InvitationProvider.ts +45 -0
  110. package/src/api/invitations/schemas/createInvitationSchema.ts +12 -0
  111. package/src/api/invitations/schemas/invitationConfigAtom.ts +20 -0
  112. package/src/api/invitations/schemas/invitationQuerySchema.ts +15 -0
  113. package/src/api/invitations/schemas/invitationResourceSchema.ts +6 -0
  114. package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +22 -0
  115. package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +10 -0
  116. package/src/api/invitations/services/InvitationService.ts +556 -0
  117. package/src/api/jobs/__tests__/$job.spec.ts +876 -0
  118. package/src/api/jobs/controllers/AdminJobController.ts +44 -0
  119. package/src/api/jobs/entities/jobExecutionEntity.ts +0 -2
  120. package/src/api/jobs/index.ts +0 -3
  121. package/src/api/jobs/primitives/$job.ts +22 -11
  122. package/src/api/jobs/providers/JobProvider.ts +239 -25
  123. package/src/api/jobs/schemas/jobConfigAtom.ts +4 -0
  124. package/src/api/jobs/schemas/jobCronInfoSchema.ts +1 -0
  125. package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +0 -1
  126. package/src/api/jobs/schemas/jobQueueDepthSchema.ts +1 -0
  127. package/src/api/jobs/schemas/jobRegistrationSchema.ts +1 -6
  128. package/src/api/jobs/services/JobService.ts +51 -12
  129. package/src/api/notifications/schemas/notificationQuerySchema.ts +0 -1
  130. package/src/api/parameters/__tests__/$parameter.spec.ts +327 -0
  131. package/src/api/parameters/controllers/AdminParameterController.ts +29 -3
  132. package/src/api/parameters/index.browser.ts +12 -0
  133. package/src/api/parameters/primitives/$parameter.ts +20 -3
  134. package/src/api/parameters/services/ParameterProvider.ts +48 -7
  135. package/src/{billing → api/payments}/__tests__/PaymentMethodService.spec.ts +32 -6
  136. package/src/api/payments/__tests__/PaymentService.spec.ts +279 -0
  137. package/src/{billing/controllers/AdminBillingController.ts → api/payments/controllers/AdminPaymentController.ts} +26 -21
  138. package/src/{billing/controllers/BillingController.ts → api/payments/controllers/PaymentController.ts} +23 -11
  139. package/src/{billing → api/payments}/entities/paymentIntents.ts +1 -0
  140. package/src/{billing/errors/BillingError.ts → api/payments/errors/PaymentError.ts} +1 -1
  141. package/src/{billing → api/payments}/index.ts +31 -25
  142. package/src/{billing/providers/MemoryBillingProvider.ts → api/payments/providers/MemoryPaymentProvider.ts} +4 -4
  143. package/src/{billing/providers/BillingProvider.ts → api/payments/providers/PaymentProvider.ts} +9 -2
  144. package/src/{billing → api/payments}/services/PaymentMethodService.ts +5 -5
  145. package/src/{billing/services/BillingService.ts → api/payments/services/PaymentService.ts} +94 -18
  146. package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
  147. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
  148. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
  149. package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
  150. package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
  151. package/src/api/subscriptions/entities/subscriptions.ts +68 -0
  152. package/src/api/subscriptions/index.ts +144 -0
  153. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
  154. package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
  155. package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
  156. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
  157. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
  158. package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
  159. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
  160. package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
  161. package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
  162. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
  163. package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
  164. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
  165. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
  166. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
  167. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
  168. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
  169. package/src/api/subscriptions/services/BillingService.ts +437 -0
  170. package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
  171. package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
  172. package/src/api/subscriptions/services/UsageService.ts +118 -0
  173. package/src/api/users/__tests__/AdminUserController.spec.ts +80 -1
  174. package/src/api/users/__tests__/CredentialService.spec.ts +177 -0
  175. package/src/api/users/__tests__/EmailVerification.spec.ts +29 -18
  176. package/src/api/users/__tests__/PasswordReset.spec.ts +3 -0
  177. package/src/api/users/__tests__/RegistrationService.spec.ts +148 -1
  178. package/src/api/users/__tests__/SessionService.spec.ts +142 -1
  179. package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -1
  180. package/src/api/users/controllers/UserController.ts +3 -8
  181. package/src/api/users/notifications/UserNotifications.ts +23 -0
  182. package/src/api/users/schemas/loginSchema.ts +1 -1
  183. package/src/api/users/services/CredentialService.ts +51 -4
  184. package/src/api/users/services/RegistrationService.ts +38 -9
  185. package/src/api/users/services/SessionService.ts +62 -9
  186. package/src/api/users/services/UserService.ts +21 -12
  187. package/src/api/workflows/__tests__/$workflow.spec.ts +616 -0
  188. package/src/api/workflows/controllers/AdminWorkflowController.ts +191 -0
  189. package/src/api/workflows/entities/workflowExecutions.ts +74 -0
  190. package/src/api/workflows/entities/workflowStepExecutions.ts +74 -0
  191. package/src/api/workflows/entities/workflowStepLogs.ts +13 -0
  192. package/src/api/workflows/index.browser.ts +22 -0
  193. package/src/api/workflows/index.ts +124 -0
  194. package/src/api/workflows/jobs/WorkflowJobs.ts +77 -0
  195. package/src/api/workflows/primitives/$workflow.ts +202 -0
  196. package/src/api/workflows/providers/WorkflowProvider.ts +1284 -0
  197. package/src/api/workflows/schemas/workflowActivitySchema.ts +15 -0
  198. package/src/api/workflows/schemas/workflowConfigAtom.ts +51 -0
  199. package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +18 -0
  200. package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +26 -0
  201. package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +30 -0
  202. package/src/api/workflows/schemas/workflowRegistrationSchema.ts +26 -0
  203. package/src/api/workflows/schemas/workflowStatsSchema.ts +16 -0
  204. package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +15 -0
  205. package/src/api/workflows/services/WorkflowService.ts +382 -0
  206. package/src/cli/config/defineConfig.ts +17 -46
  207. package/src/cli/core/providers/ViteDevServerProvider.ts +45 -3
  208. package/src/cli/core/services/PackageManagerUtils.ts +3 -1
  209. package/src/cli/core/services/ProjectScaffolder.ts +5 -5
  210. package/src/cli/core/templates/agentMd.ts +14 -5
  211. package/src/cli/core/templates/webAppRouterTs.ts +5 -58
  212. package/src/cli/devtools/index.ts +21 -1
  213. package/src/cli/platform/index.ts +23 -2
  214. package/src/cli/vendor/__tests__/VendorService.spec.ts +283 -178
  215. package/src/cli/vendor/index.ts +20 -3
  216. package/src/cli/vendor/services/VendorService.ts +126 -27
  217. package/src/core/Alepha.ts +10 -0
  218. package/src/core/__tests__/TypeProvider.spec.ts +4 -2
  219. package/src/core/providers/SchemaValidator.ts +1 -1
  220. package/src/core/providers/TypeProvider.ts +46 -3
  221. package/src/logger/index.ts +6 -1
  222. package/src/orm/__tests__/enums.spec.ts +22 -29
  223. package/src/orm/__tests__/orm-showcase-tests.ts +430 -0
  224. package/src/orm/__tests__/orm-showcase.spec.ts +167 -0
  225. package/src/orm/core/providers/DatabaseTypeProvider.ts +0 -29
  226. package/src/orm/core/providers/DrizzleKitProvider.ts +56 -105
  227. package/src/orm/postgres/services/PostgresModelBuilder.ts +3 -6
  228. package/src/react/router/__tests__/$page.browser.spec.tsx +157 -0
  229. package/src/react/router/providers/ReactBrowserProvider.ts +39 -0
  230. package/src/react/router/providers/ReactBrowserRouterProvider.ts +22 -0
  231. package/src/security/__tests__/$secure-combinations.spec.ts +945 -0
  232. package/src/security/primitives/$secure.ts +28 -0
  233. package/tsconfig.base.json +0 -1
  234. package/dist/billing/index.d.ts.map +0 -1
  235. package/dist/billing/index.js.map +0 -1
  236. package/src/billing/__tests__/BillingService.spec.ts +0 -136
  237. /package/src/{billing → api/payments}/entities/paymentMethods.ts +0 -0
  238. /package/src/{billing → api/payments}/entities/refunds.ts +0 -0
  239. /package/src/{billing → api/payments}/schemas/intentSchemas.ts +0 -0
  240. /package/src/{billing → api/payments}/schemas/paymentMethodSchemas.ts +0 -0
  241. /package/src/{billing → api/payments}/schemas/refundSchemas.ts +0 -0
@@ -2,7 +2,8 @@ import { randomUUID } from "node:crypto";
2
2
  import { Alepha } from "alepha";
3
3
  import { AlephaOrmPostgres } from "alepha/orm/postgres";
4
4
  import { describe, it } from "vitest";
5
- import { AlephaBilling } from "../index.ts";
5
+ import { PaymentError } from "../errors/PaymentError.ts";
6
+ import { AlephaApiPayments } from "../index.ts";
6
7
  import { PaymentMethodService } from "../services/PaymentMethodService.ts";
7
8
 
8
9
  describe("PaymentMethodService", () => {
@@ -10,7 +11,9 @@ describe("PaymentMethodService", () => {
10
11
  const userId2 = randomUUID();
11
12
  const orgId = randomUUID();
12
13
  it("should add a payment method", async ({ expect }) => {
13
- const alepha = Alepha.create().with(AlephaOrmPostgres).with(AlephaBilling);
14
+ const alepha = Alepha.create()
15
+ .with(AlephaOrmPostgres)
16
+ .with(AlephaApiPayments);
14
17
  const service = alepha.inject(PaymentMethodService);
15
18
  await alepha.start();
16
19
 
@@ -21,7 +24,9 @@ describe("PaymentMethodService", () => {
21
24
  });
22
25
 
23
26
  it("should list payment methods for a user", async ({ expect }) => {
24
- const alepha = Alepha.create().with(AlephaOrmPostgres).with(AlephaBilling);
27
+ const alepha = Alepha.create()
28
+ .with(AlephaOrmPostgres)
29
+ .with(AlephaApiPayments);
25
30
  const service = alepha.inject(PaymentMethodService);
26
31
  await alepha.start();
27
32
 
@@ -33,7 +38,9 @@ describe("PaymentMethodService", () => {
33
38
  });
34
39
 
35
40
  it("should remove a payment method", async ({ expect }) => {
36
- const alepha = Alepha.create().with(AlephaOrmPostgres).with(AlephaBilling);
41
+ const alepha = Alepha.create()
42
+ .with(AlephaOrmPostgres)
43
+ .with(AlephaApiPayments);
37
44
  const service = alepha.inject(PaymentMethodService);
38
45
  await alepha.start();
39
46
 
@@ -45,7 +52,9 @@ describe("PaymentMethodService", () => {
45
52
  });
46
53
 
47
54
  it("should set a default payment method", async ({ expect }) => {
48
- const alepha = Alepha.create().with(AlephaOrmPostgres).with(AlephaBilling);
55
+ const alepha = Alepha.create()
56
+ .with(AlephaOrmPostgres)
57
+ .with(AlephaApiPayments);
49
58
  const service = alepha.inject(PaymentMethodService);
50
59
  await alepha.start();
51
60
 
@@ -66,7 +75,9 @@ describe("PaymentMethodService", () => {
66
75
  it("should reject removing another user's payment method", async ({
67
76
  expect,
68
77
  }) => {
69
- const alepha = Alepha.create().with(AlephaOrmPostgres).with(AlephaBilling);
78
+ const alepha = Alepha.create()
79
+ .with(AlephaOrmPostgres)
80
+ .with(AlephaApiPayments);
70
81
  const service = alepha.inject(PaymentMethodService);
71
82
  await alepha.start();
72
83
 
@@ -75,4 +86,19 @@ describe("PaymentMethodService", () => {
75
86
  service.removePaymentMethod(method.id, userId2),
76
87
  ).rejects.toThrowError();
77
88
  });
89
+
90
+ it("should reject setting default for another user's payment method", async ({
91
+ expect,
92
+ }) => {
93
+ const alepha = Alepha.create()
94
+ .with(AlephaOrmPostgres)
95
+ .with(AlephaApiPayments);
96
+ const service = alepha.inject(PaymentMethodService);
97
+ await alepha.start();
98
+
99
+ const method = await service.addPaymentMethod(userId, orgId, "tok_visa");
100
+ await expect(service.setDefault(method.id, userId2)).rejects.toThrowError(
101
+ PaymentError,
102
+ );
103
+ });
78
104
  });
@@ -0,0 +1,279 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Alepha } from "alepha";
3
+ import { AlephaOrmPostgres } from "alepha/orm/postgres";
4
+ import { describe, it } from "vitest";
5
+ import { PaymentError } from "../errors/PaymentError.ts";
6
+ import { AlephaApiPayments } from "../index.ts";
7
+ import { PaymentService } from "../services/PaymentService.ts";
8
+
9
+ const setup = async () => {
10
+ const alepha = Alepha.create()
11
+ .with(AlephaOrmPostgres)
12
+ .with(AlephaApiPayments);
13
+ const payments = alepha.inject(PaymentService);
14
+ await alepha.start();
15
+ return { alepha, payments };
16
+ };
17
+
18
+ describe("PaymentService", () => {
19
+ it("should create an intent in 'created' status", async ({ expect }) => {
20
+ const { payments } = await setup();
21
+
22
+ const intent = await payments.createIntent(1500, "eur");
23
+ expect(intent.amount).toBe(1500);
24
+ expect(intent.currency).toBe("eur");
25
+ expect(intent.status).toBe("created");
26
+ });
27
+
28
+ it("should create a session and transition to 'processing'", async ({
29
+ expect,
30
+ }) => {
31
+ const { payments } = await setup();
32
+
33
+ const intent = await payments.createIntent(1500, "eur");
34
+ const session = await payments.createSession(
35
+ intent.id,
36
+ "https://example.com/return",
37
+ );
38
+
39
+ expect(session.url).toContain("/payments/mock-checkout/");
40
+ expect(session.intentId).toBe(intent.id);
41
+ });
42
+
43
+ it("should capture an authorized intent", async ({ expect }) => {
44
+ const { payments } = await setup();
45
+
46
+ const intent = await payments.createIntent(1500, "eur");
47
+ await payments.createSession(intent.id, "https://example.com", true);
48
+ await payments.handleWebhookEvent(intent.id, "authorized");
49
+
50
+ const captured = await payments.capture(intent.id);
51
+ expect(captured.status).toBe("captured");
52
+ });
53
+
54
+ it("should void an authorized intent", async ({ expect }) => {
55
+ const { payments } = await setup();
56
+
57
+ const intent = await payments.createIntent(1500, "eur");
58
+ await payments.createSession(intent.id, "https://example.com", true);
59
+ await payments.handleWebhookEvent(intent.id, "authorized");
60
+
61
+ const voided = await payments.void(intent.id);
62
+ expect(voided.status).toBe("voided");
63
+ });
64
+
65
+ it("should refund a captured intent", async ({ expect }) => {
66
+ const { payments } = await setup();
67
+
68
+ const intent = await payments.createIntent(1500, "eur");
69
+ await payments.createSession(intent.id, "https://example.com");
70
+ await payments.handleWebhookEvent(intent.id, "captured");
71
+
72
+ const refund = await payments.refund(intent.id, 500, "Customer request");
73
+ expect(refund.amount).toBe(500);
74
+ expect(refund.status).toBe("completed");
75
+ });
76
+
77
+ it("should record a cash payment directly as captured", async ({
78
+ expect,
79
+ }) => {
80
+ const { payments } = await setup();
81
+
82
+ const intent = await payments.recordCashPayment(1500, "eur", {
83
+ orderId: "order-1",
84
+ });
85
+ expect(intent.status).toBe("captured");
86
+ expect(intent.metadata).toEqual({ orderId: "order-1" });
87
+ });
88
+
89
+ it("should cancel a created intent", async ({ expect }) => {
90
+ const { payments } = await setup();
91
+
92
+ const intent = await payments.createIntent(1500, "eur");
93
+ const cancelled = await payments.cancel(intent.id);
94
+ expect(cancelled.status).toBe("cancelled");
95
+ });
96
+
97
+ it("should reject capture from wrong status", async ({ expect }) => {
98
+ const { payments } = await setup();
99
+
100
+ const intent = await payments.createIntent(1500, "eur");
101
+ await expect(payments.capture(intent.id)).rejects.toThrowError();
102
+ });
103
+
104
+ it("should reject refund from wrong status", async ({ expect }) => {
105
+ const { payments } = await setup();
106
+
107
+ const intent = await payments.createIntent(1500, "eur");
108
+ await expect(payments.refund(intent.id, 500)).rejects.toThrowError();
109
+ });
110
+
111
+ it("should reject void from wrong status", async ({ expect }) => {
112
+ const { payments } = await setup();
113
+
114
+ const intent = await payments.createIntent(1500, "eur");
115
+ await expect(payments.void(intent.id)).rejects.toThrowError();
116
+ });
117
+
118
+ it("should reject cancel from wrong status", async ({ expect }) => {
119
+ const { payments } = await setup();
120
+
121
+ const intent = await payments.createIntent(1500, "eur");
122
+ await payments.createSession(intent.id, "https://example.com");
123
+ await expect(payments.cancel(intent.id)).rejects.toThrowError();
124
+ });
125
+
126
+ it("should reject capture amount exceeding authorized amount", async ({
127
+ expect,
128
+ }) => {
129
+ const { payments } = await setup();
130
+
131
+ const intent = await payments.createIntent(1500, "eur");
132
+ await payments.createSession(intent.id, "https://example.com", true);
133
+ await payments.handleWebhookEvent(intent.id, "authorized");
134
+
135
+ await expect(payments.capture(intent.id, 5000)).rejects.toThrowError(
136
+ PaymentError,
137
+ );
138
+ });
139
+
140
+ it("should reject refund exceeding captured amount", async ({ expect }) => {
141
+ const { payments } = await setup();
142
+
143
+ const intent = await payments.createIntent(1500, "eur");
144
+ await payments.createSession(intent.id, "https://example.com");
145
+ await payments.handleWebhookEvent(intent.id, "captured");
146
+
147
+ await expect(payments.refund(intent.id, 5000)).rejects.toThrowError(
148
+ PaymentError,
149
+ );
150
+ });
151
+
152
+ it("should allow multiple partial refunds up to captured amount", async ({
153
+ expect,
154
+ }) => {
155
+ const { payments } = await setup();
156
+
157
+ const intent = await payments.createIntent(1500, "eur");
158
+ await payments.createSession(intent.id, "https://example.com");
159
+ await payments.handleWebhookEvent(intent.id, "captured");
160
+
161
+ await payments.refund(intent.id, 500);
162
+ const after1 = await payments.getIntent(intent.id);
163
+ expect(after1.status).toBe("partially_refunded");
164
+
165
+ await payments.refund(intent.id, 500);
166
+ const after2 = await payments.getIntent(intent.id);
167
+ expect(after2.status).toBe("partially_refunded");
168
+
169
+ await payments.refund(intent.id, 500);
170
+ const after3 = await payments.getIntent(intent.id);
171
+ expect(after3.status).toBe("refunded");
172
+ });
173
+
174
+ it("should reject refund that would exceed remaining amount", async ({
175
+ expect,
176
+ }) => {
177
+ const { payments } = await setup();
178
+
179
+ const intent = await payments.createIntent(1500, "eur");
180
+ await payments.createSession(intent.id, "https://example.com");
181
+ await payments.handleWebhookEvent(intent.id, "captured");
182
+
183
+ await payments.refund(intent.id, 1000);
184
+
185
+ await expect(payments.refund(intent.id, 1000)).rejects.toThrowError(
186
+ PaymentError,
187
+ );
188
+ });
189
+
190
+ it("should ignore webhook that would downgrade status", async ({
191
+ expect,
192
+ }) => {
193
+ const { payments } = await setup();
194
+
195
+ const intent = await payments.createIntent(1500, "eur");
196
+ await payments.createSession(intent.id, "https://example.com");
197
+ await payments.handleWebhookEvent(intent.id, "captured");
198
+
199
+ await payments.handleWebhookEvent(intent.id, "authorized");
200
+
201
+ const current = await payments.getIntent(intent.id);
202
+ expect(current.status).toBe("captured");
203
+ });
204
+
205
+ it("should ignore duplicate webhook for same status", async ({ expect }) => {
206
+ const { payments } = await setup();
207
+
208
+ const intent = await payments.createIntent(1500, "eur");
209
+ await payments.createSession(intent.id, "https://example.com", true);
210
+ await payments.handleWebhookEvent(intent.id, "authorized");
211
+
212
+ await payments.handleWebhookEvent(intent.id, "authorized");
213
+
214
+ const current = await payments.getIntent(intent.id);
215
+ expect(current.status).toBe("authorized");
216
+ });
217
+
218
+ it("should normalize currency to lowercase", async ({ expect }) => {
219
+ const { payments } = await setup();
220
+
221
+ const intent = await payments.createIntent(1500, "EUR");
222
+ expect(intent.currency).toBe("eur");
223
+ });
224
+
225
+ it("should emit payments:cancelled event on cancel", async ({ expect }) => {
226
+ const { alepha, payments } = await setup();
227
+
228
+ const intent = await payments.createIntent(1500, "eur");
229
+
230
+ let emitted: unknown = null;
231
+ alepha.events.on("payments:cancelled", (payload: unknown) => {
232
+ emitted = payload;
233
+ });
234
+
235
+ await payments.cancel(intent.id);
236
+
237
+ expect(emitted).toEqual({
238
+ intentId: intent.id,
239
+ amount: 1500,
240
+ currency: "eur",
241
+ metadata: intent.metadata,
242
+ });
243
+ });
244
+
245
+ it("should reject checkout for intent belonging to another user", async ({
246
+ expect,
247
+ }) => {
248
+ const { payments } = await setup();
249
+
250
+ const userA = randomUUID();
251
+ const userB = randomUUID();
252
+ const intent = await payments.createIntent(1500, "eur", undefined, {
253
+ userId: userA,
254
+ });
255
+
256
+ await expect(
257
+ payments.createSession(intent.id, "https://example.com", false, userB),
258
+ ).rejects.toThrowError(PaymentError);
259
+ });
260
+
261
+ it("should set userId on intent during checkout if not already set", async ({
262
+ expect,
263
+ }) => {
264
+ const { payments } = await setup();
265
+
266
+ const userX = randomUUID();
267
+ const intent = await payments.createIntent(1500, "eur");
268
+
269
+ await payments.createSession(
270
+ intent.id,
271
+ "https://example.com",
272
+ false,
273
+ userX,
274
+ );
275
+
276
+ const updated = await payments.getIntent(intent.id);
277
+ expect(updated.userId).toBe(userX);
278
+ });
279
+ });
@@ -9,12 +9,12 @@ import {
9
9
  refundIntentSchema,
10
10
  } from "../schemas/intentSchemas.ts";
11
11
  import { refundResourceSchema } from "../schemas/refundSchemas.ts";
12
- import { BillingService } from "../services/BillingService.ts";
12
+ import { PaymentService } from "../services/PaymentService.ts";
13
13
 
14
- export class AdminBillingController {
15
- protected readonly url = "/admin/billing";
16
- protected readonly group = "admin:billing";
17
- protected readonly billing = $inject(BillingService);
14
+ export class AdminPaymentController {
15
+ protected readonly url = "/admin/payments";
16
+ protected readonly group = "admin:payments";
17
+ protected readonly payments = $inject(PaymentService);
18
18
 
19
19
  /**
20
20
  * List payment intents with pagination and filtering.
@@ -22,13 +22,13 @@ export class AdminBillingController {
22
22
  public readonly listIntents = $action({
23
23
  path: `${this.url}/intents`,
24
24
  group: this.group,
25
- use: [$secure({ permissions: ["billing:read"] })],
25
+ use: [$secure({ permissions: ["payments:read"] })],
26
26
  description: "List payment intents",
27
27
  schema: {
28
28
  query: intentQuerySchema,
29
29
  response: t.page(intentResourceSchema),
30
30
  },
31
- handler: ({ query }) => this.billing.findIntents(query),
31
+ handler: ({ query }) => this.payments.findIntents(query),
32
32
  });
33
33
 
34
34
  /**
@@ -37,13 +37,13 @@ export class AdminBillingController {
37
37
  public readonly getIntent = $action({
38
38
  path: `${this.url}/intents/:id`,
39
39
  group: this.group,
40
- use: [$secure({ permissions: ["billing:read"] })],
40
+ use: [$secure({ permissions: ["payments:read"] })],
41
41
  description: "Get payment intent details",
42
42
  schema: {
43
43
  params: t.object({ id: t.uuid() }),
44
44
  response: intentResourceSchema,
45
45
  },
46
- handler: ({ params }) => this.billing.getIntent(params.id),
46
+ handler: ({ params }) => this.payments.getIntent(params.id),
47
47
  });
48
48
 
49
49
  /**
@@ -53,14 +53,15 @@ export class AdminBillingController {
53
53
  method: "POST",
54
54
  path: `${this.url}/intents/:id/capture`,
55
55
  group: this.group,
56
- use: [$secure({ permissions: ["billing:write"] })],
56
+ use: [$secure({ permissions: ["payments:write"] })],
57
57
  description: "Capture an authorized payment intent",
58
58
  schema: {
59
59
  params: t.object({ id: t.uuid() }),
60
60
  body: captureIntentSchema,
61
61
  response: intentResourceSchema,
62
62
  },
63
- handler: ({ params, body }) => this.billing.capture(params.id, body.amount),
63
+ handler: ({ params, body }) =>
64
+ this.payments.capture(params.id, body.amount),
64
65
  });
65
66
 
66
67
  /**
@@ -70,13 +71,13 @@ export class AdminBillingController {
70
71
  method: "POST",
71
72
  path: `${this.url}/intents/:id/void`,
72
73
  group: this.group,
73
- use: [$secure({ permissions: ["billing:write"] })],
74
+ use: [$secure({ permissions: ["payments:write"] })],
74
75
  description: "Void an authorized payment intent",
75
76
  schema: {
76
77
  params: t.object({ id: t.uuid() }),
77
78
  response: intentResourceSchema,
78
79
  },
79
- handler: ({ params }) => this.billing.void(params.id),
80
+ handler: ({ params }) => this.payments.void(params.id),
80
81
  });
81
82
 
82
83
  /**
@@ -86,7 +87,7 @@ export class AdminBillingController {
86
87
  method: "POST",
87
88
  path: `${this.url}/intents/:id/refund`,
88
89
  group: this.group,
89
- use: [$secure({ permissions: ["billing:write"] })],
90
+ use: [$secure({ permissions: ["payments:write"] })],
90
91
  description: "Issue partial or full refund",
91
92
  schema: {
92
93
  params: t.object({ id: t.uuid() }),
@@ -94,7 +95,7 @@ export class AdminBillingController {
94
95
  response: refundResourceSchema,
95
96
  },
96
97
  handler: ({ params, body }) =>
97
- this.billing.refund(params.id, body.amount, body.reason),
98
+ this.payments.refund(params.id, body.amount, body.reason),
98
99
  });
99
100
 
100
101
  /**
@@ -104,13 +105,13 @@ export class AdminBillingController {
104
105
  method: "POST",
105
106
  path: `${this.url}/intents/:id/cancel`,
106
107
  group: this.group,
107
- use: [$secure({ permissions: ["billing:write"] })],
108
+ use: [$secure({ permissions: ["payments:write"] })],
108
109
  description: "Cancel a created payment intent",
109
110
  schema: {
110
111
  params: t.object({ id: t.uuid() }),
111
112
  response: intentResourceSchema,
112
113
  },
113
- handler: ({ params }) => this.billing.cancel(params.id),
114
+ handler: ({ params }) => this.payments.cancel(params.id),
114
115
  });
115
116
 
116
117
  /**
@@ -120,14 +121,18 @@ export class AdminBillingController {
120
121
  method: "POST",
121
122
  path: `${this.url}/cash`,
122
123
  group: this.group,
123
- use: [$secure({ permissions: ["billing:write"] })],
124
+ use: [$secure({ permissions: ["payments:write"] })],
124
125
  description: "Record a cash payment",
125
126
  schema: {
126
127
  body: recordCashSchema,
127
128
  response: intentResourceSchema,
128
129
  },
129
130
  handler: ({ body }) =>
130
- this.billing.recordCashPayment(body.amount, body.currency, body.metadata),
131
+ this.payments.recordCashPayment(
132
+ body.amount,
133
+ body.currency,
134
+ body.metadata,
135
+ ),
131
136
  });
132
137
 
133
138
  /**
@@ -135,14 +140,14 @@ export class AdminBillingController {
135
140
  */
136
141
  public readonly webhook = $action({
137
142
  method: "POST",
138
- path: "/billing/webhook",
143
+ path: "/payments/webhook",
139
144
  group: this.group,
140
145
  description: "PSP webhook endpoint",
141
146
  schema: {
142
147
  response: okSchema,
143
148
  },
144
149
  handler: async (request) => {
145
- await this.billing.handleWebhook(request.raw.web!.req);
150
+ await this.payments.handleWebhook(request.raw.web!.req);
146
151
  return { ok: true };
147
152
  },
148
153
  });
@@ -1,6 +1,7 @@
1
1
  import { $inject, t } from "alepha";
2
2
  import { $secure } from "alepha/security";
3
3
  import { $action, okSchema } from "alepha/server";
4
+ import { PaymentError } from "../errors/PaymentError.ts";
4
5
  import {
5
6
  checkoutResponseSchema,
6
7
  createCheckoutSchema,
@@ -9,13 +10,13 @@ import {
9
10
  addPaymentMethodSchema,
10
11
  paymentMethodResourceSchema,
11
12
  } from "../schemas/paymentMethodSchemas.ts";
12
- import { BillingService } from "../services/BillingService.ts";
13
13
  import { PaymentMethodService } from "../services/PaymentMethodService.ts";
14
+ import { PaymentService } from "../services/PaymentService.ts";
14
15
 
15
- export class BillingController {
16
- protected readonly url = "/billing";
17
- protected readonly group = "billing";
18
- protected readonly billing = $inject(BillingService);
16
+ export class PaymentController {
17
+ protected readonly url = "/payments";
18
+ protected readonly group = "payments";
19
+ protected readonly payments = $inject(PaymentService);
19
20
  protected readonly paymentMethods = $inject(PaymentMethodService);
20
21
 
21
22
  /**
@@ -45,12 +46,18 @@ export class BillingController {
45
46
  body: addPaymentMethodSchema,
46
47
  response: paymentMethodResourceSchema,
47
48
  },
48
- handler: ({ body, user }) =>
49
- this.paymentMethods.addPaymentMethod(
49
+ handler: ({ body, user }) => {
50
+ if (!user.organization) {
51
+ throw new PaymentError(
52
+ "Organization is required to add a payment method",
53
+ );
54
+ }
55
+ return this.paymentMethods.addPaymentMethod(
50
56
  user.id,
51
- user.organization!,
57
+ user.organization,
52
58
  body.token,
53
- ),
59
+ );
60
+ },
54
61
  });
55
62
 
56
63
  /**
@@ -102,7 +109,12 @@ export class BillingController {
102
109
  body: createCheckoutSchema,
103
110
  response: checkoutResponseSchema,
104
111
  },
105
- handler: ({ body }) =>
106
- this.billing.createSession(body.intentId, body.returnUrl, body.authorize),
112
+ handler: ({ body, user }) =>
113
+ this.payments.createSession(
114
+ body.intentId,
115
+ body.returnUrl,
116
+ body.authorize,
117
+ user.id,
118
+ ),
107
119
  });
108
120
  }
@@ -16,6 +16,7 @@ export const paymentIntents = $entity({
16
16
  "processing",
17
17
  "authorized",
18
18
  "captured",
19
+ "partially_refunded",
19
20
  "voided",
20
21
  "failed",
21
22
  "cancelled",
@@ -1,5 +1,5 @@
1
1
  import { AlephaError } from "alepha";
2
2
 
3
- export class BillingError extends AlephaError {
3
+ export class PaymentError extends AlephaError {
4
4
  public readonly status = 400;
5
5
  }