@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,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
+ });