@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,120 @@
|
|
|
1
|
+
// kumiko-feature-version: 1
|
|
2
|
+
//
|
|
3
|
+
// subscription-stripe — Stripe-Plugin für die subscription-foundation
|
|
4
|
+
// Plugin-API.
|
|
5
|
+
//
|
|
6
|
+
// **Factory-Pattern (= createSubscriptionStripeFeature(options)):**
|
|
7
|
+
// Im Gegensatz zu mail-transport-smtp / file-provider-s3 (die ihre
|
|
8
|
+
// secrets aus tenant-secrets lesen) ist Stripe's webhook-secret
|
|
9
|
+
// **app-wide** — App-Owner hat einen Stripe-account, alle Webhooks
|
|
10
|
+
// gehen dorthin. Plugin braucht den secret beim webhook-sig-verify-
|
|
11
|
+
// Zeitpunkt, der ist PRE-tenant-resolution (kein ctx).
|
|
12
|
+
//
|
|
13
|
+
// Lösung: factory-Funktion `createSubscriptionStripeFeature(options)`
|
|
14
|
+
// liest webhook-secret + apiKey beim mount-time aus dem Caller (= App-
|
|
15
|
+
// Builder's bin/server.ts der's aus process.env zieht). Closure
|
|
16
|
+
// hält's für den verifyAndParseWebhook-call.
|
|
17
|
+
//
|
|
18
|
+
// Beispiel-Verwendung in run-config.ts:
|
|
19
|
+
//
|
|
20
|
+
// import { createSubscriptionStripeFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-stripe";
|
|
21
|
+
//
|
|
22
|
+
// const features = [
|
|
23
|
+
// billingFoundationFeature,
|
|
24
|
+
// createSubscriptionStripeFeature({
|
|
25
|
+
// webhookSecret: process.env.STRIPE_WEBHOOK_SECRET ?? "",
|
|
26
|
+
// apiKey: process.env.STRIPE_API_KEY ?? "",
|
|
27
|
+
// priceToTier: {
|
|
28
|
+
// "price_1ABC": "pro",
|
|
29
|
+
// "price_1XYZ": "business",
|
|
30
|
+
// },
|
|
31
|
+
// }),
|
|
32
|
+
// ];
|
|
33
|
+
//
|
|
34
|
+
// **Pattern-Vorbild:** mirrors createFeatureTogglesFeature(options) —
|
|
35
|
+
// gleiche factory-Form für features die module-load-time-Konfiguration
|
|
36
|
+
// haben (analog zum FeatureToggle-runtime-holder).
|
|
37
|
+
|
|
38
|
+
import type { SubscriptionProviderPlugin } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
39
|
+
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
40
|
+
import Stripe from "stripe";
|
|
41
|
+
import { STRIPE_PROVIDER_NAME, SUBSCRIPTION_STRIPE_FEATURE } from "./constants";
|
|
42
|
+
import {
|
|
43
|
+
createStripeCancelSubscription,
|
|
44
|
+
createStripeCheckoutSession,
|
|
45
|
+
createStripePortalSession,
|
|
46
|
+
} from "./plugin-methods";
|
|
47
|
+
import { verifyAndParseStripeWebhook } from "./verify-webhook";
|
|
48
|
+
|
|
49
|
+
export type SubscriptionStripeOptions = {
|
|
50
|
+
/** Webhook-secret aus dem Stripe-Dashboard. App-wide. Plugin throws
|
|
51
|
+
* beim runtime wenn empty (= App-Owner hat sub-stripe gemountet
|
|
52
|
+
* aber Stripe-Account nicht konfiguriert). */
|
|
53
|
+
readonly webhookSecret: string;
|
|
54
|
+
/** Stripe-API-key (sk_live_... / sk_test_...). Heute nur für
|
|
55
|
+
* constructEvent-API-Version-Pin gebraucht; Phase 5.2b nutzt's
|
|
56
|
+
* für outgoing-API-calls (createPortalSession etc.). */
|
|
57
|
+
readonly apiKey: string;
|
|
58
|
+
/** Price-to-tier-Mapping. Plugin liest die price-id aus Stripe-event
|
|
59
|
+
* (subscription.items.data[0].price.id) und mappt auf einen tier-
|
|
60
|
+
* name. Fehlt die price-id im Mapping → null (foundation 200
|
|
61
|
+
* ignored — App-Owner-Bug, hat den Stripe-price angelegt aber
|
|
62
|
+
* nicht zur tier zugeordnet). */
|
|
63
|
+
readonly priceToTier: Readonly<Record<string, string>>;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Factory für das subscription-stripe-feature. Wird mit den App-Owner-
|
|
68
|
+
* eigenen Stripe-Credentials gemountet. Der returnte FeatureDefinition
|
|
69
|
+
* registriert den Plugin gegen subscription-foundation's
|
|
70
|
+
* "subscriptionProvider"-extension-point unter entityName "stripe".
|
|
71
|
+
*/
|
|
72
|
+
export function createSubscriptionStripeFeature(
|
|
73
|
+
options: SubscriptionStripeOptions,
|
|
74
|
+
): FeatureDefinition {
|
|
75
|
+
// Module-load-Validation: ohne webhook-secret kann der Plugin keinen
|
|
76
|
+
// single Webhook verifizieren. Throw vor dem mount damit der App-
|
|
77
|
+
// Owner nicht zur Laufzeit Mystery-401s sieht.
|
|
78
|
+
if (options.webhookSecret.length === 0) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
"subscription-stripe: webhookSecret is empty. Set STRIPE_WEBHOOK_SECRET (or system-config) before mounting.",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (options.apiKey.length === 0) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
"subscription-stripe: apiKey is empty. Set STRIPE_API_KEY (or system-config) before mounting.",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// EIN Stripe-Client für alle vier plugin-methods (verify-webhook +
|
|
90
|
+
// checkout + portal + cancel). API-version-pin zentral, kein
|
|
91
|
+
// Connection-Duplikat.
|
|
92
|
+
const stripe = new Stripe(options.apiKey, { apiVersion: "2026-04-22.dahlia" });
|
|
93
|
+
|
|
94
|
+
const verifyAndParse = verifyAndParseStripeWebhook(stripe, {
|
|
95
|
+
webhookSecret: options.webhookSecret,
|
|
96
|
+
priceToTier: options.priceToTier,
|
|
97
|
+
});
|
|
98
|
+
const checkoutSession = createStripeCheckoutSession(stripe);
|
|
99
|
+
const portalSession = createStripePortalSession(stripe);
|
|
100
|
+
const cancel = createStripeCancelSubscription(stripe);
|
|
101
|
+
|
|
102
|
+
return defineFeature(SUBSCRIPTION_STRIPE_FEATURE, (r) => {
|
|
103
|
+
// Hard-deps: subscription-foundation als plugin-host. KEIN
|
|
104
|
+
// `r.requires("config", "secrets")` — der Plugin nutzt weder
|
|
105
|
+
// tenant-config noch tenant-secrets (alles app-wide via factory-
|
|
106
|
+
// options).
|
|
107
|
+
r.requires("billing-foundation");
|
|
108
|
+
|
|
109
|
+
// Plugin: register against subscription-foundation's
|
|
110
|
+
// "subscriptionProvider" extension. entityName "stripe" matcht den
|
|
111
|
+
// path-segment in der webhook-URL (`/api/subscription/webhook/stripe`).
|
|
112
|
+
const plugin: SubscriptionProviderPlugin = {
|
|
113
|
+
verifyAndParseWebhook: verifyAndParse,
|
|
114
|
+
createCheckoutSession: checkoutSession,
|
|
115
|
+
createPortalSession: portalSession,
|
|
116
|
+
cancelSubscription: cancel,
|
|
117
|
+
};
|
|
118
|
+
r.useExtension("subscriptionProvider", STRIPE_PROVIDER_NAME, plugin);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Public API of the subscription-stripe bundled-feature.
|
|
2
|
+
//
|
|
3
|
+
// **Internal-only** (NICHT im public-barrel — App-Builder nutzt das nie direkt):
|
|
4
|
+
// - StripeWebhookOptions / verifyAndParseStripeWebhook (intern vom
|
|
5
|
+
// feature.ts factory aufgerufen)
|
|
6
|
+
// - mapStripeEventType / mapStripeStatus (pure helpers, test-only;
|
|
7
|
+
// direct-import aus dem File wenn echt mal extern gebraucht)
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
STRIPE_PROVIDER_NAME,
|
|
11
|
+
StripeEventTypes,
|
|
12
|
+
SUBSCRIPTION_STRIPE_FEATURE,
|
|
13
|
+
} from "./constants";
|
|
14
|
+
export { createSubscriptionStripeFeature, type SubscriptionStripeOptions } from "./feature";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Stripe-Plugin-Methoden für die POST-tenant-resolution-Phase:
|
|
2
|
+
// createCheckoutSession, createPortalSession, cancelSubscription.
|
|
3
|
+
//
|
|
4
|
+
// Werden vom Plugin-build (feature.ts) als methods auf dem
|
|
5
|
+
// SubscriptionProviderPlugin registriert. Anders als
|
|
6
|
+
// verifyAndParseWebhook (= pre-tenant) bekommen diese den vollen
|
|
7
|
+
// HandlerContext (für ggf. tenant-spezifische Lookups).
|
|
8
|
+
//
|
|
9
|
+
// **Type-Ableitung:** die options-shapes der drei methods werden
|
|
10
|
+
// **direkt vom Plugin-Contract** abgeleitet (`Parameters<NonNullable
|
|
11
|
+
// <SubscriptionProviderPlugin["...method"]>>[1]`). Wenn Foundation den
|
|
12
|
+
// Contract erweitert (z.B. neuer optionaler Field), bemerkt der
|
|
13
|
+
// Stripe-Plugin das beim TS-Compile, nicht erst zur Laufzeit.
|
|
14
|
+
|
|
15
|
+
import type { SubscriptionProviderPlugin } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
16
|
+
import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
17
|
+
import type Stripe from "stripe";
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// createCheckoutSession
|
|
21
|
+
// =============================================================================
|
|
22
|
+
//
|
|
23
|
+
// Stripe-Checkout-Session erstellen. Der hosted-page-URL wird returnt;
|
|
24
|
+
// der App-Builder redirected den Tenant-Admin dorthin. Nach erfolgreichem
|
|
25
|
+
// checkout sendet Stripe `customer.subscription.created` mit
|
|
26
|
+
// `metadata.tenantId` zurück — das ist wie der subsequent webhook den
|
|
27
|
+
// Tenant resolved.
|
|
28
|
+
|
|
29
|
+
export type StripeCheckoutOptions = Parameters<
|
|
30
|
+
NonNullable<SubscriptionProviderPlugin["createCheckoutSession"]>
|
|
31
|
+
>[1];
|
|
32
|
+
|
|
33
|
+
export function createStripeCheckoutSession(stripe: Stripe) {
|
|
34
|
+
return async (_ctx: HandlerContext, options: StripeCheckoutOptions): Promise<{ url: string }> => {
|
|
35
|
+
const session = await stripe.checkout.sessions.create({
|
|
36
|
+
mode: "subscription",
|
|
37
|
+
line_items: [{ price: options.priceId, quantity: 1 }],
|
|
38
|
+
success_url: options.successUrl,
|
|
39
|
+
cancel_url: options.cancelUrl,
|
|
40
|
+
// metadata.tenantId landet auf der subscription die durch diese
|
|
41
|
+
// checkout-session entsteht — beim subsequent webhook lesen wir's
|
|
42
|
+
// aus subscription.metadata.tenantId zurück.
|
|
43
|
+
subscription_data: {
|
|
44
|
+
metadata: { tenantId: options.tenantId },
|
|
45
|
+
},
|
|
46
|
+
...(options.providerCustomerId && { customer: options.providerCustomerId }),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!session.url) {
|
|
50
|
+
// Stripe garantiert url für mode: subscription. Defensive für
|
|
51
|
+
// zukünftige API-Drift.
|
|
52
|
+
throw new Error("subscription-stripe: checkout.sessions.create returned no url");
|
|
53
|
+
}
|
|
54
|
+
return { url: session.url };
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// createPortalSession
|
|
60
|
+
// =============================================================================
|
|
61
|
+
//
|
|
62
|
+
// Stripe Customer-Portal-Session — Tenant verwaltet seine subscription
|
|
63
|
+
// selbst (cancel, payment-method, invoice-history).
|
|
64
|
+
|
|
65
|
+
export type StripePortalOptions = Parameters<
|
|
66
|
+
NonNullable<SubscriptionProviderPlugin["createPortalSession"]>
|
|
67
|
+
>[1];
|
|
68
|
+
|
|
69
|
+
export function createStripePortalSession(stripe: Stripe) {
|
|
70
|
+
return async (_ctx: HandlerContext, options: StripePortalOptions): Promise<{ url: string }> => {
|
|
71
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
72
|
+
customer: options.providerCustomerId,
|
|
73
|
+
return_url: options.returnUrl,
|
|
74
|
+
});
|
|
75
|
+
return { url: session.url };
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// =============================================================================
|
|
80
|
+
// cancelSubscription
|
|
81
|
+
// =============================================================================
|
|
82
|
+
//
|
|
83
|
+
// Stripe sendet danach `customer.subscription.deleted`-webhook → der
|
|
84
|
+
// state-update läuft über den normalen webhook-pfad. Diese function
|
|
85
|
+
// triggert nur die API-Cancellation.
|
|
86
|
+
|
|
87
|
+
export function createStripeCancelSubscription(stripe: Stripe) {
|
|
88
|
+
return async (_ctx: HandlerContext, providerSubscriptionId: string): Promise<void> => {
|
|
89
|
+
await stripe.subscriptions.cancel(providerSubscriptionId);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// verifyAndParseStripeWebhook — Stripe-spezifische sig-verify +
|
|
2
|
+
// event-mapping. Wird vom Plugin-Build (feature.ts) als
|
|
3
|
+
// `verifyAndParseWebhook` registriert.
|
|
4
|
+
//
|
|
5
|
+
// **Drei Schritte:**
|
|
6
|
+
// 1. Sig-verify via stripe.webhooks.constructEvent (HMAC-SHA-256
|
|
7
|
+
// gegen rawBody + Stripe-Signature-Header). Wirft bei mismatch
|
|
8
|
+
// oder älter als 5min (Replay-Protection).
|
|
9
|
+
// 2. Event-type-Filter: nur die 5 event-types die wir auf
|
|
10
|
+
// SubscriptionEventTypes mappen kommen weiter; alles andere
|
|
11
|
+
// returnt null (foundation antwortet 200 ignored).
|
|
12
|
+
// 3. Stripe-payload → SubscriptionEvent normalisieren
|
|
13
|
+
// (status-mapping, tenant-id aus metadata, price-to-tier-Lookup).
|
|
14
|
+
//
|
|
15
|
+
// **Invoice-event lazy-fetch (Phase 5.2b):**
|
|
16
|
+
// Bei `invoice.paid` und `invoice.payment_failed` enthält der webhook-
|
|
17
|
+
// payload nur die subscription-id (Stripe-Webhooks expanden subscription
|
|
18
|
+
// nicht automatisch). Plugin macht einen lazy-fetch via
|
|
19
|
+
// `stripe.subscriptions.retrieve(subId)` um an das full subscription-
|
|
20
|
+
// Object für status/tier/period-end-mapping zu kommen. Bei Stripe-API-
|
|
21
|
+
// failure (= subscription gelöscht zwischen webhook + retrieve)
|
|
22
|
+
// returnt der Plugin defensiv null — der nächste subscription-event
|
|
23
|
+
// wird den state korrekt handhaben.
|
|
24
|
+
|
|
25
|
+
import type { SubscriptionEvent } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
26
|
+
import {
|
|
27
|
+
type SubscriptionEventType,
|
|
28
|
+
SubscriptionEventTypes,
|
|
29
|
+
type SubscriptionStatus,
|
|
30
|
+
SubscriptionStatuses,
|
|
31
|
+
} from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
32
|
+
import type Stripe from "stripe";
|
|
33
|
+
import { STRIPE_PROVIDER_NAME, StripeEventTypes } from "./constants";
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// Sig-verify + parse
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
export type StripeWebhookOptions = {
|
|
40
|
+
/** Webhook-secret aus dem Stripe-Dashboard. **App-wide**, nicht
|
|
41
|
+
* per-tenant. Liest aus ENV-VAR oder system-config beim Plugin-
|
|
42
|
+
* build. */
|
|
43
|
+
readonly webhookSecret: string;
|
|
44
|
+
/** Price-to-tier-Map. Plugin liest die price-id aus dem event und
|
|
45
|
+
* mapped auf tier-name. Fehlt die price-id im Mapping → null. */
|
|
46
|
+
readonly priceToTier: Readonly<Record<string, string>>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Stripe-webhook-handler. Implementiert den Plugin-Contract
|
|
51
|
+
* `verifyAndParseWebhook`. Closure über die `options` + den shared
|
|
52
|
+
* Stripe-Client (kein ctx-arg — das ist die Pre-tenant-resolution-Phase).
|
|
53
|
+
*
|
|
54
|
+
* **Shared Stripe-Client:** Der Caller (feature.ts) baut EINEN Stripe-
|
|
55
|
+
* Client beim mount und gibt ihn an alle vier plugin-methods weiter.
|
|
56
|
+
* Konstruktor-API-version-pin ist damit zentral; verify-webhook nutzt
|
|
57
|
+
* den client für `webhooks.constructEvent` (sig-verify) + lazy-fetch
|
|
58
|
+
* der invoice-events.
|
|
59
|
+
*/
|
|
60
|
+
export function verifyAndParseStripeWebhook(
|
|
61
|
+
stripe: Stripe,
|
|
62
|
+
options: StripeWebhookOptions,
|
|
63
|
+
): (rawBody: string, headers: Record<string, string>) => Promise<SubscriptionEvent | null> {
|
|
64
|
+
return async (rawBody, headers) => {
|
|
65
|
+
const sigHeader = headers["stripe-signature"];
|
|
66
|
+
if (!sigHeader) {
|
|
67
|
+
throw new Error("subscription-stripe: stripe-signature header missing");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 1. Sig-verify. constructEvent throws bei mismatch (= invalid sig)
|
|
71
|
+
// oder timestamp-tolerance-violation (default 5min). Foundation
|
|
72
|
+
// mapped throw → HTTP 401.
|
|
73
|
+
let event: Stripe.Event;
|
|
74
|
+
try {
|
|
75
|
+
event = stripe.webhooks.constructEvent(rawBody, sigHeader, options.webhookSecret);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
78
|
+
throw new Error(`subscription-stripe: webhook signature verify failed — ${msg}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 2. Event-type-Filter — wir kennen nur 5.
|
|
82
|
+
const normalizedType = mapStripeEventType(event.type);
|
|
83
|
+
if (!normalizedType) {
|
|
84
|
+
return null; // foundation returnt 200 ignored
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 3. Payload-extraction. Stripe liefert je nach event.type
|
|
88
|
+
// verschiedene data.object-shapes. Wir extrahieren die
|
|
89
|
+
// Subscription-Daten — entweder direkt (subscription-events)
|
|
90
|
+
// oder via lazy-fetch (invoice-events, Phase 5.2b).
|
|
91
|
+
const sub = await extractSubscriptionFromEvent(event, stripe);
|
|
92
|
+
if (!sub) {
|
|
93
|
+
// event-type war unter den 5 (oben gefiltert), aber payload-shape
|
|
94
|
+
// matched nicht — Stripe-SDK-Schema-Drift, defensive null.
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 4. Tenant-resolution aus metadata. App-Builder setzt
|
|
99
|
+
// `metadata.tenantId` beim createCheckoutSession-call.
|
|
100
|
+
const tenantId = sub.metadata?.["tenantId"];
|
|
101
|
+
if (!tenantId || tenantId.length === 0) {
|
|
102
|
+
// Subscription ohne tenant-metadata → kann kein subscription
|
|
103
|
+
// erstellen ohne tenant-resolution. Drop the event silent
|
|
104
|
+
// (foundation 200 ignored). App-Owner-Bug, nicht foundation-Bug.
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 5. Price-to-tier-Mapping. Stripe-subscription hat items[0].price.id.
|
|
109
|
+
const priceId = sub.items.data[0]?.price.id;
|
|
110
|
+
if (!priceId) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const tier = options.priceToTier[priceId];
|
|
114
|
+
if (!tier) {
|
|
115
|
+
// Price-id nicht im Mapping → App-Owner hat den Stripe-price
|
|
116
|
+
// angelegt aber nicht zur tier zugeordnet. Drop silent.
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 6. Status-Mapping + period-end. Stripe hat den period-end seit
|
|
121
|
+
// 2024 vom subscription-level auf item-level migriert (=
|
|
122
|
+
// subscription.items.data[i].current_period_end). Wir lesen
|
|
123
|
+
// das vom ersten item; multi-item-subs (Add-Ons) sind kein
|
|
124
|
+
// Phase-5-Scope.
|
|
125
|
+
const status = mapStripeStatus(sub.status);
|
|
126
|
+
const periodEndUnixSec = sub.items.data[0]?.current_period_end ?? 0;
|
|
127
|
+
// Stripe returns Unix-seconds; Temporal.Instant.fromEpochMilliseconds
|
|
128
|
+
// expects ms. Multiply, then ISO. (No-Date-API-Guard verbietet
|
|
129
|
+
// `new Date()` — Temporal ist global verfügbar via polyfill.)
|
|
130
|
+
const currentPeriodEnd = Temporal.Instant.fromEpochMilliseconds(
|
|
131
|
+
periodEndUnixSec * 1000,
|
|
132
|
+
).toString();
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
providerEventId: event.id,
|
|
136
|
+
providerName: STRIPE_PROVIDER_NAME,
|
|
137
|
+
type: normalizedType,
|
|
138
|
+
tenantId,
|
|
139
|
+
providerCustomerId: typeof sub.customer === "string" ? sub.customer : sub.customer.id,
|
|
140
|
+
providerSubscriptionId: sub.id,
|
|
141
|
+
status,
|
|
142
|
+
tier,
|
|
143
|
+
currentPeriodEnd,
|
|
144
|
+
rawPayload: JSON.stringify(event),
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// =============================================================================
|
|
150
|
+
// Helpers
|
|
151
|
+
// =============================================================================
|
|
152
|
+
|
|
153
|
+
/** Stripe-event-type → normalisiert. null = ignore. */
|
|
154
|
+
export function mapStripeEventType(stripeType: string): SubscriptionEventType | null {
|
|
155
|
+
switch (stripeType) {
|
|
156
|
+
case StripeEventTypes.customerSubscriptionCreated:
|
|
157
|
+
return SubscriptionEventTypes.created;
|
|
158
|
+
case StripeEventTypes.customerSubscriptionUpdated:
|
|
159
|
+
return SubscriptionEventTypes.updated;
|
|
160
|
+
case StripeEventTypes.customerSubscriptionDeleted:
|
|
161
|
+
return SubscriptionEventTypes.canceled;
|
|
162
|
+
case StripeEventTypes.invoicePaid:
|
|
163
|
+
return SubscriptionEventTypes.invoicePaid;
|
|
164
|
+
case StripeEventTypes.invoicePaymentFailed:
|
|
165
|
+
return SubscriptionEventTypes.invoicePaymentFailed;
|
|
166
|
+
default:
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Stripe-status → normalisiert. Defensive: unbekannte Status →
|
|
172
|
+
* incomplete (Plugin sollte das nicht erreichen, aber wir wollen
|
|
173
|
+
* kein ungültiger status-string in der DB). */
|
|
174
|
+
export function mapStripeStatus(stripeStatus: Stripe.Subscription.Status): SubscriptionStatus {
|
|
175
|
+
switch (stripeStatus) {
|
|
176
|
+
case "active":
|
|
177
|
+
return SubscriptionStatuses.active;
|
|
178
|
+
case "trialing":
|
|
179
|
+
return SubscriptionStatuses.trialing;
|
|
180
|
+
case "past_due":
|
|
181
|
+
case "unpaid":
|
|
182
|
+
case "paused":
|
|
183
|
+
return SubscriptionStatuses.pastDue;
|
|
184
|
+
case "canceled":
|
|
185
|
+
return SubscriptionStatuses.canceled;
|
|
186
|
+
case "incomplete":
|
|
187
|
+
case "incomplete_expired":
|
|
188
|
+
return SubscriptionStatuses.incomplete;
|
|
189
|
+
default:
|
|
190
|
+
return SubscriptionStatuses.incomplete;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Holt die Subscription aus dem Event. Subscription-events haben sie
|
|
195
|
+
* direkt im data.object; invoice-events haben nur die subscription-id
|
|
196
|
+
* und brauchen einen lazy-fetch via stripe.subscriptions.retrieve
|
|
197
|
+
* (Phase 5.2b). */
|
|
198
|
+
async function extractSubscriptionFromEvent(
|
|
199
|
+
event: Stripe.Event,
|
|
200
|
+
stripe: Stripe,
|
|
201
|
+
): Promise<Stripe.Subscription | null> {
|
|
202
|
+
switch (event.type) {
|
|
203
|
+
case StripeEventTypes.customerSubscriptionCreated:
|
|
204
|
+
case StripeEventTypes.customerSubscriptionUpdated:
|
|
205
|
+
case StripeEventTypes.customerSubscriptionDeleted:
|
|
206
|
+
return event.data.object as Stripe.Subscription;
|
|
207
|
+
case StripeEventTypes.invoicePaid:
|
|
208
|
+
case StripeEventTypes.invoicePaymentFailed: {
|
|
209
|
+
// Lazy-fetch der subscription. invoice.subscription ist eine
|
|
210
|
+
// string-id (Stripe-Webhooks expanden nicht auto). Wir holen das
|
|
211
|
+
// full subscription-Object damit der downstream-mapping
|
|
212
|
+
// (status, tier via priceId, period-end) konsistent funktioniert.
|
|
213
|
+
const invoice = event.data.object as Stripe.Invoice;
|
|
214
|
+
const subRef = (invoice as { subscription?: string | Stripe.Subscription | null })
|
|
215
|
+
.subscription;
|
|
216
|
+
if (!subRef) {
|
|
217
|
+
// Invoice ohne subscription-reference (= one-shot-invoice, nicht
|
|
218
|
+
// recurring). Nicht unsere Domain — ignorieren.
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const subId = typeof subRef === "string" ? subRef : subRef.id;
|
|
222
|
+
try {
|
|
223
|
+
return await stripe.subscriptions.retrieve(subId);
|
|
224
|
+
} catch {
|
|
225
|
+
// Stripe-API-failure beim retrieve (z.B. subscription gelöscht
|
|
226
|
+
// zwischen webhook + retrieve). Defensive: null returnen, damit
|
|
227
|
+
// foundation 200 ignored returnt — der nächste subscription-
|
|
228
|
+
// event wird's korrekt handhaben.
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
default:
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|