@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,421 @@
|
|
|
1
|
+
// Integration-test: Mollie-Plugin → subscription-foundation → DB.
|
|
2
|
+
//
|
|
3
|
+
// Beweist die echte Verdrahtung — analog stripe-foundation.integration:
|
|
4
|
+
// 1. Mollie-webhook (form-urlencoded `id=tr_xxx`) kommt am
|
|
5
|
+
// webhook-handler an
|
|
6
|
+
// 2. createSubscriptionMollieFeature (echter factory) verifiziert +
|
|
7
|
+
// parsed via gemocktem MollieClientShape (= injection-port
|
|
8
|
+
// `_clientShapeForTests` damit Mollie-SDK 4.5.0 keine HTTP-calls
|
|
9
|
+
// macht; vi.mock ist im integration-guard-blocked)
|
|
10
|
+
// 3. Plugin returnt SubscriptionEvent → webhook-handler dispatched
|
|
11
|
+
// zu process-event-handler
|
|
12
|
+
// 4. process-event-handler schreibt subscription + subscription-event
|
|
13
|
+
// in die DB
|
|
14
|
+
//
|
|
15
|
+
// Drift-vector der ohne diesen Test fehlen würde: factory-Logik in
|
|
16
|
+
// createSubscriptionMollieFeature (drift-validation, plugin-registration,
|
|
17
|
+
// fetchAdapter-binding). Die plugin-methods sind separat in den
|
|
18
|
+
// Unit-Tests (verify-webhook.test.ts) abgedeckt, aber die Verdrahtung
|
|
19
|
+
// von factory bis foundation-DB-row beweist nur dieser Test.
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
billingFoundationFeature,
|
|
23
|
+
createSubscriptionWebhookHandler,
|
|
24
|
+
type SubscriptionProviderPlugin,
|
|
25
|
+
subscriptionAggregateId,
|
|
26
|
+
} from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
27
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
28
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
29
|
+
import { createEventsTable, loadAggregate } from "@cosmicdrift/kumiko-framework/event-store";
|
|
30
|
+
import {
|
|
31
|
+
createTestUser,
|
|
32
|
+
setupTestStack,
|
|
33
|
+
type TestStack,
|
|
34
|
+
testTenantId,
|
|
35
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
36
|
+
import type {
|
|
37
|
+
Payment as MolliePayment,
|
|
38
|
+
Subscription as MollieSubscription,
|
|
39
|
+
} from "@mollie/api-client";
|
|
40
|
+
import { Hono } from "hono";
|
|
41
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
42
|
+
import { createSubscriptionMollieFeature } from "../feature";
|
|
43
|
+
import type { MollieClientShape } from "../verify-webhook";
|
|
44
|
+
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// Mock-MollieClient — replay-fähige in-memory state
|
|
47
|
+
//
|
|
48
|
+
// Der test-state ist module-level, wird aber per `beforeEach` reset —
|
|
49
|
+
// jeder Test sieht eine clean state. Der mock-client closured den state
|
|
50
|
+
// einmal beim factory-mount; reset() mutiert die Maps in-place damit
|
|
51
|
+
// die Closure-Referenzen aktuell bleiben.
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
type MollieMockState = {
|
|
55
|
+
payments: Map<string, MolliePayment>;
|
|
56
|
+
subscriptionsByCustomer: Map<string, MollieSubscription[]>;
|
|
57
|
+
/** Tracked-create-calls für drift-pins. */
|
|
58
|
+
createCallCount: number;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const mockState: MollieMockState = {
|
|
62
|
+
payments: new Map(),
|
|
63
|
+
subscriptionsByCustomer: new Map(),
|
|
64
|
+
createCallCount: 0,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const mollieMockClient: MollieClientShape = {
|
|
68
|
+
payments: {
|
|
69
|
+
get: async (id: string) => {
|
|
70
|
+
const payment = mockState.payments.get(id);
|
|
71
|
+
if (!payment) throw new Error(`Mollie 404: payment ${id} not found`);
|
|
72
|
+
return payment;
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
customerSubscriptions: {
|
|
76
|
+
get: async (subId: string, customerId: string) => {
|
|
77
|
+
const subs = mockState.subscriptionsByCustomer.get(customerId) ?? [];
|
|
78
|
+
const sub = subs.find((s) => s.id === subId);
|
|
79
|
+
if (!sub) throw new Error(`Mollie 404: subscription ${subId} for ${customerId}`);
|
|
80
|
+
return sub;
|
|
81
|
+
},
|
|
82
|
+
list: async (customerId: string) => {
|
|
83
|
+
return mockState.subscriptionsByCustomer.get(customerId) ?? [];
|
|
84
|
+
},
|
|
85
|
+
create: async (customerId, params) => {
|
|
86
|
+
mockState.createCallCount += 1;
|
|
87
|
+
const newSub = buildMockSubscription({
|
|
88
|
+
id: `sub_created_${mockState.createCallCount}`,
|
|
89
|
+
customerId,
|
|
90
|
+
status: "active",
|
|
91
|
+
metadata: params.metadata,
|
|
92
|
+
});
|
|
93
|
+
const existing = mockState.subscriptionsByCustomer.get(customerId) ?? [];
|
|
94
|
+
mockState.subscriptionsByCustomer.set(customerId, [...existing, newSub]);
|
|
95
|
+
return newSub;
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const PRICE_TO_TIER = { plan_pro: "pro", plan_business: "business" };
|
|
101
|
+
const PRICE_TO_CONFIG = {
|
|
102
|
+
plan_pro: {
|
|
103
|
+
amountValue: "9.99",
|
|
104
|
+
amountCurrency: "EUR",
|
|
105
|
+
interval: "1 month",
|
|
106
|
+
description: "Pro Monthly",
|
|
107
|
+
},
|
|
108
|
+
plan_business: {
|
|
109
|
+
amountValue: "29.99",
|
|
110
|
+
amountCurrency: "EUR",
|
|
111
|
+
interval: "1 month",
|
|
112
|
+
description: "Business Monthly",
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// =============================================================================
|
|
117
|
+
// Setup
|
|
118
|
+
// =============================================================================
|
|
119
|
+
|
|
120
|
+
let stack: TestStack;
|
|
121
|
+
let db: DbConnection;
|
|
122
|
+
let webhookApp: Hono;
|
|
123
|
+
|
|
124
|
+
beforeAll(async () => {
|
|
125
|
+
// Echte factory mit injection-port. Das beweist factory-Logik
|
|
126
|
+
// (drift-validation, plugin-registration) im Test-pfad.
|
|
127
|
+
const mollieFeature = createSubscriptionMollieFeature({
|
|
128
|
+
apiKey: "test_dummy_apikey",
|
|
129
|
+
webhookUrl: "https://test.example.com/api/subscription/webhook/mollie",
|
|
130
|
+
priceToTier: PRICE_TO_TIER,
|
|
131
|
+
priceToConfig: PRICE_TO_CONFIG,
|
|
132
|
+
_clientShapeForTests: mollieMockClient,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
stack = await setupTestStack({
|
|
136
|
+
features: [billingFoundationFeature, mollieFeature],
|
|
137
|
+
});
|
|
138
|
+
db = stack.db;
|
|
139
|
+
// subscriptionsProjectionTable wird von setupTestStack automatisch
|
|
140
|
+
// gepusht (r.projection mit `table`-Property → auto-push).
|
|
141
|
+
await createEventsTable(db);
|
|
142
|
+
|
|
143
|
+
webhookApp = new Hono();
|
|
144
|
+
webhookApp.post(
|
|
145
|
+
"/api/subscription/webhook/:providerName",
|
|
146
|
+
createSubscriptionWebhookHandler({
|
|
147
|
+
dispatchWrite: async ({ handlerQn, payload, tenantId }) => {
|
|
148
|
+
const systemUser = createTestUser({
|
|
149
|
+
id: 1,
|
|
150
|
+
tenantId: tenantId as TenantId,
|
|
151
|
+
roles: ["SystemAdmin"],
|
|
152
|
+
});
|
|
153
|
+
const res = await stack.http.write(handlerQn, payload, systemUser);
|
|
154
|
+
const body = (await res.json()) as {
|
|
155
|
+
isSuccess?: boolean;
|
|
156
|
+
data?: unknown;
|
|
157
|
+
error?: unknown;
|
|
158
|
+
};
|
|
159
|
+
return body.isSuccess
|
|
160
|
+
? { isSuccess: true, ...(body.data !== undefined && { data: body.data }) }
|
|
161
|
+
: { isSuccess: false, ...(body.error !== undefined && { error: body.error }) };
|
|
162
|
+
},
|
|
163
|
+
resolveProvider: (providerName) => {
|
|
164
|
+
const usage = stack.registry
|
|
165
|
+
.getExtensionUsages("subscriptionProvider")
|
|
166
|
+
.find((u) => u.entityName === providerName);
|
|
167
|
+
// @cast-boundary engine-payload — extension-usage carries unknown options
|
|
168
|
+
return usage?.options as SubscriptionProviderPlugin | undefined;
|
|
169
|
+
},
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
afterAll(async () => {
|
|
175
|
+
await stack.cleanup();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
beforeEach(() => {
|
|
179
|
+
mockState.payments.clear();
|
|
180
|
+
mockState.subscriptionsByCustomer.clear();
|
|
181
|
+
mockState.createCallCount = 0;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// =============================================================================
|
|
185
|
+
// Fixtures
|
|
186
|
+
// =============================================================================
|
|
187
|
+
|
|
188
|
+
function buildMockPayment(overrides: Partial<Record<string, unknown>> = {}): MolliePayment {
|
|
189
|
+
// @cast-boundary mollie-sdk — minimal mock-shape, nur Felder die der Plugin liest
|
|
190
|
+
return {
|
|
191
|
+
id: "tr_test_001",
|
|
192
|
+
customerId: "cst_test_001",
|
|
193
|
+
subscriptionId: "sub_test_001",
|
|
194
|
+
sequenceType: "first",
|
|
195
|
+
status: "paid",
|
|
196
|
+
metadata: {},
|
|
197
|
+
...overrides,
|
|
198
|
+
} as MolliePayment;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function buildMockSubscription(
|
|
202
|
+
overrides: Partial<Record<string, unknown>> = {},
|
|
203
|
+
): MollieSubscription {
|
|
204
|
+
// @cast-boundary mollie-sdk — minimal mock-shape, nur Felder die der Plugin liest
|
|
205
|
+
return {
|
|
206
|
+
id: "sub_test_001",
|
|
207
|
+
customerId: "cst_test_001",
|
|
208
|
+
status: "active",
|
|
209
|
+
nextPaymentDate: "2026-12-01",
|
|
210
|
+
startDate: "2026-05-01",
|
|
211
|
+
metadata: {},
|
|
212
|
+
...overrides,
|
|
213
|
+
} as MollieSubscription;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function postMollieWebhook(id: string) {
|
|
217
|
+
return webhookApp.request("/api/subscription/webhook/mollie", {
|
|
218
|
+
method: "POST",
|
|
219
|
+
body: `id=${id}`,
|
|
220
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// =============================================================================
|
|
225
|
+
// Scenarios
|
|
226
|
+
// =============================================================================
|
|
227
|
+
|
|
228
|
+
describe("scenario 1: Mollie-event → DB happy path", () => {
|
|
229
|
+
test("recurring-payment paid → fetch sub → invoicePaid event → DB-row geupdated", async () => {
|
|
230
|
+
const tenantStringId = testTenantId(5001);
|
|
231
|
+
|
|
232
|
+
// 1. First mandate-setup-payment (= subscription-created event).
|
|
233
|
+
// Real-world: Mollie sendet first-payment-paid → Plugin
|
|
234
|
+
// erstellt sub on-the-fly → Created-event landed in DB.
|
|
235
|
+
mockState.payments.set(
|
|
236
|
+
"tr_5001_first",
|
|
237
|
+
buildMockPayment({
|
|
238
|
+
id: "tr_5001_first",
|
|
239
|
+
customerId: "cst_5001",
|
|
240
|
+
subscriptionId: null,
|
|
241
|
+
sequenceType: "first",
|
|
242
|
+
status: "paid",
|
|
243
|
+
metadata: { tenantId: tenantStringId, priceId: "plan_pro" },
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
await postMollieWebhook("tr_5001_first");
|
|
247
|
+
|
|
248
|
+
// 2. Existing sub jetzt im mock-state. Recurring-charge kommt:
|
|
249
|
+
mockState.payments.set(
|
|
250
|
+
"tr_5001_renewal",
|
|
251
|
+
buildMockPayment({
|
|
252
|
+
id: "tr_5001_renewal",
|
|
253
|
+
customerId: "cst_5001",
|
|
254
|
+
subscriptionId: "sub_created_1",
|
|
255
|
+
sequenceType: "recurring",
|
|
256
|
+
status: "paid",
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const res = await postMollieWebhook("tr_5001_renewal");
|
|
261
|
+
expect(res.status).toBe(200);
|
|
262
|
+
|
|
263
|
+
const admin = createTestUser({
|
|
264
|
+
id: 5001,
|
|
265
|
+
tenantId: tenantStringId,
|
|
266
|
+
roles: ["TenantAdmin", "SystemAdmin"],
|
|
267
|
+
});
|
|
268
|
+
const subs = (await stack.http.queryOk(
|
|
269
|
+
"billing-foundation:query:subscription:list",
|
|
270
|
+
{},
|
|
271
|
+
admin,
|
|
272
|
+
)) as { rows: Array<Record<string, unknown>> };
|
|
273
|
+
expect(subs.rows).toHaveLength(1);
|
|
274
|
+
expect(subs.rows[0]?.["providerName"]).toBe("mollie");
|
|
275
|
+
// Looser assertion: dependet nicht auf mock-impl-detail (= "sub_created_${count}").
|
|
276
|
+
// Wichtig: subscriptionId ist VOM PLUGIN gesetzt (kein payment-id), nicht null.
|
|
277
|
+
expect(subs.rows[0]?.["providerSubscriptionId"]).toMatch(/^sub_/);
|
|
278
|
+
expect(subs.rows[0]?.["tier"]).toBe("pro");
|
|
279
|
+
expect(subs.rows[0]?.["status"]).toBe("active");
|
|
280
|
+
expect(subs.rows[0]?.["id"]).toBe(subscriptionAggregateId(tenantStringId));
|
|
281
|
+
|
|
282
|
+
const esEvents = await loadAggregate(
|
|
283
|
+
db,
|
|
284
|
+
subscriptionAggregateId(tenantStringId),
|
|
285
|
+
tenantStringId,
|
|
286
|
+
);
|
|
287
|
+
// create + renewal beide im stream
|
|
288
|
+
expect(esEvents).toHaveLength(2);
|
|
289
|
+
expect(esEvents[0]?.type).toBe("billing-foundation:event:subscription-created");
|
|
290
|
+
expect(esEvents[1]?.type).toBe("billing-foundation:event:invoice-paid");
|
|
291
|
+
expect(esEvents[1]?.metadata.headers?.["providerEventId"]).toBe("tr_5001_renewal");
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe("scenario 2: mandate-setup-flow — first-payment-paid OHNE existing sub → Plugin erstellt sub on-the-fly", () => {
|
|
296
|
+
test("Plugin ruft customerSubscriptions.create + emit Created-Event → subscription-row in DB", async () => {
|
|
297
|
+
const tenantStringId = testTenantId(5002);
|
|
298
|
+
|
|
299
|
+
mockState.payments.set(
|
|
300
|
+
"tr_5002_first",
|
|
301
|
+
buildMockPayment({
|
|
302
|
+
id: "tr_5002_first",
|
|
303
|
+
customerId: "cst_5002",
|
|
304
|
+
subscriptionId: null,
|
|
305
|
+
sequenceType: "first",
|
|
306
|
+
status: "paid",
|
|
307
|
+
metadata: { tenantId: tenantStringId, priceId: "plan_business" },
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const res = await postMollieWebhook("tr_5002_first");
|
|
312
|
+
expect(res.status).toBe(200);
|
|
313
|
+
|
|
314
|
+
expect(mockState.createCallCount).toBe(1);
|
|
315
|
+
|
|
316
|
+
const admin = createTestUser({
|
|
317
|
+
id: 5002,
|
|
318
|
+
tenantId: tenantStringId,
|
|
319
|
+
roles: ["TenantAdmin", "SystemAdmin"],
|
|
320
|
+
});
|
|
321
|
+
const subs = (await stack.http.queryOk(
|
|
322
|
+
"billing-foundation:query:subscription:list",
|
|
323
|
+
{},
|
|
324
|
+
admin,
|
|
325
|
+
)) as { rows: Array<Record<string, unknown>> };
|
|
326
|
+
expect(subs.rows).toHaveLength(1);
|
|
327
|
+
expect(subs.rows[0]?.["tier"]).toBe("business");
|
|
328
|
+
expect(subs.rows[0]?.["status"]).toBe("active");
|
|
329
|
+
// Drift-pin: providerSubscriptionId ist die VOM PLUGIN ERSTELLTE sub-id,
|
|
330
|
+
// nicht der payment-id.
|
|
331
|
+
// Looser assertion: dependet nicht auf mock-impl-detail (= "sub_created_${count}").
|
|
332
|
+
// Wichtig: subscriptionId ist VOM PLUGIN gesetzt (kein payment-id), nicht null.
|
|
333
|
+
expect(subs.rows[0]?.["providerSubscriptionId"]).toMatch(/^sub_/);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe("scenario 3: idempotency via Mollie-retry", () => {
|
|
338
|
+
test("derselbe tr_xxx 2× → 2. Mal foundation duplicate=true, kein zweiter event-row, kein zweiter sub-create", async () => {
|
|
339
|
+
const tenantStringId = testTenantId(5003);
|
|
340
|
+
|
|
341
|
+
mockState.payments.set(
|
|
342
|
+
"tr_5003_retry",
|
|
343
|
+
buildMockPayment({
|
|
344
|
+
id: "tr_5003_retry",
|
|
345
|
+
customerId: "cst_5003",
|
|
346
|
+
subscriptionId: null,
|
|
347
|
+
sequenceType: "first",
|
|
348
|
+
status: "paid",
|
|
349
|
+
metadata: { tenantId: tenantStringId, priceId: "plan_pro" },
|
|
350
|
+
}),
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
const res1 = await postMollieWebhook("tr_5003_retry");
|
|
354
|
+
expect(res1.status).toBe(200);
|
|
355
|
+
const body1 = (await res1.json()) as { processed?: boolean; duplicate?: boolean };
|
|
356
|
+
expect(body1.duplicate).toBe(false);
|
|
357
|
+
|
|
358
|
+
const res2 = await postMollieWebhook("tr_5003_retry");
|
|
359
|
+
expect(res2.status).toBe(200);
|
|
360
|
+
const body2 = (await res2.json()) as { duplicate?: boolean };
|
|
361
|
+
expect(body2.duplicate).toBe(true);
|
|
362
|
+
|
|
363
|
+
// Drift-pin: ensureSubscriptionForMandate fand die existing sub via list.
|
|
364
|
+
expect(mockState.createCallCount).toBe(1);
|
|
365
|
+
|
|
366
|
+
const esEvents = await loadAggregate(
|
|
367
|
+
db,
|
|
368
|
+
subscriptionAggregateId(tenantStringId),
|
|
369
|
+
tenantStringId,
|
|
370
|
+
);
|
|
371
|
+
expect(esEvents).toHaveLength(1);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
describe("scenario 4: error + ignored paths", () => {
|
|
376
|
+
test("body ohne id → 401 (Plugin throws, foundation mapped auf signature_invalid)", async () => {
|
|
377
|
+
const res = await webhookApp.request("/api/subscription/webhook/mollie", {
|
|
378
|
+
method: "POST",
|
|
379
|
+
body: "no-id-field",
|
|
380
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
381
|
+
});
|
|
382
|
+
expect(res.status).toBe(401);
|
|
383
|
+
const body = (await res.json()) as { error: { code: string } };
|
|
384
|
+
expect(body.error.code).toBe("subscription_webhook_signature_invalid");
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("sub_xxx-direct-event → 200 ignored, kein DB-write", async () => {
|
|
388
|
+
const tenantStringId = testTenantId(5004);
|
|
389
|
+
const res = await postMollieWebhook("sub_5004_direct");
|
|
390
|
+
expect(res.status).toBe(200);
|
|
391
|
+
const body = (await res.json()) as { ignored?: boolean };
|
|
392
|
+
expect(body.ignored).toBe(true);
|
|
393
|
+
|
|
394
|
+
const admin = createTestUser({
|
|
395
|
+
id: 5004,
|
|
396
|
+
tenantId: tenantStringId,
|
|
397
|
+
roles: ["TenantAdmin", "SystemAdmin"],
|
|
398
|
+
});
|
|
399
|
+
const subs = (await stack.http.queryOk(
|
|
400
|
+
"billing-foundation:query:subscription:list",
|
|
401
|
+
{},
|
|
402
|
+
admin,
|
|
403
|
+
)) as { rows: Array<Record<string, unknown>> };
|
|
404
|
+
expect(subs.rows).toHaveLength(0);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("provider not mounted → 404 (multi-provider-routing drift-pin)", async () => {
|
|
408
|
+
// Drift-pin: nur "mollie" ist gemountet. Wenn jemand den webhook-
|
|
409
|
+
// handler refactored sodass er ALLE requests an das erste plugin
|
|
410
|
+
// routet, wäre dieser Test grün — bricht aber wenn der Test einen
|
|
411
|
+
// unbekannten provider-name fordert.
|
|
412
|
+
const res = await webhookApp.request("/api/subscription/webhook/paypal", {
|
|
413
|
+
method: "POST",
|
|
414
|
+
body: "id=tr_dummy",
|
|
415
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
416
|
+
});
|
|
417
|
+
expect(res.status).toBe(404);
|
|
418
|
+
const body = (await res.json()) as { error: { code: string } };
|
|
419
|
+
expect(body.error.code).toBe("subscription_provider_not_registered");
|
|
420
|
+
});
|
|
421
|
+
});
|