alepha 0.20.5 → 0.20.7
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/AGENTS.md +0 -1
- package/CLAUDE.md +0 -1
- package/assets/agents-template.md +0 -1
- package/dist/api/audits/index.browser.js +1 -0
- package/dist/api/audits/index.browser.js.map +1 -1
- package/dist/api/audits/index.d.ts +701 -654
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +24 -1
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.browser.js +1 -0
- package/dist/api/files/index.browser.js.map +1 -1
- package/dist/api/files/index.d.ts +193 -166
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +52 -0
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.browser.js +40 -14
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +639 -333
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +495 -162
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +222 -188
- package/dist/api/keys/index.d.ts.map +1 -1
- package/dist/api/keys/index.js +54 -0
- package/dist/api/keys/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +265 -236
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +55 -13
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/organizations/index.d.ts +100 -97
- package/dist/api/organizations/index.d.ts.map +1 -1
- package/dist/api/organizations/index.js.map +1 -1
- package/dist/api/parameters/index.d.ts +332 -314
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +37 -0
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/payments/index.d.ts +431 -376
- package/dist/api/payments/index.d.ts.map +1 -1
- package/dist/api/payments/index.js +202 -87
- package/dist/api/payments/index.js.map +1 -1
- package/dist/api/subscriptions/index.d.ts +1695 -0
- package/dist/api/subscriptions/index.d.ts.map +1 -0
- package/dist/api/subscriptions/index.js +1919 -0
- package/dist/api/subscriptions/index.js.map +1 -0
- package/dist/api/users/index.d.ts +1001 -844
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +237 -28
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +123 -122
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/batch/index.js.map +1 -1
- package/dist/bucket/index.d.ts +21 -2
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/bucket/index.js +47 -0
- package/dist/bucket/index.js.map +1 -1
- package/dist/bucket/index.workerd.js +24 -0
- package/dist/bucket/index.workerd.js.map +1 -1
- package/dist/cache/core/index.d.ts +134 -7
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/core/index.js +181 -15
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/core/index.workerd.js +181 -15
- package/dist/cache/core/index.workerd.js.map +1 -1
- package/dist/cache/database/index.d.ts +156 -0
- package/dist/cache/database/index.d.ts.map +1 -0
- package/dist/cache/database/index.js +266 -0
- package/dist/cache/database/index.js.map +1 -0
- package/dist/cache/redis/index.d.ts +3 -2
- package/dist/cache/redis/index.d.ts.map +1 -1
- package/dist/cache/redis/index.js.map +1 -1
- package/dist/captcha/index.js.map +1 -1
- package/dist/cli/config/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +142 -128
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +160 -13
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.d.ts +3 -2
- package/dist/cli/devtools/index.d.ts.map +1 -1
- package/dist/cli/devtools/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +346 -290
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +106 -7
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +12 -11
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/command/index.d.ts +6 -5
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +1 -1
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +119 -118
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +1 -1
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +1 -1
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/crypto/index.browser.js.map +1 -1
- package/dist/crypto/index.d.ts +3 -2
- package/dist/crypto/index.d.ts.map +1 -1
- package/dist/crypto/index.js.map +1 -1
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/brevo/index.js.map +1 -1
- package/dist/email/core/index.d.ts +3 -2
- package/dist/email/core/index.d.ts.map +1 -1
- package/dist/email/core/index.js.map +1 -1
- package/dist/email/core/index.workerd.js.map +1 -1
- package/dist/email/smtp/index.d.ts +7 -6
- package/dist/email/smtp/index.d.ts.map +1 -1
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/fake/index.js.map +1 -1
- package/dist/lock/core/index.d.ts +5 -4
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/core/index.js.map +1 -1
- package/dist/lock/redis/index.js.map +1 -1
- package/dist/logger/index.d.ts +10 -9
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.d.ts +9 -8
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +9 -3
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +31 -10
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +33 -14
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +31 -10
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.bun.js.map +1 -1
- package/dist/orm/postgres/index.d.ts +6 -5
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/orm/postgres/index.js.map +1 -1
- package/dist/queue/core/index.d.ts +5 -4
- package/dist/queue/core/index.d.ts.map +1 -1
- package/dist/queue/core/index.js.map +1 -1
- package/dist/queue/core/index.workerd.js.map +1 -1
- package/dist/queue/redis/index.d.ts +3 -2
- package/dist/queue/redis/index.d.ts.map +1 -1
- package/dist/queue/redis/index.js.map +1 -1
- package/dist/react/auth/index.browser.js.map +1 -1
- package/dist/react/auth/index.js.map +1 -1
- package/dist/react/core/index.js.map +1 -1
- package/dist/react/form/index.d.ts +5 -0
- package/dist/react/form/index.d.ts.map +1 -1
- package/dist/react/form/index.js +8 -4
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/head/index.browser.js.map +1 -1
- package/dist/react/head/index.js.map +1 -1
- package/dist/react/i18n/index.d.ts +2 -1
- package/dist/react/i18n/index.d.ts.map +1 -1
- package/dist/react/i18n/index.js.map +1 -1
- package/dist/react/intro/index.js.map +1 -1
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +206 -205
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/testing/index.js.map +1 -1
- package/dist/react/ui/index.d.ts +11 -11
- package/dist/react/ui/index.d.ts.map +1 -1
- package/dist/react/ui/index.js.map +1 -1
- package/dist/redis/index.bun.js.map +1 -1
- package/dist/redis/index.js.map +1 -1
- package/dist/retry/index.js.map +1 -1
- package/dist/router/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +25 -2
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +12 -0
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/index.workerd.js +12 -0
- package/dist/scheduler/index.workerd.js.map +1 -1
- package/dist/security/index.browser.js +29 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.d.ts +82 -35
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +56 -3
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +163 -158
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +16 -4
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cookies/index.browser.js.map +1 -1
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.browser.js.map +1 -1
- package/dist/server/core/index.d.ts +35 -34
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.d.ts +7 -6
- package/dist/server/cors/index.d.ts.map +1 -1
- package/dist/server/cors/index.js.map +1 -1
- package/dist/server/etag/index.js.map +1 -1
- package/dist/server/health/index.d.ts +16 -15
- package/dist/server/health/index.d.ts.map +1 -1
- package/dist/server/health/index.js.map +1 -1
- package/dist/server/links/index.browser.js.map +1 -1
- package/dist/server/links/index.d.ts +51 -50
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/proxy/index.js.map +1 -1
- package/dist/server/rate-limit/index.d.ts +6 -5
- package/dist/server/rate-limit/index.d.ts.map +1 -1
- package/dist/server/rate-limit/index.js.map +1 -1
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts +2 -1
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.browser.js.map +1 -1
- package/dist/system/index.js.map +1 -1
- package/dist/system/index.workerd.js.map +1 -1
- package/dist/topic/core/index.js.map +1 -1
- package/dist/topic/redis/index.d.ts +3 -2
- package/dist/topic/redis/index.d.ts.map +1 -1
- package/dist/topic/redis/index.js.map +1 -1
- package/package.json +33 -39
- package/src/api/audits/controllers/AdminAuditController.ts +29 -0
- package/src/api/audits/entities/audits.ts +1 -0
- package/src/api/files/controllers/FileController.ts +24 -0
- package/src/api/files/entities/files.ts +1 -0
- package/src/api/files/services/FileService.ts +41 -0
- package/src/api/jobs/__tests__/$job.spec.ts +501 -24
- package/src/api/jobs/entities/jobExecutionEntity.ts +4 -3
- package/src/api/jobs/index.ts +47 -10
- package/src/api/jobs/primitives/$job.ts +22 -9
- package/src/api/jobs/providers/DirectJobDispatcher.ts +71 -0
- package/src/api/jobs/providers/JobDispatcher.ts +49 -0
- package/src/api/jobs/providers/JobProvider.ts +385 -147
- package/src/api/jobs/providers/JobQueueProvider.ts +43 -18
- package/src/api/jobs/schemas/jobConfigAtom.ts +9 -3
- package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +11 -0
- package/src/api/jobs/schemas/jobRegistrationSchema.ts +4 -2
- package/src/api/jobs/services/JobService.ts +21 -11
- package/src/api/keys/controllers/AdminApiKeyController.ts +23 -0
- package/src/api/keys/entities/apiKeyEntity.ts +1 -0
- package/src/api/keys/services/ApiKeyService.ts +42 -0
- package/src/api/notifications/__tests__/AlephaApiNotifications.spec.ts +63 -0
- package/src/api/notifications/controllers/AdminNotificationController.ts +48 -1
- package/src/api/notifications/index.ts +13 -3
- package/src/api/notifications/jobs/NotificationJobs.ts +0 -6
- package/src/api/parameters/controllers/AdminParameterController.ts +26 -0
- package/src/api/parameters/services/ParameterProvider.ts +18 -0
- package/src/api/payments/controllers/MockCheckoutController.ts +146 -0
- package/src/api/payments/index.ts +3 -0
- package/src/api/payments/providers/MemoryPaymentProvider.ts +9 -4
- package/src/api/payments/providers/PaymentProvider.ts +25 -9
- package/src/api/payments/services/PaymentService.ts +3 -0
- package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
- package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
- package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
- package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
- package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
- package/src/api/subscriptions/entities/subscriptions.ts +68 -0
- package/src/api/subscriptions/index.ts +133 -0
- package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
- package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
- package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
- package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
- package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
- package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
- package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
- package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
- package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
- package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
- package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
- package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
- package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
- package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
- package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
- package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
- package/src/api/subscriptions/services/BillingService.ts +437 -0
- package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
- package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
- package/src/api/subscriptions/services/UsageService.ts +118 -0
- package/src/api/users/__tests__/Registration-emailMode.spec.ts +203 -0
- package/src/api/users/__tests__/UsernameSlugger.spec.ts +138 -0
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +41 -3
- package/src/api/users/controllers/AdminSessionController.ts +29 -0
- package/src/api/users/controllers/AdminUserController.ts +32 -0
- package/src/api/users/index.ts +3 -0
- package/src/api/users/services/CredentialService.ts +5 -0
- package/src/api/users/services/RegistrationService.ts +49 -1
- package/src/api/users/services/SessionCrudService.ts +16 -0
- package/src/api/users/services/SessionService.ts +17 -59
- package/src/api/users/services/UsernameSlugger.ts +195 -0
- package/src/bucket/primitives/$bucket.ts +21 -0
- package/src/bucket/providers/CloudflareR2Provider.ts +15 -0
- package/src/bucket/providers/FileStorageProvider.ts +9 -0
- package/src/bucket/providers/LocalFileStorageProvider.ts +14 -0
- package/src/bucket/providers/MemoryFileStorageProvider.ts +9 -0
- package/src/bucket/providers/NodeS3BucketProvider.ts +35 -0
- package/src/cache/core/__tests__/$cache.memory.spec.ts +450 -0
- package/src/cache/core/__tests__/$cache.swr.spec.ts +394 -0
- package/src/cache/core/index.ts +16 -0
- package/src/cache/core/primitives/$cache.ts +367 -24
- package/src/cache/database/__tests__/DatabaseCacheProvider.behavior.spec.ts +203 -0
- package/src/cache/database/__tests__/DatabaseCacheProvider.spec.ts +110 -0
- package/src/cache/database/entities/cacheEntries.ts +55 -0
- package/src/cache/database/index.ts +36 -0
- package/src/cache/database/providers/DatabaseCacheProvider.ts +348 -0
- package/src/cli/core/services/ProjectScaffolder.ts +0 -2
- package/src/cli/core/tasks/BuildCloudflareTask.ts +33 -3
- package/src/cli/core/tasks/BuildSitemapTask.ts +7 -0
- package/src/cli/core/tasks/BuildVercelTask.ts +82 -3
- package/src/cli/core/templates/agentMd.ts +39 -4
- package/src/cli/core/templates/biomeJson.ts +25 -1
- package/src/cli/core/templates/saasAdminLayoutTsx.ts +2 -2
- package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +117 -0
- package/src/cli/platform/__tests__/detectResources.spec.ts +96 -0
- package/src/cli/platform/adapters/CloudflareAdapter.ts +104 -7
- package/src/cli/platform/atoms/platformOptions.ts +13 -0
- package/src/cli/platform/commands/platform.ts +7 -1
- package/src/cli/platform/schemas/platform.ts +1 -0
- package/src/cli/platform/services/CloudflareApi.ts +61 -0
- package/src/cli/platform/services/PlatformOrchestrator.ts +9 -4
- package/src/core/__tests__/$module.spec.ts +2 -2
- package/src/core/primitives/$module.ts +4 -4
- package/src/mcp/providers/McpServerProvider.ts +1 -1
- package/src/orm/core/providers/DatabaseTypeProvider.ts +9 -3
- package/src/orm/core/providers/drivers/DatabaseProvider.ts +1 -1
- package/src/orm/core/schemas/insertSchema.ts +10 -2
- package/src/orm/core/services/Repository.ts +27 -7
- package/src/react/form/hooks/useFormState.ts +8 -1
- package/src/react/form/index.ts +10 -1
- package/src/react/form/services/FormModel.ts +9 -3
- package/src/scheduler/index.ts +14 -0
- package/src/scheduler/providers/CronProvider.ts +13 -0
- package/src/security/atoms/currentTenantAtom.ts +34 -0
- package/src/security/index.browser.ts +1 -0
- package/src/security/index.ts +12 -1
- package/src/security/primitives/$issuer.ts +17 -1
- package/src/security/providers/SecurityProvider.ts +37 -0
- package/src/server/auth/__tests__/validateRedirectUri.spec.ts +78 -0
- package/src/server/auth/providers/ServerAuthProvider.ts +21 -5
- package/tsconfig.base.json +2 -1
- package/dist/react/websocket/index.d.ts +0 -117
- package/dist/react/websocket/index.d.ts.map +0 -1
- package/dist/react/websocket/index.js +0 -108
- package/dist/react/websocket/index.js.map +0 -1
- package/dist/websocket/index.browser.js +0 -844
- package/dist/websocket/index.browser.js.map +0 -1
- package/dist/websocket/index.d.ts +0 -876
- package/dist/websocket/index.d.ts.map +0 -1
- package/dist/websocket/index.js +0 -1175
- package/dist/websocket/index.js.map +0 -1
- package/src/react/websocket/hooks/useRoom.tsx +0 -251
- package/src/react/websocket/index.ts +0 -7
- package/src/websocket/__tests__/$channel.spec.ts +0 -30
- package/src/websocket/__tests__/$websocket-new.spec.ts +0 -195
- package/src/websocket/__tests__/RoomManager.spec.ts +0 -146
- package/src/websocket/__tests__/websocket-integration.spec.ts +0 -951
- package/src/websocket/errors/WebSocketError.ts +0 -34
- package/src/websocket/index.browser.ts +0 -25
- package/src/websocket/index.shared.ts +0 -8
- package/src/websocket/index.ts +0 -85
- package/src/websocket/interfaces/WebSocketInterfaces.ts +0 -252
- package/src/websocket/primitives/$channel.ts +0 -131
- package/src/websocket/primitives/$websocket.ts +0 -107
- package/src/websocket/providers/NodeWebSocketServerProvider.ts +0 -617
- package/src/websocket/providers/WebSocketServerProvider.ts +0 -56
- package/src/websocket/services/RoomManager.ts +0 -160
- package/src/websocket/services/WebSocketClient.ts +0 -642
- package/src/websocket/services/WebSocketTopicService.ts +0 -108
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { $inject, t } from "alepha";
|
|
2
|
+
import { $route, type ServerReply } from "alepha/server";
|
|
3
|
+
import { MemoryPaymentProvider } from "../providers/MemoryPaymentProvider.ts";
|
|
4
|
+
import { PaymentProvider } from "../providers/PaymentProvider.ts";
|
|
5
|
+
import { PaymentService } from "../services/PaymentService.ts";
|
|
6
|
+
|
|
7
|
+
const FORBIDDEN_HTML =
|
|
8
|
+
"<!doctype html><meta charset=utf-8><title>Mock checkout</title>" +
|
|
9
|
+
"<p>Mock checkout is only available with the in-memory payment provider.</p>";
|
|
10
|
+
|
|
11
|
+
const escapeHtml = (s: string) =>
|
|
12
|
+
s.replace(/[&<>"']/g, (c) =>
|
|
13
|
+
c === "&"
|
|
14
|
+
? "&"
|
|
15
|
+
: c === "<"
|
|
16
|
+
? "<"
|
|
17
|
+
: c === ">"
|
|
18
|
+
? ">"
|
|
19
|
+
: c === '"'
|
|
20
|
+
? """
|
|
21
|
+
: "'",
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const formatAmount = (cents: number, currency: string) => {
|
|
25
|
+
const value = (cents / 100).toFixed(2);
|
|
26
|
+
return `${value} ${currency.toUpperCase()}`;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const renderPage = (opts: {
|
|
30
|
+
intentId: string;
|
|
31
|
+
amount: string;
|
|
32
|
+
returnUrl: string;
|
|
33
|
+
}) => `<!doctype html>
|
|
34
|
+
<html lang="en">
|
|
35
|
+
<head>
|
|
36
|
+
<meta charset="utf-8" />
|
|
37
|
+
<title>Mock checkout</title>
|
|
38
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
39
|
+
<style>
|
|
40
|
+
:root { color-scheme: light dark; font-family: system-ui, sans-serif; }
|
|
41
|
+
body { display:flex; min-height:100vh; align-items:center; justify-content:center; margin:0; background:#f6f7f9; }
|
|
42
|
+
.card { background:#fff; padding:2rem; border-radius:12px; box-shadow:0 8px 24px rgba(0,0,0,.08); max-width:380px; width:100%; }
|
|
43
|
+
h1 { margin:0 0 .25rem; font-size:1.1rem; }
|
|
44
|
+
.badge { display:inline-block; background:#fef3c7; color:#92400e; font-size:.7rem; padding:2px 8px; border-radius:999px; margin-bottom:.75rem; }
|
|
45
|
+
.amount { font-size:2rem; font-weight:600; margin:.75rem 0 1.25rem; }
|
|
46
|
+
.row { display:flex; gap:.5rem; }
|
|
47
|
+
button { flex:1; padding:.75rem 1rem; border-radius:8px; border:0; font-size:.95rem; cursor:pointer; }
|
|
48
|
+
.pay { background:#16a34a; color:#fff; }
|
|
49
|
+
.cancel { background:#e5e7eb; color:#111; }
|
|
50
|
+
.meta { font-size:.75rem; color:#6b7280; margin-top:1rem; word-break:break-all; }
|
|
51
|
+
</style>
|
|
52
|
+
</head>
|
|
53
|
+
<body>
|
|
54
|
+
<div class="card">
|
|
55
|
+
<span class="badge">DEV — mock checkout</span>
|
|
56
|
+
<h1>Confirm payment</h1>
|
|
57
|
+
<div class="amount">${escapeHtml(opts.amount)}</div>
|
|
58
|
+
<div class="row">
|
|
59
|
+
<form method="post" action="/payments/mock-checkout/${opts.intentId}/cancel">
|
|
60
|
+
<input type="hidden" name="returnUrl" value="${escapeHtml(opts.returnUrl)}" />
|
|
61
|
+
<button type="submit" class="cancel">Cancel</button>
|
|
62
|
+
</form>
|
|
63
|
+
<form method="post" action="/payments/mock-checkout/${opts.intentId}/confirm">
|
|
64
|
+
<input type="hidden" name="returnUrl" value="${escapeHtml(opts.returnUrl)}" />
|
|
65
|
+
<button type="submit" class="pay">Pay</button>
|
|
66
|
+
</form>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="meta">intent: ${opts.intentId}</div>
|
|
69
|
+
</div>
|
|
70
|
+
</body>
|
|
71
|
+
</html>`;
|
|
72
|
+
|
|
73
|
+
const appendStatusParam = (returnUrl: string, status: "success" | "cancel") => {
|
|
74
|
+
try {
|
|
75
|
+
const url = new URL(returnUrl, "http://placeholder.local");
|
|
76
|
+
url.searchParams.set("booking", status);
|
|
77
|
+
return returnUrl.startsWith("http")
|
|
78
|
+
? url.toString()
|
|
79
|
+
: url.pathname + url.search + url.hash;
|
|
80
|
+
} catch {
|
|
81
|
+
return returnUrl;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export class MockCheckoutController {
|
|
86
|
+
protected readonly url = "/payments/mock-checkout";
|
|
87
|
+
protected readonly payments = $inject(PaymentService);
|
|
88
|
+
protected readonly provider = $inject(PaymentProvider);
|
|
89
|
+
|
|
90
|
+
protected isMemoryProvider() {
|
|
91
|
+
return this.provider instanceof MemoryPaymentProvider;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
protected forbidden(reply: ServerReply) {
|
|
95
|
+
reply.headers["content-type"] = "text/html; charset=utf-8";
|
|
96
|
+
reply.status = 403;
|
|
97
|
+
return FORBIDDEN_HTML;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public readonly mockCheckoutPage = $route({
|
|
101
|
+
method: "GET",
|
|
102
|
+
path: `${this.url}/:id`,
|
|
103
|
+
schema: {
|
|
104
|
+
params: t.object({ id: t.uuid() }),
|
|
105
|
+
query: t.object({ returnUrl: t.optional(t.text({ size: "rich" })) }),
|
|
106
|
+
},
|
|
107
|
+
handler: async ({ params, query, reply }) => {
|
|
108
|
+
if (!this.isMemoryProvider()) return this.forbidden(reply);
|
|
109
|
+
const intent = await this.payments.getIntent(params.id);
|
|
110
|
+
reply.headers["content-type"] = "text/html; charset=utf-8";
|
|
111
|
+
return renderPage({
|
|
112
|
+
intentId: intent.id,
|
|
113
|
+
amount: formatAmount(intent.amount, intent.currency),
|
|
114
|
+
returnUrl: query.returnUrl ?? "/",
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
public readonly mockCheckoutConfirm = $route({
|
|
120
|
+
method: "POST",
|
|
121
|
+
path: `${this.url}/:id/confirm`,
|
|
122
|
+
schema: {
|
|
123
|
+
params: t.object({ id: t.uuid() }),
|
|
124
|
+
body: t.object({ returnUrl: t.optional(t.text({ size: "rich" })) }),
|
|
125
|
+
},
|
|
126
|
+
handler: async ({ params, body, reply }) => {
|
|
127
|
+
if (!this.isMemoryProvider()) return this.forbidden(reply);
|
|
128
|
+
await this.payments.handleWebhookEvent(params.id, "captured");
|
|
129
|
+
reply.redirect(appendStatusParam(body.returnUrl ?? "/", "success"), 302);
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
public readonly mockCheckoutCancel = $route({
|
|
134
|
+
method: "POST",
|
|
135
|
+
path: `${this.url}/:id/cancel`,
|
|
136
|
+
schema: {
|
|
137
|
+
params: t.object({ id: t.uuid() }),
|
|
138
|
+
body: t.object({ returnUrl: t.optional(t.text({ size: "rich" })) }),
|
|
139
|
+
},
|
|
140
|
+
handler: async ({ params, body, reply }) => {
|
|
141
|
+
if (!this.isMemoryProvider()) return this.forbidden(reply);
|
|
142
|
+
await this.payments.handleWebhookEvent(params.id, "failed");
|
|
143
|
+
reply.redirect(appendStatusParam(body.returnUrl ?? "/", "cancel"), 302);
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { $module } from "alepha";
|
|
2
2
|
import { AdminPaymentController } from "./controllers/AdminPaymentController.ts";
|
|
3
|
+
import { MockCheckoutController } from "./controllers/MockCheckoutController.ts";
|
|
3
4
|
import { PaymentController } from "./controllers/PaymentController.ts";
|
|
4
5
|
import { MemoryPaymentProvider } from "./providers/MemoryPaymentProvider.ts";
|
|
5
6
|
import { PaymentProvider } from "./providers/PaymentProvider.ts";
|
|
@@ -7,6 +8,7 @@ import { PaymentMethodService } from "./services/PaymentMethodService.ts";
|
|
|
7
8
|
import { PaymentService } from "./services/PaymentService.ts";
|
|
8
9
|
|
|
9
10
|
export * from "./controllers/AdminPaymentController.ts";
|
|
11
|
+
export * from "./controllers/MockCheckoutController.ts";
|
|
10
12
|
export * from "./controllers/PaymentController.ts";
|
|
11
13
|
export * from "./entities/paymentIntents.ts";
|
|
12
14
|
export * from "./entities/paymentMethods.ts";
|
|
@@ -67,6 +69,7 @@ export const AlephaApiPayments = $module({
|
|
|
67
69
|
services: [
|
|
68
70
|
AdminPaymentController,
|
|
69
71
|
PaymentController,
|
|
72
|
+
MockCheckoutController,
|
|
70
73
|
PaymentProvider,
|
|
71
74
|
PaymentService,
|
|
72
75
|
PaymentMethodService,
|
|
@@ -29,7 +29,12 @@ export class MemoryPaymentProvider implements PaymentProvider {
|
|
|
29
29
|
|
|
30
30
|
public async createSession(
|
|
31
31
|
intent: PaymentIntentEntity,
|
|
32
|
-
options: {
|
|
32
|
+
options: {
|
|
33
|
+
returnUrl: string;
|
|
34
|
+
authorize?: boolean;
|
|
35
|
+
stripeAccount?: string;
|
|
36
|
+
applicationFeeAmount?: number;
|
|
37
|
+
},
|
|
33
38
|
): Promise<CreateSessionResult> {
|
|
34
39
|
const providerRef = `mem_session_${randomUUID()}`;
|
|
35
40
|
const status = options.authorize ? "authorized" : "captured";
|
|
@@ -39,7 +44,7 @@ export class MemoryPaymentProvider implements PaymentProvider {
|
|
|
39
44
|
status,
|
|
40
45
|
});
|
|
41
46
|
return {
|
|
42
|
-
url: `/payments/mock-checkout/${intent.id}`,
|
|
47
|
+
url: `/payments/mock-checkout/${intent.id}?returnUrl=${encodeURIComponent(options.returnUrl)}`,
|
|
43
48
|
providerRef,
|
|
44
49
|
};
|
|
45
50
|
}
|
|
@@ -88,8 +93,8 @@ export class MemoryPaymentProvider implements PaymentProvider {
|
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
public async createPaymentMethod(
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
_userId: string,
|
|
97
|
+
_token: string,
|
|
93
98
|
): Promise<CreatePaymentMethodResult> {
|
|
94
99
|
const providerRef = `mem_pm_${randomUUID()}`;
|
|
95
100
|
const result: CreatePaymentMethodResult = {
|
|
@@ -31,7 +31,12 @@ export abstract class PaymentProvider {
|
|
|
31
31
|
*/
|
|
32
32
|
abstract createSession(
|
|
33
33
|
intent: PaymentIntentEntity,
|
|
34
|
-
options: {
|
|
34
|
+
options: {
|
|
35
|
+
returnUrl: string;
|
|
36
|
+
authorize?: boolean;
|
|
37
|
+
stripeAccount?: string;
|
|
38
|
+
applicationFeeAmount?: number;
|
|
39
|
+
},
|
|
35
40
|
): Promise<CreateSessionResult>;
|
|
36
41
|
|
|
37
42
|
/**
|
|
@@ -54,19 +59,29 @@ export abstract class PaymentProvider {
|
|
|
54
59
|
): Promise<RefundResult>;
|
|
55
60
|
|
|
56
61
|
/**
|
|
57
|
-
* Parse and verify an incoming PSP webhook request.
|
|
62
|
+
* Parse and verify the authenticity of an incoming PSP webhook request.
|
|
58
63
|
*
|
|
59
|
-
* Implementations MUST
|
|
60
|
-
*
|
|
61
|
-
* only authentication on the webhook endpoint (no $secure middleware).
|
|
64
|
+
* Implementations MUST establish authenticity before returning. Two
|
|
65
|
+
* common strategies, both acceptable:
|
|
62
66
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
67
|
+
* - Signature verification (Stripe, Adyen): verify an HMAC header
|
|
68
|
+
* against the raw body using a shared secret. Throw if invalid.
|
|
69
|
+
* - Re-fetch (Mollie, legacy webhooks): the body only carries an id;
|
|
70
|
+
* call back into the PSP API with your authenticated client to
|
|
71
|
+
* fetch the real resource state. The fetch itself is the auth —
|
|
72
|
+
* an attacker can POST a fake id but cannot influence the result.
|
|
73
|
+
*
|
|
74
|
+
* Throw an error if authenticity cannot be established. This endpoint
|
|
75
|
+
* has no $secure middleware; provider verification is its only auth.
|
|
65
76
|
*/
|
|
66
77
|
abstract parseWebhook(request: Request): Promise<WebhookEvent>;
|
|
67
78
|
|
|
68
79
|
/**
|
|
69
|
-
* Store a payment method token with the PSP
|
|
80
|
+
* Store a payment method token with the PSP and return the saved
|
|
81
|
+
* instrument's reference. Implementations that don't support
|
|
82
|
+
* tokenize-then-attach (e.g. Mollie, where mandates are created by
|
|
83
|
+
* making a "first" payment) MAY throw to signal the host app to use
|
|
84
|
+
* the checkout flow with `setup_future_usage`-style options instead.
|
|
70
85
|
*/
|
|
71
86
|
abstract createPaymentMethod(
|
|
72
87
|
userId: string,
|
|
@@ -74,7 +89,8 @@ export abstract class PaymentProvider {
|
|
|
74
89
|
): Promise<CreatePaymentMethodResult>;
|
|
75
90
|
|
|
76
91
|
/**
|
|
77
|
-
* Delete a stored payment method from the PSP.
|
|
92
|
+
* Delete a stored payment method from the PSP. Implementations that
|
|
93
|
+
* don't manage payment methods directly MAY no-op.
|
|
78
94
|
*/
|
|
79
95
|
abstract deletePaymentMethod(providerRef: string): Promise<void>;
|
|
80
96
|
|
|
@@ -79,6 +79,7 @@ export class PaymentService {
|
|
|
79
79
|
returnUrl: string,
|
|
80
80
|
authorize?: boolean,
|
|
81
81
|
userId?: string,
|
|
82
|
+
options?: { stripeAccount?: string; applicationFeeAmount?: number },
|
|
82
83
|
): Promise<{ url: string; intentId: string }> {
|
|
83
84
|
const intent = await this.getIntent(intentId);
|
|
84
85
|
this.assertStatus(intent, "created", "createSession");
|
|
@@ -96,6 +97,8 @@ export class PaymentService {
|
|
|
96
97
|
const result = await this.provider.createSession(intent, {
|
|
97
98
|
returnUrl,
|
|
98
99
|
authorize,
|
|
100
|
+
stripeAccount: options?.stripeAccount,
|
|
101
|
+
applicationFeeAmount: options?.applicationFeeAmount,
|
|
99
102
|
});
|
|
100
103
|
|
|
101
104
|
await this.intentRepo.updateById(intent.id, {
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { Alepha } from "alepha";
|
|
3
|
+
import { $repository } from "alepha/orm";
|
|
4
|
+
import { AlephaOrmPostgres } from "alepha/orm/postgres";
|
|
5
|
+
import { describe, it } from "vitest";
|
|
6
|
+
import { subscriptions } from "../entities/subscriptions.ts";
|
|
7
|
+
import { AlephaApiSubscriptions } from "../index.ts";
|
|
8
|
+
import type { PlanDefinition } from "../schemas/planDefinitionSchema.ts";
|
|
9
|
+
import { SubscriptionConfig } from "../services/SubscriptionConfig.ts";
|
|
10
|
+
import { SubscriptionService } from "../services/SubscriptionService.ts";
|
|
11
|
+
|
|
12
|
+
// -----------------------------------------------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
class TestSubscriptionConfig extends SubscriptionConfig {
|
|
15
|
+
public async seedPlans(plans: PlanDefinition[]): Promise<void> {
|
|
16
|
+
await this.plans.set({ plans });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Helper to directly update subscription records for test setup.
|
|
22
|
+
*/
|
|
23
|
+
class TestRepositories {
|
|
24
|
+
subscriptionRepo = $repository(subscriptions);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// -----------------------------------------------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
const testPlans: PlanDefinition[] = [
|
|
30
|
+
{
|
|
31
|
+
id: "free",
|
|
32
|
+
name: "Free",
|
|
33
|
+
available: true,
|
|
34
|
+
pricing: [{ interval: "monthly", amount: 0, currency: "usd" }],
|
|
35
|
+
features: ["dashboard"],
|
|
36
|
+
limits: { seats: 1, "api-calls": 100 },
|
|
37
|
+
order: 0,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "pro",
|
|
41
|
+
name: "Pro",
|
|
42
|
+
available: true,
|
|
43
|
+
pricing: [
|
|
44
|
+
{ interval: "monthly", amount: 2900, currency: "usd" },
|
|
45
|
+
{ interval: "yearly", amount: 29000, currency: "usd" },
|
|
46
|
+
],
|
|
47
|
+
trial: { days: 14, requirePaymentMethod: false },
|
|
48
|
+
features: ["dashboard", "analytics", "export"],
|
|
49
|
+
limits: { seats: 10, "api-calls": 10000 },
|
|
50
|
+
order: 1,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// -----------------------------------------------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
const setup = async () => {
|
|
57
|
+
const alepha = Alepha.create()
|
|
58
|
+
.with(AlephaOrmPostgres)
|
|
59
|
+
.with({ provide: SubscriptionConfig, use: TestSubscriptionConfig })
|
|
60
|
+
.with(AlephaApiSubscriptions);
|
|
61
|
+
|
|
62
|
+
const service = alepha.inject(SubscriptionService);
|
|
63
|
+
const config = alepha.inject(
|
|
64
|
+
TestSubscriptionConfig,
|
|
65
|
+
) as TestSubscriptionConfig;
|
|
66
|
+
const repos = alepha.inject(TestRepositories);
|
|
67
|
+
await alepha.start();
|
|
68
|
+
|
|
69
|
+
await config.seedPlans(testPlans);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Helper: create a subscription and attach a payment intent ID for billing lookup.
|
|
73
|
+
*/
|
|
74
|
+
const createSubscriptionWithIntent = async (
|
|
75
|
+
planId: string,
|
|
76
|
+
intentId: string,
|
|
77
|
+
options?: { skipTrial?: boolean },
|
|
78
|
+
) => {
|
|
79
|
+
const orgId = randomUUID();
|
|
80
|
+
const sub = await service.subscribe(orgId, planId, "monthly", options);
|
|
81
|
+
await repos.subscriptionRepo.updateById(sub.id, {
|
|
82
|
+
lastPaymentIntentId: intentId,
|
|
83
|
+
});
|
|
84
|
+
return service.getSubscription(sub.id);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return { alepha, service, config, repos, createSubscriptionWithIntent };
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// -----------------------------------------------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
describe("BillingService", () => {
|
|
93
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
describe("payment captured", () => {
|
|
96
|
+
it("should activate a trialing subscription after payment", async ({
|
|
97
|
+
expect,
|
|
98
|
+
}) => {
|
|
99
|
+
const { alepha, service, createSubscriptionWithIntent } = await setup();
|
|
100
|
+
|
|
101
|
+
const intentId = randomUUID();
|
|
102
|
+
const sub = await createSubscriptionWithIntent("pro", intentId);
|
|
103
|
+
expect(sub.status).toBe("trialing");
|
|
104
|
+
|
|
105
|
+
await alepha.events.emit("payments:captured", {
|
|
106
|
+
intentId,
|
|
107
|
+
amount: 2900,
|
|
108
|
+
currency: "usd",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const updated = await service.getSubscription(sub.id);
|
|
112
|
+
expect(updated.status).toBe("active");
|
|
113
|
+
expect(updated.lastPaymentIntentId).toBe(intentId);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should renew an active subscription and advance period", async ({
|
|
117
|
+
expect,
|
|
118
|
+
}) => {
|
|
119
|
+
const { alepha, service, createSubscriptionWithIntent } = await setup();
|
|
120
|
+
|
|
121
|
+
const intentId = randomUUID();
|
|
122
|
+
const sub = await createSubscriptionWithIntent("pro", intentId, {
|
|
123
|
+
skipTrial: true,
|
|
124
|
+
});
|
|
125
|
+
expect(sub.status).toBe("active");
|
|
126
|
+
|
|
127
|
+
const originalPeriodEnd = sub.currentPeriodEnd;
|
|
128
|
+
|
|
129
|
+
await alepha.events.emit("payments:captured", {
|
|
130
|
+
intentId,
|
|
131
|
+
amount: 2900,
|
|
132
|
+
currency: "usd",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const updated = await service.getSubscription(sub.id);
|
|
136
|
+
expect(updated.status).toBe("active");
|
|
137
|
+
expect(updated.currentPeriodStart).toBe(originalPeriodEnd);
|
|
138
|
+
expect(updated.currentPeriodEnd).not.toBe(originalPeriodEnd);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should recover from dunning", async ({ expect }) => {
|
|
142
|
+
const { alepha, service, repos, createSubscriptionWithIntent } =
|
|
143
|
+
await setup();
|
|
144
|
+
|
|
145
|
+
const intentId = randomUUID();
|
|
146
|
+
const sub = await createSubscriptionWithIntent("pro", intentId, {
|
|
147
|
+
skipTrial: true,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await repos.subscriptionRepo.updateById(sub.id, {
|
|
151
|
+
status: "past_due",
|
|
152
|
+
dunningAttempt: 2,
|
|
153
|
+
dunningStartedAt: new Date().toISOString(),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await alepha.events.emit("payments:captured", {
|
|
157
|
+
intentId,
|
|
158
|
+
amount: 2900,
|
|
159
|
+
currency: "usd",
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const updated = await service.getSubscription(sub.id);
|
|
163
|
+
expect(updated.status).toBe("active");
|
|
164
|
+
expect(updated.dunningAttempt).toBe(0);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
describe("payment failed", () => {
|
|
171
|
+
it("should start dunning on first failure", async ({ expect }) => {
|
|
172
|
+
const { alepha, service, createSubscriptionWithIntent } = await setup();
|
|
173
|
+
|
|
174
|
+
const intentId = randomUUID();
|
|
175
|
+
const sub = await createSubscriptionWithIntent("pro", intentId, {
|
|
176
|
+
skipTrial: true,
|
|
177
|
+
});
|
|
178
|
+
expect(sub.status).toBe("active");
|
|
179
|
+
|
|
180
|
+
await alepha.events.emit("payments:failed", {
|
|
181
|
+
intentId,
|
|
182
|
+
amount: 2900,
|
|
183
|
+
currency: "usd",
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const updated = await service.getSubscription(sub.id);
|
|
187
|
+
expect(updated.status).toBe("past_due");
|
|
188
|
+
expect(updated.dunningAttempt).toBe(1);
|
|
189
|
+
expect(updated.dunningStartedAt).toBeDefined();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should increment dunning on subsequent failure", async ({ expect }) => {
|
|
193
|
+
const { alepha, service, repos, createSubscriptionWithIntent } =
|
|
194
|
+
await setup();
|
|
195
|
+
|
|
196
|
+
const intentId = randomUUID();
|
|
197
|
+
const sub = await createSubscriptionWithIntent("pro", intentId, {
|
|
198
|
+
skipTrial: true,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await repos.subscriptionRepo.updateById(sub.id, {
|
|
202
|
+
status: "past_due",
|
|
203
|
+
dunningAttempt: 1,
|
|
204
|
+
dunningStartedAt: new Date().toISOString(),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await alepha.events.emit("payments:failed", {
|
|
208
|
+
intentId,
|
|
209
|
+
amount: 2900,
|
|
210
|
+
currency: "usd",
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const updated = await service.getSubscription(sub.id);
|
|
214
|
+
expect(updated.status).toBe("past_due");
|
|
215
|
+
expect(updated.dunningAttempt).toBe(2);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|