@cosmicdrift/kumiko-bundled-features 0.1.0
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/package.json +90 -0
- package/src/audit/__tests__/audit.integration.ts +328 -0
- package/src/audit/constants.ts +7 -0
- package/src/audit/feature.ts +23 -0
- package/src/audit/handlers/list.query.ts +98 -0
- package/src/audit/index.ts +2 -0
- package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.ts +149 -0
- package/src/auth-email-password/__tests__/account-lockout.integration.ts +308 -0
- package/src/auth-email-password/__tests__/auth-claims.integration.ts +512 -0
- package/src/auth-email-password/__tests__/auth.integration.ts +610 -0
- package/src/auth-email-password/__tests__/confirm-token-flow.test.ts +67 -0
- package/src/auth-email-password/__tests__/email-templates.test.ts +106 -0
- package/src/auth-email-password/__tests__/email-verification.integration.ts +327 -0
- package/src/auth-email-password/__tests__/identity-v3-hash.test.ts +174 -0
- package/src/auth-email-password/__tests__/identity-v3-login.integration.ts +150 -0
- package/src/auth-email-password/__tests__/invite-flow.integration.ts +458 -0
- package/src/auth-email-password/__tests__/multi-roles.integration.ts +256 -0
- package/src/auth-email-password/__tests__/password-reset.integration.ts +346 -0
- package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.ts +144 -0
- package/src/auth-email-password/__tests__/seed-admin.integration.ts +176 -0
- package/src/auth-email-password/__tests__/session-callbacks.integration.ts +310 -0
- package/src/auth-email-password/__tests__/session-strict-mode.integration.ts +101 -0
- package/src/auth-email-password/__tests__/signed-token.test.ts +78 -0
- package/src/auth-email-password/__tests__/signup-flow.integration.ts +259 -0
- package/src/auth-email-password/auth-user-row.ts +41 -0
- package/src/auth-email-password/constants.ts +101 -0
- package/src/auth-email-password/email-templates.ts +283 -0
- package/src/auth-email-password/feature.ts +140 -0
- package/src/auth-email-password/handlers/change-password.write.ts +58 -0
- package/src/auth-email-password/handlers/confirm-token-flow.ts +191 -0
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +203 -0
- package/src/auth-email-password/handlers/invite-accept.write.ts +189 -0
- package/src/auth-email-password/handlers/invite-create.write.ts +145 -0
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +192 -0
- package/src/auth-email-password/handlers/login.write.ts +208 -0
- package/src/auth-email-password/handlers/logout.write.ts +12 -0
- package/src/auth-email-password/handlers/request-email-verification.write.ts +29 -0
- package/src/auth-email-password/handlers/request-password-reset.write.ts +31 -0
- package/src/auth-email-password/handlers/reset-password.write.ts +61 -0
- package/src/auth-email-password/handlers/signup-confirm.write.ts +170 -0
- package/src/auth-email-password/handlers/signup-request.write.ts +104 -0
- package/src/auth-email-password/handlers/token-request-handler.ts +114 -0
- package/src/auth-email-password/handlers/verify-email.write.ts +62 -0
- package/src/auth-email-password/i18n.ts +211 -0
- package/src/auth-email-password/identity-v3-hash.ts +97 -0
- package/src/auth-email-password/index.ts +35 -0
- package/src/auth-email-password/invite-token-store.ts +92 -0
- package/src/auth-email-password/lockout-store.ts +118 -0
- package/src/auth-email-password/password-hashing.ts +43 -0
- package/src/auth-email-password/reset-token.ts +28 -0
- package/src/auth-email-password/seeding.ts +183 -0
- package/src/auth-email-password/signed-token.ts +85 -0
- package/src/auth-email-password/signup-token-store.ts +104 -0
- package/src/auth-email-password/stream-tenant.ts +31 -0
- package/src/auth-email-password/testing.ts +5 -0
- package/src/auth-email-password/token-burn-store.ts +57 -0
- package/src/auth-email-password/verification-token.ts +27 -0
- package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +51 -0
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +80 -0
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +94 -0
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +108 -0
- package/src/auth-email-password/web/__tests__/session-roles.test.ts +54 -0
- package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +100 -0
- package/src/auth-email-password/web/__tests__/test-utils.tsx +73 -0
- package/src/auth-email-password/web/__tests__/user-menu.test.tsx +55 -0
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +59 -0
- package/src/auth-email-password/web/auth-client.ts +350 -0
- package/src/auth-email-password/web/auth-form-primitives.tsx +70 -0
- package/src/auth-email-password/web/auth-gate.tsx +33 -0
- package/src/auth-email-password/web/client-plugin.ts +48 -0
- package/src/auth-email-password/web/default-topbar-actions.tsx +47 -0
- package/src/auth-email-password/web/forgot-password-screen.tsx +110 -0
- package/src/auth-email-password/web/index.ts +56 -0
- package/src/auth-email-password/web/invite-accept-screen.tsx +220 -0
- package/src/auth-email-password/web/login-screen.tsx +150 -0
- package/src/auth-email-password/web/reset-password-screen.tsx +152 -0
- package/src/auth-email-password/web/session.tsx +171 -0
- package/src/auth-email-password/web/signup-complete-screen.tsx +150 -0
- package/src/auth-email-password/web/signup-screen.tsx +130 -0
- package/src/auth-email-password/web/tenant-switcher.tsx +116 -0
- package/src/auth-email-password/web/use-shell-user.ts +34 -0
- package/src/auth-email-password/web/user-menu.tsx +89 -0
- package/src/auth-email-password/web/verify-email-screen.tsx +102 -0
- package/src/billing-foundation/__tests__/billing-foundation.integration.ts +568 -0
- package/src/billing-foundation/__tests__/feature.test.ts +110 -0
- package/src/billing-foundation/__tests__/webhook-handler.test.ts +199 -0
- package/src/billing-foundation/aggregate-id.ts +21 -0
- package/src/billing-foundation/constants.ts +70 -0
- package/src/billing-foundation/entities.ts +50 -0
- package/src/billing-foundation/events.ts +71 -0
- package/src/billing-foundation/feature.ts +122 -0
- package/src/billing-foundation/get-subscription-for-tenant.ts +39 -0
- package/src/billing-foundation/handlers/create-checkout-session.write.ts +79 -0
- package/src/billing-foundation/handlers/create-portal-session.write.ts +73 -0
- package/src/billing-foundation/handlers/list-subscriptions.query.ts +20 -0
- package/src/billing-foundation/handlers/process-event.write.ts +160 -0
- package/src/billing-foundation/index.ts +42 -0
- package/src/billing-foundation/projection.ts +135 -0
- package/src/billing-foundation/types.ts +157 -0
- package/src/billing-foundation/webhook-handler.ts +184 -0
- package/src/cap-counter/__tests__/cap-counter.integration.ts +566 -0
- package/src/cap-counter/__tests__/enforce-cap.test.ts +422 -0
- package/src/cap-counter/__tests__/with-cap-enforcement.integration.ts +265 -0
- package/src/cap-counter/aggregate-id.ts +61 -0
- package/src/cap-counter/constants.ts +32 -0
- package/src/cap-counter/enforce-cap.ts +404 -0
- package/src/cap-counter/entity.ts +48 -0
- package/src/cap-counter/feature.ts +90 -0
- package/src/cap-counter/handlers/get-counter.query.ts +43 -0
- package/src/cap-counter/handlers/increment-rolling.write.ts +79 -0
- package/src/cap-counter/handlers/increment.write.ts +92 -0
- package/src/cap-counter/handlers/mark-soft-warned.write.ts +57 -0
- package/src/cap-counter/index.ts +34 -0
- package/src/cap-counter/with-cap-enforcement.ts +179 -0
- package/src/channel-email/email-channel.ts +48 -0
- package/src/channel-email/feature.ts +15 -0
- package/src/channel-email/index.ts +4 -0
- package/src/channel-email/smtp-transport.ts +65 -0
- package/src/channel-email/types.ts +34 -0
- package/src/channel-in-app/constants.ts +11 -0
- package/src/channel-in-app/feature.ts +30 -0
- package/src/channel-in-app/handlers/inbox.query.ts +28 -0
- package/src/channel-in-app/handlers/mark-all-read.write.ts +21 -0
- package/src/channel-in-app/handlers/mark-read.write.ts +32 -0
- package/src/channel-in-app/handlers/unread-count.query.ts +20 -0
- package/src/channel-in-app/in-app-channel.ts +44 -0
- package/src/channel-in-app/index.ts +4 -0
- package/src/channel-in-app/tables.ts +22 -0
- package/src/channel-push/feature.ts +15 -0
- package/src/channel-push/index.ts +3 -0
- package/src/channel-push/push-channel.ts +33 -0
- package/src/channel-push/types.ts +22 -0
- package/src/config/__tests__/app-overrides.test.ts +118 -0
- package/src/config/__tests__/config.integration.ts +1246 -0
- package/src/config/constants.ts +23 -0
- package/src/config/feature.ts +117 -0
- package/src/config/handlers/__tests__/prepare-config-write.test.ts +209 -0
- package/src/config/handlers/reset.write.ts +45 -0
- package/src/config/handlers/schema.query.ts +22 -0
- package/src/config/handlers/set.write.ts +93 -0
- package/src/config/handlers/values.query.ts +43 -0
- package/src/config/index.ts +15 -0
- package/src/config/resolver.ts +283 -0
- package/src/config/table.ts +35 -0
- package/src/config/write-helpers.ts +268 -0
- package/src/delivery/__tests__/delivery-events.integration.ts +166 -0
- package/src/delivery/__tests__/delivery.integration.ts +1405 -0
- package/src/delivery/constants.ts +33 -0
- package/src/delivery/delivery-service.ts +489 -0
- package/src/delivery/events.ts +18 -0
- package/src/delivery/feature.ts +70 -0
- package/src/delivery/handlers/log.query.ts +21 -0
- package/src/delivery/handlers/preferences.query.ts +18 -0
- package/src/delivery/handlers/set-preference.write.ts +28 -0
- package/src/delivery/index.ts +35 -0
- package/src/delivery/tables.ts +74 -0
- package/src/delivery/testing.ts +47 -0
- package/src/delivery/types.ts +71 -0
- package/src/delivery/unsubscribe.ts +99 -0
- package/src/delivery/upsert-preference.ts +145 -0
- package/src/feature-toggles/__tests__/feature-toggles.integration.ts +687 -0
- package/src/feature-toggles/constants.ts +20 -0
- package/src/feature-toggles/events.ts +18 -0
- package/src/feature-toggles/feature.ts +98 -0
- package/src/feature-toggles/global-feature-state-table.ts +28 -0
- package/src/feature-toggles/handlers/list.query.ts +26 -0
- package/src/feature-toggles/handlers/registered.query.ts +56 -0
- package/src/feature-toggles/handlers/set.write.ts +158 -0
- package/src/feature-toggles/index.ts +9 -0
- package/src/feature-toggles/toggle-runtime.ts +73 -0
- package/src/file-foundation/__tests__/feature.test.ts +35 -0
- package/src/file-foundation/__tests__/file-foundation.integration.ts +235 -0
- package/src/file-foundation/feature.ts +123 -0
- package/src/file-foundation/index.ts +7 -0
- package/src/file-provider-inmemory/__tests__/feature.test.ts +35 -0
- package/src/file-provider-inmemory/feature.ts +73 -0
- package/src/file-provider-inmemory/index.ts +3 -0
- package/src/file-provider-s3/__tests__/feature.test.ts +54 -0
- package/src/file-provider-s3/feature.ts +169 -0
- package/src/file-provider-s3/index.ts +3 -0
- package/src/files-provider-s3/__tests__/env-helper.test.ts +161 -0
- package/src/files-provider-s3/__tests__/s3-provider.integration.ts +134 -0
- package/src/files-provider-s3/__tests__/s3-provider.test.ts +36 -0
- package/src/files-provider-s3/env-helper.ts +49 -0
- package/src/files-provider-s3/index.ts +3 -0
- package/src/files-provider-s3/s3-provider.ts +114 -0
- package/src/foundation-shared/config-helpers.ts +67 -0
- package/src/foundation-shared/index.ts +4 -0
- package/src/jobs/__tests__/job-system-user.integration.ts +194 -0
- package/src/jobs/__tests__/jobs-events.integration.ts +143 -0
- package/src/jobs/__tests__/jobs-feature.integration.ts +342 -0
- package/src/jobs/constants.ts +21 -0
- package/src/jobs/events.ts +39 -0
- package/src/jobs/feature.ts +150 -0
- package/src/jobs/handlers/detail.query.ts +30 -0
- package/src/jobs/handlers/list.query.ts +36 -0
- package/src/jobs/handlers/retry.write.ts +69 -0
- package/src/jobs/handlers/trigger.write.ts +39 -0
- package/src/jobs/index.ts +5 -0
- package/src/jobs/job-run-logger.ts +213 -0
- package/src/jobs/job-run-table.ts +55 -0
- package/src/legal-pages/README.md +195 -0
- package/src/legal-pages/__tests__/legal-pages.integration.ts +361 -0
- package/src/legal-pages/constants.ts +36 -0
- package/src/legal-pages/feature.ts +187 -0
- package/src/legal-pages/index.ts +13 -0
- package/src/legal-pages/markdown.ts +69 -0
- package/src/mail-foundation/__tests__/feature.test.ts +46 -0
- package/src/mail-foundation/__tests__/mail-foundation.integration.ts +247 -0
- package/src/mail-foundation/feature.ts +160 -0
- package/src/mail-foundation/index.ts +14 -0
- package/src/mail-transport-inmemory/__tests__/feature.test.ts +37 -0
- package/src/mail-transport-inmemory/feature.ts +90 -0
- package/src/mail-transport-inmemory/index.ts +3 -0
- package/src/mail-transport-smtp/__tests__/feature.test.ts +61 -0
- package/src/mail-transport-smtp/feature.ts +182 -0
- package/src/mail-transport-smtp/index.ts +3 -0
- package/src/rate-limiting/__tests__/rate-limiting.integration.ts +84 -0
- package/src/rate-limiting/constants.ts +9 -0
- package/src/rate-limiting/feature.ts +16 -0
- package/src/rate-limiting/handlers/status.query.ts +52 -0
- package/src/rate-limiting/index.ts +2 -0
- package/src/renderer-simple/__tests__/simple-renderer.test.ts +97 -0
- package/src/renderer-simple/feature.ts +12 -0
- package/src/renderer-simple/index.ts +2 -0
- package/src/renderer-simple/simple-renderer.ts +72 -0
- package/src/secrets/__tests__/rotate.integration.ts +176 -0
- package/src/secrets/__tests__/secrets-events.integration.ts +125 -0
- package/src/secrets/__tests__/secrets.integration.ts +118 -0
- package/src/secrets/feature.ts +84 -0
- package/src/secrets/handlers/delete.write.ts +20 -0
- package/src/secrets/handlers/list.query.ts +38 -0
- package/src/secrets/handlers/rotate.job.ts +193 -0
- package/src/secrets/handlers/set.write.ts +50 -0
- package/src/secrets/index.ts +16 -0
- package/src/secrets/secrets-context.ts +296 -0
- package/src/secrets/table.ts +68 -0
- package/src/sessions/__tests__/cleanup.integration.ts +175 -0
- package/src/sessions/__tests__/password-auto-revoke.integration.ts +202 -0
- package/src/sessions/__tests__/sessions.integration.ts +472 -0
- package/src/sessions/__tests__/test-helpers.ts +66 -0
- package/src/sessions/constants.ts +43 -0
- package/src/sessions/feature.ts +84 -0
- package/src/sessions/handlers/cleanup.job.ts +109 -0
- package/src/sessions/handlers/list.query.ts +35 -0
- package/src/sessions/handlers/mine.query.ts +37 -0
- package/src/sessions/handlers/revoke-all-others.write.ts +42 -0
- package/src/sessions/handlers/revoke.write.ts +76 -0
- package/src/sessions/index.ts +17 -0
- package/src/sessions/schema/index.ts +5 -0
- package/src/sessions/schema/user-session.ts +67 -0
- package/src/sessions/session-callbacks.ts +110 -0
- package/src/sessions/testing.ts +42 -0
- package/src/subscription-mollie/__tests__/feature.test.ts +106 -0
- package/src/subscription-mollie/__tests__/mollie-foundation.integration.ts +421 -0
- package/src/subscription-mollie/__tests__/verify-webhook.test.ts +388 -0
- package/src/subscription-mollie/constants.ts +33 -0
- package/src/subscription-mollie/feature.ts +144 -0
- package/src/subscription-mollie/index.ts +13 -0
- package/src/subscription-mollie/plugin-methods.ts +79 -0
- package/src/subscription-mollie/verify-webhook.ts +244 -0
- package/src/subscription-stripe/__tests__/feature.test.ts +98 -0
- package/src/subscription-stripe/__tests__/plugin-methods.test.ts +161 -0
- package/src/subscription-stripe/__tests__/stripe-foundation.integration.ts +315 -0
- package/src/subscription-stripe/__tests__/verify-webhook.test.ts +306 -0
- package/src/subscription-stripe/constants.ts +20 -0
- package/src/subscription-stripe/feature.ts +120 -0
- package/src/subscription-stripe/index.ts +14 -0
- package/src/subscription-stripe/plugin-methods.ts +91 -0
- package/src/subscription-stripe/verify-webhook.ts +235 -0
- package/src/tenant/__tests__/multi-tenant.integration.ts +278 -0
- package/src/tenant/__tests__/seed-testing.integration.ts +229 -0
- package/src/tenant/__tests__/tenant.integration.ts +347 -0
- package/src/tenant/command-schemas.ts +37 -0
- package/src/tenant/constants.ts +37 -0
- package/src/tenant/feature.ts +109 -0
- package/src/tenant/handlers/active-tenant-ids.query.ts +19 -0
- package/src/tenant/handlers/add-member.write.ts +53 -0
- package/src/tenant/handlers/cancel-invitation.write.ts +87 -0
- package/src/tenant/handlers/create.write.ts +21 -0
- package/src/tenant/handlers/disable.write.ts +18 -0
- package/src/tenant/handlers/invitations.query.ts +31 -0
- package/src/tenant/handlers/list.query.ts +17 -0
- package/src/tenant/handlers/me.query.ts +17 -0
- package/src/tenant/handlers/members.query.ts +22 -0
- package/src/tenant/handlers/memberships.query.ts +24 -0
- package/src/tenant/handlers/remove-member.write.ts +40 -0
- package/src/tenant/handlers/resolve-user-ids.query.ts +43 -0
- package/src/tenant/handlers/update-member-roles.write.ts +54 -0
- package/src/tenant/handlers/update.write.ts +20 -0
- package/src/tenant/index.ts +12 -0
- package/src/tenant/invitation-table.ts +93 -0
- package/src/tenant/membership-table.ts +35 -0
- package/src/tenant/schema/index.ts +5 -0
- package/src/tenant/schema/tenant.ts +27 -0
- package/src/tenant/seeding.ts +155 -0
- package/src/tenant/testing.ts +8 -0
- package/src/text-content/README.md +190 -0
- package/src/text-content/__tests__/text-content.integration.ts +415 -0
- package/src/text-content/api.ts +92 -0
- package/src/text-content/constants.ts +19 -0
- package/src/text-content/feature.ts +29 -0
- package/src/text-content/handlers/by-slug.query.ts +55 -0
- package/src/text-content/handlers/set.write.ts +118 -0
- package/src/text-content/index.ts +14 -0
- package/src/text-content/seeding.ts +91 -0
- package/src/text-content/table.ts +45 -0
- package/src/tier-engine/__tests__/compose-app.test.ts +182 -0
- package/src/tier-engine/__tests__/drift.test.ts +42 -0
- package/src/tier-engine/__tests__/tier-engine.integration.ts +241 -0
- package/src/tier-engine/aggregate-id.ts +27 -0
- package/src/tier-engine/compose-app.ts +150 -0
- package/src/tier-engine/constants.ts +15 -0
- package/src/tier-engine/entity.ts +30 -0
- package/src/tier-engine/feature.ts +72 -0
- package/src/tier-engine/handlers/active-tier.query.ts +23 -0
- package/src/tier-engine/index.ts +22 -0
- package/src/user/__tests__/seed-testing.integration.ts +127 -0
- package/src/user/__tests__/user.integration.ts +198 -0
- package/src/user/command-schemas.ts +15 -0
- package/src/user/constants.ts +23 -0
- package/src/user/feature.ts +32 -0
- package/src/user/handlers/create.write.ts +54 -0
- package/src/user/handlers/detail.query.ts +9 -0
- package/src/user/handlers/find-for-auth.query.ts +38 -0
- package/src/user/handlers/list.query.ts +8 -0
- package/src/user/handlers/me.query.ts +15 -0
- package/src/user/handlers/update.write.ts +54 -0
- package/src/user/index.ts +4 -0
- package/src/user/schema/index.ts +5 -0
- package/src/user/schema/user.ts +69 -0
- package/src/user/seeding.ts +93 -0
- package/src/user/testing.ts +5 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// verifyAndParseMollieWebhook — Mollie-spezifische lazy-fetch +
|
|
2
|
+
// event-type-Heuristik. Wird vom Plugin-Build (feature.ts) als
|
|
3
|
+
// `verifyAndParseWebhook` registriert.
|
|
4
|
+
//
|
|
5
|
+
// Mollie's classic-webhook sendet nur eine `id` (form-urlencoded oder
|
|
6
|
+
// JSON). Wir lazy-fetchen payment + subscription via Mollie-API. Sub-
|
|
7
|
+
// xxx-events werden NICHT supported — App-Builder bekommt sie indirekt
|
|
8
|
+
// via tr_xxx-payment-events (Mollie sendet beide parallel bei normalen
|
|
9
|
+
// Lifecycle-Events).
|
|
10
|
+
//
|
|
11
|
+
// **Mandate-setup-flow:** first-payment-paid kommt mit
|
|
12
|
+
// `payment.subscriptionId === null` (Mollie's Pattern: App-Builder
|
|
13
|
+
// muss `customerSubscriptions.create` selbst aufrufen). Wir machen
|
|
14
|
+
// das im Plugin (idempotent via list-check), damit Foundation einen
|
|
15
|
+
// Created-Event mit subscription-id bekommt.
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
type SubscriptionEvent,
|
|
19
|
+
type SubscriptionEventType,
|
|
20
|
+
SubscriptionEventTypes,
|
|
21
|
+
type SubscriptionStatus,
|
|
22
|
+
SubscriptionStatuses,
|
|
23
|
+
} from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
24
|
+
import type {
|
|
25
|
+
Payment as MolliePayment,
|
|
26
|
+
Subscription as MollieSubscription,
|
|
27
|
+
} from "@mollie/api-client";
|
|
28
|
+
import { MOLLIE_PROVIDER_NAME } from "./constants";
|
|
29
|
+
import type { MolliePriceConfig } from "./plugin-methods";
|
|
30
|
+
|
|
31
|
+
/** Minimal-Subset des Mollie-Clients, das der Plugin nutzt — separat
|
|
32
|
+
* damit Tests ohne den vollen MollieClient mocken können. Adapter in
|
|
33
|
+
* feature.ts bridged das gegen den echten SDK. */
|
|
34
|
+
export type MollieClientShape = {
|
|
35
|
+
readonly payments: { readonly get: (id: string) => Promise<MolliePayment> };
|
|
36
|
+
readonly customerSubscriptions: {
|
|
37
|
+
readonly get: (subId: string, customerId: string) => Promise<MollieSubscription>;
|
|
38
|
+
readonly list: (customerId: string) => Promise<readonly MollieSubscription[]>;
|
|
39
|
+
readonly create: (
|
|
40
|
+
customerId: string,
|
|
41
|
+
params: {
|
|
42
|
+
amount: { currency: string; value: string };
|
|
43
|
+
interval: string;
|
|
44
|
+
description: string;
|
|
45
|
+
metadata: Record<string, string>;
|
|
46
|
+
},
|
|
47
|
+
) => Promise<MollieSubscription>;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type MollieWebhookOptions = {
|
|
52
|
+
/** priceId (= virtueller Schlüssel aus subscription.metadata) → tier-name. */
|
|
53
|
+
readonly priceToTier: Readonly<Record<string, string>>;
|
|
54
|
+
/** priceId → amount/interval/description. Wird beim mandate-setup-
|
|
55
|
+
* flow zum `customerSubscriptions.create`-Call gebraucht. App-
|
|
56
|
+
* Builder pflegt die Map einmal in den factory-options. */
|
|
57
|
+
readonly priceToConfig: Readonly<Record<string, MolliePriceConfig>>;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export function verifyAndParseMollieWebhook(
|
|
61
|
+
client: MollieClientShape,
|
|
62
|
+
options: MollieWebhookOptions,
|
|
63
|
+
): (rawBody: string, headers: Record<string, string>) => Promise<SubscriptionEvent | null> {
|
|
64
|
+
return async (rawBody, headers) => {
|
|
65
|
+
const id = extractMollieId(rawBody, headers);
|
|
66
|
+
if (!id) {
|
|
67
|
+
throw new Error("subscription-mollie: webhook body has no `id` field");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let subscription: MollieSubscription | null = null;
|
|
71
|
+
let triggerPayment: MolliePayment | null = null;
|
|
72
|
+
|
|
73
|
+
if (id.startsWith("tr_")) {
|
|
74
|
+
let payment: MolliePayment;
|
|
75
|
+
try {
|
|
76
|
+
payment = await client.payments.get(id);
|
|
77
|
+
} catch {
|
|
78
|
+
// Garbage-id → Mollie 404 → Foundation 200 ignored.
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
triggerPayment = payment;
|
|
82
|
+
const customerId = payment.customerId;
|
|
83
|
+
if (!customerId) return null;
|
|
84
|
+
|
|
85
|
+
if (payment.subscriptionId) {
|
|
86
|
+
try {
|
|
87
|
+
subscription = await client.customerSubscriptions.get(payment.subscriptionId, customerId);
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
} else if (payment.sequenceType === "first" && payment.status === "paid") {
|
|
92
|
+
subscription = await ensureSubscriptionForMandate(client, options, payment);
|
|
93
|
+
if (!subscription) return null;
|
|
94
|
+
} else {
|
|
95
|
+
// One-shot oder first-payment-failed → nicht unsere Domain.
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
} else if (id.startsWith("sub_")) {
|
|
99
|
+
// sub_xxx-events kommen indirekt via parallele tr_xxx-events.
|
|
100
|
+
return null;
|
|
101
|
+
} else {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const metadata = (subscription.metadata as Record<string, string> | null) ?? {};
|
|
106
|
+
const tenantId = metadata["tenantId"];
|
|
107
|
+
if (!tenantId || tenantId.length === 0) return null;
|
|
108
|
+
const priceId = metadata["priceId"];
|
|
109
|
+
if (!priceId) return null;
|
|
110
|
+
const tier = options.priceToTier[priceId];
|
|
111
|
+
if (!tier) return null;
|
|
112
|
+
|
|
113
|
+
const type = mapMollieEventType(subscription, triggerPayment);
|
|
114
|
+
if (!type) return null;
|
|
115
|
+
|
|
116
|
+
const status = mapMollieStatus(subscription.status);
|
|
117
|
+
const periodEndSource = subscription.nextPaymentDate ?? subscription.startDate;
|
|
118
|
+
if (!periodEndSource) {
|
|
119
|
+
// Mollie-API-Drift: valid Subs haben mindestens startDate. Loud-
|
|
120
|
+
// fail damit App-Owner's monitoring den drift sieht.
|
|
121
|
+
throw new Error(
|
|
122
|
+
`subscription-mollie: subscription ${subscription.id} has neither nextPaymentDate nor startDate`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const currentPeriodEnd = mollieDateStringToInstantIso(periodEndSource);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
providerEventId: id,
|
|
129
|
+
providerName: MOLLIE_PROVIDER_NAME,
|
|
130
|
+
type,
|
|
131
|
+
tenantId,
|
|
132
|
+
providerCustomerId: subscription.customerId,
|
|
133
|
+
providerSubscriptionId: subscription.id,
|
|
134
|
+
status,
|
|
135
|
+
tier,
|
|
136
|
+
currentPeriodEnd,
|
|
137
|
+
rawPayload: JSON.stringify({ webhookId: id, subscription, triggerPayment }),
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// =============================================================================
|
|
143
|
+
// Mandate-setup: subscription on-the-fly erstellen
|
|
144
|
+
// =============================================================================
|
|
145
|
+
|
|
146
|
+
/** first-payment-paid OHNE subscriptionId → Mollie-Sub erstellen.
|
|
147
|
+
* Idempotent via list-check (replay-safe). Returns null bei
|
|
148
|
+
* unvollständiger metadata oder unbekanntem priceId. */
|
|
149
|
+
async function ensureSubscriptionForMandate(
|
|
150
|
+
client: MollieClientShape,
|
|
151
|
+
options: MollieWebhookOptions,
|
|
152
|
+
payment: MolliePayment,
|
|
153
|
+
): Promise<MollieSubscription | null> {
|
|
154
|
+
const customerId = payment.customerId;
|
|
155
|
+
if (!customerId) return null;
|
|
156
|
+
const paymentMetadata = (payment.metadata as Record<string, string> | null) ?? {};
|
|
157
|
+
const tenantId = paymentMetadata["tenantId"];
|
|
158
|
+
const priceId = paymentMetadata["priceId"];
|
|
159
|
+
if (!tenantId || !priceId) return null;
|
|
160
|
+
const priceCfg = options.priceToConfig[priceId];
|
|
161
|
+
if (!priceCfg) return null;
|
|
162
|
+
|
|
163
|
+
const existing = await client.customerSubscriptions.list(customerId);
|
|
164
|
+
const matchingExisting = existing.find(
|
|
165
|
+
(sub) =>
|
|
166
|
+
(sub.metadata as Record<string, string> | null)?.["priceId"] === priceId &&
|
|
167
|
+
(sub.status === "active" || sub.status === "pending"),
|
|
168
|
+
);
|
|
169
|
+
if (matchingExisting) return matchingExisting;
|
|
170
|
+
|
|
171
|
+
return await client.customerSubscriptions.create(customerId, {
|
|
172
|
+
amount: { currency: priceCfg.amountCurrency, value: priceCfg.amountValue },
|
|
173
|
+
interval: priceCfg.interval,
|
|
174
|
+
description: priceCfg.description,
|
|
175
|
+
metadata: { tenantId, priceId },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// =============================================================================
|
|
180
|
+
// Helpers (exported für Tests)
|
|
181
|
+
// =============================================================================
|
|
182
|
+
|
|
183
|
+
/** Extract `id` aus rawBody — handelt form-urlencoded + JSON. */
|
|
184
|
+
export function extractMollieId(rawBody: string, headers: Record<string, string>): string | null {
|
|
185
|
+
const contentType = headers["content-type"] ?? "";
|
|
186
|
+
if (contentType.includes("application/json")) {
|
|
187
|
+
try {
|
|
188
|
+
const parsed = JSON.parse(rawBody) as { id?: unknown };
|
|
189
|
+
return typeof parsed.id === "string" ? parsed.id : null;
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const params = new URLSearchParams(rawBody);
|
|
195
|
+
return params.get("id");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Mollie-subscription + optional payment → SubscriptionEventType.
|
|
199
|
+
* Heuristik (Mollie hat keine explicit-typed events). */
|
|
200
|
+
export function mapMollieEventType(
|
|
201
|
+
subscription: MollieSubscription,
|
|
202
|
+
triggerPayment: MolliePayment | null,
|
|
203
|
+
): SubscriptionEventType | null {
|
|
204
|
+
if (subscription.status === "canceled" || subscription.status === "completed") {
|
|
205
|
+
return SubscriptionEventTypes.canceled;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (triggerPayment) {
|
|
209
|
+
const seq = triggerPayment.sequenceType;
|
|
210
|
+
const paid = triggerPayment.status === "paid";
|
|
211
|
+
const failed = triggerPayment.status === "failed" || triggerPayment.status === "expired";
|
|
212
|
+
if (seq === "first" && paid) return SubscriptionEventTypes.created;
|
|
213
|
+
if (seq === "recurring" && paid) return SubscriptionEventTypes.invoicePaid;
|
|
214
|
+
if (seq === "recurring" && failed) return SubscriptionEventTypes.invoicePaymentFailed;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (subscription.status === "active") return SubscriptionEventTypes.updated;
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Mollie-status → normalized. */
|
|
222
|
+
export function mapMollieStatus(mollieStatus: MollieSubscription["status"]): SubscriptionStatus {
|
|
223
|
+
switch (mollieStatus) {
|
|
224
|
+
case "active":
|
|
225
|
+
return SubscriptionStatuses.active;
|
|
226
|
+
case "canceled":
|
|
227
|
+
case "completed":
|
|
228
|
+
// completed = alle `times`-charges durchgelaufen → wie canceled.
|
|
229
|
+
return SubscriptionStatuses.canceled;
|
|
230
|
+
case "suspended":
|
|
231
|
+
// Mandate ungültig / payment-method failed → grace-period.
|
|
232
|
+
return SubscriptionStatuses.pastDue;
|
|
233
|
+
case "pending":
|
|
234
|
+
return SubscriptionStatuses.incomplete;
|
|
235
|
+
default:
|
|
236
|
+
return SubscriptionStatuses.incomplete;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Mollie liefert dates als YYYY-MM-DD; foundation will ISO-Instant.
|
|
241
|
+
* Throws bei malformed input (= Mollie-API-Drift, soll loud-fail). */
|
|
242
|
+
function mollieDateStringToInstantIso(dateString: string): string {
|
|
243
|
+
return Temporal.Instant.from(`${dateString.slice(0, 10)}T00:00:00Z`).toString();
|
|
244
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// feature.ts contract tests for subscription-stripe.
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from "vitest";
|
|
4
|
+
import { STRIPE_PROVIDER_NAME, StripeEventTypes, SUBSCRIPTION_STRIPE_FEATURE } from "../constants";
|
|
5
|
+
import { createSubscriptionStripeFeature } from "../feature";
|
|
6
|
+
|
|
7
|
+
const VALID_OPTIONS = {
|
|
8
|
+
webhookSecret: "whsec_test_dummy",
|
|
9
|
+
apiKey: "sk_test_dummy",
|
|
10
|
+
priceToTier: { price_test: "pro" },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe("createSubscriptionStripeFeature — shape", () => {
|
|
14
|
+
test("has the expected name", () => {
|
|
15
|
+
const feature = createSubscriptionStripeFeature(VALID_OPTIONS);
|
|
16
|
+
expect(feature.name).toBe(SUBSCRIPTION_STRIPE_FEATURE);
|
|
17
|
+
expect(feature.name).toBe("subscription-stripe");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("requires only subscription-foundation (NICHT config/secrets — alles app-wide via factory-options)", () => {
|
|
21
|
+
const feature = createSubscriptionStripeFeature(VALID_OPTIONS);
|
|
22
|
+
expect(feature.requires).toContain("billing-foundation");
|
|
23
|
+
// Drift-Pin: webhook-secret + apiKey kommen aus factory-options
|
|
24
|
+
// (= module-load-Closure), NICHT aus tenant-config/-secrets.
|
|
25
|
+
expect(feature.requires).not.toContain("config");
|
|
26
|
+
expect(feature.requires).not.toContain("secrets");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("createSubscriptionStripeFeature — module-load validation", () => {
|
|
31
|
+
test("throws bei empty webhookSecret (= App-Owner hat sub-stripe gemountet aber Stripe-Account nicht konfiguriert)", () => {
|
|
32
|
+
expect(() =>
|
|
33
|
+
createSubscriptionStripeFeature({
|
|
34
|
+
...VALID_OPTIONS,
|
|
35
|
+
webhookSecret: "",
|
|
36
|
+
}),
|
|
37
|
+
).toThrow(/webhookSecret is empty/);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("throws bei empty apiKey", () => {
|
|
41
|
+
expect(() =>
|
|
42
|
+
createSubscriptionStripeFeature({
|
|
43
|
+
...VALID_OPTIONS,
|
|
44
|
+
apiKey: "",
|
|
45
|
+
}),
|
|
46
|
+
).toThrow(/apiKey is empty/);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("subscription-stripe — plugin-registration", () => {
|
|
51
|
+
test("registers itself under entityName 'stripe' for subscription-foundation's extension", () => {
|
|
52
|
+
const feature = createSubscriptionStripeFeature(VALID_OPTIONS);
|
|
53
|
+
const usages = feature.extensionUsages;
|
|
54
|
+
expect(
|
|
55
|
+
usages.some(
|
|
56
|
+
(u) => u.extensionName === "subscriptionProvider" && u.entityName === STRIPE_PROVIDER_NAME,
|
|
57
|
+
),
|
|
58
|
+
).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("plugin-options have a valid SubscriptionProviderPlugin shape (drift-pin alle 4 methods)", () => {
|
|
62
|
+
// Stärker als nur "extension-usage existiert": wenn jemand eine der
|
|
63
|
+
// plugin-methods aus dem plugin-build entfernt, würde der
|
|
64
|
+
// entsprechende Foundation-write-handler zur Laufzeit als
|
|
65
|
+
// "method not supported"-error brechen — type-check würde es nicht
|
|
66
|
+
// fangen weil die useExtension-options als `unknown` durchgereicht
|
|
67
|
+
// werden.
|
|
68
|
+
const feature = createSubscriptionStripeFeature(VALID_OPTIONS);
|
|
69
|
+
const usage = feature.extensionUsages.find((u) => u.entityName === STRIPE_PROVIDER_NAME);
|
|
70
|
+
expect(usage).toBeDefined();
|
|
71
|
+
const options = usage?.options as {
|
|
72
|
+
verifyAndParseWebhook?: unknown;
|
|
73
|
+
createCheckoutSession?: unknown;
|
|
74
|
+
createPortalSession?: unknown;
|
|
75
|
+
cancelSubscription?: unknown;
|
|
76
|
+
};
|
|
77
|
+
expect(typeof options?.verifyAndParseWebhook).toBe("function");
|
|
78
|
+
expect(typeof options?.createCheckoutSession).toBe("function");
|
|
79
|
+
expect(typeof options?.createPortalSession).toBe("function");
|
|
80
|
+
expect(typeof options?.cancelSubscription).toBe("function");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("constants — Stripe-event-types die wir mappen", () => {
|
|
85
|
+
test("StripeEventTypes whitelist — was die Foundation verarbeitet", () => {
|
|
86
|
+
// Drift-Pin: ein Refactor das einen event-type rauswirft (z.B.
|
|
87
|
+
// invoice.paid weil "wir nutzen das nicht") würde tier-grace-period-
|
|
88
|
+
// tracking brechen. Plus: ein Refactor der einen NICHT-existing
|
|
89
|
+
// Stripe-event-type reinschreibt (typo) würde silent ignored werden.
|
|
90
|
+
expect(Object.values(StripeEventTypes)).toEqual([
|
|
91
|
+
"customer.subscription.created",
|
|
92
|
+
"customer.subscription.updated",
|
|
93
|
+
"customer.subscription.deleted",
|
|
94
|
+
"invoice.paid",
|
|
95
|
+
"invoice.payment_failed",
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Unit-Tests für die Stripe-Plugin-Methoden (createCheckoutSession,
|
|
2
|
+
// createPortalSession, cancelSubscription). Stripe-SDK-calls werden via
|
|
3
|
+
// vi.spyOn gemockt — wir testen unsere Mapping-Logik (Argumente die wir
|
|
4
|
+
// an Stripe schicken + Antwort-Parsing), NICHT Stripe selbst.
|
|
5
|
+
|
|
6
|
+
import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
import Stripe from "stripe";
|
|
8
|
+
import { describe, expect, test, vi } from "vitest";
|
|
9
|
+
import {
|
|
10
|
+
createStripeCancelSubscription,
|
|
11
|
+
createStripeCheckoutSession,
|
|
12
|
+
createStripePortalSession,
|
|
13
|
+
} from "../plugin-methods";
|
|
14
|
+
|
|
15
|
+
const TEST_API_KEY = "sk_test_dummy";
|
|
16
|
+
|
|
17
|
+
function buildStripe(): Stripe {
|
|
18
|
+
return new Stripe(TEST_API_KEY, { apiVersion: "2026-04-22.dahlia" });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const stubCtx = {} as HandlerContext;
|
|
22
|
+
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// createCheckoutSession
|
|
25
|
+
// =============================================================================
|
|
26
|
+
|
|
27
|
+
describe("createStripeCheckoutSession", () => {
|
|
28
|
+
test("ruft stripe.checkout.sessions.create mit mode=subscription + tenant-metadata", async () => {
|
|
29
|
+
const stripe = buildStripe();
|
|
30
|
+
const createMock = vi
|
|
31
|
+
.spyOn(stripe.checkout.sessions, "create")
|
|
32
|
+
// biome-ignore lint/suspicious/noExplicitAny: Stripe-SDK-typed mock-return
|
|
33
|
+
.mockResolvedValue({ url: "https://checkout.stripe.com/c/pay/test" } as any);
|
|
34
|
+
|
|
35
|
+
const checkout = createStripeCheckoutSession(stripe);
|
|
36
|
+
const result = await checkout(stubCtx, {
|
|
37
|
+
priceId: "price_pro_monthly",
|
|
38
|
+
tenantId: "tenant-001",
|
|
39
|
+
successUrl: "https://example.com/success",
|
|
40
|
+
cancelUrl: "https://example.com/cancel",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(result).toEqual({ url: "https://checkout.stripe.com/c/pay/test" });
|
|
44
|
+
expect(createMock).toHaveBeenCalledExactlyOnceWith({
|
|
45
|
+
mode: "subscription",
|
|
46
|
+
line_items: [{ price: "price_pro_monthly", quantity: 1 }],
|
|
47
|
+
success_url: "https://example.com/success",
|
|
48
|
+
cancel_url: "https://example.com/cancel",
|
|
49
|
+
// Drift-Pin: metadata.tenantId LANDET auf der subscription, NICHT
|
|
50
|
+
// auf der checkout-session direkt — sonst kann verifyAndParse-
|
|
51
|
+
// Webhook den tenant beim subsequent webhook nicht resolven.
|
|
52
|
+
subscription_data: {
|
|
53
|
+
metadata: { tenantId: "tenant-001" },
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("passes existing customer-id wenn gesetzt (Plan-Wechsel-Flow)", async () => {
|
|
59
|
+
const stripe = buildStripe();
|
|
60
|
+
const createMock = vi
|
|
61
|
+
.spyOn(stripe.checkout.sessions, "create")
|
|
62
|
+
// biome-ignore lint/suspicious/noExplicitAny: Stripe-SDK-typed mock-return
|
|
63
|
+
.mockResolvedValue({ url: "https://x" } as any);
|
|
64
|
+
|
|
65
|
+
const checkout = createStripeCheckoutSession(stripe);
|
|
66
|
+
await checkout(stubCtx, {
|
|
67
|
+
priceId: "price_x",
|
|
68
|
+
tenantId: "tenant-002",
|
|
69
|
+
successUrl: "https://x/s",
|
|
70
|
+
cancelUrl: "https://x/c",
|
|
71
|
+
providerCustomerId: "cus_existing_123",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(createMock).toHaveBeenCalledWith(
|
|
75
|
+
expect.objectContaining({ customer: "cus_existing_123" }),
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("throws wenn Stripe keine url returnt (defensive — sollte nie passieren bei mode=subscription)", async () => {
|
|
80
|
+
const stripe = buildStripe();
|
|
81
|
+
vi.spyOn(stripe.checkout.sessions, "create")
|
|
82
|
+
// biome-ignore lint/suspicious/noExplicitAny: SDK-Drift-Test
|
|
83
|
+
.mockResolvedValue({ url: null } as any);
|
|
84
|
+
|
|
85
|
+
const checkout = createStripeCheckoutSession(stripe);
|
|
86
|
+
await expect(
|
|
87
|
+
checkout(stubCtx, {
|
|
88
|
+
priceId: "p",
|
|
89
|
+
tenantId: "t",
|
|
90
|
+
successUrl: "https://x/s",
|
|
91
|
+
cancelUrl: "https://x/c",
|
|
92
|
+
}),
|
|
93
|
+
).rejects.toThrow(/returned no url/);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("Stripe-API-failure (z.B. 500 / network) → propagated zum Caller (Foundation mapped auf 500)", async () => {
|
|
97
|
+
// Drift-Pin: Plugin schluckt KEINE Stripe-Errors. Foundation
|
|
98
|
+
// verlässt sich darauf dass create-checkout-session-handler einen
|
|
99
|
+
// throw kriegt + zur HTTP 500 mapped (transient — Provider/Stripe
|
|
100
|
+
// soll retried werden statt silent-success-mit-leerer-URL).
|
|
101
|
+
const stripe = buildStripe();
|
|
102
|
+
vi.spyOn(stripe.checkout.sessions, "create").mockRejectedValue(
|
|
103
|
+
new Error("Stripe API: Internal server error"),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const checkout = createStripeCheckoutSession(stripe);
|
|
107
|
+
await expect(
|
|
108
|
+
checkout(stubCtx, {
|
|
109
|
+
priceId: "p",
|
|
110
|
+
tenantId: "t",
|
|
111
|
+
successUrl: "https://x/s",
|
|
112
|
+
cancelUrl: "https://x/c",
|
|
113
|
+
}),
|
|
114
|
+
).rejects.toThrow(/Internal server error/);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// =============================================================================
|
|
119
|
+
// createPortalSession
|
|
120
|
+
// =============================================================================
|
|
121
|
+
|
|
122
|
+
describe("createStripePortalSession", () => {
|
|
123
|
+
test("ruft stripe.billingPortal.sessions.create mit customer + return_url", async () => {
|
|
124
|
+
const stripe = buildStripe();
|
|
125
|
+
const createMock = vi
|
|
126
|
+
.spyOn(stripe.billingPortal.sessions, "create")
|
|
127
|
+
// biome-ignore lint/suspicious/noExplicitAny: Stripe-SDK-typed mock-return
|
|
128
|
+
.mockResolvedValue({ url: "https://billing.stripe.com/p/session/test" } as any);
|
|
129
|
+
|
|
130
|
+
const portal = createStripePortalSession(stripe);
|
|
131
|
+
const result = await portal(stubCtx, {
|
|
132
|
+
providerCustomerId: "cus_001",
|
|
133
|
+
returnUrl: "https://example.com/return",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(result).toEqual({ url: "https://billing.stripe.com/p/session/test" });
|
|
137
|
+
expect(createMock).toHaveBeenCalledExactlyOnceWith({
|
|
138
|
+
customer: "cus_001",
|
|
139
|
+
return_url: "https://example.com/return",
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// =============================================================================
|
|
145
|
+
// cancelSubscription
|
|
146
|
+
// =============================================================================
|
|
147
|
+
|
|
148
|
+
describe("createStripeCancelSubscription", () => {
|
|
149
|
+
test("ruft stripe.subscriptions.cancel mit subscription-id", async () => {
|
|
150
|
+
const stripe = buildStripe();
|
|
151
|
+
const cancelMock = vi
|
|
152
|
+
.spyOn(stripe.subscriptions, "cancel")
|
|
153
|
+
// biome-ignore lint/suspicious/noExplicitAny: Stripe-SDK-typed mock-return
|
|
154
|
+
.mockResolvedValue({ id: "sub_001", status: "canceled" } as any);
|
|
155
|
+
|
|
156
|
+
const cancel = createStripeCancelSubscription(stripe);
|
|
157
|
+
await cancel(stubCtx, "sub_001");
|
|
158
|
+
|
|
159
|
+
expect(cancelMock).toHaveBeenCalledExactlyOnceWith("sub_001");
|
|
160
|
+
});
|
|
161
|
+
});
|