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,71 +0,0 @@
1
- import { type Static, t } from "alepha";
2
-
3
- export const planDefinitionSchema = t.object({
4
- /**
5
- * Unique plan identifier (e.g., "free", "starter", "pro", "enterprise").
6
- */
7
- id: t.string({ minLength: 1, maxLength: 50 }),
8
-
9
- /**
10
- * Display name (e.g., "Pro Plan").
11
- */
12
- name: t.string(),
13
-
14
- /**
15
- * Optional description.
16
- */
17
- description: t.optional(t.string()),
18
-
19
- /**
20
- * Whether this plan is available for new subscriptions.
21
- */
22
- available: t.boolean({ default: true }),
23
-
24
- /**
25
- * Pricing per billing interval.
26
- * Multiple entries for monthly/yearly.
27
- */
28
- pricing: t.array(
29
- t.object({
30
- interval: t.enum(["monthly", "yearly"]),
31
- amount: t.integer({ minimum: 0 }),
32
- currency: t.string({ minLength: 3, maxLength: 3 }),
33
- }),
34
- ),
35
-
36
- /**
37
- * Trial configuration for this plan.
38
- * Overrides global settings.trialDays if set.
39
- */
40
- trial: t.optional(
41
- t.object({
42
- days: t.integer({ minimum: 0, maximum: 365 }),
43
- requirePaymentMethod: t.boolean({ default: false }),
44
- }),
45
- ),
46
-
47
- /**
48
- * Feature entitlements. Boolean flags for feature access.
49
- * Checked via SubscriptionService.can("feature-name").
50
- */
51
- features: t.array(t.string()),
52
-
53
- /**
54
- * Usage limits. Numeric caps on resources.
55
- * Checked via SubscriptionService.limit("resource-name").
56
- * -1 = unlimited.
57
- */
58
- limits: t.record(t.text(), t.integer()),
59
-
60
- /**
61
- * Sort order for display (lower = first).
62
- */
63
- order: t.integer({ default: 0 }),
64
-
65
- /**
66
- * Metadata for app-specific plan data.
67
- */
68
- metadata: t.optional(t.record(t.text(), t.any())),
69
- });
70
-
71
- export type PlanDefinition = Static<typeof planDefinitionSchema>;
@@ -1,25 +0,0 @@
1
- import { type Static, t } from "alepha";
2
-
3
- export const planResourceSchema = t.object({
4
- id: t.string(),
5
- name: t.string(),
6
- description: t.optional(t.string()),
7
- pricing: t.array(
8
- t.object({
9
- interval: t.enum(["monthly", "yearly"]),
10
- amount: t.integer(),
11
- currency: t.string(),
12
- }),
13
- ),
14
- features: t.array(t.string()),
15
- limits: t.record(t.text(), t.integer()),
16
- trial: t.optional(
17
- t.object({
18
- days: t.integer(),
19
- requirePaymentMethod: t.boolean(),
20
- }),
21
- ),
22
- order: t.integer(),
23
- });
24
-
25
- export type PlanResource = Static<typeof planResourceSchema>;
@@ -1,8 +0,0 @@
1
- import type { Static } from "alepha";
2
- import { subscriptionEvents } from "../entities/subscriptionEvents.ts";
3
-
4
- export const subscriptionEventResourceSchema = subscriptionEvents.schema;
5
-
6
- export type SubscriptionEventResource = Static<
7
- typeof subscriptionEventResourceSchema
8
- >;
@@ -1,19 +0,0 @@
1
- import { type Static, t } from "alepha";
2
- import { pageQuerySchema } from "alepha/orm";
3
-
4
- export const subscriptionQuerySchema = t.extend(pageQuerySchema, {
5
- status: t.optional(
6
- t.enum([
7
- "trialing",
8
- "active",
9
- "past_due",
10
- "suspended",
11
- "cancelled",
12
- "expired",
13
- ]),
14
- ),
15
- planId: t.optional(t.string()),
16
- organizationId: t.optional(t.uuid()),
17
- });
18
-
19
- export type SubscriptionQuery = Static<typeof subscriptionQuerySchema>;
@@ -1,6 +0,0 @@
1
- import type { Static } from "alepha";
2
- import { subscriptions } from "../entities/subscriptions.ts";
3
-
4
- export const subscriptionResourceSchema = subscriptions.schema;
5
-
6
- export type SubscriptionResource = Static<typeof subscriptionResourceSchema>;
@@ -1,32 +0,0 @@
1
- import { type Static, t } from "alepha";
2
-
3
- export const subscriptionSettingsSchema = t.object({
4
- /**
5
- * Default trial days (overridden per-plan if plan.trial.days is set).
6
- */
7
- trialDays: t.integer({ default: 14, minimum: 0, maximum: 365 }),
8
-
9
- /**
10
- * Days after payment failure before suspension.
11
- * During grace period, subscription remains active but flagged.
12
- */
13
- gracePeriodDays: t.integer({ default: 7, minimum: 0, maximum: 30 }),
14
-
15
- /**
16
- * Days after first payment failure to retry, relative to the failure date.
17
- * e.g., [1, 3, 5, 7] means retry on day 1, 3, 5, 7 after failure.
18
- */
19
- dunningSchedule: t.array(t.integer({ minimum: 1 })),
20
-
21
- /**
22
- * When user cancels, wait until period end (true) or cancel immediately (false).
23
- */
24
- cancelAtPeriodEnd: t.boolean({ default: true }),
25
-
26
- /**
27
- * Prorate charges when changing plans mid-cycle.
28
- */
29
- prorateOnChange: t.boolean({ default: true }),
30
- });
31
-
32
- export type SubscriptionSettings = Static<typeof subscriptionSettingsSchema>;
@@ -1,23 +0,0 @@
1
- import { type Static, t } from "alepha";
2
-
3
- export const subscriptionStatsSchema = t.object({
4
- total: t.integer(),
5
- trialing: t.integer(),
6
- active: t.integer(),
7
- pastDue: t.integer(),
8
- suspended: t.integer(),
9
- cancelled: t.integer(),
10
- expired: t.integer(),
11
- trialConversionRate: t.number(),
12
- churnRate: t.number(),
13
- byPlan: t.record(
14
- t.text(),
15
- t.object({
16
- active: t.integer(),
17
- trialing: t.integer(),
18
- total: t.integer(),
19
- }),
20
- ),
21
- });
22
-
23
- export type SubscriptionStats = Static<typeof subscriptionStatsSchema>;
@@ -1,437 +0,0 @@
1
- import { $hook, $inject, Alepha } from "alepha";
2
- import { PaymentService } from "alepha/api/payments";
3
- import { DateTimeProvider } from "alepha/datetime";
4
- import { $logger } from "alepha/logger";
5
- import { $repository } from "alepha/orm";
6
- import type { SubscriptionEventEntity } from "../entities/subscriptionEvents.ts";
7
- import { subscriptionEvents } from "../entities/subscriptionEvents.ts";
8
- import {
9
- type SubscriptionEntity,
10
- subscriptions,
11
- } from "../entities/subscriptions.ts";
12
- import { SubscriptionConfig } from "./SubscriptionConfig.ts";
13
-
14
- // -----------------------------------------------------------------------------------------------------------------
15
-
16
- interface PaymentEvent {
17
- intentId: string;
18
- amount: number;
19
- currency: string;
20
- metadata?: unknown;
21
- }
22
-
23
- // -----------------------------------------------------------------------------------------------------------------
24
-
25
- interface EventContext {
26
- previousStatus?: string;
27
- newStatus?: string;
28
- previousPlanId?: string;
29
- newPlanId?: string;
30
- paymentIntentId?: string;
31
- amount?: number;
32
- currency?: string;
33
- triggeredBy?: string;
34
- userId?: string;
35
- note?: string;
36
- }
37
-
38
- // -----------------------------------------------------------------------------------------------------------------
39
-
40
- export class BillingService {
41
- protected readonly alepha = $inject(Alepha);
42
- protected readonly log = $logger();
43
- protected readonly dateTime = $inject(DateTimeProvider);
44
- protected readonly subscriptionRepo = $repository(subscriptions);
45
- protected readonly eventRepo = $repository(subscriptionEvents);
46
- protected readonly paymentService = $inject(PaymentService);
47
- protected readonly config = $inject(SubscriptionConfig);
48
-
49
- // ---------------------------------------------------------------------------------------------------------------
50
- // Payment hook listeners
51
- // ---------------------------------------------------------------------------------------------------------------
52
-
53
- /**
54
- * React to successful payment capture.
55
- * Routes to the appropriate handler based on subscription status.
56
- */
57
- protected readonly onPaymentCaptured = $hook({
58
- on: "payments:captured",
59
- handler: async (event) => {
60
- const sub = await this.findByPaymentIntent(event.intentId);
61
- if (!sub) return;
62
-
63
- if (sub.status === "trialing") {
64
- await this.activate(sub, event);
65
- } else if (sub.status === "active") {
66
- await this.renew(sub, event);
67
- } else if (sub.status === "past_due") {
68
- await this.recoverFromDunning(sub, event);
69
- } else if (sub.status === "suspended") {
70
- await this.reactivateFromPayment(sub, event);
71
- }
72
- },
73
- });
74
-
75
- /**
76
- * React to failed payment.
77
- * Starts or advances the dunning flow.
78
- */
79
- protected readonly onPaymentFailed = $hook({
80
- on: "payments:failed",
81
- handler: async (event) => {
82
- const sub = await this.findByPaymentIntent(event.intentId);
83
- if (!sub) return;
84
-
85
- await this.handlePaymentFailure(sub, event);
86
- },
87
- });
88
-
89
- // ---------------------------------------------------------------------------------------------------------------
90
- // Lookup
91
- // ---------------------------------------------------------------------------------------------------------------
92
-
93
- /**
94
- * Find a subscription by its last payment intent ID.
95
- * Returns null if no subscription matches.
96
- */
97
- protected async findByPaymentIntent(
98
- intentId: string,
99
- ): Promise<SubscriptionEntity | null> {
100
- const sub = await this.subscriptionRepo.findOne({
101
- where: { lastPaymentIntentId: { eq: intentId } },
102
- });
103
- return sub ?? null;
104
- }
105
-
106
- // ---------------------------------------------------------------------------------------------------------------
107
- // Lifecycle transitions
108
- // ---------------------------------------------------------------------------------------------------------------
109
-
110
- /**
111
- * Trial to active transition.
112
- * Sets the first paid billing period and records activation events.
113
- */
114
- protected async activate(
115
- sub: SubscriptionEntity,
116
- event: PaymentEvent,
117
- ): Promise<void> {
118
- const orgId = sub.organizationId as string;
119
- const now = this.dateTime.now();
120
- const nowISO = now.toISOString();
121
- const periodEnd = this.computeIntervalEnd(nowISO, sub.interval);
122
-
123
- await this.subscriptionRepo.updateById(sub.id, {
124
- status: "active",
125
- lastPaymentAt: nowISO,
126
- lastPaymentIntentId: event.intentId,
127
- currentPeriodStart: nowISO,
128
- currentPeriodEnd: periodEnd,
129
- nextBillingAt: periodEnd,
130
- });
131
-
132
- await this.recordEvent(sub.id, orgId, "trial_ended", {
133
- previousStatus: "trialing",
134
- newStatus: "active",
135
- paymentIntentId: event.intentId,
136
- amount: event.amount,
137
- currency: event.currency,
138
- });
139
-
140
- await this.recordEvent(sub.id, orgId, "activated", {
141
- previousStatus: "trialing",
142
- newStatus: "active",
143
- paymentIntentId: event.intentId,
144
- amount: event.amount,
145
- currency: event.currency,
146
- });
147
-
148
- this.log.info("Subscription activated from trial", {
149
- id: sub.id,
150
- organizationId: orgId,
151
- planId: sub.planId,
152
- });
153
-
154
- await this.alepha.events.emit("subscription:activated" as any, {
155
- subscriptionId: sub.id,
156
- organizationId: orgId,
157
- planId: sub.planId,
158
- });
159
- }
160
-
161
- // ---------------------------------------------------------------------------------------------------------------
162
-
163
- /**
164
- * Active to active cycle renewal.
165
- * Applies any pending plan change, then advances the billing period.
166
- */
167
- protected async renew(
168
- sub: SubscriptionEntity,
169
- event: PaymentEvent,
170
- ): Promise<void> {
171
- const orgId = sub.organizationId as string;
172
- let effectivePlanId = sub.planId;
173
- let effectiveInterval = sub.interval;
174
-
175
- if (sub.pendingPlanId) {
176
- effectivePlanId = sub.pendingPlanId;
177
- effectiveInterval = sub.pendingInterval ?? sub.interval;
178
-
179
- await this.subscriptionRepo.updateById(sub.id, {
180
- planId: effectivePlanId,
181
- interval: effectiveInterval,
182
- pendingPlanId: undefined,
183
- pendingInterval: undefined,
184
- });
185
-
186
- await this.recordEvent(sub.id, orgId, "plan_changed", {
187
- previousPlanId: sub.planId,
188
- newPlanId: effectivePlanId,
189
- note: `Plan changed on renewal from '${sub.planId}' to '${effectivePlanId}'`,
190
- });
191
- }
192
-
193
- const newPeriodStart = sub.currentPeriodEnd;
194
- const newPeriodEnd = this.computeIntervalEnd(
195
- newPeriodStart,
196
- effectiveInterval,
197
- );
198
-
199
- await this.subscriptionRepo.updateById(sub.id, {
200
- currentPeriodStart: newPeriodStart,
201
- currentPeriodEnd: newPeriodEnd,
202
- lastPaymentAt: this.dateTime.now().toISOString(),
203
- lastPaymentIntentId: event.intentId,
204
- nextBillingAt: newPeriodEnd,
205
- });
206
-
207
- await this.recordEvent(sub.id, orgId, "renewed", {
208
- paymentIntentId: event.intentId,
209
- amount: event.amount,
210
- currency: event.currency,
211
- });
212
-
213
- this.log.info("Subscription renewed", {
214
- id: sub.id,
215
- organizationId: orgId,
216
- planId: effectivePlanId,
217
- periodEnd: newPeriodEnd,
218
- });
219
-
220
- await this.alepha.events.emit("subscription:renewed" as any, {
221
- subscriptionId: sub.id,
222
- organizationId: orgId,
223
- planId: effectivePlanId,
224
- });
225
- }
226
-
227
- // ---------------------------------------------------------------------------------------------------------------
228
-
229
- /**
230
- * Recover from dunning: past_due to active.
231
- * Resets all dunning state and records reactivation.
232
- */
233
- protected async recoverFromDunning(
234
- sub: SubscriptionEntity,
235
- event: PaymentEvent,
236
- ): Promise<void> {
237
- const orgId = sub.organizationId as string;
238
- const nowISO = this.dateTime.now().toISOString();
239
-
240
- await this.subscriptionRepo.updateById(sub.id, {
241
- status: "active",
242
- lastPaymentAt: nowISO,
243
- lastPaymentIntentId: event.intentId,
244
- dunningStartedAt: undefined,
245
- dunningAttempt: 0,
246
- dunningNextRetryAt: undefined,
247
- });
248
-
249
- await this.recordEvent(sub.id, orgId, "reactivated", {
250
- previousStatus: "past_due",
251
- newStatus: "active",
252
- paymentIntentId: event.intentId,
253
- amount: event.amount,
254
- currency: event.currency,
255
- });
256
-
257
- this.log.info("Subscription recovered from dunning", {
258
- id: sub.id,
259
- organizationId: orgId,
260
- });
261
-
262
- await this.alepha.events.emit("subscription:reactivated" as any, {
263
- subscriptionId: sub.id,
264
- organizationId: orgId,
265
- planId: sub.planId,
266
- });
267
- }
268
-
269
- // ---------------------------------------------------------------------------------------------------------------
270
-
271
- /**
272
- * Reactivate from suspended state after a successful payment.
273
- * Resets dunning, sets a fresh billing period, and records reactivation.
274
- */
275
- protected async reactivateFromPayment(
276
- sub: SubscriptionEntity,
277
- event: PaymentEvent,
278
- ): Promise<void> {
279
- const orgId = sub.organizationId as string;
280
- const now = this.dateTime.now();
281
- const nowISO = now.toISOString();
282
- const periodEnd = this.computeIntervalEnd(nowISO, sub.interval);
283
-
284
- await this.subscriptionRepo.updateById(sub.id, {
285
- status: "active",
286
- currentPeriodStart: nowISO,
287
- currentPeriodEnd: periodEnd,
288
- nextBillingAt: periodEnd,
289
- lastPaymentAt: nowISO,
290
- lastPaymentIntentId: event.intentId,
291
- dunningStartedAt: undefined,
292
- dunningAttempt: 0,
293
- dunningNextRetryAt: undefined,
294
- });
295
-
296
- await this.recordEvent(sub.id, orgId, "reactivated", {
297
- previousStatus: "suspended",
298
- newStatus: "active",
299
- paymentIntentId: event.intentId,
300
- amount: event.amount,
301
- currency: event.currency,
302
- });
303
-
304
- this.log.info("Subscription reactivated from suspended", {
305
- id: sub.id,
306
- organizationId: orgId,
307
- planId: sub.planId,
308
- });
309
-
310
- await this.alepha.events.emit("subscription:reactivated" as any, {
311
- subscriptionId: sub.id,
312
- organizationId: orgId,
313
- planId: sub.planId,
314
- });
315
- }
316
-
317
- // ---------------------------------------------------------------------------------------------------------------
318
-
319
- /**
320
- * Handle a failed payment: start or advance dunning.
321
- * Updates dunning state and transitions to past_due if needed.
322
- */
323
- protected async handlePaymentFailure(
324
- sub: SubscriptionEntity,
325
- event: PaymentEvent,
326
- ): Promise<void> {
327
- const orgId = sub.organizationId as string;
328
- const now = this.dateTime.now();
329
- const nowISO = now.toISOString();
330
- const settings = await this.config.getSettings();
331
- const schedule = settings.dunningSchedule;
332
-
333
- let attempt: number;
334
-
335
- if (sub.dunningAttempt === 0) {
336
- attempt = 1;
337
- await this.subscriptionRepo.updateById(sub.id, {
338
- dunningStartedAt: nowISO,
339
- dunningAttempt: attempt,
340
- });
341
- } else {
342
- attempt = sub.dunningAttempt + 1;
343
- await this.subscriptionRepo.updateById(sub.id, {
344
- dunningAttempt: attempt,
345
- });
346
- }
347
-
348
- if (sub.status !== "past_due") {
349
- await this.subscriptionRepo.updateById(sub.id, {
350
- status: "past_due",
351
- });
352
-
353
- await this.recordEvent(sub.id, orgId, "past_due", {
354
- previousStatus: sub.status,
355
- newStatus: "past_due",
356
- paymentIntentId: event.intentId,
357
- });
358
- }
359
-
360
- const scheduleDays = schedule[attempt - 1];
361
-
362
- if (scheduleDays !== undefined) {
363
- const nextRetry = now.add(scheduleDays, "days").toISOString();
364
-
365
- await this.subscriptionRepo.updateById(sub.id, {
366
- dunningNextRetryAt: nextRetry,
367
- });
368
- } else {
369
- await this.subscriptionRepo.updateById(sub.id, {
370
- dunningNextRetryAt: undefined,
371
- });
372
- }
373
-
374
- await this.recordEvent(sub.id, orgId, "payment_failed", {
375
- paymentIntentId: event.intentId,
376
- amount: event.amount,
377
- currency: event.currency,
378
- note: `Dunning attempt ${attempt}/${schedule.length}`,
379
- });
380
-
381
- this.log.warn("Subscription payment failed", {
382
- id: sub.id,
383
- organizationId: orgId,
384
- attempt,
385
- maxAttempts: schedule.length,
386
- });
387
-
388
- await this.alepha.events.emit("subscription:payment_failed" as any, {
389
- subscriptionId: sub.id,
390
- organizationId: orgId,
391
- planId: sub.planId,
392
- attempt,
393
- });
394
- }
395
-
396
- // ---------------------------------------------------------------------------------------------------------------
397
- // Helpers
398
- // ---------------------------------------------------------------------------------------------------------------
399
-
400
- /**
401
- * Compute the end of a billing interval from a start date.
402
- */
403
- protected computeIntervalEnd(
404
- start: string,
405
- interval: "monthly" | "yearly",
406
- ): string {
407
- const startDate = this.dateTime.of(start);
408
- const unit = interval === "monthly" ? "months" : "years";
409
- return startDate.add(1, unit).toISOString();
410
- }
411
-
412
- /**
413
- * Record a subscription event in the event log.
414
- */
415
- protected async recordEvent(
416
- subscriptionId: string,
417
- organizationId: string,
418
- type: SubscriptionEventEntity["type"],
419
- context?: EventContext,
420
- ): Promise<void> {
421
- await this.eventRepo.create({
422
- subscriptionId,
423
- organizationId,
424
- type,
425
- previousStatus: context?.previousStatus,
426
- newStatus: context?.newStatus,
427
- previousPlanId: context?.previousPlanId,
428
- newPlanId: context?.newPlanId,
429
- paymentIntentId: context?.paymentIntentId,
430
- amount: context?.amount,
431
- currency: context?.currency,
432
- triggeredBy: context?.triggeredBy,
433
- userId: context?.userId,
434
- note: context?.note,
435
- });
436
- }
437
- }
@@ -1,56 +0,0 @@
1
- import { t } from "alepha";
2
- import { $parameter } from "alepha/api/parameters";
3
- import { BadRequestError } from "alepha/server";
4
- import {
5
- type PlanDefinition,
6
- planDefinitionSchema,
7
- } from "../schemas/planDefinitionSchema.ts";
8
- import {
9
- type SubscriptionSettings,
10
- subscriptionSettingsSchema,
11
- } from "../schemas/subscriptionSettingsSchema.ts";
12
-
13
- export class SubscriptionConfig {
14
- protected readonly plans = $parameter({
15
- name: "subscriptions.plans",
16
- description: "Subscription plan definitions",
17
- schema: t.object({ plans: t.array(planDefinitionSchema) }),
18
- default: { plans: [] },
19
- });
20
-
21
- protected readonly settings = $parameter({
22
- name: "subscriptions.settings",
23
- description: "Global subscription settings",
24
- schema: subscriptionSettingsSchema,
25
- default: {
26
- trialDays: 14,
27
- gracePeriodDays: 7,
28
- dunningSchedule: [1, 3, 5, 7],
29
- cancelAtPeriodEnd: true,
30
- prorateOnChange: true,
31
- },
32
- });
33
-
34
- public async getPlans(): Promise<PlanDefinition[]> {
35
- return (await this.plans.get()).plans;
36
- }
37
-
38
- public async getSettings(): Promise<SubscriptionSettings> {
39
- return this.settings.get();
40
- }
41
-
42
- public async getPlan(planId: string): Promise<PlanDefinition> {
43
- const plans = await this.getPlans();
44
- const plan = plans.find((p) => p.id === planId);
45
- if (!plan) throw new BadRequestError(`Plan '${planId}' not found`);
46
- return plan;
47
- }
48
-
49
- public async getPlanPricing(planId: string, interval: "monthly" | "yearly") {
50
- const plan = await this.getPlan(planId);
51
- const pricing = plan.pricing.find((p) => p.interval === interval);
52
- if (!pricing)
53
- throw new BadRequestError(`No ${interval} pricing for plan '${planId}'`);
54
- return pricing;
55
- }
56
- }