@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,110 @@
1
+ // feature.ts contract tests for subscription-foundation.
2
+
3
+ import { describe, expect, test } from "vitest";
4
+ import { subscriptionAggregateId } from "../aggregate-id";
5
+ import {
6
+ BILLING_FOUNDATION_FEATURE,
7
+ SUBSCRIPTION_PROVIDER_EXTENSION,
8
+ SubscriptionEventTypes,
9
+ SubscriptionFoundationHandlers,
10
+ SubscriptionStatuses,
11
+ } from "../constants";
12
+ import { billingFoundationFeature } from "../feature";
13
+
14
+ describe("billingFoundationFeature — shape", () => {
15
+ test("has the expected name", () => {
16
+ expect(billingFoundationFeature.name).toBe(BILLING_FOUNDATION_FEATURE);
17
+ // Naming-Disziplin pin: NICHT "billing-foundation" — damit
18
+ // marketplace-foundation später ohne Rename dazukommt (siehe
19
+ // docs/plans/architecture/subscription-foundation.md).
20
+ expect(billingFoundationFeature.name).toBe("billing-foundation");
21
+ });
22
+
23
+ test("does NOT require config (Multi-Provider — config liegt in den Plugins)", () => {
24
+ // Drift-Pin: foundation hat KEIN globales `provider`-config-key
25
+ // mehr. Webhook-URL trägt providerName als Pfad-Parameter, jeder
26
+ // gemountete Plugin ist gleichzeitig aktiv. Wenn jemand wieder
27
+ // `r.requires("config")` einbaut, verstößt das gegen die
28
+ // Multi-Provider-Architektur.
29
+ expect(billingFoundationFeature.requires).not.toContain("config");
30
+ });
31
+
32
+ test("does NOT require secrets — Provider-Plugins owne ihre eigenen API-Keys", () => {
33
+ expect(billingFoundationFeature.requires).not.toContain("secrets");
34
+ });
35
+
36
+ test("foundation has NO config-keys (alle config-keys liegen in den Plugins)", () => {
37
+ // Multi-Provider-Drift-Pin: foundation darf keine config-keys
38
+ // exportieren weil sonst sowas wie "globaler price-to-tier-Map"
39
+ // erzwungen wäre — der existiert NUR pro Plugin (Stripe-priceIds
40
+ // vs PayPal-plan-ids vs Apple-product-ids sind verschieden).
41
+ expect(Object.keys(billingFoundationFeature.configKeys)).toHaveLength(0);
42
+ });
43
+ });
44
+
45
+ describe("billingFoundationFeature — registers extension-point", () => {
46
+ test("declares 'subscriptionProvider' extension-point", () => {
47
+ expect(
48
+ billingFoundationFeature.registrarExtensions[SUBSCRIPTION_PROVIDER_EXTENSION],
49
+ ).toBeDefined();
50
+ });
51
+ });
52
+
53
+ describe("billingFoundationFeature — events + projection + handlers registered", () => {
54
+ test("5 domain-events registriert (created/updated/canceled/invoice-paid/invoice-payment-failed)", () => {
55
+ const events = billingFoundationFeature.events;
56
+ expect(events["subscription-created"]).toBeDefined();
57
+ expect(events["subscription-updated"]).toBeDefined();
58
+ expect(events["subscription-canceled"]).toBeDefined();
59
+ expect(events["invoice-paid"]).toBeDefined();
60
+ expect(events["invoice-payment-failed"]).toBeDefined();
61
+ });
62
+
63
+ test("subscription-projection registriert mit 5 apply-keys", () => {
64
+ const proj = billingFoundationFeature.projections["subscription"];
65
+ expect(proj).toBeDefined();
66
+ const applyKeys = Object.keys(proj?.apply ?? {});
67
+ expect(applyKeys).toHaveLength(5);
68
+ });
69
+
70
+ test("process-event write-handler registriert mit erwarteter QN", () => {
71
+ expect(billingFoundationFeature.writeHandlers["process-event"]).toBeDefined();
72
+ expect(SubscriptionFoundationHandlers.processEvent).toBe(
73
+ "billing-foundation:write:process-event",
74
+ );
75
+ });
76
+
77
+ test("process-event ist SystemAdmin-only (programmatic-only entry-point)", () => {
78
+ const handler = billingFoundationFeature.writeHandlers["process-event"];
79
+ const access = handler?.access as { roles?: readonly string[] } | undefined;
80
+ expect(access?.roles).toEqual(["SystemAdmin"]);
81
+ });
82
+ });
83
+
84
+ describe("aggregate-id namespace — drift-pin", () => {
85
+ test("subscriptionAggregateId stable per tenantId", () => {
86
+ expect(subscriptionAggregateId("tenant-1")).toBe("bfe0d98f-293c-5215-af7a-3282629aa5d3");
87
+ });
88
+ });
89
+
90
+ describe("normalized constants — provider-agnostic event-types + statuses", () => {
91
+ test("EventTypes whitelist — was die Foundation kennt", () => {
92
+ expect(Object.values(SubscriptionEventTypes)).toEqual([
93
+ "subscription.created",
94
+ "subscription.updated",
95
+ "subscription.canceled",
96
+ "invoice.paid",
97
+ "invoice.payment-failed",
98
+ ]);
99
+ });
100
+
101
+ test("Statuses normalized über Stripe + Mollie", () => {
102
+ expect(Object.values(SubscriptionStatuses)).toEqual([
103
+ "active",
104
+ "trialing",
105
+ "past_due",
106
+ "canceled",
107
+ "incomplete",
108
+ ]);
109
+ });
110
+ });
@@ -0,0 +1,199 @@
1
+ // Unit-Tests für createSubscriptionWebhookHandler. Treibt alle 5
2
+ // HTTP-Pfade durch (400/401/404/500 + 200) via Hono `app.request()` —
3
+ // ohne setupTestStack, weil der webhook-handler nur über die deps-
4
+ // injection geht und keinen DB-roundtrip braucht.
5
+
6
+ import { Hono } from "hono";
7
+ import { describe, expect, test, vi } from "vitest";
8
+ import { SubscriptionEventTypes, SubscriptionStatuses } from "../constants";
9
+ import type { SubscriptionEvent, SubscriptionProviderPlugin } from "../types";
10
+ import { createSubscriptionWebhookHandler, type SubscriptionWebhookDeps } from "../webhook-handler";
11
+
12
+ // =============================================================================
13
+ // Test-helpers
14
+ // =============================================================================
15
+
16
+ function buildEvent(): SubscriptionEvent {
17
+ return {
18
+ providerEventId: "evt_test_001",
19
+ providerName: "stripe",
20
+ type: SubscriptionEventTypes.created,
21
+ tenantId: "tenant-test",
22
+ providerCustomerId: "cus_test",
23
+ providerSubscriptionId: "sub_test",
24
+ status: SubscriptionStatuses.active,
25
+ tier: "pro",
26
+ currentPeriodEnd: "2026-06-01T00:00:00Z",
27
+ rawPayload: '{"raw":"payload"}',
28
+ };
29
+ }
30
+
31
+ function buildPlugin(
32
+ overrides: Partial<SubscriptionProviderPlugin> = {},
33
+ ): SubscriptionProviderPlugin {
34
+ return {
35
+ verifyAndParseWebhook: async () => buildEvent(),
36
+ ...overrides,
37
+ };
38
+ }
39
+
40
+ function buildDeps(overrides: Partial<SubscriptionWebhookDeps> = {}): SubscriptionWebhookDeps {
41
+ return {
42
+ dispatchWrite: async () => ({ isSuccess: true, data: { duplicate: false } }),
43
+ resolveProvider: () => buildPlugin(),
44
+ ...overrides,
45
+ };
46
+ }
47
+
48
+ function buildApp(deps: SubscriptionWebhookDeps) {
49
+ const app = new Hono();
50
+ app.post("/api/subscription/webhook/:providerName", createSubscriptionWebhookHandler(deps));
51
+ return app;
52
+ }
53
+
54
+ async function postWebhook(app: Hono, providerName: string, body = '{"id":"evt_test"}') {
55
+ return app.request(`/api/subscription/webhook/${providerName}`, {
56
+ method: "POST",
57
+ body,
58
+ headers: { "stripe-signature": "test_sig" },
59
+ });
60
+ }
61
+
62
+ // =============================================================================
63
+ // Happy path — 200 OK + processed
64
+ // =============================================================================
65
+
66
+ describe("webhook-handler — happy path", () => {
67
+ test("verifyAndParseWebhook → SubscriptionEvent → dispatchWrite → 200 processed", async () => {
68
+ const dispatchWrite = vi.fn(async () => ({
69
+ isSuccess: true,
70
+ data: { duplicate: false, eventAggregateId: "evt-id" },
71
+ }));
72
+ const app = buildApp(buildDeps({ dispatchWrite }));
73
+
74
+ const res = await postWebhook(app, "stripe");
75
+ expect(res.status).toBe(200);
76
+ const body = (await res.json()) as { processed: boolean; duplicate: boolean };
77
+ expect(body.processed).toBe(true);
78
+ expect(body.duplicate).toBe(false);
79
+
80
+ expect(dispatchWrite).toHaveBeenCalledExactlyOnceWith(
81
+ expect.objectContaining({
82
+ handlerQn: "billing-foundation:write:process-event",
83
+ tenantId: "tenant-test",
84
+ payload: expect.objectContaining({
85
+ providerEventId: "evt_test_001",
86
+ providerName: "stripe",
87
+ type: "subscription.created",
88
+ tier: "pro",
89
+ }),
90
+ }),
91
+ );
92
+ });
93
+
94
+ test("plugin returns null (= unbekannter event-type) → 200 ignored, kein dispatch", async () => {
95
+ const dispatchWrite = vi.fn();
96
+ const plugin = buildPlugin({ verifyAndParseWebhook: async () => null });
97
+ const app = buildApp(buildDeps({ dispatchWrite, resolveProvider: () => plugin }));
98
+
99
+ const res = await postWebhook(app, "stripe");
100
+ expect(res.status).toBe(200);
101
+ const body = (await res.json()) as { ignored: boolean };
102
+ expect(body.ignored).toBe(true);
103
+ expect(dispatchWrite).not.toHaveBeenCalled();
104
+ });
105
+ });
106
+
107
+ // =============================================================================
108
+ // Error paths — 400/401/404/500
109
+ // =============================================================================
110
+
111
+ describe("webhook-handler — error paths", () => {
112
+ test("provider not registered → 404 mit subscription_provider_not_registered", async () => {
113
+ const app = buildApp(buildDeps({ resolveProvider: () => undefined }));
114
+
115
+ const res = await postWebhook(app, "ghost");
116
+ expect(res.status).toBe(404);
117
+ // Drift-Pin: error-code stable damit Stripe-/Mollie-retry-Logik
118
+ // sich auf den Code verlassen kann.
119
+ const body = (await res.json()) as { error: { code: string; message: string } };
120
+ expect(body.error.code).toBe("subscription_provider_not_registered");
121
+ expect(body.error.message).toMatch(/ghost/);
122
+ });
123
+
124
+ test("plugin throws beim sig-verify → 401 mit subscription_webhook_signature_invalid", async () => {
125
+ const plugin = buildPlugin({
126
+ verifyAndParseWebhook: async () => {
127
+ throw new Error("HMAC mismatch — wrong webhook secret?");
128
+ },
129
+ });
130
+ const app = buildApp(buildDeps({ resolveProvider: () => plugin }));
131
+
132
+ const res = await postWebhook(app, "stripe");
133
+ expect(res.status).toBe(401);
134
+ const body = (await res.json()) as { error: { code: string; message: string } };
135
+ expect(body.error.code).toBe("subscription_webhook_signature_invalid");
136
+ expect(body.error.message).toMatch(/HMAC mismatch/);
137
+ });
138
+
139
+ test("dispatchWrite returns isSuccess: false → 500 mit subscription_webhook_processing_failed", async () => {
140
+ const dispatchWrite = vi.fn(async () => ({
141
+ isSuccess: false,
142
+ error: { code: "internal_error", message: "DB unavailable" },
143
+ }));
144
+ const app = buildApp(buildDeps({ dispatchWrite }));
145
+
146
+ const res = await postWebhook(app, "stripe");
147
+ expect(res.status).toBe(500);
148
+ const body = (await res.json()) as {
149
+ error: { code: string; message: string; details: unknown };
150
+ };
151
+ expect(body.error.code).toBe("subscription_webhook_processing_failed");
152
+ // Provider sees 500 + retries — that's the design (transient failure).
153
+ expect(body.error.details).toMatchObject({ code: "internal_error" });
154
+ });
155
+ });
156
+
157
+ // =============================================================================
158
+ // Mounting-Pattern
159
+ // =============================================================================
160
+
161
+ describe("webhook-handler — Mounting-Pattern", () => {
162
+ test("Multi-Provider: zwei verschiedene URL-Pfade → unterschiedliche plugins", async () => {
163
+ // Drift-Pin für die Multi-Provider-Architektur: stripe-Plugin hat
164
+ // einen anderen verifyAndParseWebhook als paypal-Plugin. Wenn ein
165
+ // Refactor den path-segment-Lookup auf etwas anderes umstellt
166
+ // (z.B. globaler config-key wieder), würde dieser Test failen.
167
+ const stripeCalls: string[] = [];
168
+ const paypalCalls: string[] = [];
169
+ const stripePlugin = buildPlugin({
170
+ verifyAndParseWebhook: async () => {
171
+ stripeCalls.push("called");
172
+ return buildEvent();
173
+ },
174
+ });
175
+ const paypalPlugin = buildPlugin({
176
+ verifyAndParseWebhook: async () => {
177
+ paypalCalls.push("called");
178
+ return { ...buildEvent(), providerName: "paypal", providerEventId: "I-PAYPAL-001" };
179
+ },
180
+ });
181
+ const app = buildApp(
182
+ buildDeps({
183
+ resolveProvider: (name) => {
184
+ if (name === "stripe") return stripePlugin;
185
+ if (name === "paypal") return paypalPlugin;
186
+ return undefined;
187
+ },
188
+ }),
189
+ );
190
+
191
+ const stripeRes = await postWebhook(app, "stripe");
192
+ const paypalRes = await postWebhook(app, "paypal");
193
+
194
+ expect(stripeRes.status).toBe(200);
195
+ expect(paypalRes.status).toBe(200);
196
+ expect(stripeCalls).toHaveLength(1);
197
+ expect(paypalCalls).toHaveLength(1);
198
+ });
199
+ });
@@ -0,0 +1,21 @@
1
+ import { v5 as uuidv5 } from "uuid";
2
+
3
+ // Fixed UUID-namespace für die deterministic aggregate-id-Ableitung.
4
+ // Generiert einmalig (2026-05-03), in Stein gemeißelt: ein Wechsel
5
+ // würde jeden existing aggregate-Stream re-keyen → kaputter
6
+ // Subscription-State, kaputte Audit-History. Drift-Pin in
7
+ // __tests__/feature.test.ts pinnt die UUID.
8
+
9
+ /** Pro Plattform-Tenant existiert genau EIN subscription-Aggregate. */
10
+ const SUBSCRIPTION_NAMESPACE = "5c3b2d1e-9a4f-4e8c-b7a3-1f8d6c2e9a4b";
11
+
12
+ /**
13
+ * Deterministic aggregate-id für die subscription eines Plattform-
14
+ * Tenants. EINE Subscription pro Tenant (Add-Ons sind line-items in
15
+ * derselben subscription, nicht eigene). Provider-Wechsel (z.B.
16
+ * Stripe→Mollie-Migration) appended einen neuen event auf denselben
17
+ * Stream — selber Tenant, selber Aggregate-Stream.
18
+ */
19
+ export function subscriptionAggregateId(tenantId: string): string {
20
+ return uuidv5(tenantId, SUBSCRIPTION_NAMESPACE);
21
+ }
@@ -0,0 +1,70 @@
1
+ // Feature name
2
+ export const BILLING_FOUNDATION_FEATURE = "billing-foundation" as const;
3
+
4
+ // Extension-point name fuer Provider-Plugins (subscription-stripe,
5
+ // subscription-mollie, ...).
6
+ export const SUBSCRIPTION_PROVIDER_EXTENSION = "subscriptionProvider" as const;
7
+
8
+ // Qualified write handler names (QN format: scope:type:name).
9
+ export const SubscriptionFoundationHandlers = {
10
+ /** Programmatic entry-point für den webhook-handler. Receives the
11
+ * parsed SubscriptionEvent (vom Plugin schon verifiziert) + macht
12
+ * insert-event + upsert-subscription + tier-sync atomic. */
13
+ processEvent: "billing-foundation:write:process-event",
14
+ /** Tenant-Admin klickt "Upgrade to Pro" → handler findet den
15
+ * gewählten provider-plugin + ruft seine createCheckoutSession-
16
+ * Methode + returnt die hosted-page-URL. Tenant-Admin wird dorthin
17
+ * redirected, der subsequent provider-webhook erstellt die
18
+ * subscription. */
19
+ createCheckoutSession: "billing-foundation:write:create-checkout-session",
20
+ /** Tenant-Admin klickt "Manage Subscription" → handler findet
21
+ * current subscription, ruft plugin.createPortalSession, returnt
22
+ * hosted-portal-URL. */
23
+ createPortalSession: "billing-foundation:write:create-portal-session",
24
+ } as const;
25
+
26
+ // Qualified query handler names.
27
+ export const SubscriptionFoundationQueries = {
28
+ /** Sysadmin-cross-tenant + tenant-scoped self-list auf der
29
+ * read_subscriptions-projection. Tenant-Admin sieht via ctx.db
30
+ * tenant-scoping nur die eigene row. */
31
+ listSubscriptions: "billing-foundation:query:subscription:list",
32
+ } as const;
33
+
34
+ // Normalized subscription-event types — provider-agnostic.
35
+ // Alle Provider-Plugins normalisieren ihre eigenen event-types auf einen
36
+ // dieser. Whitelist: was die Foundation kennt; alles andere muss der
37
+ // Plugin filtern und null returnen aus verifyAndParseWebhook.
38
+ export const SubscriptionEventTypes = {
39
+ created: "subscription.created",
40
+ updated: "subscription.updated",
41
+ canceled: "subscription.canceled",
42
+ invoicePaid: "invoice.paid",
43
+ invoicePaymentFailed: "invoice.payment-failed",
44
+ } as const;
45
+ export type SubscriptionEventType =
46
+ (typeof SubscriptionEventTypes)[keyof typeof SubscriptionEventTypes];
47
+
48
+ // Normalized subscription-status values — provider-agnostic.
49
+ // Stripe + Mollie haben verschiedene Status-Sets; Plugin mapped auf
50
+ // diesen common-subset.
51
+ export const SubscriptionStatuses = {
52
+ active: "active",
53
+ trialing: "trialing",
54
+ pastDue: "past_due",
55
+ canceled: "canceled",
56
+ incomplete: "incomplete",
57
+ } as const;
58
+ export type SubscriptionStatus = (typeof SubscriptionStatuses)[keyof typeof SubscriptionStatuses];
59
+
60
+ // **Multi-Provider von Tag 1:** subscription-foundation hat KEIN
61
+ // `provider`-config-key. Alle gemounteten Plugins sind aktiv parallel —
62
+ // der Endkunde wählt beim Subscribe-Klick zwischen Karte/PayPal/
63
+ // Apple-Pay/Klarna/SEPA (Disney+-Pattern). Welcher Provider die
64
+ // aktuelle subscription des Tenants gerade hält steht in
65
+ // subscription.providerName, kommt aus dem checkout-flow.
66
+ //
67
+ // price-to-tier-Map ist KEIN foundation-config — pro-Plugin, weil
68
+ // Stripe-priceIds vs PayPal-plan-ids vs Apple-product-ids
69
+ // unterschiedliche IDs sind. Jeder Plugin definiert seinen eigenen
70
+ // `<plugin-name>:config:price-to-tier`-Key.
@@ -0,0 +1,50 @@
1
+ import {
2
+ createEntity,
3
+ createTextField,
4
+ createTimestampField,
5
+ } from "@cosmicdrift/kumiko-framework/engine";
6
+
7
+ // =============================================================================
8
+ // `subscription` — current state pro Plattform-Tenant (= Read-Model)
9
+ // =============================================================================
10
+ //
11
+ // Inline-Projection-Target. Geschrieben vom `subscription`-projection-
12
+ // apply (siehe feature.ts), NIE direkt vom handler. Source-of-truth ist
13
+ // der event-store stream `subscription` mit aggregate-id =
14
+ // uuidv5(SUBSCRIPTION_NAMESPACE, tenantId).
15
+ //
16
+ // EINE Row pro Plattform-Tenant. Aggregate-ID ist deterministic, damit
17
+ // Webhook-Replays (Stripe sendet bei Hängern bis zu 5x in 4h) auf
18
+ // denselben Stream schreiben statt zwei Rows zu erzeugen.
19
+ //
20
+ // **Felder:**
21
+ // - providerName: "stripe" / "mollie" — welcher Provider die
22
+ // Subscription hält. Provider-Wechsel = neuer event auf demselben
23
+ // Stream, projection überschreibt.
24
+ // - providerCustomerId / providerSubscriptionId: provider-eigene
25
+ // IDs.
26
+ // - status: active / past_due / canceled / trialing / incomplete —
27
+ // normalisiert über provider-grenzen hinweg.
28
+ // - tier: "free" / "pro" / ... — vom tier-engine konsumiert. Aus
29
+ // price-to-tier-Map resolved im Plugin.
30
+ // - currentPeriodEnd: wann läuft die aktuelle Billing-Period aus.
31
+ //
32
+ // **Was hier NICHT ist:**
33
+ // - invoice-history, payment-method, line-items, tax-info → all das
34
+ // fetcht der Tenant via customer-portal-session direkt vom Provider.
35
+ // - cancelAt, cancelAtPeriodEnd → Provider-Sache.
36
+ //
37
+ // **Audit/event-history:** lebt im event-store unter dem `subscription`-
38
+ // stream — KEIN eigene `subscription-event`-Tabelle mehr (= ES ist die
39
+ // audit-truth, replay-fähig durch upcasters).
40
+ export const subscriptionEntity = createEntity({
41
+ table: "read_subscriptions",
42
+ fields: {
43
+ providerName: createTextField({ required: true, maxLength: 50 }),
44
+ providerCustomerId: createTextField({ required: true, maxLength: 200 }),
45
+ providerSubscriptionId: createTextField({ required: true, maxLength: 200 }),
46
+ status: createTextField({ required: true, maxLength: 30 }),
47
+ tier: createTextField({ required: true, maxLength: 50 }),
48
+ currentPeriodEnd: createTimestampField({ required: true }),
49
+ },
50
+ });
@@ -0,0 +1,71 @@
1
+ // Domain-events für subscription-foundation.
2
+ //
3
+ // **Pattern:** event-sourced — jeder Provider-Webhook (nach Plugin-
4
+ // verify) wird zu einem domain-event auf dem subscription-stream
5
+ // (= ein stream pro Tenant via subscriptionAggregateId(tenantId)).
6
+ // Read-model = inline projection in `subscriptions`-Tabelle.
7
+ //
8
+ // 5 fine-grained event-typen statt einem generic "webhook-received"-
9
+ // bucket — events sind business-facts: future-consumer (billing-history,
10
+ // accounting-export, churn-analytics) listenen direkt auf den event-
11
+ // type ohne payload-discriminator.
12
+
13
+ import { z } from "zod";
14
+ import { BILLING_FOUNDATION_FEATURE, SubscriptionStatuses } from "./constants";
15
+
16
+ // Aggregate-type für den event-store. Eine subscription pro Tenant ist
17
+ // ein stream; der subscriptionAggregateId-helper liefert die stream-id.
18
+ export const SUBSCRIPTION_AGGREGATE_TYPE = "subscription" as const;
19
+
20
+ // Event-name-Konstanten — short-form (für r.defineEvent) + qualifizierte
21
+ // FQN (für ctx.appendEventUnsafe + projection-apply-keys).
22
+ export const SUBSCRIPTION_CREATED_EVENT_SHORT = "subscription-created" as const;
23
+ export const SUBSCRIPTION_UPDATED_EVENT_SHORT = "subscription-updated" as const;
24
+ export const SUBSCRIPTION_CANCELED_EVENT_SHORT = "subscription-canceled" as const;
25
+ export const INVOICE_PAID_EVENT_SHORT = "invoice-paid" as const;
26
+ export const INVOICE_PAYMENT_FAILED_EVENT_SHORT = "invoice-payment-failed" as const;
27
+
28
+ export const SUBSCRIPTION_CREATED_EVENT_QN =
29
+ `${BILLING_FOUNDATION_FEATURE}:event:${SUBSCRIPTION_CREATED_EVENT_SHORT}` as const;
30
+ export const SUBSCRIPTION_UPDATED_EVENT_QN =
31
+ `${BILLING_FOUNDATION_FEATURE}:event:${SUBSCRIPTION_UPDATED_EVENT_SHORT}` as const;
32
+ export const SUBSCRIPTION_CANCELED_EVENT_QN =
33
+ `${BILLING_FOUNDATION_FEATURE}:event:${SUBSCRIPTION_CANCELED_EVENT_SHORT}` as const;
34
+ export const INVOICE_PAID_EVENT_QN =
35
+ `${BILLING_FOUNDATION_FEATURE}:event:${INVOICE_PAID_EVENT_SHORT}` as const;
36
+ export const INVOICE_PAYMENT_FAILED_EVENT_QN =
37
+ `${BILLING_FOUNDATION_FEATURE}:event:${INVOICE_PAYMENT_FAILED_EVENT_SHORT}` as const;
38
+
39
+ // Status-enum für event-payloads (= subscription-state-snapshot vom Provider).
40
+ const statusEnum = z.enum([
41
+ SubscriptionStatuses.active,
42
+ SubscriptionStatuses.trialing,
43
+ SubscriptionStatuses.pastDue,
44
+ SubscriptionStatuses.canceled,
45
+ SubscriptionStatuses.incomplete,
46
+ ]);
47
+
48
+ // Common payload — alle 5 events tragen denselben subscription-state-
49
+ // snapshot. Event-type tagged was passiert ist, payload den state-after.
50
+ // Provider-spezifischer rawPayload ist in metadata.rawPayload (nicht in
51
+ // payload — payload ist domain-clean, metadata ist provider-truth).
52
+ export const subscriptionEventPayloadSchema = z.object({
53
+ providerName: z.string().min(1).max(50),
54
+ providerCustomerId: z.string().min(1).max(200),
55
+ providerSubscriptionId: z.string().min(1).max(200),
56
+ status: statusEnum,
57
+ tier: z.string().min(1).max(50),
58
+ currentPeriodEndIso: z.string().min(1),
59
+ });
60
+ export type SubscriptionEventPayload = z.infer<typeof subscriptionEventPayloadSchema>;
61
+
62
+ // Headers-shape — wird im event-store als event.metadata.headers
63
+ // persistiert (open-shape jsonb-column, primitives only).
64
+ // Idempotency-anchor: providerEventId pro provider, foundation checked
65
+ // vor append ob bereits gesehen. rawPayload ist als string archiviert
66
+ // damit Plugin-bug-fix-replays from-source machbar bleiben.
67
+ export type SubscriptionEventHeaders = {
68
+ readonly providerEventId: string;
69
+ readonly providerName: string;
70
+ readonly rawPayload: string;
71
+ };
@@ -0,0 +1,122 @@
1
+ // kumiko-feature-version: 1
2
+ //
3
+ // subscription-foundation als Kumiko bundled feature (Plugin-Host).
4
+ //
5
+ // **Multi-Provider von Tag 1** — der App-Builder kann mehrere Plugins
6
+ // parallel mounten (subscription-stripe + subscription-paypal +
7
+ // subscription-apple-iap + ...) und der Endkunde wählt beim Subscribe-
8
+ // Klick zwischen Karte/PayPal/Apple-Pay/Klarna/SEPA (Disney+-Pattern).
9
+ // KEIN globaler `provider`-config-key — alle gemounteten Plugins sind
10
+ // aktiv.
11
+ //
12
+ // **Was diese Foundation liefert:**
13
+ // 1. **Plugin-API** für Subscription-Provider via `r.extendsRegistrar(
14
+ // "subscriptionProvider", ...)`.
15
+ // 2. **5 Domain-Events** auf dem `subscription`-stream (eine
16
+ // stream-id pro Tenant): created/updated/canceled/invoice-paid/
17
+ // invoice-payment-failed. Audit-history kommt frei vom event-store.
18
+ // 3. **Inline-Projection** auf `read_subscriptions` (= current state
19
+ // pro Tenant). Apply läuft in derselben TX wie der event-append
20
+ // → read-your-own-write semantics.
21
+ // 4. **process-event-handler**: programmatic write-handler den der
22
+ // webhook-handler aufruft, dispatcht zu type-passendem appendEvent.
23
+ // 5. **createSubscriptionWebhookHandler**: factory für die HTTP-Route
24
+ // `/api/subscription/webhook/:providerName`.
25
+ //
26
+ // **Was diese Foundation NICHT macht:**
27
+ // - Kein r.entity für `subscription`. Die Tabelle ist eine reine
28
+ // Read-Projection — kein CRUD-Pfad. Schreibt wird ausschließlich
29
+ // via projection-apply, getriggert von einem der 5 events.
30
+ // - Kein Tier-Sync zum tier-engine. App-Owner liest die subscription-
31
+ // row via `getSubscriptionForTenant(ctx, tenantId)` wenn er möchte.
32
+ // - Keine provider-spezifischen Configs.
33
+ // - Kein Marketplace-Use-Case (App-Tenant billed Endkunden via
34
+ // Stripe Connect). Kommt als separate `marketplace-foundation`.
35
+ //
36
+ // **Provider-Wechsel:** Disney+-Pattern (= Tenant cancelt Stripe-sub,
37
+ // startet neue mit PayPal) wird heute als zweiter `subscription-created`-
38
+ // event modelliert. UPSERT in der projection-apply überschreibt den
39
+ // existing row mit dem neuen providerName. Reicht für MVP. Wenn das
40
+ // business-fact "Provider-Wechsel" ein eigenes domain-event braucht
41
+ // (z.B. für analytics: "wie viele Wechsel im Monat?"), kommt ein
42
+ // `subscription-provider-changed`-event-type später.
43
+
44
+ import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
45
+ import { BILLING_FOUNDATION_FEATURE, SUBSCRIPTION_PROVIDER_EXTENSION } from "./constants";
46
+ import {
47
+ INVOICE_PAID_EVENT_QN,
48
+ INVOICE_PAID_EVENT_SHORT,
49
+ INVOICE_PAYMENT_FAILED_EVENT_QN,
50
+ INVOICE_PAYMENT_FAILED_EVENT_SHORT,
51
+ SUBSCRIPTION_AGGREGATE_TYPE,
52
+ SUBSCRIPTION_CANCELED_EVENT_QN,
53
+ SUBSCRIPTION_CANCELED_EVENT_SHORT,
54
+ SUBSCRIPTION_CREATED_EVENT_QN,
55
+ SUBSCRIPTION_CREATED_EVENT_SHORT,
56
+ SUBSCRIPTION_UPDATED_EVENT_QN,
57
+ SUBSCRIPTION_UPDATED_EVENT_SHORT,
58
+ subscriptionEventPayloadSchema,
59
+ } from "./events";
60
+ import { createCheckoutSessionHandler } from "./handlers/create-checkout-session.write";
61
+ import { createPortalSessionHandler } from "./handlers/create-portal-session.write";
62
+ import { listSubscriptionsQuery } from "./handlers/list-subscriptions.query";
63
+ import { processEventHandler } from "./handlers/process-event.write";
64
+ import {
65
+ applyInvoicePaid,
66
+ applyInvoicePaymentFailed,
67
+ applySubscriptionCanceled,
68
+ applySubscriptionCreated,
69
+ applySubscriptionUpdated,
70
+ subscriptionsProjectionTable,
71
+ } from "./projection";
72
+
73
+ export const billingFoundationFeature: FeatureDefinition = defineFeature(
74
+ BILLING_FOUNDATION_FEATURE,
75
+ (r) => {
76
+ // 5 fine-grained domain-events. Alle 5 nutzen denselben payload-
77
+ // shape (= subscription-state-snapshot); der event-type taggt was
78
+ // passiert ist. Future-consumer (billing-history, accounting)
79
+ // listenen direkt auf den event-type ohne payload-discriminator.
80
+ r.defineEvent(SUBSCRIPTION_CREATED_EVENT_SHORT, subscriptionEventPayloadSchema);
81
+ r.defineEvent(SUBSCRIPTION_UPDATED_EVENT_SHORT, subscriptionEventPayloadSchema);
82
+ r.defineEvent(SUBSCRIPTION_CANCELED_EVENT_SHORT, subscriptionEventPayloadSchema);
83
+ r.defineEvent(INVOICE_PAID_EVENT_SHORT, subscriptionEventPayloadSchema);
84
+ r.defineEvent(INVOICE_PAYMENT_FAILED_EVENT_SHORT, subscriptionEventPayloadSchema);
85
+
86
+ // Inline projection: materialized current state in `read_subscriptions`.
87
+ // Apply läuft in derselben TX wie ctx.appendEventUnsafe — read-your-
88
+ // own-write ohne dispatcher-tick.
89
+ r.projection({
90
+ name: "subscription",
91
+ source: SUBSCRIPTION_AGGREGATE_TYPE,
92
+ table: subscriptionsProjectionTable,
93
+ apply: {
94
+ [SUBSCRIPTION_CREATED_EVENT_QN]: applySubscriptionCreated,
95
+ [SUBSCRIPTION_UPDATED_EVENT_QN]: applySubscriptionUpdated,
96
+ [SUBSCRIPTION_CANCELED_EVENT_QN]: applySubscriptionCanceled,
97
+ [INVOICE_PAID_EVENT_QN]: applyInvoicePaid,
98
+ [INVOICE_PAYMENT_FAILED_EVENT_QN]: applyInvoicePaymentFailed,
99
+ },
100
+ });
101
+
102
+ // Plugin extension-point. Provider-Plugins registrieren sich hier.
103
+ r.extendsRegistrar(SUBSCRIPTION_PROVIDER_EXTENSION, {
104
+ onRegister: () => {
105
+ // No side-effects at register-time.
106
+ },
107
+ });
108
+
109
+ // Custom write-handlers:
110
+ // - process-event: programmatic entry-point vom webhook-handler;
111
+ // dispatcht zu type-passendem appendEvent
112
+ // - create-checkout-session: Tenant-Admin "Upgrade to Pro"-flow
113
+ // - create-portal-session: Tenant-Admin "Manage Subscription"-flow
114
+ r.writeHandler(processEventHandler);
115
+ r.writeHandler(createCheckoutSessionHandler);
116
+ r.writeHandler(createPortalSessionHandler);
117
+
118
+ // Custom list-query auf der subscription-projection (raw drizzle-
119
+ // table; kein r.entity weil Schreiben via projection-apply läuft).
120
+ r.queryHandler(listSubscriptionsQuery);
121
+ },
122
+ );