@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,315 @@
|
|
|
1
|
+
// Integration-test: Stripe-Plugin → subscription-foundation → DB.
|
|
2
|
+
//
|
|
3
|
+
// Beweist die echte Verdrahtung:
|
|
4
|
+
// 1. Stripe-event mit valider Signatur kommt am webhook-handler an
|
|
5
|
+
// 2. Stripe-Plugin verifiziert + parsed → SubscriptionEvent
|
|
6
|
+
// 3. webhook-handler dispatched zu process-event-handler
|
|
7
|
+
// 4. process-event-handler schreibt subscription + subscription-event
|
|
8
|
+
// in die DB
|
|
9
|
+
//
|
|
10
|
+
// Type-checks fangen struct-mismatch, NICHT runtime-mismatches (Zod-
|
|
11
|
+
// validation des process-event-schema könnte stricter sein als der
|
|
12
|
+
// Stripe-output liefert). Dieser Test fängt das Spalten-Mapping +
|
|
13
|
+
// Verdrahtungs-Bugs ab.
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
billingFoundationFeature,
|
|
17
|
+
createSubscriptionWebhookHandler,
|
|
18
|
+
type SubscriptionProviderPlugin,
|
|
19
|
+
subscriptionAggregateId,
|
|
20
|
+
} from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
21
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
22
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
23
|
+
import { createEventsTable, loadAggregate } from "@cosmicdrift/kumiko-framework/event-store";
|
|
24
|
+
import {
|
|
25
|
+
createTestUser,
|
|
26
|
+
setupTestStack,
|
|
27
|
+
type TestStack,
|
|
28
|
+
testTenantId,
|
|
29
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
30
|
+
import { Hono } from "hono";
|
|
31
|
+
import Stripe from "stripe";
|
|
32
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
33
|
+
import { createSubscriptionStripeFeature } from "../feature";
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// Setup
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
const TEST_SECRET = "whsec_test_integration_secret";
|
|
40
|
+
const TEST_API_KEY = "sk_test_integration_apikey";
|
|
41
|
+
const PRICE_TO_TIER = { price_pro_monthly: "pro", price_business_yearly: "business" };
|
|
42
|
+
|
|
43
|
+
let stack: TestStack;
|
|
44
|
+
let db: DbConnection;
|
|
45
|
+
let webhookApp: Hono;
|
|
46
|
+
|
|
47
|
+
const stripeForFixtures = new Stripe(TEST_API_KEY, { apiVersion: "2026-04-22.dahlia" });
|
|
48
|
+
|
|
49
|
+
beforeAll(async () => {
|
|
50
|
+
const stripeFeature = createSubscriptionStripeFeature({
|
|
51
|
+
webhookSecret: TEST_SECRET,
|
|
52
|
+
apiKey: TEST_API_KEY,
|
|
53
|
+
priceToTier: PRICE_TO_TIER,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
stack = await setupTestStack({
|
|
57
|
+
features: [billingFoundationFeature, stripeFeature],
|
|
58
|
+
});
|
|
59
|
+
db = stack.db;
|
|
60
|
+
// subscriptionsProjectionTable wird von setupTestStack automatisch
|
|
61
|
+
// gepusht (r.projection mit `table`-Property → auto-push).
|
|
62
|
+
await createEventsTable(db);
|
|
63
|
+
|
|
64
|
+
// Webhook-app: Hono mit der webhook-handler-Route.
|
|
65
|
+
// dispatchWrite ruft `stack.http.write` mit dem System-User des
|
|
66
|
+
// resolved-Tenants — das ist exakt was der App-Builder im echten
|
|
67
|
+
// bin/server.ts via extraRoutes wireup macht.
|
|
68
|
+
webhookApp = new Hono();
|
|
69
|
+
webhookApp.post(
|
|
70
|
+
"/api/subscription/webhook/:providerName",
|
|
71
|
+
createSubscriptionWebhookHandler({
|
|
72
|
+
dispatchWrite: async ({ handlerQn, payload, tenantId }) => {
|
|
73
|
+
const systemUser = createTestUser({
|
|
74
|
+
id: 1,
|
|
75
|
+
tenantId: tenantId as TenantId,
|
|
76
|
+
roles: ["SystemAdmin"],
|
|
77
|
+
});
|
|
78
|
+
const res = await stack.http.write(handlerQn, payload, systemUser);
|
|
79
|
+
const body = await res.json();
|
|
80
|
+
return body.isSuccess
|
|
81
|
+
? { isSuccess: true, data: body.data }
|
|
82
|
+
: { isSuccess: false, error: body.error };
|
|
83
|
+
},
|
|
84
|
+
resolveProvider: (providerName) => {
|
|
85
|
+
const usage = stack.registry
|
|
86
|
+
.getExtensionUsages("subscriptionProvider")
|
|
87
|
+
.find((u) => u.entityName === providerName);
|
|
88
|
+
return usage?.options as SubscriptionProviderPlugin | undefined;
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
afterAll(async () => {
|
|
95
|
+
await stack.cleanup();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// =============================================================================
|
|
99
|
+
// Fixtures
|
|
100
|
+
// =============================================================================
|
|
101
|
+
|
|
102
|
+
function buildStripeSubscriptionEvent(overrides: {
|
|
103
|
+
eventId?: string;
|
|
104
|
+
tenantId?: string;
|
|
105
|
+
priceId?: string;
|
|
106
|
+
status?: string;
|
|
107
|
+
customerId?: string;
|
|
108
|
+
subscriptionId?: string;
|
|
109
|
+
eventType?: string;
|
|
110
|
+
}) {
|
|
111
|
+
const eventId = overrides.eventId ?? "evt_integration_001";
|
|
112
|
+
return {
|
|
113
|
+
id: eventId,
|
|
114
|
+
object: "event",
|
|
115
|
+
api_version: "2026-04-22.dahlia",
|
|
116
|
+
created: 1_770_000_000,
|
|
117
|
+
type: overrides.eventType ?? "customer.subscription.created",
|
|
118
|
+
livemode: false,
|
|
119
|
+
pending_webhooks: 1,
|
|
120
|
+
request: { id: null, idempotency_key: null },
|
|
121
|
+
data: {
|
|
122
|
+
object: {
|
|
123
|
+
id: overrides.subscriptionId ?? "sub_integration_001",
|
|
124
|
+
object: "subscription",
|
|
125
|
+
customer: overrides.customerId ?? "cus_integration_001",
|
|
126
|
+
status: overrides.status ?? "active",
|
|
127
|
+
metadata: { tenantId: overrides.tenantId ?? "tenant-int-1" },
|
|
128
|
+
items: {
|
|
129
|
+
object: "list",
|
|
130
|
+
data: [
|
|
131
|
+
{
|
|
132
|
+
id: "si_int",
|
|
133
|
+
object: "subscription_item",
|
|
134
|
+
current_period_end: 1_780_000_000,
|
|
135
|
+
price: { id: overrides.priceId ?? "price_pro_monthly", object: "price" },
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
has_more: false,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function signEvent(payload: string): string {
|
|
146
|
+
return stripeForFixtures.webhooks.generateTestHeaderString({
|
|
147
|
+
payload,
|
|
148
|
+
secret: TEST_SECRET,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function postStripeWebhook(payload: string, sig: string) {
|
|
153
|
+
return webhookApp.request("/api/subscription/webhook/stripe", {
|
|
154
|
+
method: "POST",
|
|
155
|
+
body: payload,
|
|
156
|
+
headers: { "stripe-signature": sig, "content-type": "application/json" },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// =============================================================================
|
|
161
|
+
// Scenarios
|
|
162
|
+
// =============================================================================
|
|
163
|
+
|
|
164
|
+
describe("scenario 1: Stripe-event → DB happy path", () => {
|
|
165
|
+
test("valid sig + bekannter event-type → subscription-row + subscription-event-row in DB", async () => {
|
|
166
|
+
const tenantStringId = testTenantId(4001);
|
|
167
|
+
const stripeEvent = buildStripeSubscriptionEvent({
|
|
168
|
+
eventId: "evt_4001_create",
|
|
169
|
+
tenantId: tenantStringId,
|
|
170
|
+
subscriptionId: "sub_4001",
|
|
171
|
+
customerId: "cus_4001",
|
|
172
|
+
priceId: "price_business_yearly",
|
|
173
|
+
});
|
|
174
|
+
const payload = JSON.stringify(stripeEvent);
|
|
175
|
+
const sig = signEvent(payload);
|
|
176
|
+
|
|
177
|
+
const res = await postStripeWebhook(payload, sig);
|
|
178
|
+
expect(res.status).toBe(200);
|
|
179
|
+
|
|
180
|
+
// Prüfe DB-state: subscription-row + subscription-event-row für
|
|
181
|
+
// diesen Tenant.
|
|
182
|
+
const admin = createTestUser({
|
|
183
|
+
id: 4001,
|
|
184
|
+
tenantId: tenantStringId,
|
|
185
|
+
roles: ["TenantAdmin", "SystemAdmin"],
|
|
186
|
+
});
|
|
187
|
+
const subs = (await stack.http.queryOk(
|
|
188
|
+
"billing-foundation:query:subscription:list",
|
|
189
|
+
{},
|
|
190
|
+
admin,
|
|
191
|
+
)) as { rows: Array<Record<string, unknown>> };
|
|
192
|
+
expect(subs.rows).toHaveLength(1);
|
|
193
|
+
expect(subs.rows[0]?.["providerName"]).toBe("stripe");
|
|
194
|
+
expect(subs.rows[0]?.["providerSubscriptionId"]).toBe("sub_4001");
|
|
195
|
+
expect(subs.rows[0]?.["providerCustomerId"]).toBe("cus_4001");
|
|
196
|
+
expect(subs.rows[0]?.["tier"]).toBe("business");
|
|
197
|
+
expect(subs.rows[0]?.["status"]).toBe("active");
|
|
198
|
+
// Drift-pin: deterministic aggregate-id matched zwischen Stripe-Plugin
|
|
199
|
+
// (foundation-side) und expected uuid.
|
|
200
|
+
expect(subs.rows[0]?.["id"]).toBe(subscriptionAggregateId(tenantStringId));
|
|
201
|
+
|
|
202
|
+
const esEvents = await loadAggregate(
|
|
203
|
+
db,
|
|
204
|
+
subscriptionAggregateId(tenantStringId),
|
|
205
|
+
tenantStringId,
|
|
206
|
+
);
|
|
207
|
+
expect(esEvents).toHaveLength(1);
|
|
208
|
+
expect(esEvents[0]?.type).toBe("billing-foundation:event:subscription-created");
|
|
209
|
+
expect(esEvents[0]?.metadata.headers?.["providerName"]).toBe("stripe");
|
|
210
|
+
expect(esEvents[0]?.metadata.headers?.["providerEventId"]).toBe("evt_4001_create");
|
|
211
|
+
// rawPayload wurde 1:1 in headers archiviert
|
|
212
|
+
const rawHeader = esEvents[0]?.metadata.headers?.["rawPayload"] as string;
|
|
213
|
+
const archivedRaw = JSON.parse(rawHeader) as { id: string };
|
|
214
|
+
expect(archivedRaw.id).toBe("evt_4001_create");
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("scenario 2: invalid sig → 401, kein DB-write", () => {
|
|
219
|
+
test("wrong webhook-secret → 401, foundation sieht keinen event", async () => {
|
|
220
|
+
const tenantStringId = testTenantId(4002);
|
|
221
|
+
const stripeEvent = buildStripeSubscriptionEvent({
|
|
222
|
+
eventId: "evt_4002_bad",
|
|
223
|
+
tenantId: tenantStringId,
|
|
224
|
+
subscriptionId: "sub_4002",
|
|
225
|
+
});
|
|
226
|
+
const payload = JSON.stringify(stripeEvent);
|
|
227
|
+
// Wrong secret = invalid sig.
|
|
228
|
+
const wrongSig = stripeForFixtures.webhooks.generateTestHeaderString({
|
|
229
|
+
payload,
|
|
230
|
+
secret: "whsec_wrong_secret",
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const res = await postStripeWebhook(payload, wrongSig);
|
|
234
|
+
expect(res.status).toBe(401);
|
|
235
|
+
const body = (await res.json()) as { error: { code: string } };
|
|
236
|
+
expect(body.error.code).toBe("subscription_webhook_signature_invalid");
|
|
237
|
+
|
|
238
|
+
// Drift-pin: foundation-DB ist unberührt — kein subscription-row
|
|
239
|
+
// für diesen Tenant entstanden.
|
|
240
|
+
const admin = createTestUser({
|
|
241
|
+
id: 4002,
|
|
242
|
+
tenantId: tenantStringId,
|
|
243
|
+
roles: ["TenantAdmin", "SystemAdmin"],
|
|
244
|
+
});
|
|
245
|
+
const subs = (await stack.http.queryOk(
|
|
246
|
+
"billing-foundation:query:subscription:list",
|
|
247
|
+
{},
|
|
248
|
+
admin,
|
|
249
|
+
)) as { rows: Array<Record<string, unknown>> };
|
|
250
|
+
expect(subs.rows).toHaveLength(0);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe("scenario 3: idempotency via Stripe-retry", () => {
|
|
255
|
+
test("derselbe Stripe-event 2× → 2. Mal foundation duplicate=true, kein zweiter event-row", async () => {
|
|
256
|
+
const tenantStringId = testTenantId(4003);
|
|
257
|
+
const stripeEvent = buildStripeSubscriptionEvent({
|
|
258
|
+
eventId: "evt_4003_retry",
|
|
259
|
+
tenantId: tenantStringId,
|
|
260
|
+
subscriptionId: "sub_4003",
|
|
261
|
+
});
|
|
262
|
+
const payload = JSON.stringify(stripeEvent);
|
|
263
|
+
const sig = signEvent(payload);
|
|
264
|
+
|
|
265
|
+
const res1 = await postStripeWebhook(payload, sig);
|
|
266
|
+
expect(res1.status).toBe(200);
|
|
267
|
+
const body1 = (await res1.json()) as { processed: boolean; duplicate: boolean };
|
|
268
|
+
expect(body1.duplicate).toBe(false);
|
|
269
|
+
|
|
270
|
+
// Stripe retry-storm — selber event mit selber providerEventId
|
|
271
|
+
const res2 = await postStripeWebhook(payload, sig);
|
|
272
|
+
expect(res2.status).toBe(200);
|
|
273
|
+
const body2 = (await res2.json()) as { processed: boolean; duplicate: boolean };
|
|
274
|
+
expect(body2.duplicate).toBe(true);
|
|
275
|
+
|
|
276
|
+
// Drift-pin: nur ein event im subscription-stream
|
|
277
|
+
const esEvents = await loadAggregate(
|
|
278
|
+
db,
|
|
279
|
+
subscriptionAggregateId(tenantStringId),
|
|
280
|
+
tenantStringId,
|
|
281
|
+
);
|
|
282
|
+
expect(esEvents).toHaveLength(1);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe("scenario 4: ignored event-types pass through", () => {
|
|
287
|
+
test("customer.created → 200 ignored, kein dispatch", async () => {
|
|
288
|
+
const tenantStringId = testTenantId(4004);
|
|
289
|
+
const stripeEvent = buildStripeSubscriptionEvent({
|
|
290
|
+
eventId: "evt_4004_ignored",
|
|
291
|
+
eventType: "customer.created",
|
|
292
|
+
tenantId: tenantStringId,
|
|
293
|
+
});
|
|
294
|
+
const payload = JSON.stringify(stripeEvent);
|
|
295
|
+
const sig = signEvent(payload);
|
|
296
|
+
|
|
297
|
+
const res = await postStripeWebhook(payload, sig);
|
|
298
|
+
expect(res.status).toBe(200);
|
|
299
|
+
const body = (await res.json()) as { ignored?: boolean; processed?: boolean };
|
|
300
|
+
expect(body.ignored).toBe(true);
|
|
301
|
+
expect(body.processed).toBeUndefined();
|
|
302
|
+
|
|
303
|
+
const admin = createTestUser({
|
|
304
|
+
id: 4004,
|
|
305
|
+
tenantId: tenantStringId,
|
|
306
|
+
roles: ["TenantAdmin", "SystemAdmin"],
|
|
307
|
+
});
|
|
308
|
+
const subs = (await stack.http.queryOk(
|
|
309
|
+
"billing-foundation:query:subscription:list",
|
|
310
|
+
{},
|
|
311
|
+
admin,
|
|
312
|
+
)) as { rows: Array<Record<string, unknown>> };
|
|
313
|
+
expect(subs.rows).toHaveLength(0);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// Unit-Tests für verifyAndParseStripeWebhook. Nutzt stripe.webhooks.
|
|
2
|
+
// generateTestHeaderString um valid sigs zu erzeugen — kein Mock,
|
|
3
|
+
// echter Stripe-SDK-roundtrip.
|
|
4
|
+
//
|
|
5
|
+
// Stripe-Event-Fixtures sind hier minimale Stripe-payloads die nur
|
|
6
|
+
// die Felder enthalten die der Plugin tatsächlich liest. Real Stripe-
|
|
7
|
+
// events sind >100 Felder; full-fidelity-fixtures wären Maintenance-
|
|
8
|
+
// Aufwand ohne Test-Wert.
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
SubscriptionEventTypes,
|
|
12
|
+
SubscriptionStatuses,
|
|
13
|
+
} from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
14
|
+
import Stripe from "stripe";
|
|
15
|
+
import { describe, expect, test } from "vitest";
|
|
16
|
+
import {
|
|
17
|
+
mapStripeEventType,
|
|
18
|
+
mapStripeStatus,
|
|
19
|
+
verifyAndParseStripeWebhook,
|
|
20
|
+
} from "../verify-webhook";
|
|
21
|
+
|
|
22
|
+
const TEST_SECRET = "whsec_test_secret_12345";
|
|
23
|
+
const TEST_API_KEY = "sk_test_dummy_apikey";
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// Test-helpers
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
const stripeForFixtures = new Stripe(TEST_API_KEY, { apiVersion: "2026-04-22.dahlia" });
|
|
30
|
+
|
|
31
|
+
function buildSubscriptionEvent(overrides: {
|
|
32
|
+
eventType?: string;
|
|
33
|
+
eventId?: string;
|
|
34
|
+
tenantId?: string;
|
|
35
|
+
status?: string;
|
|
36
|
+
priceId?: string;
|
|
37
|
+
customerId?: string;
|
|
38
|
+
subscriptionId?: string;
|
|
39
|
+
currentPeriodEndUnix?: number;
|
|
40
|
+
}) {
|
|
41
|
+
const eventId = overrides.eventId ?? "evt_test_001";
|
|
42
|
+
const eventType = overrides.eventType ?? "customer.subscription.created";
|
|
43
|
+
const subscriptionId = overrides.subscriptionId ?? "sub_test_001";
|
|
44
|
+
const customerId = overrides.customerId ?? "cus_test_001";
|
|
45
|
+
const periodEnd = overrides.currentPeriodEndUnix ?? 1_780_000_000;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
id: eventId,
|
|
49
|
+
object: "event",
|
|
50
|
+
api_version: "2026-04-22.dahlia",
|
|
51
|
+
created: 1_770_000_000,
|
|
52
|
+
type: eventType,
|
|
53
|
+
livemode: false,
|
|
54
|
+
pending_webhooks: 1,
|
|
55
|
+
request: { id: null, idempotency_key: null },
|
|
56
|
+
data: {
|
|
57
|
+
object: {
|
|
58
|
+
id: subscriptionId,
|
|
59
|
+
object: "subscription",
|
|
60
|
+
customer: customerId,
|
|
61
|
+
status: overrides.status ?? "active",
|
|
62
|
+
metadata: {
|
|
63
|
+
tenantId: overrides.tenantId ?? "tenant-test-1",
|
|
64
|
+
},
|
|
65
|
+
items: {
|
|
66
|
+
object: "list",
|
|
67
|
+
data: [
|
|
68
|
+
{
|
|
69
|
+
id: "si_test",
|
|
70
|
+
object: "subscription_item",
|
|
71
|
+
current_period_end: periodEnd,
|
|
72
|
+
price: {
|
|
73
|
+
id: overrides.priceId ?? "price_pro_monthly",
|
|
74
|
+
object: "price",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
has_more: false,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Erstellt einen valid Stripe-signed-Header für ein gegebenes payload. */
|
|
86
|
+
function signEvent(payload: string, secret = TEST_SECRET): string {
|
|
87
|
+
return stripeForFixtures.webhooks.generateTestHeaderString({
|
|
88
|
+
payload,
|
|
89
|
+
secret,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// =============================================================================
|
|
94
|
+
// Sig-verify
|
|
95
|
+
// =============================================================================
|
|
96
|
+
|
|
97
|
+
describe("verifyAndParseStripeWebhook — sig-verify", () => {
|
|
98
|
+
const verify = verifyAndParseStripeWebhook(stripeForFixtures, {
|
|
99
|
+
webhookSecret: TEST_SECRET,
|
|
100
|
+
priceToTier: { price_pro_monthly: "pro" },
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("happy path: valid sig + bekannter event-type → SubscriptionEvent", async () => {
|
|
104
|
+
const payload = JSON.stringify(buildSubscriptionEvent({}));
|
|
105
|
+
const sig = signEvent(payload);
|
|
106
|
+
|
|
107
|
+
const event = await verify(payload, { "stripe-signature": sig });
|
|
108
|
+
expect(event).not.toBeNull();
|
|
109
|
+
expect(event?.providerName).toBe("stripe");
|
|
110
|
+
expect(event?.providerEventId).toBe("evt_test_001");
|
|
111
|
+
expect(event?.type).toBe(SubscriptionEventTypes.created);
|
|
112
|
+
expect(event?.tenantId).toBe("tenant-test-1");
|
|
113
|
+
expect(event?.tier).toBe("pro");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("missing stripe-signature header → throws", async () => {
|
|
117
|
+
const payload = JSON.stringify(buildSubscriptionEvent({}));
|
|
118
|
+
await expect(verify(payload, {})).rejects.toThrow(/stripe-signature header missing/);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("wrong secret → sig-verify failed → throws", async () => {
|
|
122
|
+
const payload = JSON.stringify(buildSubscriptionEvent({}));
|
|
123
|
+
const sig = signEvent(payload, "whsec_wrong_secret");
|
|
124
|
+
await expect(verify(payload, { "stripe-signature": sig })).rejects.toThrow(
|
|
125
|
+
/signature verify failed/,
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("modified body → sig-verify failed (Replay-Protection)", async () => {
|
|
130
|
+
const original = JSON.stringify(buildSubscriptionEvent({}));
|
|
131
|
+
const sig = signEvent(original);
|
|
132
|
+
// Tamper with body — Stripe-sig matched die exakten bytes.
|
|
133
|
+
const tampered = original.replace("tenant-test-1", "tenant-attacker");
|
|
134
|
+
await expect(verify(tampered, { "stripe-signature": sig })).rejects.toThrow(
|
|
135
|
+
/signature verify failed/,
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// =============================================================================
|
|
141
|
+
// Event-filter + payload-extraction
|
|
142
|
+
// =============================================================================
|
|
143
|
+
|
|
144
|
+
describe("verifyAndParseStripeWebhook — event-filter", () => {
|
|
145
|
+
const verify = verifyAndParseStripeWebhook(stripeForFixtures, {
|
|
146
|
+
webhookSecret: TEST_SECRET,
|
|
147
|
+
priceToTier: { price_pro_monthly: "pro" },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("unbekannter event-type → null (foundation 200 ignored)", async () => {
|
|
151
|
+
// customer.created ist gültiger Stripe-event aber nicht in unserer
|
|
152
|
+
// 5-types-Whitelist.
|
|
153
|
+
const payload = JSON.stringify(buildSubscriptionEvent({ eventType: "customer.created" }));
|
|
154
|
+
const sig = signEvent(payload);
|
|
155
|
+
const event = await verify(payload, { "stripe-signature": sig });
|
|
156
|
+
expect(event).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("subscription.updated → SubscriptionEventTypes.updated", async () => {
|
|
160
|
+
const payload = JSON.stringify(
|
|
161
|
+
buildSubscriptionEvent({ eventType: "customer.subscription.updated" }),
|
|
162
|
+
);
|
|
163
|
+
const sig = signEvent(payload);
|
|
164
|
+
const event = await verify(payload, { "stripe-signature": sig });
|
|
165
|
+
expect(event?.type).toBe(SubscriptionEventTypes.updated);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("subscription.deleted → SubscriptionEventTypes.canceled", async () => {
|
|
169
|
+
const payload = JSON.stringify(
|
|
170
|
+
buildSubscriptionEvent({
|
|
171
|
+
eventType: "customer.subscription.deleted",
|
|
172
|
+
status: "canceled",
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
const sig = signEvent(payload);
|
|
176
|
+
const event = await verify(payload, { "stripe-signature": sig });
|
|
177
|
+
expect(event?.type).toBe(SubscriptionEventTypes.canceled);
|
|
178
|
+
expect(event?.status).toBe(SubscriptionStatuses.canceled);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("invoice-event ohne subscription-reference → null (one-shot-invoice)", async () => {
|
|
182
|
+
// Drift-Pin: Stripe one-shot-invoice (nicht recurring). Plugin
|
|
183
|
+
// versucht NICHT zu lazy-fetchen weil's keine sub-id zum fetchen
|
|
184
|
+
// gibt. Foundation 200 ignored.
|
|
185
|
+
const ev = {
|
|
186
|
+
id: "evt_invoice_oneshot",
|
|
187
|
+
object: "event",
|
|
188
|
+
api_version: "2026-04-22.dahlia",
|
|
189
|
+
created: 1_770_000_000,
|
|
190
|
+
type: "invoice.paid",
|
|
191
|
+
livemode: false,
|
|
192
|
+
pending_webhooks: 1,
|
|
193
|
+
request: { id: null, idempotency_key: null },
|
|
194
|
+
data: {
|
|
195
|
+
object: {
|
|
196
|
+
id: "in_001",
|
|
197
|
+
object: "invoice",
|
|
198
|
+
subscription: null,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
const payload = JSON.stringify(ev);
|
|
203
|
+
const sig = signEvent(payload);
|
|
204
|
+
const event = await verify(payload, { "stripe-signature": sig });
|
|
205
|
+
expect(event).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// =============================================================================
|
|
210
|
+
// Tenant-resolution + price-to-tier
|
|
211
|
+
// =============================================================================
|
|
212
|
+
|
|
213
|
+
describe("verifyAndParseStripeWebhook — tenant-resolution + price-to-tier", () => {
|
|
214
|
+
const verify = verifyAndParseStripeWebhook(stripeForFixtures, {
|
|
215
|
+
webhookSecret: TEST_SECRET,
|
|
216
|
+
priceToTier: { price_pro_monthly: "pro", price_business_yearly: "business" },
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("metadata.tenantId fehlt → null (App-Owner-Bug, foundation 200 ignored)", async () => {
|
|
220
|
+
const ev = buildSubscriptionEvent({});
|
|
221
|
+
// @ts-expect-error — entferne metadata für Test
|
|
222
|
+
ev.data.object.metadata = {};
|
|
223
|
+
const payload = JSON.stringify(ev);
|
|
224
|
+
const sig = signEvent(payload);
|
|
225
|
+
const event = await verify(payload, { "stripe-signature": sig });
|
|
226
|
+
expect(event).toBeNull();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("price-id im Mapping → korrekter tier-Wert", async () => {
|
|
230
|
+
const payload = JSON.stringify(buildSubscriptionEvent({ priceId: "price_business_yearly" }));
|
|
231
|
+
const sig = signEvent(payload);
|
|
232
|
+
const event = await verify(payload, { "stripe-signature": sig });
|
|
233
|
+
expect(event?.tier).toBe("business");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("price-id NICHT im Mapping → null", async () => {
|
|
237
|
+
const payload = JSON.stringify(buildSubscriptionEvent({ priceId: "price_unknown_xyz" }));
|
|
238
|
+
const sig = signEvent(payload);
|
|
239
|
+
const event = await verify(payload, { "stripe-signature": sig });
|
|
240
|
+
expect(event).toBeNull();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("currentPeriodEnd wird aus subscription.items[0].current_period_end (Unix-sec) zu ISO konvertiert", async () => {
|
|
244
|
+
const periodEndUnix = 1_780_000_000;
|
|
245
|
+
const payload = JSON.stringify(buildSubscriptionEvent({ currentPeriodEndUnix: periodEndUnix }));
|
|
246
|
+
const sig = signEvent(payload);
|
|
247
|
+
const event = await verify(payload, { "stripe-signature": sig });
|
|
248
|
+
// 1_780_000_000 sec = 2026-05-28T20:26:40Z (in ms: 1.78e12)
|
|
249
|
+
// Temporal.Instant.toString() droppt Trailing-Zeros — keine .000Z
|
|
250
|
+
expect(event?.currentPeriodEnd).toBe("2026-05-28T20:26:40Z");
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// =============================================================================
|
|
255
|
+
// Mapping-helpers (pure functions, kein Stripe-mock nötig)
|
|
256
|
+
// =============================================================================
|
|
257
|
+
|
|
258
|
+
describe("mapStripeEventType — drift-pin pro mapping", () => {
|
|
259
|
+
test("alle 5 Stripe-event-types → SubscriptionEventTypes", () => {
|
|
260
|
+
expect(mapStripeEventType("customer.subscription.created")).toBe(
|
|
261
|
+
SubscriptionEventTypes.created,
|
|
262
|
+
);
|
|
263
|
+
expect(mapStripeEventType("customer.subscription.updated")).toBe(
|
|
264
|
+
SubscriptionEventTypes.updated,
|
|
265
|
+
);
|
|
266
|
+
expect(mapStripeEventType("customer.subscription.deleted")).toBe(
|
|
267
|
+
SubscriptionEventTypes.canceled,
|
|
268
|
+
);
|
|
269
|
+
expect(mapStripeEventType("invoice.paid")).toBe(SubscriptionEventTypes.invoicePaid);
|
|
270
|
+
expect(mapStripeEventType("invoice.payment_failed")).toBe(
|
|
271
|
+
SubscriptionEventTypes.invoicePaymentFailed,
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("alles andere → null", () => {
|
|
276
|
+
expect(mapStripeEventType("customer.created")).toBeNull();
|
|
277
|
+
expect(mapStripeEventType("checkout.session.completed")).toBeNull();
|
|
278
|
+
expect(mapStripeEventType("ping")).toBeNull();
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe("mapStripeStatus — Stripe-status → normalized", () => {
|
|
283
|
+
test("active/trialing direkt", () => {
|
|
284
|
+
expect(mapStripeStatus("active")).toBe(SubscriptionStatuses.active);
|
|
285
|
+
expect(mapStripeStatus("trialing")).toBe(SubscriptionStatuses.trialing);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("past_due / unpaid / paused → past_due (= grace-period im Plattform-Tier)", () => {
|
|
289
|
+
// Drift-Pin: alle drei Stripe-grace-Status werden auf den einen
|
|
290
|
+
// normalisierten "past_due"-Status mapped. Wenn Stripe einen vierten
|
|
291
|
+
// grace-Status einführt müssen wir den explizit hinzufügen statt
|
|
292
|
+
// auf "incomplete" fallback'n (= würde tenant downgraden).
|
|
293
|
+
expect(mapStripeStatus("past_due")).toBe(SubscriptionStatuses.pastDue);
|
|
294
|
+
expect(mapStripeStatus("unpaid")).toBe(SubscriptionStatuses.pastDue);
|
|
295
|
+
expect(mapStripeStatus("paused")).toBe(SubscriptionStatuses.pastDue);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("canceled → canceled", () => {
|
|
299
|
+
expect(mapStripeStatus("canceled")).toBe(SubscriptionStatuses.canceled);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("incomplete / incomplete_expired → incomplete", () => {
|
|
303
|
+
expect(mapStripeStatus("incomplete")).toBe(SubscriptionStatuses.incomplete);
|
|
304
|
+
expect(mapStripeStatus("incomplete_expired")).toBe(SubscriptionStatuses.incomplete);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Feature name
|
|
2
|
+
export const SUBSCRIPTION_STRIPE_FEATURE = "subscription-stripe" as const;
|
|
3
|
+
|
|
4
|
+
// entityName under den der Plugin gegen "subscriptionProvider"
|
|
5
|
+
// registriert. Matcht den path-segment in der webhook-URL
|
|
6
|
+
// `/api/subscription/webhook/stripe`.
|
|
7
|
+
export const STRIPE_PROVIDER_NAME = "stripe" as const;
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Stripe-event-types die wir auf normalisierte SubscriptionEventTypes
|
|
11
|
+
// mappen. Stripe hat ~80 event-types insgesamt; wir filtern auf 5.
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
export const StripeEventTypes = {
|
|
15
|
+
customerSubscriptionCreated: "customer.subscription.created",
|
|
16
|
+
customerSubscriptionUpdated: "customer.subscription.updated",
|
|
17
|
+
customerSubscriptionDeleted: "customer.subscription.deleted",
|
|
18
|
+
invoicePaid: "invoice.paid",
|
|
19
|
+
invoicePaymentFailed: "invoice.payment_failed",
|
|
20
|
+
} as const;
|