@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,39 @@
1
+ // Resolver-helper: liest die current subscription-row für einen Tenant
2
+ // aus der read_subscriptions-projection.
3
+
4
+ import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
5
+ import { eq } from "drizzle-orm";
6
+ import { subscriptionAggregateId } from "./aggregate-id";
7
+ import { subscriptionsProjectionTable } from "./projection";
8
+
9
+ export type SubscriptionView = {
10
+ readonly tier: string;
11
+ readonly status: string;
12
+ readonly providerName: string;
13
+ readonly providerCustomerId: string;
14
+ readonly providerSubscriptionId: string;
15
+ };
16
+
17
+ /** Liefert die einzige subscription-row für den Tenant (deterministic
18
+ * aggregate-id), oder null wenn der Tenant nie subscribed hat. Status
19
+ * kann active/canceled/past_due/etc sein — Caller entscheidet was tun. */
20
+ export async function getSubscriptionForTenant(
21
+ ctx: HandlerContext,
22
+ tenantId: string,
23
+ ): Promise<SubscriptionView | null> {
24
+ const aggId = subscriptionAggregateId(tenantId);
25
+ const [row] = await ctx.db
26
+ .select()
27
+ .from(subscriptionsProjectionTable)
28
+ .where(eq(subscriptionsProjectionTable["id"], aggId))
29
+ .limit(1);
30
+ if (!row) return null;
31
+ // @cast-boundary db-row — drizzle-row carries column-as-unknown
32
+ return {
33
+ tier: row["tier"] as string,
34
+ status: row["status"] as string,
35
+ providerName: row["providerName"] as string,
36
+ providerCustomerId: row["providerCustomerId"] as string,
37
+ providerSubscriptionId: row["providerSubscriptionId"] as string,
38
+ };
39
+ }
@@ -0,0 +1,79 @@
1
+ // create-checkout-session — Tenant-Admin klickt "Upgrade to Pro" (oder
2
+ // wählt zwischen Karte/PayPal/Apple-Pay). Handler findet den
3
+ // providerName aus der payload, lookuppt den Plugin, ruft
4
+ // `plugin.createCheckoutSession(ctx, ...)`, returnt hosted-page-URL.
5
+ // Tenant-Admin wird dorthin redirected.
6
+ //
7
+ // **Multi-Provider-Pfad:** payload.providerName ist der entityName
8
+ // eines registrierten Plugins ("stripe" / "paypal" / "mollie" / ...).
9
+ // App-Builder zeigt eine UI-Liste der gemounteten provider, Endkunde
10
+ // pickt einen, dieser handler dispatched zum richtigen Plugin.
11
+ //
12
+ // **Tenant-resolution:** ctx.user.tenantId wird als metadata an den
13
+ // Provider mitgegeben. Beim subsequent webhook (subscription.created)
14
+ // liest verifyAndParseWebhook das aus dem provider-payload zurück und
15
+ // resolved den Tenant.
16
+
17
+ import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
18
+ import { z } from "zod";
19
+ import { SUBSCRIPTION_PROVIDER_EXTENSION } from "../constants";
20
+ import type { SubscriptionProviderPlugin } from "../types";
21
+
22
+ const createCheckoutSessionSchema = z.object({
23
+ /** Welcher Provider — entityName eines registrierten subscription-
24
+ * Plugins ("stripe" / "paypal" / ...). */
25
+ providerName: z.string().min(1).max(50),
26
+ /** Provider-eigene price/plan-ID. */
27
+ priceId: z.string().min(1).max(200),
28
+ /** Wo der Endkunde nach erfolgreichem checkout landed. */
29
+ successUrl: z.string().url(),
30
+ /** Wo der Endkunde landed wenn er abbricht. */
31
+ cancelUrl: z.string().url(),
32
+ /** Optional: existierender provider-customer wenn der Tenant schon
33
+ * einen account beim Provider hat (= Plan-Wechsel statt Neuregistrierung). */
34
+ providerCustomerId: z.string().max(200).optional(),
35
+ });
36
+ type CreateCheckoutSessionPayload = z.infer<typeof createCheckoutSessionSchema>;
37
+
38
+ export const createCheckoutSessionHandler: WriteHandlerDef = {
39
+ name: "create-checkout-session",
40
+ schema: createCheckoutSessionSchema,
41
+ // Tenant-Admin-only — der Tenant muss bewusst seine Subscription
42
+ // konfigurieren. SystemAdmin als Fallback für Operator-Initiated-Flows.
43
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
44
+ handler: async (event, ctx) => {
45
+ // @cast-boundary engine-payload — dispatcher-zod-validated payload
46
+ const payload = event.payload as CreateCheckoutSessionPayload;
47
+
48
+ // Plugin-Lookup via registry. Dispatcher-context hat ctx.registry —
49
+ // wir suchen den entityName-match in den extension-usages.
50
+ const usages = ctx.registry.getExtensionUsages(SUBSCRIPTION_PROVIDER_EXTENSION);
51
+ const usage = usages.find((u) => u.entityName === payload.providerName);
52
+ if (!usage) {
53
+ const known = usages.map((u) => u.entityName).join(", ") || "<none>";
54
+ throw new Error(
55
+ `subscription-foundation: provider "${payload.providerName}" not registered. Known: ${known}.`,
56
+ );
57
+ }
58
+ // @cast-boundary engine-payload — extension-usage carries unknown options
59
+ const plugin = usage.options as SubscriptionProviderPlugin;
60
+ if (!plugin.createCheckoutSession) {
61
+ throw new Error(
62
+ `subscription-foundation: provider "${payload.providerName}" has no createCheckoutSession-method (e.g. Apple-IAP-only providers). Use the provider's native checkout flow.`,
63
+ );
64
+ }
65
+
66
+ const result = await plugin.createCheckoutSession(ctx, {
67
+ priceId: payload.priceId,
68
+ tenantId: event.user.tenantId,
69
+ successUrl: payload.successUrl,
70
+ cancelUrl: payload.cancelUrl,
71
+ ...(payload.providerCustomerId && { providerCustomerId: payload.providerCustomerId }),
72
+ });
73
+
74
+ return {
75
+ isSuccess: true as const,
76
+ data: { url: result.url, providerName: payload.providerName },
77
+ };
78
+ },
79
+ };
@@ -0,0 +1,73 @@
1
+ // create-portal-session — Tenant-Admin klickt "Manage Subscription".
2
+ // Handler findet die current subscription des Tenants, lookuppt den
3
+ // passenden Plugin (= subscription.providerName-Spalte), ruft
4
+ // `plugin.createPortalSession(ctx, ...)`, returnt hosted-portal-URL.
5
+ //
6
+ // **Provider-resolution:** anders als create-checkout-session (= der
7
+ // Tenant wählt einen NEUEN Provider beim Subscribe) ist hier der
8
+ // Provider durch die existing subscription-row festgelegt — Tenant
9
+ // kann nicht zum Portal eines OTHER Providers, weil der ihn nicht
10
+ // kennt.
11
+
12
+ import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
13
+ import { eq } from "drizzle-orm";
14
+ import { z } from "zod";
15
+ import { subscriptionAggregateId } from "../aggregate-id";
16
+ import { SUBSCRIPTION_PROVIDER_EXTENSION } from "../constants";
17
+ import { subscriptionsProjectionTable as subTable } from "../projection";
18
+ import type { SubscriptionProviderPlugin } from "../types";
19
+
20
+ const createPortalSessionSchema = z.object({
21
+ /** Wo der Endkunde nach Portal-Session landed. */
22
+ returnUrl: z.string().url(),
23
+ });
24
+ type CreatePortalSessionPayload = z.infer<typeof createPortalSessionSchema>;
25
+
26
+ export const createPortalSessionHandler: WriteHandlerDef = {
27
+ name: "create-portal-session",
28
+ schema: createPortalSessionSchema,
29
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
30
+ handler: async (event, ctx) => {
31
+ const payload = event.payload as CreatePortalSessionPayload;
32
+ const tenantId = event.user.tenantId;
33
+
34
+ // 1. Hol current subscription-row für den Tenant. Aggregate-id ist
35
+ // deterministic per tenant — eine row pro tenant.
36
+ const subAggId = subscriptionAggregateId(tenantId);
37
+ const rows = await ctx.db.select().from(subTable).where(eq(subTable["id"], subAggId)).limit(1);
38
+ const row = rows[0];
39
+ if (!row) {
40
+ throw new Error(
41
+ "subscription-foundation: no active subscription for this tenant. Create one via create-checkout-session first.",
42
+ );
43
+ }
44
+ const providerName = row["providerName"] as string;
45
+ const providerCustomerId = row["providerCustomerId"] as string;
46
+
47
+ // 2. Plugin-Lookup
48
+ const usages = ctx.registry.getExtensionUsages(SUBSCRIPTION_PROVIDER_EXTENSION);
49
+ const usage = usages.find((u) => u.entityName === providerName);
50
+ if (!usage) {
51
+ throw new Error(
52
+ `subscription-foundation: subscription belongs to provider "${providerName}" but the matching plugin is not mounted.`,
53
+ );
54
+ }
55
+ // @cast-boundary engine-payload — extension-usage carries unknown options
56
+ const plugin = usage.options as SubscriptionProviderPlugin;
57
+ if (!plugin.createPortalSession) {
58
+ throw new Error(
59
+ `subscription-foundation: provider "${providerName}" has no createPortalSession-method (e.g. Apple-IAP managed Subs in der Apple-App).`,
60
+ );
61
+ }
62
+
63
+ const result = await plugin.createPortalSession(ctx, {
64
+ providerCustomerId,
65
+ returnUrl: payload.returnUrl,
66
+ });
67
+
68
+ return {
69
+ isSuccess: true as const,
70
+ data: { url: result.url, providerName },
71
+ };
72
+ },
73
+ };
@@ -0,0 +1,20 @@
1
+ // list-subscriptions — sysadmin-cross-tenant list-query auf der
2
+ // `read_subscriptions`-projection. Tenant-Admins lesen ihre eigene
3
+ // subscription via getSubscriptionForTenant-helper (= ctx.db ist
4
+ // tenant-scoped, gibt automatisch nur die row des Callers zurück).
5
+
6
+ import type { QueryHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
7
+ import { z } from "zod";
8
+ import { subscriptionsProjectionTable } from "../projection";
9
+
10
+ const listSchema = z.object({}).strict();
11
+
12
+ export const listSubscriptionsQuery: QueryHandlerDef = {
13
+ name: "subscription:list",
14
+ schema: listSchema,
15
+ access: { roles: ["SystemAdmin", "TenantAdmin"] },
16
+ handler: async (_query, ctx) => {
17
+ const rows = await ctx.db.select().from(subscriptionsProjectionTable);
18
+ return { rows };
19
+ },
20
+ };
@@ -0,0 +1,160 @@
1
+ // process-event — programmatic write-handler den der webhook-handler
2
+ // (createSubscriptionWebhookHandler) aufruft NACHDEM Plugin den raw-body
3
+ // verifiziert + zu SubscriptionEvent normalisiert hat.
4
+ //
5
+ // **ES-Pattern:**
6
+ // 1. Idempotency-check: lädt subscription-stream + scannt nach
7
+ // bereits gesehenem `metadata.providerEventId`. Provider-Replay
8
+ // (Stripe-Retry-Storm) sieht denselben event-id → duplicate=true,
9
+ // kein zweiter append.
10
+ // 2. Type-mapping: SubscriptionEvent.type (= normalisiert vom Plugin)
11
+ // → einer der 5 ES-event-typen.
12
+ // 3. ctx.appendEventUnsafe — Inline-projection materialisiert die
13
+ // `read_subscriptions`-row in derselben TX.
14
+
15
+ import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
16
+ import { z } from "zod";
17
+ import { subscriptionAggregateId } from "../aggregate-id";
18
+ import { SubscriptionEventTypes, SubscriptionStatuses } from "../constants";
19
+ import {
20
+ INVOICE_PAID_EVENT_QN,
21
+ INVOICE_PAYMENT_FAILED_EVENT_QN,
22
+ SUBSCRIPTION_AGGREGATE_TYPE,
23
+ SUBSCRIPTION_CANCELED_EVENT_QN,
24
+ SUBSCRIPTION_CREATED_EVENT_QN,
25
+ SUBSCRIPTION_UPDATED_EVENT_QN,
26
+ type SubscriptionEventHeaders,
27
+ type SubscriptionEventPayload,
28
+ } from "../events";
29
+
30
+ // =============================================================================
31
+ // Input-Schema = der normalisierte SubscriptionEvent (ohne tenantId, der
32
+ // kommt aus event.user.tenantId — der webhook-handler setzt den
33
+ // programmatic-user mit der vom Plugin aufgelösten tenantId).
34
+ // =============================================================================
35
+
36
+ const eventTypeSchema = z.enum([
37
+ SubscriptionEventTypes.created,
38
+ SubscriptionEventTypes.updated,
39
+ SubscriptionEventTypes.canceled,
40
+ SubscriptionEventTypes.invoicePaid,
41
+ SubscriptionEventTypes.invoicePaymentFailed,
42
+ ]);
43
+
44
+ const statusSchema = z.enum([
45
+ SubscriptionStatuses.active,
46
+ SubscriptionStatuses.trialing,
47
+ SubscriptionStatuses.pastDue,
48
+ SubscriptionStatuses.canceled,
49
+ SubscriptionStatuses.incomplete,
50
+ ]);
51
+
52
+ export const processEventSchema = z.object({
53
+ providerEventId: z.string().min(1).max(200),
54
+ providerName: z.string().min(1).max(50),
55
+ type: eventTypeSchema,
56
+ providerCustomerId: z.string().min(1).max(200),
57
+ providerSubscriptionId: z.string().min(1).max(200),
58
+ status: statusSchema,
59
+ tier: z.string().min(1).max(50),
60
+ currentPeriodEndIso: z.string().min(1),
61
+ rawPayload: z.string().min(1),
62
+ });
63
+ type ProcessEventPayload = z.infer<typeof processEventSchema>;
64
+
65
+ // Map normalized SubscriptionEventType → fully-qualified ES event-name.
66
+ const NORMALIZED_TO_ES_EVENT: Readonly<Record<string, string>> = {
67
+ [SubscriptionEventTypes.created]: SUBSCRIPTION_CREATED_EVENT_QN,
68
+ [SubscriptionEventTypes.updated]: SUBSCRIPTION_UPDATED_EVENT_QN,
69
+ [SubscriptionEventTypes.canceled]: SUBSCRIPTION_CANCELED_EVENT_QN,
70
+ [SubscriptionEventTypes.invoicePaid]: INVOICE_PAID_EVENT_QN,
71
+ [SubscriptionEventTypes.invoicePaymentFailed]: INVOICE_PAYMENT_FAILED_EVENT_QN,
72
+ };
73
+
74
+ // =============================================================================
75
+ // Handler
76
+ // =============================================================================
77
+ //
78
+ // SystemAdmin-only: dieser handler wird ausschließlich vom programmatic
79
+ // webhook-handler aufgerufen (mit einem internal SystemUser), nie vom
80
+ // Tenant-Admin direkt.
81
+ export const processEventHandler: WriteHandlerDef = {
82
+ name: "process-event",
83
+ schema: processEventSchema,
84
+ access: { roles: ["SystemAdmin"] },
85
+ handler: async (event, ctx) => {
86
+ // @cast-boundary engine-payload — dispatcher-zod-validated payload
87
+ const payload = event.payload as ProcessEventPayload;
88
+ const tenantId = event.user.tenantId;
89
+ const aggId = subscriptionAggregateId(tenantId);
90
+
91
+ // ---------------------------------------------------------------
92
+ // 1. Idempotency: load subscription-stream + check ob dieser
93
+ // providerEventId bereits gesehen wurde. Provider-Retry-Storm
94
+ // (Stripe sendet bis zu 5x in 4h) trifft denselben Stream und
95
+ // findet den event-id in metadata.
96
+ //
97
+ // **Performance-caveat:** O(N) pro stream. Bei 5 Jahren history
98
+ // (recurring monatlich = ~60 events) noch <50ms. Bei deutlich
99
+ // längeren streams optimieren via snapshot oder per-tenant
100
+ // dedup-table als idempotency-anchor (analog cap-counter).
101
+ // ---------------------------------------------------------------
102
+ const existingEvents = await ctx.loadAggregate(aggId);
103
+ const alreadySeen = existingEvents.some((e) => {
104
+ const headers = e.metadata.headers ?? {};
105
+ return (
106
+ headers["providerEventId"] === payload.providerEventId &&
107
+ headers["providerName"] === payload.providerName
108
+ );
109
+ });
110
+ if (alreadySeen) {
111
+ return {
112
+ isSuccess: true as const,
113
+ data: { duplicate: true as const, subscriptionAggregateId: aggId },
114
+ };
115
+ }
116
+
117
+ // ---------------------------------------------------------------
118
+ // 2. Map normalized event-type → ES event-FQN.
119
+ // ---------------------------------------------------------------
120
+ const esEventType = NORMALIZED_TO_ES_EVENT[payload.type];
121
+ if (!esEventType) {
122
+ // Schema-validation oben sollte das schon fangen, aber defensive
123
+ // gegen drift im SubscriptionEventTypes-enum vs NORMALIZED-Map.
124
+ throw new Error(`subscription-foundation: no ES event-type mapping for "${payload.type}"`);
125
+ }
126
+
127
+ // ---------------------------------------------------------------
128
+ // 3. Append event auf den subscription-stream. Inline-projection
129
+ // materialisiert die read_subscriptions-row in derselben TX.
130
+ // ---------------------------------------------------------------
131
+ const eventPayload: SubscriptionEventPayload = {
132
+ providerName: payload.providerName,
133
+ providerCustomerId: payload.providerCustomerId,
134
+ providerSubscriptionId: payload.providerSubscriptionId,
135
+ status: payload.status,
136
+ tier: payload.tier,
137
+ currentPeriodEndIso: payload.currentPeriodEndIso,
138
+ };
139
+ const headers: SubscriptionEventHeaders = {
140
+ providerEventId: payload.providerEventId,
141
+ providerName: payload.providerName,
142
+ rawPayload: payload.rawPayload,
143
+ };
144
+ await ctx.appendEventUnsafe({
145
+ aggregateId: aggId,
146
+ aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
147
+ type: esEventType,
148
+ payload: eventPayload,
149
+ headers,
150
+ });
151
+
152
+ return {
153
+ isSuccess: true as const,
154
+ data: {
155
+ duplicate: false as const,
156
+ subscriptionAggregateId: aggId,
157
+ },
158
+ };
159
+ },
160
+ };
@@ -0,0 +1,42 @@
1
+ // Public API of the subscription-foundation bundled-feature.
2
+
3
+ export { subscriptionAggregateId } from "./aggregate-id";
4
+ export {
5
+ BILLING_FOUNDATION_FEATURE,
6
+ SUBSCRIPTION_PROVIDER_EXTENSION,
7
+ type SubscriptionEventType,
8
+ SubscriptionEventTypes,
9
+ SubscriptionFoundationHandlers,
10
+ SubscriptionFoundationQueries,
11
+ type SubscriptionStatus,
12
+ SubscriptionStatuses,
13
+ } from "./constants";
14
+ export { subscriptionEntity } from "./entities";
15
+ export {
16
+ INVOICE_PAID_EVENT_QN,
17
+ INVOICE_PAID_EVENT_SHORT,
18
+ INVOICE_PAYMENT_FAILED_EVENT_QN,
19
+ INVOICE_PAYMENT_FAILED_EVENT_SHORT,
20
+ SUBSCRIPTION_AGGREGATE_TYPE,
21
+ SUBSCRIPTION_CANCELED_EVENT_QN,
22
+ SUBSCRIPTION_CANCELED_EVENT_SHORT,
23
+ SUBSCRIPTION_CREATED_EVENT_QN,
24
+ SUBSCRIPTION_CREATED_EVENT_SHORT,
25
+ SUBSCRIPTION_UPDATED_EVENT_QN,
26
+ SUBSCRIPTION_UPDATED_EVENT_SHORT,
27
+ type SubscriptionEventHeaders,
28
+ type SubscriptionEventPayload,
29
+ subscriptionEventPayloadSchema,
30
+ } from "./events";
31
+ export { billingFoundationFeature } from "./feature";
32
+ export { getSubscriptionForTenant, type SubscriptionView } from "./get-subscription-for-tenant";
33
+ export { subscriptionsProjectionTable } from "./projection";
34
+ export type {
35
+ SubscriptionEvent,
36
+ SubscriptionProviderPlugin,
37
+ } from "./types";
38
+ export {
39
+ createSubscriptionWebhookHandler,
40
+ type SubscriptionWebhookDeps,
41
+ type SubscriptionWebhookHandler,
42
+ } from "./webhook-handler";
@@ -0,0 +1,135 @@
1
+ // Inline-projection für `read_subscriptions`. Materialisiert die 5
2
+ // subscription-events in eine row pro Tenant.
3
+ //
4
+ // Apply läuft in derselben TX wie ctx.appendEventUnsafe — Caller sieht
5
+ // seinen Schreib-State sofort (kein dispatcher-tick nötig). PK = event.
6
+ // aggregateId (= deterministic uuidv5 pro Tenant) → replays kollidieren
7
+ // auf der PK statt doppelte rows zu erzeugen.
8
+ //
9
+ // **Production-deployment caveat:** der Generator in
10
+ // `samples/apps/platform/drizzle/generate.ts` scant `feature.entities` —
11
+ // `subscriptionsProjectionTable` ist als raw drizzle-pgTable in der
12
+ // projection registriert, NICHT als r.entity. Apps die subscription-
13
+ // foundation production mounten müssen die Tabelle in ihre eigene
14
+ // `drizzle/generate.ts` ergänzen (= via subscriptionsProjectionTable-
15
+ // import). setupTestStack pusht sie automatisch via r.projection.table.
16
+
17
+ import { buildDrizzleTable } from "@cosmicdrift/kumiko-framework/db";
18
+ import { defineApply } from "@cosmicdrift/kumiko-framework/engine";
19
+ import { subscriptionEntity } from "./entities";
20
+ import type { SubscriptionEventPayload } from "./events";
21
+
22
+ // Drizzle-table-instance aus dem entity-shape. Wird sowohl von der
23
+ // projection-apply als auch von list-query / get-helper genutzt damit
24
+ // alle drei Stellen denselben column-namespace teilen.
25
+ export const subscriptionsProjectionTable = buildDrizzleTable("subscription", subscriptionEntity);
26
+
27
+ // =============================================================================
28
+ // Shared helpers
29
+ // =============================================================================
30
+
31
+ /** Felder die alle 5 events vollständig zur Verfügung haben. */
32
+ function fullSetFromPayload(p: SubscriptionEventPayload) {
33
+ return {
34
+ providerName: p.providerName,
35
+ providerCustomerId: p.providerCustomerId,
36
+ providerSubscriptionId: p.providerSubscriptionId,
37
+ status: p.status,
38
+ tier: p.tier,
39
+ currentPeriodEnd: p.currentPeriodEndIso,
40
+ };
41
+ }
42
+
43
+ /** UPSERT-helper für defensive apply: wenn die row nicht existiert
44
+ * (= z.B. Plugin sendet "updated" als ersten event eines streams,
45
+ * oder rebuild-aus-dem-Nichts), legen wir sie an statt fail-silent
46
+ * zu sein. Apply läuft in der event-TX, expectedVersion macht
47
+ * drizzle-on-conflict korrekt. */
48
+ async function upsert(
49
+ tx: Parameters<Parameters<typeof defineApply<SubscriptionEventPayload>>[0]>[1],
50
+ event: { aggregateId: string; tenantId: string },
51
+ set: Partial<{
52
+ providerName: string;
53
+ providerCustomerId: string;
54
+ providerSubscriptionId: string;
55
+ status: string;
56
+ tier: string;
57
+ currentPeriodEnd: string;
58
+ }>,
59
+ fullPayload: SubscriptionEventPayload,
60
+ ): Promise<void> {
61
+ // INSERT-fallback braucht ALL fields (NOT NULL constraints). Wenn
62
+ // jemand nur teil-felder updated (z.B. invoice-payment-failed nur
63
+ // status+tier), nutzen wir trotzdem den vollen payload für den
64
+ // INSERT-Pfad und nur den teil-`set` für ON CONFLICT.
65
+ await tx
66
+ .insert(subscriptionsProjectionTable)
67
+ .values({
68
+ id: event.aggregateId,
69
+ tenantId: event.tenantId,
70
+ ...fullSetFromPayload(fullPayload),
71
+ })
72
+ .onConflictDoUpdate({
73
+ target: subscriptionsProjectionTable["id"],
74
+ set,
75
+ });
76
+ }
77
+
78
+ // =============================================================================
79
+ // Apply-functions — eine pro event-typ
80
+ //
81
+ // Alle UPSERT für defensive consistency: ein out-of-order event
82
+ // (z.B. rebuild-from-events) kann in jeder Reihenfolge ankommen
83
+ // und die row korrekt materialisieren.
84
+ // =============================================================================
85
+
86
+ /** subscription-created → UPSERT mit allen Feldern. PK = aggregateId =
87
+ * subscriptionAggregateId(tenantId), one row pro Tenant. UPSERT damit
88
+ * Disney+-Wechsel-Pattern (= zweiter Provider sendet create für selben
89
+ * Tenant) den existing row überschreibt statt PK-conflict. */
90
+ export const applySubscriptionCreated = defineApply<SubscriptionEventPayload>(async (event, tx) => {
91
+ const full = fullSetFromPayload(event.payload);
92
+ await upsert(tx, event, full, event.payload);
93
+ });
94
+
95
+ /** subscription-updated → UPSERT mit allen Feldern. */
96
+ export const applySubscriptionUpdated = defineApply<SubscriptionEventPayload>(async (event, tx) => {
97
+ const full = fullSetFromPayload(event.payload);
98
+ await upsert(tx, event, full, event.payload);
99
+ });
100
+
101
+ /** subscription-canceled → status/tier/currentPeriodEnd patchen. */
102
+ export const applySubscriptionCanceled = defineApply<SubscriptionEventPayload>(
103
+ async (event, tx) => {
104
+ const p = event.payload;
105
+ await upsert(
106
+ tx,
107
+ event,
108
+ { status: p.status, tier: p.tier, currentPeriodEnd: p.currentPeriodEndIso },
109
+ p,
110
+ );
111
+ },
112
+ );
113
+
114
+ /** invoice-paid → state-update (status, tier, currentPeriodEnd).
115
+ * Invoice-history selbst lebt im event-store (= Replay-fähig). */
116
+ export const applyInvoicePaid = defineApply<SubscriptionEventPayload>(async (event, tx) => {
117
+ const p = event.payload;
118
+ await upsert(
119
+ tx,
120
+ event,
121
+ { status: p.status, tier: p.tier, currentPeriodEnd: p.currentPeriodEndIso },
122
+ p,
123
+ );
124
+ });
125
+
126
+ /** invoice-payment-failed → status (typisch past_due) + tier. tier-
127
+ * engine liest die row + entscheidet ob downgrade. currentPeriodEnd
128
+ * bewusst nicht — die Period ist noch nicht "vorbei", payment hat
129
+ * nur nicht geklappt. */
130
+ export const applyInvoicePaymentFailed = defineApply<SubscriptionEventPayload>(
131
+ async (event, tx) => {
132
+ const p = event.payload;
133
+ await upsert(tx, event, { status: p.status, tier: p.tier }, p);
134
+ },
135
+ );