@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.
Files changed (333) hide show
  1. package/package.json +90 -0
  2. package/src/audit/__tests__/audit.integration.ts +328 -0
  3. package/src/audit/constants.ts +7 -0
  4. package/src/audit/feature.ts +23 -0
  5. package/src/audit/handlers/list.query.ts +98 -0
  6. package/src/audit/index.ts +2 -0
  7. package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.ts +149 -0
  8. package/src/auth-email-password/__tests__/account-lockout.integration.ts +308 -0
  9. package/src/auth-email-password/__tests__/auth-claims.integration.ts +512 -0
  10. package/src/auth-email-password/__tests__/auth.integration.ts +610 -0
  11. package/src/auth-email-password/__tests__/confirm-token-flow.test.ts +67 -0
  12. package/src/auth-email-password/__tests__/email-templates.test.ts +106 -0
  13. package/src/auth-email-password/__tests__/email-verification.integration.ts +327 -0
  14. package/src/auth-email-password/__tests__/identity-v3-hash.test.ts +174 -0
  15. package/src/auth-email-password/__tests__/identity-v3-login.integration.ts +150 -0
  16. package/src/auth-email-password/__tests__/invite-flow.integration.ts +458 -0
  17. package/src/auth-email-password/__tests__/multi-roles.integration.ts +256 -0
  18. package/src/auth-email-password/__tests__/password-reset.integration.ts +346 -0
  19. package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.ts +144 -0
  20. package/src/auth-email-password/__tests__/seed-admin.integration.ts +176 -0
  21. package/src/auth-email-password/__tests__/session-callbacks.integration.ts +310 -0
  22. package/src/auth-email-password/__tests__/session-strict-mode.integration.ts +101 -0
  23. package/src/auth-email-password/__tests__/signed-token.test.ts +78 -0
  24. package/src/auth-email-password/__tests__/signup-flow.integration.ts +259 -0
  25. package/src/auth-email-password/auth-user-row.ts +41 -0
  26. package/src/auth-email-password/constants.ts +101 -0
  27. package/src/auth-email-password/email-templates.ts +283 -0
  28. package/src/auth-email-password/feature.ts +140 -0
  29. package/src/auth-email-password/handlers/change-password.write.ts +58 -0
  30. package/src/auth-email-password/handlers/confirm-token-flow.ts +191 -0
  31. package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +203 -0
  32. package/src/auth-email-password/handlers/invite-accept.write.ts +189 -0
  33. package/src/auth-email-password/handlers/invite-create.write.ts +145 -0
  34. package/src/auth-email-password/handlers/invite-signup-complete.write.ts +192 -0
  35. package/src/auth-email-password/handlers/login.write.ts +208 -0
  36. package/src/auth-email-password/handlers/logout.write.ts +12 -0
  37. package/src/auth-email-password/handlers/request-email-verification.write.ts +29 -0
  38. package/src/auth-email-password/handlers/request-password-reset.write.ts +31 -0
  39. package/src/auth-email-password/handlers/reset-password.write.ts +61 -0
  40. package/src/auth-email-password/handlers/signup-confirm.write.ts +170 -0
  41. package/src/auth-email-password/handlers/signup-request.write.ts +104 -0
  42. package/src/auth-email-password/handlers/token-request-handler.ts +114 -0
  43. package/src/auth-email-password/handlers/verify-email.write.ts +62 -0
  44. package/src/auth-email-password/i18n.ts +211 -0
  45. package/src/auth-email-password/identity-v3-hash.ts +97 -0
  46. package/src/auth-email-password/index.ts +35 -0
  47. package/src/auth-email-password/invite-token-store.ts +92 -0
  48. package/src/auth-email-password/lockout-store.ts +118 -0
  49. package/src/auth-email-password/password-hashing.ts +43 -0
  50. package/src/auth-email-password/reset-token.ts +28 -0
  51. package/src/auth-email-password/seeding.ts +183 -0
  52. package/src/auth-email-password/signed-token.ts +85 -0
  53. package/src/auth-email-password/signup-token-store.ts +104 -0
  54. package/src/auth-email-password/stream-tenant.ts +31 -0
  55. package/src/auth-email-password/testing.ts +5 -0
  56. package/src/auth-email-password/token-burn-store.ts +57 -0
  57. package/src/auth-email-password/verification-token.ts +27 -0
  58. package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +51 -0
  59. package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +80 -0
  60. package/src/auth-email-password/web/__tests__/login-screen.test.tsx +94 -0
  61. package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +108 -0
  62. package/src/auth-email-password/web/__tests__/session-roles.test.ts +54 -0
  63. package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +100 -0
  64. package/src/auth-email-password/web/__tests__/test-utils.tsx +73 -0
  65. package/src/auth-email-password/web/__tests__/user-menu.test.tsx +55 -0
  66. package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +59 -0
  67. package/src/auth-email-password/web/auth-client.ts +350 -0
  68. package/src/auth-email-password/web/auth-form-primitives.tsx +70 -0
  69. package/src/auth-email-password/web/auth-gate.tsx +33 -0
  70. package/src/auth-email-password/web/client-plugin.ts +48 -0
  71. package/src/auth-email-password/web/default-topbar-actions.tsx +47 -0
  72. package/src/auth-email-password/web/forgot-password-screen.tsx +110 -0
  73. package/src/auth-email-password/web/index.ts +56 -0
  74. package/src/auth-email-password/web/invite-accept-screen.tsx +220 -0
  75. package/src/auth-email-password/web/login-screen.tsx +150 -0
  76. package/src/auth-email-password/web/reset-password-screen.tsx +152 -0
  77. package/src/auth-email-password/web/session.tsx +171 -0
  78. package/src/auth-email-password/web/signup-complete-screen.tsx +150 -0
  79. package/src/auth-email-password/web/signup-screen.tsx +130 -0
  80. package/src/auth-email-password/web/tenant-switcher.tsx +116 -0
  81. package/src/auth-email-password/web/use-shell-user.ts +34 -0
  82. package/src/auth-email-password/web/user-menu.tsx +89 -0
  83. package/src/auth-email-password/web/verify-email-screen.tsx +102 -0
  84. package/src/billing-foundation/__tests__/billing-foundation.integration.ts +568 -0
  85. package/src/billing-foundation/__tests__/feature.test.ts +110 -0
  86. package/src/billing-foundation/__tests__/webhook-handler.test.ts +199 -0
  87. package/src/billing-foundation/aggregate-id.ts +21 -0
  88. package/src/billing-foundation/constants.ts +70 -0
  89. package/src/billing-foundation/entities.ts +50 -0
  90. package/src/billing-foundation/events.ts +71 -0
  91. package/src/billing-foundation/feature.ts +122 -0
  92. package/src/billing-foundation/get-subscription-for-tenant.ts +39 -0
  93. package/src/billing-foundation/handlers/create-checkout-session.write.ts +79 -0
  94. package/src/billing-foundation/handlers/create-portal-session.write.ts +73 -0
  95. package/src/billing-foundation/handlers/list-subscriptions.query.ts +20 -0
  96. package/src/billing-foundation/handlers/process-event.write.ts +160 -0
  97. package/src/billing-foundation/index.ts +42 -0
  98. package/src/billing-foundation/projection.ts +135 -0
  99. package/src/billing-foundation/types.ts +157 -0
  100. package/src/billing-foundation/webhook-handler.ts +184 -0
  101. package/src/cap-counter/__tests__/cap-counter.integration.ts +566 -0
  102. package/src/cap-counter/__tests__/enforce-cap.test.ts +422 -0
  103. package/src/cap-counter/__tests__/with-cap-enforcement.integration.ts +265 -0
  104. package/src/cap-counter/aggregate-id.ts +61 -0
  105. package/src/cap-counter/constants.ts +32 -0
  106. package/src/cap-counter/enforce-cap.ts +404 -0
  107. package/src/cap-counter/entity.ts +48 -0
  108. package/src/cap-counter/feature.ts +90 -0
  109. package/src/cap-counter/handlers/get-counter.query.ts +43 -0
  110. package/src/cap-counter/handlers/increment-rolling.write.ts +79 -0
  111. package/src/cap-counter/handlers/increment.write.ts +92 -0
  112. package/src/cap-counter/handlers/mark-soft-warned.write.ts +57 -0
  113. package/src/cap-counter/index.ts +34 -0
  114. package/src/cap-counter/with-cap-enforcement.ts +179 -0
  115. package/src/channel-email/email-channel.ts +48 -0
  116. package/src/channel-email/feature.ts +15 -0
  117. package/src/channel-email/index.ts +4 -0
  118. package/src/channel-email/smtp-transport.ts +65 -0
  119. package/src/channel-email/types.ts +34 -0
  120. package/src/channel-in-app/constants.ts +11 -0
  121. package/src/channel-in-app/feature.ts +30 -0
  122. package/src/channel-in-app/handlers/inbox.query.ts +28 -0
  123. package/src/channel-in-app/handlers/mark-all-read.write.ts +21 -0
  124. package/src/channel-in-app/handlers/mark-read.write.ts +32 -0
  125. package/src/channel-in-app/handlers/unread-count.query.ts +20 -0
  126. package/src/channel-in-app/in-app-channel.ts +44 -0
  127. package/src/channel-in-app/index.ts +4 -0
  128. package/src/channel-in-app/tables.ts +22 -0
  129. package/src/channel-push/feature.ts +15 -0
  130. package/src/channel-push/index.ts +3 -0
  131. package/src/channel-push/push-channel.ts +33 -0
  132. package/src/channel-push/types.ts +22 -0
  133. package/src/config/__tests__/app-overrides.test.ts +118 -0
  134. package/src/config/__tests__/config.integration.ts +1246 -0
  135. package/src/config/constants.ts +23 -0
  136. package/src/config/feature.ts +117 -0
  137. package/src/config/handlers/__tests__/prepare-config-write.test.ts +209 -0
  138. package/src/config/handlers/reset.write.ts +45 -0
  139. package/src/config/handlers/schema.query.ts +22 -0
  140. package/src/config/handlers/set.write.ts +93 -0
  141. package/src/config/handlers/values.query.ts +43 -0
  142. package/src/config/index.ts +15 -0
  143. package/src/config/resolver.ts +283 -0
  144. package/src/config/table.ts +35 -0
  145. package/src/config/write-helpers.ts +268 -0
  146. package/src/delivery/__tests__/delivery-events.integration.ts +166 -0
  147. package/src/delivery/__tests__/delivery.integration.ts +1405 -0
  148. package/src/delivery/constants.ts +33 -0
  149. package/src/delivery/delivery-service.ts +489 -0
  150. package/src/delivery/events.ts +18 -0
  151. package/src/delivery/feature.ts +70 -0
  152. package/src/delivery/handlers/log.query.ts +21 -0
  153. package/src/delivery/handlers/preferences.query.ts +18 -0
  154. package/src/delivery/handlers/set-preference.write.ts +28 -0
  155. package/src/delivery/index.ts +35 -0
  156. package/src/delivery/tables.ts +74 -0
  157. package/src/delivery/testing.ts +47 -0
  158. package/src/delivery/types.ts +71 -0
  159. package/src/delivery/unsubscribe.ts +99 -0
  160. package/src/delivery/upsert-preference.ts +145 -0
  161. package/src/feature-toggles/__tests__/feature-toggles.integration.ts +687 -0
  162. package/src/feature-toggles/constants.ts +20 -0
  163. package/src/feature-toggles/events.ts +18 -0
  164. package/src/feature-toggles/feature.ts +98 -0
  165. package/src/feature-toggles/global-feature-state-table.ts +28 -0
  166. package/src/feature-toggles/handlers/list.query.ts +26 -0
  167. package/src/feature-toggles/handlers/registered.query.ts +56 -0
  168. package/src/feature-toggles/handlers/set.write.ts +158 -0
  169. package/src/feature-toggles/index.ts +9 -0
  170. package/src/feature-toggles/toggle-runtime.ts +73 -0
  171. package/src/file-foundation/__tests__/feature.test.ts +35 -0
  172. package/src/file-foundation/__tests__/file-foundation.integration.ts +235 -0
  173. package/src/file-foundation/feature.ts +123 -0
  174. package/src/file-foundation/index.ts +7 -0
  175. package/src/file-provider-inmemory/__tests__/feature.test.ts +35 -0
  176. package/src/file-provider-inmemory/feature.ts +73 -0
  177. package/src/file-provider-inmemory/index.ts +3 -0
  178. package/src/file-provider-s3/__tests__/feature.test.ts +54 -0
  179. package/src/file-provider-s3/feature.ts +169 -0
  180. package/src/file-provider-s3/index.ts +3 -0
  181. package/src/files-provider-s3/__tests__/env-helper.test.ts +161 -0
  182. package/src/files-provider-s3/__tests__/s3-provider.integration.ts +134 -0
  183. package/src/files-provider-s3/__tests__/s3-provider.test.ts +36 -0
  184. package/src/files-provider-s3/env-helper.ts +49 -0
  185. package/src/files-provider-s3/index.ts +3 -0
  186. package/src/files-provider-s3/s3-provider.ts +114 -0
  187. package/src/foundation-shared/config-helpers.ts +67 -0
  188. package/src/foundation-shared/index.ts +4 -0
  189. package/src/jobs/__tests__/job-system-user.integration.ts +194 -0
  190. package/src/jobs/__tests__/jobs-events.integration.ts +143 -0
  191. package/src/jobs/__tests__/jobs-feature.integration.ts +342 -0
  192. package/src/jobs/constants.ts +21 -0
  193. package/src/jobs/events.ts +39 -0
  194. package/src/jobs/feature.ts +150 -0
  195. package/src/jobs/handlers/detail.query.ts +30 -0
  196. package/src/jobs/handlers/list.query.ts +36 -0
  197. package/src/jobs/handlers/retry.write.ts +69 -0
  198. package/src/jobs/handlers/trigger.write.ts +39 -0
  199. package/src/jobs/index.ts +5 -0
  200. package/src/jobs/job-run-logger.ts +213 -0
  201. package/src/jobs/job-run-table.ts +55 -0
  202. package/src/legal-pages/README.md +195 -0
  203. package/src/legal-pages/__tests__/legal-pages.integration.ts +361 -0
  204. package/src/legal-pages/constants.ts +36 -0
  205. package/src/legal-pages/feature.ts +187 -0
  206. package/src/legal-pages/index.ts +13 -0
  207. package/src/legal-pages/markdown.ts +69 -0
  208. package/src/mail-foundation/__tests__/feature.test.ts +46 -0
  209. package/src/mail-foundation/__tests__/mail-foundation.integration.ts +247 -0
  210. package/src/mail-foundation/feature.ts +160 -0
  211. package/src/mail-foundation/index.ts +14 -0
  212. package/src/mail-transport-inmemory/__tests__/feature.test.ts +37 -0
  213. package/src/mail-transport-inmemory/feature.ts +90 -0
  214. package/src/mail-transport-inmemory/index.ts +3 -0
  215. package/src/mail-transport-smtp/__tests__/feature.test.ts +61 -0
  216. package/src/mail-transport-smtp/feature.ts +182 -0
  217. package/src/mail-transport-smtp/index.ts +3 -0
  218. package/src/rate-limiting/__tests__/rate-limiting.integration.ts +84 -0
  219. package/src/rate-limiting/constants.ts +9 -0
  220. package/src/rate-limiting/feature.ts +16 -0
  221. package/src/rate-limiting/handlers/status.query.ts +52 -0
  222. package/src/rate-limiting/index.ts +2 -0
  223. package/src/renderer-simple/__tests__/simple-renderer.test.ts +97 -0
  224. package/src/renderer-simple/feature.ts +12 -0
  225. package/src/renderer-simple/index.ts +2 -0
  226. package/src/renderer-simple/simple-renderer.ts +72 -0
  227. package/src/secrets/__tests__/rotate.integration.ts +176 -0
  228. package/src/secrets/__tests__/secrets-events.integration.ts +125 -0
  229. package/src/secrets/__tests__/secrets.integration.ts +118 -0
  230. package/src/secrets/feature.ts +84 -0
  231. package/src/secrets/handlers/delete.write.ts +20 -0
  232. package/src/secrets/handlers/list.query.ts +38 -0
  233. package/src/secrets/handlers/rotate.job.ts +193 -0
  234. package/src/secrets/handlers/set.write.ts +50 -0
  235. package/src/secrets/index.ts +16 -0
  236. package/src/secrets/secrets-context.ts +296 -0
  237. package/src/secrets/table.ts +68 -0
  238. package/src/sessions/__tests__/cleanup.integration.ts +175 -0
  239. package/src/sessions/__tests__/password-auto-revoke.integration.ts +202 -0
  240. package/src/sessions/__tests__/sessions.integration.ts +472 -0
  241. package/src/sessions/__tests__/test-helpers.ts +66 -0
  242. package/src/sessions/constants.ts +43 -0
  243. package/src/sessions/feature.ts +84 -0
  244. package/src/sessions/handlers/cleanup.job.ts +109 -0
  245. package/src/sessions/handlers/list.query.ts +35 -0
  246. package/src/sessions/handlers/mine.query.ts +37 -0
  247. package/src/sessions/handlers/revoke-all-others.write.ts +42 -0
  248. package/src/sessions/handlers/revoke.write.ts +76 -0
  249. package/src/sessions/index.ts +17 -0
  250. package/src/sessions/schema/index.ts +5 -0
  251. package/src/sessions/schema/user-session.ts +67 -0
  252. package/src/sessions/session-callbacks.ts +110 -0
  253. package/src/sessions/testing.ts +42 -0
  254. package/src/subscription-mollie/__tests__/feature.test.ts +106 -0
  255. package/src/subscription-mollie/__tests__/mollie-foundation.integration.ts +421 -0
  256. package/src/subscription-mollie/__tests__/verify-webhook.test.ts +388 -0
  257. package/src/subscription-mollie/constants.ts +33 -0
  258. package/src/subscription-mollie/feature.ts +144 -0
  259. package/src/subscription-mollie/index.ts +13 -0
  260. package/src/subscription-mollie/plugin-methods.ts +79 -0
  261. package/src/subscription-mollie/verify-webhook.ts +244 -0
  262. package/src/subscription-stripe/__tests__/feature.test.ts +98 -0
  263. package/src/subscription-stripe/__tests__/plugin-methods.test.ts +161 -0
  264. package/src/subscription-stripe/__tests__/stripe-foundation.integration.ts +315 -0
  265. package/src/subscription-stripe/__tests__/verify-webhook.test.ts +306 -0
  266. package/src/subscription-stripe/constants.ts +20 -0
  267. package/src/subscription-stripe/feature.ts +120 -0
  268. package/src/subscription-stripe/index.ts +14 -0
  269. package/src/subscription-stripe/plugin-methods.ts +91 -0
  270. package/src/subscription-stripe/verify-webhook.ts +235 -0
  271. package/src/tenant/__tests__/multi-tenant.integration.ts +278 -0
  272. package/src/tenant/__tests__/seed-testing.integration.ts +229 -0
  273. package/src/tenant/__tests__/tenant.integration.ts +347 -0
  274. package/src/tenant/command-schemas.ts +37 -0
  275. package/src/tenant/constants.ts +37 -0
  276. package/src/tenant/feature.ts +109 -0
  277. package/src/tenant/handlers/active-tenant-ids.query.ts +19 -0
  278. package/src/tenant/handlers/add-member.write.ts +53 -0
  279. package/src/tenant/handlers/cancel-invitation.write.ts +87 -0
  280. package/src/tenant/handlers/create.write.ts +21 -0
  281. package/src/tenant/handlers/disable.write.ts +18 -0
  282. package/src/tenant/handlers/invitations.query.ts +31 -0
  283. package/src/tenant/handlers/list.query.ts +17 -0
  284. package/src/tenant/handlers/me.query.ts +17 -0
  285. package/src/tenant/handlers/members.query.ts +22 -0
  286. package/src/tenant/handlers/memberships.query.ts +24 -0
  287. package/src/tenant/handlers/remove-member.write.ts +40 -0
  288. package/src/tenant/handlers/resolve-user-ids.query.ts +43 -0
  289. package/src/tenant/handlers/update-member-roles.write.ts +54 -0
  290. package/src/tenant/handlers/update.write.ts +20 -0
  291. package/src/tenant/index.ts +12 -0
  292. package/src/tenant/invitation-table.ts +93 -0
  293. package/src/tenant/membership-table.ts +35 -0
  294. package/src/tenant/schema/index.ts +5 -0
  295. package/src/tenant/schema/tenant.ts +27 -0
  296. package/src/tenant/seeding.ts +155 -0
  297. package/src/tenant/testing.ts +8 -0
  298. package/src/text-content/README.md +190 -0
  299. package/src/text-content/__tests__/text-content.integration.ts +415 -0
  300. package/src/text-content/api.ts +92 -0
  301. package/src/text-content/constants.ts +19 -0
  302. package/src/text-content/feature.ts +29 -0
  303. package/src/text-content/handlers/by-slug.query.ts +55 -0
  304. package/src/text-content/handlers/set.write.ts +118 -0
  305. package/src/text-content/index.ts +14 -0
  306. package/src/text-content/seeding.ts +91 -0
  307. package/src/text-content/table.ts +45 -0
  308. package/src/tier-engine/__tests__/compose-app.test.ts +182 -0
  309. package/src/tier-engine/__tests__/drift.test.ts +42 -0
  310. package/src/tier-engine/__tests__/tier-engine.integration.ts +241 -0
  311. package/src/tier-engine/aggregate-id.ts +27 -0
  312. package/src/tier-engine/compose-app.ts +150 -0
  313. package/src/tier-engine/constants.ts +15 -0
  314. package/src/tier-engine/entity.ts +30 -0
  315. package/src/tier-engine/feature.ts +72 -0
  316. package/src/tier-engine/handlers/active-tier.query.ts +23 -0
  317. package/src/tier-engine/index.ts +22 -0
  318. package/src/user/__tests__/seed-testing.integration.ts +127 -0
  319. package/src/user/__tests__/user.integration.ts +198 -0
  320. package/src/user/command-schemas.ts +15 -0
  321. package/src/user/constants.ts +23 -0
  322. package/src/user/feature.ts +32 -0
  323. package/src/user/handlers/create.write.ts +54 -0
  324. package/src/user/handlers/detail.query.ts +9 -0
  325. package/src/user/handlers/find-for-auth.query.ts +38 -0
  326. package/src/user/handlers/list.query.ts +8 -0
  327. package/src/user/handlers/me.query.ts +15 -0
  328. package/src/user/handlers/update.write.ts +54 -0
  329. package/src/user/index.ts +4 -0
  330. package/src/user/schema/index.ts +5 -0
  331. package/src/user/schema/user.ts +69 -0
  332. package/src/user/seeding.ts +93 -0
  333. 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;