@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,244 @@
1
+ // verifyAndParseMollieWebhook — Mollie-spezifische lazy-fetch +
2
+ // event-type-Heuristik. Wird vom Plugin-Build (feature.ts) als
3
+ // `verifyAndParseWebhook` registriert.
4
+ //
5
+ // Mollie's classic-webhook sendet nur eine `id` (form-urlencoded oder
6
+ // JSON). Wir lazy-fetchen payment + subscription via Mollie-API. Sub-
7
+ // xxx-events werden NICHT supported — App-Builder bekommt sie indirekt
8
+ // via tr_xxx-payment-events (Mollie sendet beide parallel bei normalen
9
+ // Lifecycle-Events).
10
+ //
11
+ // **Mandate-setup-flow:** first-payment-paid kommt mit
12
+ // `payment.subscriptionId === null` (Mollie's Pattern: App-Builder
13
+ // muss `customerSubscriptions.create` selbst aufrufen). Wir machen
14
+ // das im Plugin (idempotent via list-check), damit Foundation einen
15
+ // Created-Event mit subscription-id bekommt.
16
+
17
+ import {
18
+ type SubscriptionEvent,
19
+ type SubscriptionEventType,
20
+ SubscriptionEventTypes,
21
+ type SubscriptionStatus,
22
+ SubscriptionStatuses,
23
+ } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
24
+ import type {
25
+ Payment as MolliePayment,
26
+ Subscription as MollieSubscription,
27
+ } from "@mollie/api-client";
28
+ import { MOLLIE_PROVIDER_NAME } from "./constants";
29
+ import type { MolliePriceConfig } from "./plugin-methods";
30
+
31
+ /** Minimal-Subset des Mollie-Clients, das der Plugin nutzt — separat
32
+ * damit Tests ohne den vollen MollieClient mocken können. Adapter in
33
+ * feature.ts bridged das gegen den echten SDK. */
34
+ export type MollieClientShape = {
35
+ readonly payments: { readonly get: (id: string) => Promise<MolliePayment> };
36
+ readonly customerSubscriptions: {
37
+ readonly get: (subId: string, customerId: string) => Promise<MollieSubscription>;
38
+ readonly list: (customerId: string) => Promise<readonly MollieSubscription[]>;
39
+ readonly create: (
40
+ customerId: string,
41
+ params: {
42
+ amount: { currency: string; value: string };
43
+ interval: string;
44
+ description: string;
45
+ metadata: Record<string, string>;
46
+ },
47
+ ) => Promise<MollieSubscription>;
48
+ };
49
+ };
50
+
51
+ export type MollieWebhookOptions = {
52
+ /** priceId (= virtueller Schlüssel aus subscription.metadata) → tier-name. */
53
+ readonly priceToTier: Readonly<Record<string, string>>;
54
+ /** priceId → amount/interval/description. Wird beim mandate-setup-
55
+ * flow zum `customerSubscriptions.create`-Call gebraucht. App-
56
+ * Builder pflegt die Map einmal in den factory-options. */
57
+ readonly priceToConfig: Readonly<Record<string, MolliePriceConfig>>;
58
+ };
59
+
60
+ export function verifyAndParseMollieWebhook(
61
+ client: MollieClientShape,
62
+ options: MollieWebhookOptions,
63
+ ): (rawBody: string, headers: Record<string, string>) => Promise<SubscriptionEvent | null> {
64
+ return async (rawBody, headers) => {
65
+ const id = extractMollieId(rawBody, headers);
66
+ if (!id) {
67
+ throw new Error("subscription-mollie: webhook body has no `id` field");
68
+ }
69
+
70
+ let subscription: MollieSubscription | null = null;
71
+ let triggerPayment: MolliePayment | null = null;
72
+
73
+ if (id.startsWith("tr_")) {
74
+ let payment: MolliePayment;
75
+ try {
76
+ payment = await client.payments.get(id);
77
+ } catch {
78
+ // Garbage-id → Mollie 404 → Foundation 200 ignored.
79
+ return null;
80
+ }
81
+ triggerPayment = payment;
82
+ const customerId = payment.customerId;
83
+ if (!customerId) return null;
84
+
85
+ if (payment.subscriptionId) {
86
+ try {
87
+ subscription = await client.customerSubscriptions.get(payment.subscriptionId, customerId);
88
+ } catch {
89
+ return null;
90
+ }
91
+ } else if (payment.sequenceType === "first" && payment.status === "paid") {
92
+ subscription = await ensureSubscriptionForMandate(client, options, payment);
93
+ if (!subscription) return null;
94
+ } else {
95
+ // One-shot oder first-payment-failed → nicht unsere Domain.
96
+ return null;
97
+ }
98
+ } else if (id.startsWith("sub_")) {
99
+ // sub_xxx-events kommen indirekt via parallele tr_xxx-events.
100
+ return null;
101
+ } else {
102
+ return null;
103
+ }
104
+
105
+ const metadata = (subscription.metadata as Record<string, string> | null) ?? {};
106
+ const tenantId = metadata["tenantId"];
107
+ if (!tenantId || tenantId.length === 0) return null;
108
+ const priceId = metadata["priceId"];
109
+ if (!priceId) return null;
110
+ const tier = options.priceToTier[priceId];
111
+ if (!tier) return null;
112
+
113
+ const type = mapMollieEventType(subscription, triggerPayment);
114
+ if (!type) return null;
115
+
116
+ const status = mapMollieStatus(subscription.status);
117
+ const periodEndSource = subscription.nextPaymentDate ?? subscription.startDate;
118
+ if (!periodEndSource) {
119
+ // Mollie-API-Drift: valid Subs haben mindestens startDate. Loud-
120
+ // fail damit App-Owner's monitoring den drift sieht.
121
+ throw new Error(
122
+ `subscription-mollie: subscription ${subscription.id} has neither nextPaymentDate nor startDate`,
123
+ );
124
+ }
125
+ const currentPeriodEnd = mollieDateStringToInstantIso(periodEndSource);
126
+
127
+ return {
128
+ providerEventId: id,
129
+ providerName: MOLLIE_PROVIDER_NAME,
130
+ type,
131
+ tenantId,
132
+ providerCustomerId: subscription.customerId,
133
+ providerSubscriptionId: subscription.id,
134
+ status,
135
+ tier,
136
+ currentPeriodEnd,
137
+ rawPayload: JSON.stringify({ webhookId: id, subscription, triggerPayment }),
138
+ };
139
+ };
140
+ }
141
+
142
+ // =============================================================================
143
+ // Mandate-setup: subscription on-the-fly erstellen
144
+ // =============================================================================
145
+
146
+ /** first-payment-paid OHNE subscriptionId → Mollie-Sub erstellen.
147
+ * Idempotent via list-check (replay-safe). Returns null bei
148
+ * unvollständiger metadata oder unbekanntem priceId. */
149
+ async function ensureSubscriptionForMandate(
150
+ client: MollieClientShape,
151
+ options: MollieWebhookOptions,
152
+ payment: MolliePayment,
153
+ ): Promise<MollieSubscription | null> {
154
+ const customerId = payment.customerId;
155
+ if (!customerId) return null;
156
+ const paymentMetadata = (payment.metadata as Record<string, string> | null) ?? {};
157
+ const tenantId = paymentMetadata["tenantId"];
158
+ const priceId = paymentMetadata["priceId"];
159
+ if (!tenantId || !priceId) return null;
160
+ const priceCfg = options.priceToConfig[priceId];
161
+ if (!priceCfg) return null;
162
+
163
+ const existing = await client.customerSubscriptions.list(customerId);
164
+ const matchingExisting = existing.find(
165
+ (sub) =>
166
+ (sub.metadata as Record<string, string> | null)?.["priceId"] === priceId &&
167
+ (sub.status === "active" || sub.status === "pending"),
168
+ );
169
+ if (matchingExisting) return matchingExisting;
170
+
171
+ return await client.customerSubscriptions.create(customerId, {
172
+ amount: { currency: priceCfg.amountCurrency, value: priceCfg.amountValue },
173
+ interval: priceCfg.interval,
174
+ description: priceCfg.description,
175
+ metadata: { tenantId, priceId },
176
+ });
177
+ }
178
+
179
+ // =============================================================================
180
+ // Helpers (exported für Tests)
181
+ // =============================================================================
182
+
183
+ /** Extract `id` aus rawBody — handelt form-urlencoded + JSON. */
184
+ export function extractMollieId(rawBody: string, headers: Record<string, string>): string | null {
185
+ const contentType = headers["content-type"] ?? "";
186
+ if (contentType.includes("application/json")) {
187
+ try {
188
+ const parsed = JSON.parse(rawBody) as { id?: unknown };
189
+ return typeof parsed.id === "string" ? parsed.id : null;
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
194
+ const params = new URLSearchParams(rawBody);
195
+ return params.get("id");
196
+ }
197
+
198
+ /** Mollie-subscription + optional payment → SubscriptionEventType.
199
+ * Heuristik (Mollie hat keine explicit-typed events). */
200
+ export function mapMollieEventType(
201
+ subscription: MollieSubscription,
202
+ triggerPayment: MolliePayment | null,
203
+ ): SubscriptionEventType | null {
204
+ if (subscription.status === "canceled" || subscription.status === "completed") {
205
+ return SubscriptionEventTypes.canceled;
206
+ }
207
+
208
+ if (triggerPayment) {
209
+ const seq = triggerPayment.sequenceType;
210
+ const paid = triggerPayment.status === "paid";
211
+ const failed = triggerPayment.status === "failed" || triggerPayment.status === "expired";
212
+ if (seq === "first" && paid) return SubscriptionEventTypes.created;
213
+ if (seq === "recurring" && paid) return SubscriptionEventTypes.invoicePaid;
214
+ if (seq === "recurring" && failed) return SubscriptionEventTypes.invoicePaymentFailed;
215
+ }
216
+
217
+ if (subscription.status === "active") return SubscriptionEventTypes.updated;
218
+ return null;
219
+ }
220
+
221
+ /** Mollie-status → normalized. */
222
+ export function mapMollieStatus(mollieStatus: MollieSubscription["status"]): SubscriptionStatus {
223
+ switch (mollieStatus) {
224
+ case "active":
225
+ return SubscriptionStatuses.active;
226
+ case "canceled":
227
+ case "completed":
228
+ // completed = alle `times`-charges durchgelaufen → wie canceled.
229
+ return SubscriptionStatuses.canceled;
230
+ case "suspended":
231
+ // Mandate ungültig / payment-method failed → grace-period.
232
+ return SubscriptionStatuses.pastDue;
233
+ case "pending":
234
+ return SubscriptionStatuses.incomplete;
235
+ default:
236
+ return SubscriptionStatuses.incomplete;
237
+ }
238
+ }
239
+
240
+ /** Mollie liefert dates als YYYY-MM-DD; foundation will ISO-Instant.
241
+ * Throws bei malformed input (= Mollie-API-Drift, soll loud-fail). */
242
+ function mollieDateStringToInstantIso(dateString: string): string {
243
+ return Temporal.Instant.from(`${dateString.slice(0, 10)}T00:00:00Z`).toString();
244
+ }
@@ -0,0 +1,98 @@
1
+ // feature.ts contract tests for subscription-stripe.
2
+
3
+ import { describe, expect, test } from "vitest";
4
+ import { STRIPE_PROVIDER_NAME, StripeEventTypes, SUBSCRIPTION_STRIPE_FEATURE } from "../constants";
5
+ import { createSubscriptionStripeFeature } from "../feature";
6
+
7
+ const VALID_OPTIONS = {
8
+ webhookSecret: "whsec_test_dummy",
9
+ apiKey: "sk_test_dummy",
10
+ priceToTier: { price_test: "pro" },
11
+ };
12
+
13
+ describe("createSubscriptionStripeFeature — shape", () => {
14
+ test("has the expected name", () => {
15
+ const feature = createSubscriptionStripeFeature(VALID_OPTIONS);
16
+ expect(feature.name).toBe(SUBSCRIPTION_STRIPE_FEATURE);
17
+ expect(feature.name).toBe("subscription-stripe");
18
+ });
19
+
20
+ test("requires only subscription-foundation (NICHT config/secrets — alles app-wide via factory-options)", () => {
21
+ const feature = createSubscriptionStripeFeature(VALID_OPTIONS);
22
+ expect(feature.requires).toContain("billing-foundation");
23
+ // Drift-Pin: webhook-secret + apiKey kommen aus factory-options
24
+ // (= module-load-Closure), NICHT aus tenant-config/-secrets.
25
+ expect(feature.requires).not.toContain("config");
26
+ expect(feature.requires).not.toContain("secrets");
27
+ });
28
+ });
29
+
30
+ describe("createSubscriptionStripeFeature — module-load validation", () => {
31
+ test("throws bei empty webhookSecret (= App-Owner hat sub-stripe gemountet aber Stripe-Account nicht konfiguriert)", () => {
32
+ expect(() =>
33
+ createSubscriptionStripeFeature({
34
+ ...VALID_OPTIONS,
35
+ webhookSecret: "",
36
+ }),
37
+ ).toThrow(/webhookSecret is empty/);
38
+ });
39
+
40
+ test("throws bei empty apiKey", () => {
41
+ expect(() =>
42
+ createSubscriptionStripeFeature({
43
+ ...VALID_OPTIONS,
44
+ apiKey: "",
45
+ }),
46
+ ).toThrow(/apiKey is empty/);
47
+ });
48
+ });
49
+
50
+ describe("subscription-stripe — plugin-registration", () => {
51
+ test("registers itself under entityName 'stripe' for subscription-foundation's extension", () => {
52
+ const feature = createSubscriptionStripeFeature(VALID_OPTIONS);
53
+ const usages = feature.extensionUsages;
54
+ expect(
55
+ usages.some(
56
+ (u) => u.extensionName === "subscriptionProvider" && u.entityName === STRIPE_PROVIDER_NAME,
57
+ ),
58
+ ).toBe(true);
59
+ });
60
+
61
+ test("plugin-options have a valid SubscriptionProviderPlugin shape (drift-pin alle 4 methods)", () => {
62
+ // Stärker als nur "extension-usage existiert": wenn jemand eine der
63
+ // plugin-methods aus dem plugin-build entfernt, würde der
64
+ // entsprechende Foundation-write-handler zur Laufzeit als
65
+ // "method not supported"-error brechen — type-check würde es nicht
66
+ // fangen weil die useExtension-options als `unknown` durchgereicht
67
+ // werden.
68
+ const feature = createSubscriptionStripeFeature(VALID_OPTIONS);
69
+ const usage = feature.extensionUsages.find((u) => u.entityName === STRIPE_PROVIDER_NAME);
70
+ expect(usage).toBeDefined();
71
+ const options = usage?.options as {
72
+ verifyAndParseWebhook?: unknown;
73
+ createCheckoutSession?: unknown;
74
+ createPortalSession?: unknown;
75
+ cancelSubscription?: unknown;
76
+ };
77
+ expect(typeof options?.verifyAndParseWebhook).toBe("function");
78
+ expect(typeof options?.createCheckoutSession).toBe("function");
79
+ expect(typeof options?.createPortalSession).toBe("function");
80
+ expect(typeof options?.cancelSubscription).toBe("function");
81
+ });
82
+ });
83
+
84
+ describe("constants — Stripe-event-types die wir mappen", () => {
85
+ test("StripeEventTypes whitelist — was die Foundation verarbeitet", () => {
86
+ // Drift-Pin: ein Refactor das einen event-type rauswirft (z.B.
87
+ // invoice.paid weil "wir nutzen das nicht") würde tier-grace-period-
88
+ // tracking brechen. Plus: ein Refactor der einen NICHT-existing
89
+ // Stripe-event-type reinschreibt (typo) würde silent ignored werden.
90
+ expect(Object.values(StripeEventTypes)).toEqual([
91
+ "customer.subscription.created",
92
+ "customer.subscription.updated",
93
+ "customer.subscription.deleted",
94
+ "invoice.paid",
95
+ "invoice.payment_failed",
96
+ ]);
97
+ });
98
+ });
@@ -0,0 +1,161 @@
1
+ // Unit-Tests für die Stripe-Plugin-Methoden (createCheckoutSession,
2
+ // createPortalSession, cancelSubscription). Stripe-SDK-calls werden via
3
+ // vi.spyOn gemockt — wir testen unsere Mapping-Logik (Argumente die wir
4
+ // an Stripe schicken + Antwort-Parsing), NICHT Stripe selbst.
5
+
6
+ import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
7
+ import Stripe from "stripe";
8
+ import { describe, expect, test, vi } from "vitest";
9
+ import {
10
+ createStripeCancelSubscription,
11
+ createStripeCheckoutSession,
12
+ createStripePortalSession,
13
+ } from "../plugin-methods";
14
+
15
+ const TEST_API_KEY = "sk_test_dummy";
16
+
17
+ function buildStripe(): Stripe {
18
+ return new Stripe(TEST_API_KEY, { apiVersion: "2026-04-22.dahlia" });
19
+ }
20
+
21
+ const stubCtx = {} as HandlerContext;
22
+
23
+ // =============================================================================
24
+ // createCheckoutSession
25
+ // =============================================================================
26
+
27
+ describe("createStripeCheckoutSession", () => {
28
+ test("ruft stripe.checkout.sessions.create mit mode=subscription + tenant-metadata", async () => {
29
+ const stripe = buildStripe();
30
+ const createMock = vi
31
+ .spyOn(stripe.checkout.sessions, "create")
32
+ // biome-ignore lint/suspicious/noExplicitAny: Stripe-SDK-typed mock-return
33
+ .mockResolvedValue({ url: "https://checkout.stripe.com/c/pay/test" } as any);
34
+
35
+ const checkout = createStripeCheckoutSession(stripe);
36
+ const result = await checkout(stubCtx, {
37
+ priceId: "price_pro_monthly",
38
+ tenantId: "tenant-001",
39
+ successUrl: "https://example.com/success",
40
+ cancelUrl: "https://example.com/cancel",
41
+ });
42
+
43
+ expect(result).toEqual({ url: "https://checkout.stripe.com/c/pay/test" });
44
+ expect(createMock).toHaveBeenCalledExactlyOnceWith({
45
+ mode: "subscription",
46
+ line_items: [{ price: "price_pro_monthly", quantity: 1 }],
47
+ success_url: "https://example.com/success",
48
+ cancel_url: "https://example.com/cancel",
49
+ // Drift-Pin: metadata.tenantId LANDET auf der subscription, NICHT
50
+ // auf der checkout-session direkt — sonst kann verifyAndParse-
51
+ // Webhook den tenant beim subsequent webhook nicht resolven.
52
+ subscription_data: {
53
+ metadata: { tenantId: "tenant-001" },
54
+ },
55
+ });
56
+ });
57
+
58
+ test("passes existing customer-id wenn gesetzt (Plan-Wechsel-Flow)", async () => {
59
+ const stripe = buildStripe();
60
+ const createMock = vi
61
+ .spyOn(stripe.checkout.sessions, "create")
62
+ // biome-ignore lint/suspicious/noExplicitAny: Stripe-SDK-typed mock-return
63
+ .mockResolvedValue({ url: "https://x" } as any);
64
+
65
+ const checkout = createStripeCheckoutSession(stripe);
66
+ await checkout(stubCtx, {
67
+ priceId: "price_x",
68
+ tenantId: "tenant-002",
69
+ successUrl: "https://x/s",
70
+ cancelUrl: "https://x/c",
71
+ providerCustomerId: "cus_existing_123",
72
+ });
73
+
74
+ expect(createMock).toHaveBeenCalledWith(
75
+ expect.objectContaining({ customer: "cus_existing_123" }),
76
+ );
77
+ });
78
+
79
+ test("throws wenn Stripe keine url returnt (defensive — sollte nie passieren bei mode=subscription)", async () => {
80
+ const stripe = buildStripe();
81
+ vi.spyOn(stripe.checkout.sessions, "create")
82
+ // biome-ignore lint/suspicious/noExplicitAny: SDK-Drift-Test
83
+ .mockResolvedValue({ url: null } as any);
84
+
85
+ const checkout = createStripeCheckoutSession(stripe);
86
+ await expect(
87
+ checkout(stubCtx, {
88
+ priceId: "p",
89
+ tenantId: "t",
90
+ successUrl: "https://x/s",
91
+ cancelUrl: "https://x/c",
92
+ }),
93
+ ).rejects.toThrow(/returned no url/);
94
+ });
95
+
96
+ test("Stripe-API-failure (z.B. 500 / network) → propagated zum Caller (Foundation mapped auf 500)", async () => {
97
+ // Drift-Pin: Plugin schluckt KEINE Stripe-Errors. Foundation
98
+ // verlässt sich darauf dass create-checkout-session-handler einen
99
+ // throw kriegt + zur HTTP 500 mapped (transient — Provider/Stripe
100
+ // soll retried werden statt silent-success-mit-leerer-URL).
101
+ const stripe = buildStripe();
102
+ vi.spyOn(stripe.checkout.sessions, "create").mockRejectedValue(
103
+ new Error("Stripe API: Internal server error"),
104
+ );
105
+
106
+ const checkout = createStripeCheckoutSession(stripe);
107
+ await expect(
108
+ checkout(stubCtx, {
109
+ priceId: "p",
110
+ tenantId: "t",
111
+ successUrl: "https://x/s",
112
+ cancelUrl: "https://x/c",
113
+ }),
114
+ ).rejects.toThrow(/Internal server error/);
115
+ });
116
+ });
117
+
118
+ // =============================================================================
119
+ // createPortalSession
120
+ // =============================================================================
121
+
122
+ describe("createStripePortalSession", () => {
123
+ test("ruft stripe.billingPortal.sessions.create mit customer + return_url", async () => {
124
+ const stripe = buildStripe();
125
+ const createMock = vi
126
+ .spyOn(stripe.billingPortal.sessions, "create")
127
+ // biome-ignore lint/suspicious/noExplicitAny: Stripe-SDK-typed mock-return
128
+ .mockResolvedValue({ url: "https://billing.stripe.com/p/session/test" } as any);
129
+
130
+ const portal = createStripePortalSession(stripe);
131
+ const result = await portal(stubCtx, {
132
+ providerCustomerId: "cus_001",
133
+ returnUrl: "https://example.com/return",
134
+ });
135
+
136
+ expect(result).toEqual({ url: "https://billing.stripe.com/p/session/test" });
137
+ expect(createMock).toHaveBeenCalledExactlyOnceWith({
138
+ customer: "cus_001",
139
+ return_url: "https://example.com/return",
140
+ });
141
+ });
142
+ });
143
+
144
+ // =============================================================================
145
+ // cancelSubscription
146
+ // =============================================================================
147
+
148
+ describe("createStripeCancelSubscription", () => {
149
+ test("ruft stripe.subscriptions.cancel mit subscription-id", async () => {
150
+ const stripe = buildStripe();
151
+ const cancelMock = vi
152
+ .spyOn(stripe.subscriptions, "cancel")
153
+ // biome-ignore lint/suspicious/noExplicitAny: Stripe-SDK-typed mock-return
154
+ .mockResolvedValue({ id: "sub_001", status: "canceled" } as any);
155
+
156
+ const cancel = createStripeCancelSubscription(stripe);
157
+ await cancel(stubCtx, "sub_001");
158
+
159
+ expect(cancelMock).toHaveBeenCalledExactlyOnceWith("sub_001");
160
+ });
161
+ });