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.
- package/dist/api/files/index.js +2 -1
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.browser.js +64 -148
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +371 -573
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +605 -1012
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +78 -17
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +90 -23
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/payments/index.d.ts +2 -1
- package/dist/api/payments/index.d.ts.map +1 -1
- package/dist/api/payments/index.js +4 -2
- package/dist/api/payments/index.js.map +1 -1
- package/dist/api/users/index.d.ts +34 -31
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +13 -7
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.js +2 -1
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +8 -34
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +43 -232
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +36 -11
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +93 -27
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/command/index.d.ts +1 -1
- package/dist/core/index.browser.js +6 -0
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +6 -0
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +6 -0
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/react/form/index.d.ts +60 -1
- package/dist/react/form/index.d.ts.map +1 -1
- package/dist/react/form/index.js +86 -1
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/head/index.browser.js +16 -1
- package/dist/react/head/index.browser.js.map +1 -1
- package/dist/react/head/index.d.ts +6 -0
- package/dist/react/head/index.d.ts.map +1 -1
- package/dist/react/head/index.js +16 -1
- package/dist/react/head/index.js.map +1 -1
- package/dist/react/router/index.browser.js +0 -10
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +35 -12
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +0 -10
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/ui/index.d.ts +124 -0
- package/dist/react/ui/index.d.ts.map +1 -0
- package/dist/react/ui/index.js +206 -0
- package/dist/react/ui/index.js.map +1 -0
- package/dist/router/index.d.ts +13 -13
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +45 -32
- package/dist/router/index.js.map +1 -1
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +1 -0
- package/dist/system/index.js.map +1 -1
- package/dist/topic/core/index.js +1 -1
- package/dist/topic/core/index.js.map +1 -1
- package/package.json +6 -23
- package/src/api/files/jobs/FileJobs.ts +2 -1
- package/src/api/jobs/__tests__/$job.spec.ts +316 -2867
- package/src/api/jobs/controllers/AdminJobController.ts +29 -138
- package/src/api/jobs/entities/jobExecutionEntity.ts +27 -19
- package/src/api/jobs/index.browser.ts +5 -7
- package/src/api/jobs/index.ts +23 -51
- package/src/api/jobs/primitives/$job.ts +66 -58
- package/src/api/jobs/providers/JobProvider.ts +561 -566
- package/src/api/jobs/providers/JobQueueProvider.ts +18 -19
- package/src/api/jobs/schemas/jobConfigAtom.ts +20 -23
- package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +3 -27
- package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +5 -7
- package/src/api/jobs/schemas/jobRegistrationSchema.ts +7 -4
- package/src/api/jobs/schemas/triggerJobSchema.ts +0 -1
- package/src/api/jobs/services/JobService.ts +90 -483
- package/src/api/notifications/controllers/AdminNotificationController.ts +19 -12
- package/src/api/notifications/index.ts +7 -4
- package/src/api/notifications/jobs/NotificationJobs.ts +83 -12
- package/src/api/payments/services/PaymentService.ts +4 -2
- package/src/api/users/__tests__/UserJobs.spec.ts +10 -49
- package/src/api/users/audits/UserAudits.ts +3 -1
- package/src/api/users/buckets/UserBuckets.ts +2 -1
- package/src/api/users/index.ts +1 -4
- package/src/api/users/jobs/UserJobs.ts +5 -4
- package/src/api/verifications/jobs/VerificationJobs.ts +2 -1
- package/src/cli/core/__tests__/init.spec.ts +1 -1
- package/src/cli/core/commands/init.ts +0 -12
- package/src/cli/core/services/PackageManagerUtils.ts +2 -9
- package/src/cli/core/services/ProjectScaffolder.ts +17 -65
- package/src/cli/core/templates/agentMd.ts +2 -8
- package/src/cli/core/templates/apiIndexTs.ts +4 -18
- package/src/cli/core/templates/mainCss.ts +1 -36
- package/src/cli/core/templates/vitestConfigTs.ts +17 -0
- package/src/cli/core/templates/webAppRouterTs.ts +2 -85
- package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +22 -71
- package/src/cli/platform/adapters/CloudflareAdapter.ts +12 -11
- package/src/cli/platform/atoms/platformOptions.ts +9 -0
- package/src/cli/platform/schemas/cloudflare.ts +3 -2
- package/src/cli/platform/services/CloudflareApi.ts +164 -25
- package/src/cli/platform/services/WranglerApi.ts +0 -17
- package/src/core/Alepha.ts +9 -0
- package/src/react/form/index.ts +2 -0
- package/src/react/form/services/parseField.ts +163 -0
- package/src/react/form/services/prettyName.ts +19 -0
- package/src/react/head/providers/BrowserHeadProvider.ts +31 -10
- package/src/react/router/primitives/$page.ts +35 -12
- package/src/react/ui/atoms/uiAtom.ts +28 -0
- package/src/react/ui/components/ColorScheme.tsx +36 -0
- package/src/react/ui/hooks/useColorMode.ts +49 -0
- package/src/react/ui/hooks/useSidebarState.ts +26 -0
- package/src/react/ui/hooks/useTheme.ts +22 -0
- package/src/react/ui/index.ts +35 -0
- package/src/react/ui/services/UiPersistence.ts +41 -0
- package/src/router/TemplatedPathParser.ts +50 -51
- package/src/router/__tests__/RouterProvider.spec.ts +62 -0
- package/src/router/__tests__/TemplatedPathParser.spec.ts +18 -0
- package/src/router/providers/RouterProvider.ts +10 -5
- package/src/system/providers/NodeShellProvider.ts +1 -0
- package/src/topic/core/providers/TopicProvider.ts +1 -1
- package/dist/api/invitations/index.d.ts +0 -790
- package/dist/api/invitations/index.d.ts.map +0 -1
- package/dist/api/invitations/index.js +0 -662
- package/dist/api/invitations/index.js.map +0 -1
- package/dist/api/issues/index.d.ts +0 -810
- package/dist/api/issues/index.d.ts.map +0 -1
- package/dist/api/issues/index.js +0 -444
- package/dist/api/issues/index.js.map +0 -1
- package/dist/api/subscriptions/index.d.ts +0 -1692
- package/dist/api/subscriptions/index.d.ts.map +0 -1
- package/dist/api/subscriptions/index.js +0 -1867
- package/dist/api/subscriptions/index.js.map +0 -1
- package/dist/api/workflows/index.browser.js +0 -246
- package/dist/api/workflows/index.browser.js.map +0 -1
- package/dist/api/workflows/index.d.ts +0 -1618
- package/dist/api/workflows/index.d.ts.map +0 -1
- package/dist/api/workflows/index.js +0 -1495
- package/dist/api/workflows/index.js.map +0 -1
- package/src/api/invitations/__tests__/InvitationService.spec.ts +0 -439
- package/src/api/invitations/controllers/AdminInvitationController.ts +0 -86
- package/src/api/invitations/controllers/InvitationController.ts +0 -84
- package/src/api/invitations/entities/invitations.ts +0 -33
- package/src/api/invitations/index.ts +0 -58
- package/src/api/invitations/jobs/InvitationJobs.ts +0 -37
- package/src/api/invitations/providers/InvitationProvider.ts +0 -45
- package/src/api/invitations/schemas/createInvitationSchema.ts +0 -12
- package/src/api/invitations/schemas/invitationConfigAtom.ts +0 -20
- package/src/api/invitations/schemas/invitationQuerySchema.ts +0 -15
- package/src/api/invitations/schemas/invitationResourceSchema.ts +0 -6
- package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +0 -22
- package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +0 -10
- package/src/api/invitations/services/InvitationService.ts +0 -556
- package/src/api/issues/__tests__/IssueService.spec.ts +0 -263
- package/src/api/issues/controllers/AdminIssueController.ts +0 -149
- package/src/api/issues/controllers/IssueController.ts +0 -44
- package/src/api/issues/entities/issues.ts +0 -49
- package/src/api/issues/index.ts +0 -50
- package/src/api/issues/schemas/createIssueSchema.ts +0 -13
- package/src/api/issues/schemas/issueConfigAtom.ts +0 -13
- package/src/api/issues/schemas/issueQuerySchema.ts +0 -18
- package/src/api/issues/schemas/issueResourceSchema.ts +0 -6
- package/src/api/issues/schemas/myIssueQuerySchema.ts +0 -10
- package/src/api/issues/schemas/updateIssueSchema.ts +0 -13
- package/src/api/issues/services/IssueService.ts +0 -264
- package/src/api/jobs/__tests__/$job-middleware.spec.ts +0 -126
- package/src/api/jobs/__tests__/JobService.spec.ts +0 -31
- package/src/api/jobs/entities/jobExecutionLogEntity.ts +0 -13
- package/src/api/jobs/schemas/jobActivitySchema.ts +0 -15
- package/src/api/jobs/schemas/jobCronInfoSchema.ts +0 -22
- package/src/api/jobs/schemas/jobExecutionDetailResourceSchema.ts +0 -20
- package/src/api/jobs/schemas/jobFailureSchema.ts +0 -9
- package/src/api/jobs/schemas/jobQueueDepthSchema.ts +0 -14
- package/src/api/jobs/schemas/jobStatsSchema.ts +0 -14
- package/src/api/jobs/services/JobService-tests.ts +0 -157
- package/src/api/subscriptions/__tests__/BillingService.spec.ts +0 -218
- package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +0 -278
- package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +0 -212
- package/src/api/subscriptions/controllers/SubscriptionController.ts +0 -189
- package/src/api/subscriptions/entities/subscriptionEvents.ts +0 -54
- package/src/api/subscriptions/entities/subscriptions.ts +0 -68
- package/src/api/subscriptions/index.ts +0 -133
- package/src/api/subscriptions/jobs/SubscriptionJobs.ts +0 -382
- package/src/api/subscriptions/middleware/$requireLimit.ts +0 -50
- package/src/api/subscriptions/middleware/$requirePlan.ts +0 -49
- package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +0 -110
- package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +0 -8
- package/src/api/subscriptions/schemas/changePlanSchema.ts +0 -9
- package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +0 -11
- package/src/api/subscriptions/schemas/entitlementsSchema.ts +0 -21
- package/src/api/subscriptions/schemas/mrrSchema.ts +0 -13
- package/src/api/subscriptions/schemas/planDefinitionSchema.ts +0 -71
- package/src/api/subscriptions/schemas/planResourceSchema.ts +0 -25
- package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +0 -8
- package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +0 -19
- package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +0 -6
- package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +0 -32
- package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +0 -23
- package/src/api/subscriptions/services/BillingService.ts +0 -437
- package/src/api/subscriptions/services/SubscriptionConfig.ts +0 -56
- package/src/api/subscriptions/services/SubscriptionService.ts +0 -867
- package/src/api/subscriptions/services/UsageService.ts +0 -118
- package/src/api/workflows/__tests__/$workflow.spec.ts +0 -616
- package/src/api/workflows/controllers/AdminWorkflowController.ts +0 -191
- package/src/api/workflows/entities/workflowExecutions.ts +0 -74
- package/src/api/workflows/entities/workflowStepExecutions.ts +0 -74
- package/src/api/workflows/entities/workflowStepLogs.ts +0 -13
- package/src/api/workflows/index.browser.ts +0 -22
- package/src/api/workflows/index.ts +0 -115
- package/src/api/workflows/jobs/WorkflowJobs.ts +0 -77
- package/src/api/workflows/primitives/$workflow.ts +0 -202
- package/src/api/workflows/providers/WorkflowProvider.ts +0 -1284
- package/src/api/workflows/schemas/workflowActivitySchema.ts +0 -15
- package/src/api/workflows/schemas/workflowConfigAtom.ts +0 -51
- package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +0 -18
- package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +0 -26
- package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +0 -30
- package/src/api/workflows/schemas/workflowRegistrationSchema.ts +0 -26
- package/src/api/workflows/schemas/workflowStatsSchema.ts +0 -16
- package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +0 -15
- package/src/api/workflows/services/WorkflowService.ts +0 -382
- package/src/cli/core/templates/apiAppSecurityTs.ts +0 -43
- package/src/cli/core/templates/webAdminDashboardTsx.ts +0 -17
|
@@ -1,382 +0,0 @@
|
|
|
1
|
-
import { $inject } from "alepha";
|
|
2
|
-
import { $job } from "alepha/api/jobs";
|
|
3
|
-
import { PaymentService } from "alepha/api/payments";
|
|
4
|
-
import { DateTimeProvider } from "alepha/datetime";
|
|
5
|
-
import { $logger } from "alepha/logger";
|
|
6
|
-
import { $repository } from "alepha/orm";
|
|
7
|
-
import type { SubscriptionEventEntity } from "../entities/subscriptionEvents.ts";
|
|
8
|
-
import { subscriptionEvents } from "../entities/subscriptionEvents.ts";
|
|
9
|
-
import { subscriptions } from "../entities/subscriptions.ts";
|
|
10
|
-
import { SubscriptionConfig } from "../services/SubscriptionConfig.ts";
|
|
11
|
-
|
|
12
|
-
// -----------------------------------------------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
interface EventContext {
|
|
15
|
-
previousStatus?: string;
|
|
16
|
-
newStatus?: string;
|
|
17
|
-
paymentIntentId?: string;
|
|
18
|
-
amount?: number;
|
|
19
|
-
currency?: string;
|
|
20
|
-
triggeredBy?: string;
|
|
21
|
-
note?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// -----------------------------------------------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
export class SubscriptionJobs {
|
|
27
|
-
protected readonly log = $logger();
|
|
28
|
-
protected readonly dateTime = $inject(DateTimeProvider);
|
|
29
|
-
protected readonly paymentService = $inject(PaymentService);
|
|
30
|
-
protected readonly config = $inject(SubscriptionConfig);
|
|
31
|
-
protected readonly subscriptionRepo = $repository(subscriptions);
|
|
32
|
-
protected readonly eventRepo = $repository(subscriptionEvents);
|
|
33
|
-
|
|
34
|
-
// ---------------------------------------------------------------------------------------------------------------
|
|
35
|
-
// Helpers
|
|
36
|
-
// ---------------------------------------------------------------------------------------------------------------
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Record a subscription event in the event log.
|
|
40
|
-
*/
|
|
41
|
-
protected async recordEvent(
|
|
42
|
-
subscriptionId: string,
|
|
43
|
-
organizationId: string,
|
|
44
|
-
type: SubscriptionEventEntity["type"],
|
|
45
|
-
context?: EventContext,
|
|
46
|
-
): Promise<void> {
|
|
47
|
-
await this.eventRepo.create({
|
|
48
|
-
subscriptionId,
|
|
49
|
-
organizationId,
|
|
50
|
-
type,
|
|
51
|
-
previousStatus: context?.previousStatus,
|
|
52
|
-
newStatus: context?.newStatus,
|
|
53
|
-
paymentIntentId: context?.paymentIntentId,
|
|
54
|
-
amount: context?.amount,
|
|
55
|
-
currency: context?.currency,
|
|
56
|
-
triggeredBy: context?.triggeredBy,
|
|
57
|
-
note: context?.note,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ---------------------------------------------------------------------------------------------------------------
|
|
62
|
-
// Jobs
|
|
63
|
-
// ---------------------------------------------------------------------------------------------------------------
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Creates payment intents for subscriptions due for renewal.
|
|
67
|
-
* Runs hourly.
|
|
68
|
-
*/
|
|
69
|
-
public readonly billingCycle = $job({
|
|
70
|
-
cron: "0 * * * *",
|
|
71
|
-
lock: true,
|
|
72
|
-
timeout: [10, "minute"],
|
|
73
|
-
handler: async ({ now }) => {
|
|
74
|
-
const nowISO = now.toISOString();
|
|
75
|
-
|
|
76
|
-
const due = await this.subscriptionRepo.findMany({
|
|
77
|
-
where: {
|
|
78
|
-
nextBillingAt: { lte: nowISO },
|
|
79
|
-
status: { inArray: ["active", "trialing"] },
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
this.log.info(`Billing cycle: processing ${due.length} subscription(s)`);
|
|
84
|
-
|
|
85
|
-
for (const sub of due) {
|
|
86
|
-
try {
|
|
87
|
-
const pricing = await this.config.getPlanPricing(
|
|
88
|
-
sub.planId,
|
|
89
|
-
sub.interval,
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
const intent = await this.paymentService.createIntent(
|
|
93
|
-
pricing.amount,
|
|
94
|
-
pricing.currency,
|
|
95
|
-
{ subscriptionId: sub.id },
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
await this.subscriptionRepo.updateById(sub.id, {
|
|
99
|
-
lastPaymentIntentId: intent.id,
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
this.log.debug("Created payment intent for subscription", {
|
|
103
|
-
subscriptionId: sub.id,
|
|
104
|
-
intentId: intent.id,
|
|
105
|
-
});
|
|
106
|
-
} catch (err) {
|
|
107
|
-
this.log.error("Failed to create payment intent for subscription", {
|
|
108
|
-
subscriptionId: sub.id,
|
|
109
|
-
error: err,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// ---------------------------------------------------------------------------------------------------------------
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Retries failed payments on the dunning schedule.
|
|
120
|
-
* Runs hourly.
|
|
121
|
-
*/
|
|
122
|
-
public readonly dunningRetry = $job({
|
|
123
|
-
cron: "0 * * * *",
|
|
124
|
-
lock: true,
|
|
125
|
-
timeout: [10, "minute"],
|
|
126
|
-
handler: async ({ now }) => {
|
|
127
|
-
const nowISO = now.toISOString();
|
|
128
|
-
|
|
129
|
-
const pastDue = await this.subscriptionRepo.findMany({
|
|
130
|
-
where: {
|
|
131
|
-
dunningNextRetryAt: { lte: nowISO },
|
|
132
|
-
status: { eq: "past_due" },
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
this.log.info(
|
|
137
|
-
`Dunning retry: processing ${pastDue.length} subscription(s)`,
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
const settings = await this.config.getSettings();
|
|
141
|
-
|
|
142
|
-
for (const sub of pastDue) {
|
|
143
|
-
try {
|
|
144
|
-
const pricing = await this.config.getPlanPricing(
|
|
145
|
-
sub.planId,
|
|
146
|
-
sub.interval,
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
const intent = await this.paymentService.createIntent(
|
|
150
|
-
pricing.amount,
|
|
151
|
-
pricing.currency,
|
|
152
|
-
{ subscriptionId: sub.id },
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
const newAttempt = sub.dunningAttempt + 1;
|
|
156
|
-
const scheduleDays = settings.dunningSchedule[newAttempt - 1];
|
|
157
|
-
const nextRetry =
|
|
158
|
-
scheduleDays !== undefined
|
|
159
|
-
? now.add(scheduleDays, "days").toISOString()
|
|
160
|
-
: undefined;
|
|
161
|
-
|
|
162
|
-
await this.subscriptionRepo.updateById(sub.id, {
|
|
163
|
-
lastPaymentIntentId: intent.id,
|
|
164
|
-
dunningAttempt: newAttempt,
|
|
165
|
-
dunningNextRetryAt: nextRetry,
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
await this.recordEvent(
|
|
169
|
-
sub.id,
|
|
170
|
-
sub.organizationId as string,
|
|
171
|
-
"payment_retried",
|
|
172
|
-
{
|
|
173
|
-
paymentIntentId: intent.id,
|
|
174
|
-
note: `Dunning retry attempt ${newAttempt}`,
|
|
175
|
-
},
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
this.log.debug("Dunning retry payment intent created", {
|
|
179
|
-
subscriptionId: sub.id,
|
|
180
|
-
attempt: newAttempt,
|
|
181
|
-
});
|
|
182
|
-
} catch (err) {
|
|
183
|
-
this.log.error("Failed to create dunning retry intent", {
|
|
184
|
-
subscriptionId: sub.id,
|
|
185
|
-
error: err,
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
// ---------------------------------------------------------------------------------------------------------------
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Handles trial expirations.
|
|
196
|
-
* Runs hourly.
|
|
197
|
-
*/
|
|
198
|
-
public readonly trialExpiry = $job({
|
|
199
|
-
cron: "0 * * * *",
|
|
200
|
-
lock: true,
|
|
201
|
-
handler: async ({ now }) => {
|
|
202
|
-
const nowISO = now.toISOString();
|
|
203
|
-
|
|
204
|
-
const expired = await this.subscriptionRepo.findMany({
|
|
205
|
-
where: {
|
|
206
|
-
trialEnd: { lte: nowISO },
|
|
207
|
-
status: { eq: "trialing" },
|
|
208
|
-
},
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
this.log.info(
|
|
212
|
-
`Trial expiry: processing ${expired.length} subscription(s)`,
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
for (const sub of expired) {
|
|
216
|
-
try {
|
|
217
|
-
const pricing = await this.config.getPlanPricing(
|
|
218
|
-
sub.planId,
|
|
219
|
-
sub.interval,
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
const intent = await this.paymentService.createIntent(
|
|
223
|
-
pricing.amount,
|
|
224
|
-
pricing.currency,
|
|
225
|
-
{ subscriptionId: sub.id },
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
await this.subscriptionRepo.updateById(sub.id, {
|
|
229
|
-
lastPaymentIntentId: intent.id,
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
this.log.debug("Created payment intent for trial expiry", {
|
|
233
|
-
subscriptionId: sub.id,
|
|
234
|
-
intentId: intent.id,
|
|
235
|
-
});
|
|
236
|
-
} catch (err) {
|
|
237
|
-
this.log.error("Failed to process trial expiry", {
|
|
238
|
-
subscriptionId: sub.id,
|
|
239
|
-
error: err,
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
},
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
// ---------------------------------------------------------------------------------------------------------------
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Expires cancelled subscriptions that reached period end.
|
|
250
|
-
* Runs hourly.
|
|
251
|
-
*/
|
|
252
|
-
public readonly expirationSweep = $job({
|
|
253
|
-
cron: "0 * * * *",
|
|
254
|
-
lock: true,
|
|
255
|
-
handler: async ({ now }) => {
|
|
256
|
-
const nowISO = now.toISOString();
|
|
257
|
-
|
|
258
|
-
const toExpire = await this.subscriptionRepo.findMany({
|
|
259
|
-
where: {
|
|
260
|
-
currentPeriodEnd: { lte: nowISO },
|
|
261
|
-
status: { eq: "cancelled" },
|
|
262
|
-
cancelAtPeriodEnd: { eq: true },
|
|
263
|
-
},
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
this.log.info(
|
|
267
|
-
`Expiration sweep: expiring ${toExpire.length} subscription(s)`,
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
for (const sub of toExpire) {
|
|
271
|
-
try {
|
|
272
|
-
await this.subscriptionRepo.updateById(sub.id, {
|
|
273
|
-
status: "expired",
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
await this.recordEvent(
|
|
277
|
-
sub.id,
|
|
278
|
-
sub.organizationId as string,
|
|
279
|
-
"expired",
|
|
280
|
-
{
|
|
281
|
-
previousStatus: "cancelled",
|
|
282
|
-
newStatus: "expired",
|
|
283
|
-
},
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
this.log.debug("Subscription expired", { subscriptionId: sub.id });
|
|
287
|
-
} catch (err) {
|
|
288
|
-
this.log.error("Failed to expire subscription", {
|
|
289
|
-
subscriptionId: sub.id,
|
|
290
|
-
error: err,
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
},
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
// ---------------------------------------------------------------------------------------------------------------
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Suspends past_due subscriptions where grace period has elapsed.
|
|
301
|
-
* Runs daily at 2 AM.
|
|
302
|
-
*/
|
|
303
|
-
public readonly gracePeriodSweep = $job({
|
|
304
|
-
cron: "0 2 * * *",
|
|
305
|
-
lock: true,
|
|
306
|
-
handler: async ({ now }) => {
|
|
307
|
-
const settings = await this.config.getSettings();
|
|
308
|
-
const gracePeriodDays = settings.gracePeriodDays;
|
|
309
|
-
|
|
310
|
-
const pastDueSubs = await this.subscriptionRepo.findMany({
|
|
311
|
-
where: {
|
|
312
|
-
status: { eq: "past_due" },
|
|
313
|
-
},
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
const toSuspend = pastDueSubs.filter((sub) => {
|
|
317
|
-
if (!sub.dunningStartedAt) return false;
|
|
318
|
-
const graceEnd = this.dateTime
|
|
319
|
-
.of(sub.dunningStartedAt)
|
|
320
|
-
.add(gracePeriodDays, "days");
|
|
321
|
-
return !now.isBefore(graceEnd.toISOString());
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
this.log.info(
|
|
325
|
-
`Grace period sweep: suspending ${toSuspend.length} subscription(s)`,
|
|
326
|
-
);
|
|
327
|
-
|
|
328
|
-
for (const sub of toSuspend) {
|
|
329
|
-
try {
|
|
330
|
-
await this.subscriptionRepo.updateById(sub.id, {
|
|
331
|
-
status: "suspended",
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
await this.recordEvent(
|
|
335
|
-
sub.id,
|
|
336
|
-
sub.organizationId as string,
|
|
337
|
-
"suspended",
|
|
338
|
-
{
|
|
339
|
-
previousStatus: "past_due",
|
|
340
|
-
newStatus: "suspended",
|
|
341
|
-
},
|
|
342
|
-
);
|
|
343
|
-
|
|
344
|
-
this.log.debug("Subscription suspended after grace period", {
|
|
345
|
-
subscriptionId: sub.id,
|
|
346
|
-
});
|
|
347
|
-
} catch (err) {
|
|
348
|
-
this.log.error("Failed to suspend subscription", {
|
|
349
|
-
subscriptionId: sub.id,
|
|
350
|
-
error: err,
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
},
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
// ---------------------------------------------------------------------------------------------------------------
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Purges old subscription events older than 365 days.
|
|
361
|
-
* Runs daily at 3 AM.
|
|
362
|
-
*/
|
|
363
|
-
public readonly purgeEvents = $job({
|
|
364
|
-
cron: "0 3 * * *",
|
|
365
|
-
lock: true,
|
|
366
|
-
handler: async ({ now }) => {
|
|
367
|
-
const cutoff = now.subtract(365, "days").toISOString();
|
|
368
|
-
|
|
369
|
-
const old = await this.eventRepo.findMany({
|
|
370
|
-
where: {
|
|
371
|
-
createdAt: { lt: cutoff },
|
|
372
|
-
},
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
this.log.info(`Purge events: removing ${old.length} old event(s)`);
|
|
376
|
-
|
|
377
|
-
for (const event of old) {
|
|
378
|
-
await this.eventRepo.deleteById(event.id);
|
|
379
|
-
}
|
|
380
|
-
},
|
|
381
|
-
});
|
|
382
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { $context, createMiddleware, type Middleware } from "alepha";
|
|
2
|
-
import { ForbiddenError } from "alepha/server";
|
|
3
|
-
import { UsageService } from "../services/UsageService.ts";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Middleware that enforces a per-organization usage limit for a resource.
|
|
7
|
-
*
|
|
8
|
-
* Resolves the organization from `args[0].user.organization`, increments the
|
|
9
|
-
* usage counter for the given resource, and throws `ForbiddenError` if the
|
|
10
|
-
* plan limit has been reached.
|
|
11
|
-
* Throws `ForbiddenError` if no organization is present or the limit is exceeded.
|
|
12
|
-
*
|
|
13
|
-
* ```typescript
|
|
14
|
-
* class ApiController {
|
|
15
|
-
* search = $action({
|
|
16
|
-
* use: [$requireLimit("api_calls")],
|
|
17
|
-
* handler: async ({ query }) => { ... },
|
|
18
|
-
* });
|
|
19
|
-
* }
|
|
20
|
-
* ```
|
|
21
|
-
*
|
|
22
|
-
* @param resource The resource identifier to track (e.g., "api_calls", "exports").
|
|
23
|
-
*/
|
|
24
|
-
export const $requireLimit = (resource: string): Middleware => {
|
|
25
|
-
const { alepha } = $context();
|
|
26
|
-
const usageService = alepha.inject(UsageService);
|
|
27
|
-
|
|
28
|
-
return createMiddleware({
|
|
29
|
-
name: "$requireLimit",
|
|
30
|
-
options: { resource } as Record<string, unknown>,
|
|
31
|
-
handler: ({ next }) => {
|
|
32
|
-
return async (...args: any[]) => {
|
|
33
|
-
const user = args[0]?.user;
|
|
34
|
-
if (!user?.organization) {
|
|
35
|
-
throw new ForbiddenError("Organization required");
|
|
36
|
-
}
|
|
37
|
-
const result = await usageService.increment(
|
|
38
|
-
user.organization,
|
|
39
|
-
resource,
|
|
40
|
-
);
|
|
41
|
-
if (!result.allowed) {
|
|
42
|
-
throw new ForbiddenError(
|
|
43
|
-
`Usage limit for '${resource}' has been reached (${result.current}/${result.limit})`,
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
return next(...args);
|
|
47
|
-
};
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
};
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { $context, createMiddleware, type Middleware } from "alepha";
|
|
2
|
-
import { ForbiddenError } from "alepha/server";
|
|
3
|
-
import { SubscriptionService } from "../services/SubscriptionService.ts";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Middleware that gates access to a handler behind a subscription feature flag.
|
|
7
|
-
*
|
|
8
|
-
* Resolves the organization from `args[0].user.organization` and checks whether
|
|
9
|
-
* the organization's current plan includes the given feature.
|
|
10
|
-
* Throws `ForbiddenError` if no organization is present or the feature is not available.
|
|
11
|
-
*
|
|
12
|
-
* ```typescript
|
|
13
|
-
* class ReportController {
|
|
14
|
-
* generate = $action({
|
|
15
|
-
* use: [$requirePlan("advanced_reports")],
|
|
16
|
-
* handler: async ({ user }) => { ... },
|
|
17
|
-
* });
|
|
18
|
-
* }
|
|
19
|
-
* ```
|
|
20
|
-
*
|
|
21
|
-
* @param feature The feature identifier to check against the plan's feature list.
|
|
22
|
-
*/
|
|
23
|
-
export const $requirePlan = (feature: string): Middleware => {
|
|
24
|
-
const { alepha } = $context();
|
|
25
|
-
const subscriptionService = alepha.inject(SubscriptionService);
|
|
26
|
-
|
|
27
|
-
return createMiddleware({
|
|
28
|
-
name: "$requirePlan",
|
|
29
|
-
options: { feature } as Record<string, unknown>,
|
|
30
|
-
handler: ({ next }) => {
|
|
31
|
-
return async (...args: any[]) => {
|
|
32
|
-
const user = args[0]?.user;
|
|
33
|
-
if (!user?.organization) {
|
|
34
|
-
throw new ForbiddenError("Organization required");
|
|
35
|
-
}
|
|
36
|
-
const allowed = await subscriptionService.can(
|
|
37
|
-
user.organization,
|
|
38
|
-
feature,
|
|
39
|
-
);
|
|
40
|
-
if (!allowed) {
|
|
41
|
-
throw new ForbiddenError(
|
|
42
|
-
`Feature '${feature}' not available on your plan`,
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
return next(...args);
|
|
46
|
-
};
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
};
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { t } from "alepha";
|
|
2
|
-
import { $notification } from "alepha/api/notifications";
|
|
3
|
-
|
|
4
|
-
export class SubscriptionNotifications {
|
|
5
|
-
/**
|
|
6
|
-
* Sent when a trial is ending soon.
|
|
7
|
-
*/
|
|
8
|
-
protected readonly trialEnding = $notification({
|
|
9
|
-
name: "subscription-trial-ending",
|
|
10
|
-
category: "subscriptions",
|
|
11
|
-
schema: t.object({
|
|
12
|
-
planName: t.text(),
|
|
13
|
-
trialEndDate: t.text(),
|
|
14
|
-
amount: t.text(),
|
|
15
|
-
interval: t.text(),
|
|
16
|
-
}),
|
|
17
|
-
email: {
|
|
18
|
-
subject: "Your trial is ending soon",
|
|
19
|
-
body: (v) =>
|
|
20
|
-
`Your ${v.planName} trial is ending on ${v.trialEndDate}. You'll be charged ${v.amount}/${v.interval}.`,
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Sent when a payment fails. Critical notification.
|
|
26
|
-
*/
|
|
27
|
-
protected readonly paymentFailed = $notification({
|
|
28
|
-
name: "subscription-payment-failed",
|
|
29
|
-
category: "subscriptions",
|
|
30
|
-
critical: true,
|
|
31
|
-
schema: t.object({
|
|
32
|
-
planName: t.text(),
|
|
33
|
-
amount: t.text(),
|
|
34
|
-
retryDate: t.optional(t.text()),
|
|
35
|
-
}),
|
|
36
|
-
email: {
|
|
37
|
-
subject: "Payment failed for your subscription",
|
|
38
|
-
body: (v) =>
|
|
39
|
-
`We couldn't charge your card for ${v.planName} (${v.amount}). ${v.retryDate ? `We'll retry on ${v.retryDate}.` : "Please update your payment method."}`,
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Sent when a subscription is suspended due to failed payments. Critical notification.
|
|
45
|
-
*/
|
|
46
|
-
protected readonly subscriptionSuspended = $notification({
|
|
47
|
-
name: "subscription-suspended",
|
|
48
|
-
category: "subscriptions",
|
|
49
|
-
critical: true,
|
|
50
|
-
schema: t.object({ planName: t.text() }),
|
|
51
|
-
email: {
|
|
52
|
-
subject: "Your subscription has been suspended",
|
|
53
|
-
body: (v) =>
|
|
54
|
-
`Your ${v.planName} subscription has been suspended due to failed payments. Update your payment method to reactivate.`,
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Sent when a subscription is successfully renewed.
|
|
60
|
-
*/
|
|
61
|
-
protected readonly subscriptionRenewed = $notification({
|
|
62
|
-
name: "subscription-renewed",
|
|
63
|
-
category: "subscriptions",
|
|
64
|
-
schema: t.object({
|
|
65
|
-
planName: t.text(),
|
|
66
|
-
amount: t.text(),
|
|
67
|
-
nextBillingDate: t.text(),
|
|
68
|
-
}),
|
|
69
|
-
email: {
|
|
70
|
-
subject: "Payment received — subscription renewed",
|
|
71
|
-
body: (v) =>
|
|
72
|
-
`Your ${v.planName} subscription has been renewed. Amount: ${v.amount}. Next billing: ${v.nextBillingDate}.`,
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Sent when a subscription plan is changed.
|
|
78
|
-
*/
|
|
79
|
-
protected readonly planChanged = $notification({
|
|
80
|
-
name: "subscription-plan-changed",
|
|
81
|
-
category: "subscriptions",
|
|
82
|
-
schema: t.object({
|
|
83
|
-
oldPlanName: t.text(),
|
|
84
|
-
newPlanName: t.text(),
|
|
85
|
-
effectiveDate: t.text(),
|
|
86
|
-
}),
|
|
87
|
-
email: {
|
|
88
|
-
subject: "Your subscription plan has been changed",
|
|
89
|
-
body: (v) =>
|
|
90
|
-
`Your plan has been changed from ${v.oldPlanName} to ${v.newPlanName}, effective ${v.effectiveDate}.`,
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Sent when a subscription is cancelled.
|
|
96
|
-
*/
|
|
97
|
-
protected readonly cancellationConfirmed = $notification({
|
|
98
|
-
name: "subscription-cancelled",
|
|
99
|
-
category: "subscriptions",
|
|
100
|
-
schema: t.object({
|
|
101
|
-
planName: t.text(),
|
|
102
|
-
accessUntil: t.optional(t.text()),
|
|
103
|
-
}),
|
|
104
|
-
email: {
|
|
105
|
-
subject: "Your subscription has been cancelled",
|
|
106
|
-
body: (v) =>
|
|
107
|
-
`Your ${v.planName} subscription has been cancelled.${v.accessUntil ? ` You'll have access until ${v.accessUntil}.` : ""}`,
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { type Static, t } from "alepha";
|
|
2
|
-
|
|
3
|
-
export const changePlanSchema = t.object({
|
|
4
|
-
planId: t.string(),
|
|
5
|
-
interval: t.optional(t.enum(["monthly", "yearly"])),
|
|
6
|
-
immediate: t.optional(t.boolean()),
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
export type ChangePlan = Static<typeof changePlanSchema>;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { type Static, t } from "alepha";
|
|
2
|
-
|
|
3
|
-
export const createSubscriptionSchema = t.object({
|
|
4
|
-
planId: t.string(),
|
|
5
|
-
interval: t.enum(["monthly", "yearly"]),
|
|
6
|
-
paymentMethodId: t.optional(t.uuid()),
|
|
7
|
-
skipTrial: t.optional(t.boolean()),
|
|
8
|
-
metadata: t.optional(t.record(t.text(), t.any())),
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
export type CreateSubscription = Static<typeof createSubscriptionSchema>;
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { type Static, t } from "alepha";
|
|
2
|
-
|
|
3
|
-
export const entitlementsSchema = t.object({
|
|
4
|
-
planId: t.string(),
|
|
5
|
-
planName: t.string(),
|
|
6
|
-
status: t.enum([
|
|
7
|
-
"trialing",
|
|
8
|
-
"active",
|
|
9
|
-
"past_due",
|
|
10
|
-
"suspended",
|
|
11
|
-
"cancelled",
|
|
12
|
-
"expired",
|
|
13
|
-
]),
|
|
14
|
-
features: t.array(t.string()),
|
|
15
|
-
limits: t.record(t.text(), t.integer()),
|
|
16
|
-
trialEndsAt: t.optional(t.datetime()),
|
|
17
|
-
periodEndsAt: t.datetime(),
|
|
18
|
-
cancelledAt: t.optional(t.datetime()),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
export type Entitlements = Static<typeof entitlementsSchema>;
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { type Static, t } from "alepha";
|
|
2
|
-
|
|
3
|
-
export const mrrSchema = t.object({
|
|
4
|
-
total: t.integer(),
|
|
5
|
-
byPlan: t.record(t.text(), t.integer()),
|
|
6
|
-
growth: t.integer(),
|
|
7
|
-
newMrr: t.integer(),
|
|
8
|
-
expansionMrr: t.integer(),
|
|
9
|
-
contractionMrr: t.integer(),
|
|
10
|
-
churnMrr: t.integer(),
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
export type MrrData = Static<typeof mrrSchema>;
|