@cosmicdrift/kumiko-samples 0.387.0 → 0.389.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 (274) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +8 -1
  3. package/packages/bundled-features/src/agent-tools/changes.json +12 -0
  4. package/packages/bundled-features/src/agent-tools/index.ts +1 -1
  5. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +48 -32
  6. package/packages/bundled-features/src/audit/changes.json +12 -0
  7. package/packages/bundled-features/src/audit/feature.ts +1 -0
  8. package/packages/bundled-features/src/audit/handlers/audit-events.ts +1 -1
  9. package/packages/bundled-features/src/auth-email-password/changes.json +12 -0
  10. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +1 -0
  11. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +1 -0
  12. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +1 -0
  13. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +4 -0
  14. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +8 -0
  15. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +16 -3
  16. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -0
  17. package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +8 -2
  18. package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +23 -21
  19. package/packages/bundled-features/src/auth-email-password/web/default-auth-pages-gate.tsx +6 -3
  20. package/packages/bundled-features/src/auth-email-password/web/forgot-password-screen.tsx +11 -1
  21. package/packages/bundled-features/src/auth-email-password/web/index.ts +5 -0
  22. package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +16 -5
  23. package/packages/bundled-features/src/auth-email-password/web/reset-password-screen.tsx +7 -1
  24. package/packages/bundled-features/src/auth-email-password/web/return-path.ts +35 -0
  25. package/packages/bundled-features/src/auth-email-password/web/session.tsx +6 -1
  26. package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +11 -3
  27. package/packages/bundled-features/src/auth-email-password/web/signup-screen.tsx +12 -2
  28. package/packages/bundled-features/src/auth-mfa/changes.json +12 -0
  29. package/packages/bundled-features/src/auth-mfa/config.ts +3 -7
  30. package/packages/bundled-features/src/auth-mfa/feature-exports.ts +2 -2
  31. package/packages/bundled-features/src/auth-mfa/feature.ts +6 -0
  32. package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +8 -5
  33. package/packages/bundled-features/src/auth-mfa/web/i18n.ts +6 -1
  34. package/packages/bundled-features/src/auth-mfa/web/mfa-code-or-recovery-field.tsx +68 -0
  35. package/packages/bundled-features/src/auth-mfa/web/mfa-disable-dialog.tsx +23 -13
  36. package/packages/bundled-features/src/auth-mfa/web/mfa-regenerate-recovery-dialog.tsx +23 -13
  37. package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +15 -6
  38. package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +65 -18
  39. package/packages/bundled-features/src/billing-foundation/changes.json +55 -0
  40. package/packages/bundled-features/src/billing-foundation/checkout-core.ts +32 -42
  41. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +8 -2
  42. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +2 -0
  43. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +14 -3
  44. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +3 -4
  45. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +12 -6
  46. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +35 -19
  47. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +12 -8
  48. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts +14 -8
  49. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +5 -0
  50. package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +8 -10
  51. package/packages/bundled-features/src/billing-foundation/events.ts +1 -0
  52. package/packages/bundled-features/src/billing-foundation/feature.ts +3 -1
  53. package/packages/bundled-features/src/billing-foundation/handlers/billing-plans.query.ts +71 -6
  54. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +1 -0
  55. package/packages/bundled-features/src/billing-foundation/handlers/plan-switch-preview.query.ts +65 -0
  56. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +9 -2
  57. package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +44 -8
  58. package/packages/bundled-features/src/billing-foundation/handlers/switch-plan.write.ts +72 -82
  59. package/packages/bundled-features/src/billing-foundation/handlers/sync-subscription.write.ts +14 -1
  60. package/packages/bundled-features/src/billing-foundation/i18n.ts +22 -0
  61. package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
  62. package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +62 -27
  63. package/packages/bundled-features/src/billing-foundation/plan-switch.ts +84 -0
  64. package/packages/bundled-features/src/billing-foundation/types.ts +80 -1
  65. package/packages/bundled-features/src/billing-foundation/validate-options.ts +19 -0
  66. package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +119 -47
  67. package/packages/bundled-features/src/billing-foundation/web/checkout-consent-dialog.tsx +89 -42
  68. package/packages/bundled-features/src/billing-foundation/web/constants.ts +4 -0
  69. package/packages/bundled-features/src/billing-foundation/web/plan-price.ts +55 -0
  70. package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +224 -0
  71. package/packages/bundled-features/src/cap-counter/cap-reservation.ts +25 -8
  72. package/packages/bundled-features/src/cap-counter/changes.json +30 -0
  73. package/packages/bundled-features/src/cap-counter/enforce-cap.ts +2 -1
  74. package/packages/bundled-features/src/cap-counter/feature.ts +1 -0
  75. package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +8 -1
  76. package/packages/bundled-features/src/cap-overview/feature.ts +1 -0
  77. package/packages/bundled-features/src/channel-email/changes.json +6 -0
  78. package/packages/bundled-features/src/channel-email/feature.ts +1 -1
  79. package/packages/bundled-features/src/config/changes.json +18 -0
  80. package/packages/bundled-features/src/config/handlers/cascade.query.ts +4 -2
  81. package/packages/bundled-features/src/config/handlers/set.write.ts +19 -2
  82. package/packages/bundled-features/src/config/handlers/values.query.ts +4 -2
  83. package/packages/bundled-features/src/config/write-helpers.ts +27 -8
  84. package/packages/bundled-features/src/crypto-shredding/changes.json +18 -0
  85. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +11 -10
  86. package/packages/bundled-features/src/custom-fields/lib/parse-serialized-field.ts +1 -1
  87. package/packages/bundled-features/src/delivery/changes.json +42 -0
  88. package/packages/bundled-features/src/delivery/channel-context.ts +9 -3
  89. package/packages/bundled-features/src/delivery/db/queries/preferences.ts +2 -2
  90. package/packages/bundled-features/src/delivery/delivery-service.ts +35 -5
  91. package/packages/bundled-features/src/delivery/feature.ts +1 -0
  92. package/packages/bundled-features/src/delivery/handlers/preferences.query.ts +20 -1
  93. package/packages/bundled-features/src/delivery/handlers/unsubscribe-page-chrome.query.ts +1 -1
  94. package/packages/bundled-features/src/delivery/index.ts +5 -0
  95. package/packages/bundled-features/src/delivery/system-channel-context.ts +33 -0
  96. package/packages/bundled-features/src/delivery/unsubscribe-link.ts +3 -1
  97. package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +1 -0
  98. package/packages/bundled-features/src/delivery/unsubscribe-page.ts +6 -0
  99. package/packages/bundled-features/src/delivery/unsubscribe.ts +36 -15
  100. package/packages/bundled-features/src/document-ingest-foundation/append-missing-extract-event.ts +84 -0
  101. package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
  102. package/packages/bundled-features/src/document-ingest-foundation/events.ts +15 -0
  103. package/packages/bundled-features/src/document-ingest-foundation/index.ts +11 -0
  104. package/packages/bundled-features/src/feature-toggles/feature.ts +1 -0
  105. package/packages/bundled-features/src/file-foundation/index.ts +1 -0
  106. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +7 -1
  107. package/packages/bundled-features/src/file-provider-s3/env-named-feature.ts +0 -2
  108. package/packages/bundled-features/src/files/README.md +1 -1
  109. package/packages/bundled-features/src/form-draft/changes.json +6 -0
  110. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +2 -2
  111. package/packages/bundled-features/src/form-draft/decrypt-draft.ts +4 -1
  112. package/packages/bundled-features/src/host-routing/README.md +1 -1
  113. package/packages/bundled-features/src/host-routing/changes.json +12 -0
  114. package/packages/bundled-features/src/host-routing/dispatch.ts +16 -17
  115. package/packages/bundled-features/src/host-routing/options.ts +2 -1
  116. package/packages/bundled-features/src/host-routing/validate-options.ts +5 -0
  117. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +18 -0
  118. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -0
  119. package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +17 -5
  120. package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +0 -1
  121. package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +0 -3
  122. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -3
  123. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +32 -16
  124. package/packages/bundled-features/src/jobs/changes.json +13 -0
  125. package/packages/bundled-features/src/jobs/feature.ts +15 -4
  126. package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +45 -40
  127. package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +63 -59
  128. package/packages/bundled-features/src/jobs/job-run-logger.ts +5 -4
  129. package/packages/bundled-features/src/ledger/web/index.ts +1 -1
  130. package/packages/bundled-features/src/legal-pages/README.md +1 -1
  131. package/packages/bundled-features/src/legal-pages/seed.ts +5 -2
  132. package/packages/bundled-features/src/mail-foundation/changes.json +6 -0
  133. package/packages/bundled-features/src/mail-foundation/feature.ts +5 -3
  134. package/packages/bundled-features/src/mail-transport-smtp/changes.json +6 -0
  135. package/packages/bundled-features/src/mail-transport-smtp/config-keys.ts +1 -1
  136. package/packages/bundled-features/src/mail-transport-smtp/feature.ts +9 -11
  137. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  138. package/packages/bundled-features/src/managed-pages/constants.ts +1 -0
  139. package/packages/bundled-features/src/managed-pages/feature.ts +3 -1
  140. package/packages/bundled-features/src/managed-pages/index.ts +1 -0
  141. package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +1 -1
  142. package/packages/bundled-features/src/personal-access-tokens/feature.ts +5 -1
  143. package/packages/bundled-features/src/renderer-pdf/changes.json +6 -0
  144. package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +27 -10
  145. package/packages/bundled-features/src/secrets/changes.json +6 -0
  146. package/packages/bundled-features/src/secrets/write-gate.ts +9 -2
  147. package/packages/bundled-features/src/sessions/changes.json +7 -0
  148. package/packages/bundled-features/src/sessions/feature.ts +8 -2
  149. package/packages/bundled-features/src/sessions/index.ts +1 -1
  150. package/packages/bundled-features/src/sessions/session-callbacks.ts +1 -9
  151. package/packages/bundled-features/src/shared/decrypt-stored-pii.ts +10 -0
  152. package/packages/bundled-features/src/shared/index.ts +1 -1
  153. package/packages/bundled-features/src/shared/mail-template-body.ts +10 -3
  154. package/packages/bundled-features/src/shared/purge-files-reaching-subject.ts +24 -2
  155. package/packages/bundled-features/src/shared/run-in-sub-transaction.ts +5 -5
  156. package/packages/bundled-features/src/step-dispatcher/changes.json +12 -0
  157. package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +17 -3
  158. package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -0
  159. package/packages/bundled-features/src/step-dispatcher/mail-runner.ts +2 -1
  160. package/packages/bundled-features/src/subscription-dodo/changes.json +14 -0
  161. package/packages/bundled-features/src/subscription-dodo/client.ts +45 -0
  162. package/packages/bundled-features/src/subscription-dodo/constants.ts +24 -0
  163. package/packages/bundled-features/src/subscription-dodo/feature.ts +207 -0
  164. package/packages/bundled-features/src/subscription-dodo/index.ts +16 -0
  165. package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +345 -0
  166. package/packages/bundled-features/src/subscription-dodo/runtime.ts +126 -0
  167. package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +262 -0
  168. package/packages/bundled-features/src/subscription-stripe/changes.json +12 -0
  169. package/packages/bundled-features/src/subscription-stripe/feature.ts +1 -0
  170. package/packages/bundled-features/src/subscription-stripe/runtime.ts +7 -6
  171. package/packages/bundled-features/src/tags/feature.ts +1 -0
  172. package/packages/bundled-features/src/template-resolver/api.ts +31 -4
  173. package/packages/bundled-features/src/template-resolver/changes.json +84 -0
  174. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +1 -0
  175. package/packages/bundled-features/src/template-resolver/document-ops.ts +20 -1
  176. package/packages/bundled-features/src/template-resolver/feature.ts +4 -4
  177. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +3 -2
  178. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +27 -5
  179. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +3 -2
  180. package/packages/bundled-features/src/template-resolver/handlers/document-base.ts +3 -2
  181. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -2
  182. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +36 -11
  183. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +18 -19
  184. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +45 -10
  185. package/packages/bundled-features/src/template-resolver/handlers/patch-draft.write.ts +1 -1
  186. package/packages/bundled-features/src/template-resolver/handlers/send-test-status.query.ts +22 -19
  187. package/packages/bundled-features/src/template-resolver/handlers/tenant-override-counts.query.ts +9 -5
  188. package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts +8 -6
  189. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +44 -5
  190. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +37 -26
  191. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +7 -2
  192. package/packages/bundled-features/src/template-resolver/seeding.ts +42 -17
  193. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +74 -12
  194. package/packages/bundled-features/src/template-resolver/template-delivery-policy.ts +0 -11
  195. package/packages/bundled-features/src/template-resolver/template-meta.ts +12 -4
  196. package/packages/bundled-features/src/template-resolver/template-preview.ts +11 -3
  197. package/packages/bundled-features/src/template-resolver/web/constants.ts +7 -5
  198. package/packages/bundled-features/src/template-resolver/web/i18n.ts +6 -0
  199. package/packages/bundled-features/src/template-resolver/web/index.ts +7 -0
  200. package/packages/bundled-features/src/template-resolver/web/mail-layout-constants.ts +8 -0
  201. package/packages/bundled-features/src/template-resolver/web/qualified-names.ts +1 -0
  202. package/packages/bundled-features/src/tenant/changes.json +6 -0
  203. package/packages/bundled-features/src/tenant/feature.ts +1 -0
  204. package/packages/bundled-features/src/tenant/handlers/member-directory.query.ts +9 -3
  205. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -3
  206. package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +7 -4
  207. package/packages/bundled-features/src/tenant-lifecycle/changes.json +12 -0
  208. package/packages/bundled-features/src/tenant-lifecycle/constants.ts +5 -0
  209. package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +5 -1
  210. package/packages/bundled-features/src/tenant-lifecycle/stages.ts +35 -1
  211. package/packages/bundled-features/src/tier-engine/changes.json +6 -0
  212. package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +4 -1
  213. package/packages/bundled-features/src/tier-engine/web/constants.ts +3 -3
  214. package/packages/bundled-features/src/user/feature.ts +1 -0
  215. package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
  216. package/packages/bundled-features/src/user-data-rights/feature.ts +1 -0
  217. package/packages/bundled-features/src/user-data-rights/index.ts +6 -1
  218. package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +3 -4
  219. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +2 -1
  220. package/packages/bundled-features/src/user-data-rights/web/constants.ts +6 -0
  221. package/packages/bundled-features/src/user-data-rights/web/index.ts +1 -0
  222. package/packages/bundled-features/src/waitlist/changes.json +36 -0
  223. package/packages/bundled-features/src/waitlist/feature.ts +1 -0
  224. package/packages/bundled-features/src/waitlist/handlers/export-purge.write.ts +2 -1
  225. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +28 -9
  226. package/packages/bundled-features/src/waitlist/import.ts +28 -3
  227. package/packages/bundled-features/src/waitlist/legacy-migrate.ts +24 -4
  228. package/packages/bundled-features/src/waitlist/link-on-signup.ts +20 -10
  229. package/packages/bundled-features/src/waitlist/options.ts +5 -1
  230. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +1 -1
  231. package/samples/apps/styleguide/src/app/client.tsx +2 -0
  232. package/samples/apps/styleguide/src/app/seed.ts +6 -4
  233. package/samples/apps/styleguide/src/app/server.ts +4 -1
  234. package/samples/apps/styleguide/src/features/widgets/feature.ts +15 -4
  235. package/samples/apps/styleguide/src/features/widgets/i18n.ts +11 -0
  236. package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +24 -0
  237. package/samples/apps/styleguide/src/features/workboard/feature.ts +185 -0
  238. package/samples/apps/styleguide/src/features/workboard/i18n.ts +27 -0
  239. package/samples/apps/styleguide/src/features/workboard/seed.ts +121 -0
  240. package/samples/apps/styleguide/src/features/workboard/web.ts +13 -0
  241. package/samples/apps/ui-walkthrough/src/features/rental/i18n.ts +2 -0
  242. package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +9 -0
  243. package/samples/apps/use-all-bundled/bin/main.ts +2 -0
  244. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +5 -0
  245. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +90 -15
  246. package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +16 -6
  247. package/samples/apps/use-all-bundled/feature-manifest.json +136 -3
  248. package/samples/apps/use-all-bundled/kumiko/schema.ts +2 -0
  249. package/samples/apps/use-all-bundled/kumiko.config.ts +2 -0
  250. package/samples/apps/use-all-bundled/package.json +2 -1
  251. package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +2 -0
  252. package/samples/apps/use-all-bundled/src/app/notes-feature.ts +2 -0
  253. package/samples/apps/use-all-bundled/src/bundled-lists.ts +15 -0
  254. package/samples/apps/use-all-bundled/src/run-config.ts +9 -3
  255. package/samples/recipes/auth-mfa/README.md +5 -0
  256. package/samples/recipes/auth-mfa/package.json +5 -2
  257. package/samples/recipes/auth-mfa/src/web/confirmation-code-step.tsx +49 -0
  258. package/samples/recipes/auth-mfa/tsconfig.json +8 -0
  259. package/samples/recipes/env-composition/README.md +3 -1
  260. package/samples/recipes/env-composition/src/env.ts +2 -0
  261. package/samples/recipes/golden-path/src/feature.ts +2 -1
  262. package/samples/recipes/legal-pages/README.md +1 -1
  263. package/samples/recipes/lifecycle-hooks/README.md +4 -0
  264. package/samples/recipes/lifecycle-hooks/src/feature.ts +26 -1
  265. package/samples/recipes/list-table-options/README.md +50 -0
  266. package/samples/recipes/list-table-options/package.json +11 -0
  267. package/samples/recipes/list-table-options/src/feature.ts +221 -0
  268. package/samples/recipes/pdf-audit-export/src/feature.ts +2 -1
  269. package/samples/recipes/rate-limiting/README.md +18 -0
  270. package/samples/recipes/rate-limiting/src/feature.ts +12 -0
  271. package/samples/recipes/state-machine/src/events.ts +3 -2
  272. package/samples/recipes/tenant-isolation/README.md +1 -0
  273. package/samples/recipes/tenant-isolation/src/feature.ts +36 -0
  274. package/packages/bundled-features/src/billing-foundation/consumer-protection/stored-user-email.ts +0 -12
@@ -28,13 +28,21 @@ const loanMonthMs = (monthIndex: number): number => Date.UTC(2020, monthIndex, 1
28
28
  // screenshots stay stable.
29
29
  const growth = (1 + LOAN_MONTHLY_RATE) ** LOAN_MONTHS;
30
30
  const annuityMinor = (LOAN_PRINCIPAL_MINOR * LOAN_MONTHLY_RATE * growth) / (growth - 1);
31
+ // Month index -> extra repayment, which shortens the term below LOAN_MONTHS.
32
+ const EXTRA_REPAYMENTS_MINOR: ReadonlyMap<number, number> = new Map([
33
+ [84, 3_000_000],
34
+ [150, 2_000_000],
35
+ ]);
31
36
  let balanceMinor: number = LOAN_PRINCIPAL_MINOR;
32
37
  let interestPaidMinor = 0;
38
+ let payoffMonth: number | undefined;
33
39
  const LOAN_REMAINING_POINTS = [{ atMs: loanMonthMs(0), value: LOAN_PRINCIPAL_MINOR }];
34
40
  const LOAN_INTEREST_POINTS = [{ atMs: loanMonthMs(0), value: 0 }];
35
41
  for (let i = 1; i <= LOAN_MONTHS; i++) {
36
42
  const interest = balanceMinor * LOAN_MONTHLY_RATE;
37
- balanceMinor = Math.max(0, balanceMinor - (annuityMinor - interest));
43
+ const extraRepayment = EXTRA_REPAYMENTS_MINOR.get(i) ?? 0;
44
+ balanceMinor = Math.max(0, balanceMinor - (annuityMinor - interest) - extraRepayment);
45
+ if (balanceMinor === 0) payoffMonth ??= i;
38
46
  interestPaidMinor += interest;
39
47
  LOAN_REMAINING_POINTS.push({ atMs: loanMonthMs(i), value: Math.round(balanceMinor) });
40
48
  LOAN_INTEREST_POINTS.push({ atMs: loanMonthMs(i), value: Math.round(interestPaidMinor) });
@@ -44,10 +52,13 @@ const LOAN_SERIES = [
44
52
  { key: "interest", label: "widgets:dashboard:loan-interest", points: LOAN_INTEREST_POINTS },
45
53
  ];
46
54
  const LOAN_MARKERS = [
47
- { atMs: loanMonthMs(84), label: { i18nKey: "widgets:dashboard:marker-extra-1" }, kind: "extra" },
48
- { atMs: loanMonthMs(150), label: { i18nKey: "widgets:dashboard:marker-extra-2" }, kind: "extra" },
55
+ ...[...EXTRA_REPAYMENTS_MINOR.keys()].map((monthIndex, i) => ({
56
+ atMs: loanMonthMs(monthIndex),
57
+ label: { i18nKey: `widgets:dashboard:marker-extra-${i + 1}` },
58
+ kind: "extra",
59
+ })),
49
60
  {
50
- atMs: loanMonthMs(LOAN_MONTHS),
61
+ atMs: loanMonthMs(payoffMonth ?? LOAN_MONTHS),
51
62
  label: { i18nKey: "widgets:dashboard:marker-payoff" },
52
63
  kind: "payoff",
53
64
  },
@@ -119,6 +119,17 @@ export const WIDGETS_I18N = {
119
119
  "widgets:catalog:status-maintenance": { de: "maintenance", en: "maintenance" },
120
120
  "widgets:catalog:repayment-model": { de: "Tilgungsmodell", en: "Repayment model" },
121
121
  "widgets:catalog:mode-annuity": { de: "Annuität", en: "Annuity" },
122
+ "widgets:catalog:choice-cards": { de: "Zahlungsanbieter", en: "Payment provider" },
123
+ "widgets:catalog:choice-direct": { de: "Direkt", en: "Direct" },
124
+ "widgets:catalog:choice-direct-description": {
125
+ de: "Rechnung vom Anbieter der App",
126
+ en: "Invoiced by the app operator",
127
+ },
128
+ "widgets:catalog:choice-reseller": { de: "Reseller", en: "Reseller" },
129
+ "widgets:catalog:choice-reseller-description": {
130
+ de: "Verkäufer ist der Reseller, inklusive Umsatzsteuer",
131
+ en: "Sold by the reseller, VAT included",
132
+ },
122
133
  "widgets:catalog:mode-fixed": { de: "Feste Rate", en: "Fixed rate" },
123
134
  "widgets:catalog:model": { de: "Modell", en: "Model" },
124
135
  "widgets:catalog:nominal-rate": { de: "Sollzins", en: "Nominal rate" },
@@ -6,6 +6,7 @@ import {
6
6
  AiTextArea,
7
7
  AiTextField,
8
8
  BooleanField,
9
+ ChoiceCards,
9
10
  CollapsibleSection,
10
11
  ComparisonTable,
11
12
  DateField,
@@ -50,6 +51,7 @@ export function Widgets(): ReactNode {
50
51
  const t = useTranslation();
51
52
  const locale = useLocale().locale();
52
53
  const [mode, setMode] = useState<"annuity" | "fixed">("annuity");
54
+ const [provider, setProvider] = useState<"direct" | "reseller">("direct");
53
55
  const [drawerOpen, setDrawerOpen] = useState(false);
54
56
  const [belowHeaderDrawerOpen, setBelowHeaderDrawerOpen] = useState(false);
55
57
  const [floatingPanelOpen, setFloatingPanelOpen] = useState(false);
@@ -181,6 +183,28 @@ export function Widgets(): ReactNode {
181
183
  />
182
184
  </SectionCard>
183
185
 
186
+ <SectionCard title={t("widgets:catalog:choice-cards")}>
187
+ <ChoiceCards
188
+ value={provider}
189
+ onChange={setProvider}
190
+ ariaLabel={t("widgets:catalog:choice-cards")}
191
+ options={[
192
+ {
193
+ value: "direct",
194
+ title: t("widgets:catalog:choice-direct"),
195
+ description: t("widgets:catalog:choice-direct-description"),
196
+ aside: "9,00 €",
197
+ },
198
+ {
199
+ value: "reseller",
200
+ title: t("widgets:catalog:choice-reseller"),
201
+ description: t("widgets:catalog:choice-reseller-description"),
202
+ aside: "9,90 €",
203
+ },
204
+ ]}
205
+ />
206
+ </SectionCard>
207
+
184
208
  <CollapsibleSection title={t("widgets:catalog:advanced-settings")}>
185
209
  <EmptyState
186
210
  title={t("widgets:catalog:no-extra-repayments-title")}
@@ -0,0 +1,185 @@
1
+ import {
2
+ createDateField,
3
+ createEntity,
4
+ createNumberField,
5
+ createSelectField,
6
+ createTextField,
7
+ defineFeature,
8
+ } from "@cosmicdrift/kumiko-framework/engine";
9
+
10
+ import { WORKBOARD_I18N } from "./i18n";
11
+
12
+ export const taskEntity = createEntity({
13
+ table: "read_workboard_tasks",
14
+ fields: {
15
+ title: createTextField({
16
+ personal: false,
17
+ reason: "is_business_data",
18
+ required: true,
19
+ searchable: true,
20
+ sortable: true,
21
+ }),
22
+ status: createSelectField({
23
+ options: ["open", "inProgress", "blocked", "done"] as const,
24
+ default: "open",
25
+ sortable: true,
26
+ filterable: true,
27
+ }),
28
+ priority: createSelectField({
29
+ options: ["low", "medium", "high"] as const,
30
+ default: "medium",
31
+ sortable: true,
32
+ filterable: true,
33
+ }),
34
+ dueDate: createDateField({ sortable: true }),
35
+ assignee: createTextField({ personal: false, reason: "is_business_data", sortable: true }),
36
+ estimateHours: createNumberField({ sortable: true, filterable: true }),
37
+ },
38
+ });
39
+
40
+ const open = {
41
+ access: {
42
+ openToAll: {
43
+ reason: "demo app: any signed-in user manages every task; there is no per-user ownership",
44
+ },
45
+ },
46
+ } as const;
47
+
48
+ // All of this round's list options on one screen: remembered state, one reset
49
+ // button, the column picker (estimateHours starts hidden) and the density choice.
50
+ export const workboardFeature = defineFeature("workboard", (r) => {
51
+ r.translations({ keys: WORKBOARD_I18N });
52
+ r.crud("task", taskEntity, { write: open, read: open });
53
+
54
+ r.screen({
55
+ id: "task-list",
56
+ type: "entityList",
57
+ entity: "task",
58
+ columns: [
59
+ "title",
60
+ "status",
61
+ "priority",
62
+ { field: "dueDate", hideOnNarrow: true },
63
+ { field: "assignee", hideOnNarrow: true },
64
+ { field: "estimateHours", hidden: true, hideOnNarrow: true },
65
+ ],
66
+ pagination: "pages",
67
+ pageSize: 25,
68
+ defaultSort: { field: "dueDate", dir: "asc" },
69
+ searchable: true,
70
+ facets: { status: { display: "chips" }, priority: {} },
71
+ table: {
72
+ rememberState: true,
73
+ resetAll: true,
74
+ columnPicker: true,
75
+ density: { default: "comfortable", userChoice: true },
76
+ export: { xlsx: true },
77
+ },
78
+ rowActions: [
79
+ { kind: "navigate", id: "edit", label: "Edit", screen: "task-edit", rowClick: true },
80
+ ],
81
+ });
82
+
83
+ // The same tasks as a spreadsheet-like board: fixed column widths, a pinned title,
84
+ // keyboard navigation between rows, row checkboxes with a bulk delete.
85
+ r.screen({
86
+ id: "task-board",
87
+ type: "entityList",
88
+ entity: "task",
89
+ columns: [
90
+ { field: "title", pinned: "left", width: 260, minWidth: 140 },
91
+ { field: "status", width: 180, minWidth: 100 },
92
+ { field: "priority", width: 180, minWidth: 100 },
93
+ { field: "dueDate", width: 180, minWidth: 100, hideOnNarrow: true },
94
+ { field: "assignee", width: 220, minWidth: 100, hideOnNarrow: true },
95
+ { field: "estimateHours", width: 180, minWidth: 100, hideOnNarrow: true },
96
+ ],
97
+ pagination: "pages",
98
+ pageSize: 25,
99
+ defaultSort: { field: "dueDate", dir: "asc" },
100
+ searchable: true,
101
+ table: {
102
+ rememberState: true,
103
+ columnPicker: true,
104
+ pinning: true,
105
+ resizing: true,
106
+ keyboardNavigation: true,
107
+ density: { default: "comfortable", userChoice: true },
108
+ },
109
+ rowActions: [
110
+ { kind: "navigate", id: "edit", label: "Edit", screen: "task-edit", rowClick: true },
111
+ {
112
+ kind: "writeHandler",
113
+ id: "delete",
114
+ label: "Delete",
115
+ handler: "workboard:write:task:delete",
116
+ style: "danger",
117
+ visible: { field: "status", ne: "done" },
118
+ },
119
+ ],
120
+ bulkActions: [{ id: "delete", rowAction: "delete" }],
121
+ });
122
+
123
+ // The same tasks grouped by status on the server: each header carries the
124
+ // group's count and estimate sum, a footer row the sum over all filtered tasks.
125
+ r.screen({
126
+ id: "task-estimates",
127
+ type: "entityList",
128
+ entity: "task",
129
+ columns: ["title", "status", "priority", "assignee", "estimateHours"],
130
+ pagination: "pages",
131
+ pageSize: 3,
132
+ defaultSort: { field: "title", dir: "asc" },
133
+ searchable: true,
134
+ facets: { estimateHours: { display: "range" } },
135
+ table: {
136
+ grouping: { field: "status", collapsedWhen: "done", userChoice: true, fields: ["priority"] },
137
+ totals: [{ field: "estimateHours", kind: "sum" }],
138
+ },
139
+ rowActions: [
140
+ { kind: "navigate", id: "edit", label: "Edit", screen: "task-edit", rowClick: true },
141
+ ],
142
+ });
143
+
144
+ r.screen({
145
+ id: "task-edit",
146
+ type: "entityEdit",
147
+ entity: "task",
148
+ dormant: true,
149
+ listScreenId: "task-list",
150
+ layout: {
151
+ sections: [
152
+ {
153
+ columns: 2,
154
+ fields: ["title", "assignee", "status", "priority", "dueDate", "estimateHours"],
155
+ },
156
+ ],
157
+ },
158
+ });
159
+
160
+ r.nav({ id: "workboard", label: "workboard:nav.workboard", icon: "list", order: 40 });
161
+ r.nav({
162
+ id: "task-list",
163
+ label: "workboard:nav.tasks",
164
+ parent: "workboard:nav:workboard",
165
+ screen: "workboard:screen:task-list",
166
+ icon: "table",
167
+ order: 10,
168
+ });
169
+ r.nav({
170
+ id: "task-board",
171
+ label: "workboard:nav.board",
172
+ parent: "workboard:nav:workboard",
173
+ screen: "workboard:screen:task-board",
174
+ icon: "table",
175
+ order: 20,
176
+ });
177
+ r.nav({
178
+ id: "task-estimates",
179
+ label: "workboard:nav.estimates",
180
+ parent: "workboard:nav:workboard",
181
+ screen: "workboard:screen:task-estimates",
182
+ icon: "table",
183
+ order: 30,
184
+ });
185
+ });
@@ -0,0 +1,27 @@
1
+ import type { LocalizedString } from "../shared-i18n";
2
+
3
+ export const WORKBOARD_I18N: Readonly<Record<string, LocalizedString>> = {
4
+ "workboard:nav.workboard": { de: "Aufgaben", en: "Workboard" },
5
+ "workboard:nav.tasks": { de: "Alle Aufgaben", en: "All tasks" },
6
+ "workboard:nav.board": { de: "Aufgaben-Board", en: "Task board" },
7
+ "workboard:nav.estimates": { de: "Aufwand je Status", en: "Estimates by status" },
8
+ "screen:task-board.title": { de: "Aufgaben-Board", en: "Task board" },
9
+ "screen:task-estimates.title": { de: "Aufwand je Status", en: "Estimates by status" },
10
+ "screen:task-list.title": { de: "Aufgaben", en: "Tasks" },
11
+ "screen:task-edit.title": { de: "Aufgabe bearbeiten", en: "Edit task" },
12
+ "screen:task-edit.create.title": { de: "Aufgabe anlegen", en: "Create task" },
13
+ "screen:task-edit.edit.title": { de: "Aufgabe bearbeiten", en: "Edit task" },
14
+ "workboard:entity:task:field:title": { de: "Titel", en: "Title" },
15
+ "workboard:entity:task:field:status": { de: "Status", en: "Status" },
16
+ "workboard:entity:task:field:status:option:open": { de: "Offen", en: "Open" },
17
+ "workboard:entity:task:field:status:option:inProgress": { de: "In Arbeit", en: "In progress" },
18
+ "workboard:entity:task:field:status:option:blocked": { de: "Blockiert", en: "Blocked" },
19
+ "workboard:entity:task:field:status:option:done": { de: "Erledigt", en: "Done" },
20
+ "workboard:entity:task:field:priority": { de: "Priorität", en: "Priority" },
21
+ "workboard:entity:task:field:priority:option:low": { de: "Niedrig", en: "Low" },
22
+ "workboard:entity:task:field:priority:option:medium": { de: "Mittel", en: "Medium" },
23
+ "workboard:entity:task:field:priority:option:high": { de: "Hoch", en: "High" },
24
+ "workboard:entity:task:field:dueDate": { de: "Fällig am", en: "Due date" },
25
+ "workboard:entity:task:field:assignee": { de: "Zuständig", en: "Assignee" },
26
+ "workboard:entity:task:field:estimateHours": { de: "Aufwand (Std.)", en: "Estimate (h)" },
27
+ };
@@ -0,0 +1,121 @@
1
+ import type { SeedFn } from "@cosmicdrift/kumiko-dev-server";
2
+ import { TestUsers } from "@cosmicdrift/kumiko-framework/seeding";
3
+
4
+ type SeedTask = {
5
+ readonly title: string;
6
+ readonly status: "open" | "inProgress" | "blocked" | "done";
7
+ readonly priority: "low" | "medium" | "high";
8
+ readonly dueDate: string;
9
+ readonly assignee: string;
10
+ readonly estimateHours: number;
11
+ };
12
+
13
+ const TASKS: readonly SeedTask[] = [
14
+ {
15
+ title: "Quarterly VAT filing",
16
+ status: "inProgress",
17
+ priority: "high",
18
+ dueDate: "2026-11-10",
19
+ assignee: "Ines Vogel",
20
+ estimateHours: 6,
21
+ },
22
+ {
23
+ title: "Renew office lease",
24
+ status: "open",
25
+ priority: "medium",
26
+ dueDate: "2026-12-01",
27
+ assignee: "Tomas Berg",
28
+ estimateHours: 3,
29
+ },
30
+ {
31
+ title: "Onboard new support hire",
32
+ status: "open",
33
+ priority: "high",
34
+ dueDate: "2026-11-03",
35
+ assignee: "Ines Vogel",
36
+ estimateHours: 12,
37
+ },
38
+ {
39
+ title: "Migrate invoices to new numbering",
40
+ status: "blocked",
41
+ priority: "high",
42
+ dueDate: "2026-11-15",
43
+ assignee: "Mara Lindqvist",
44
+ estimateHours: 16,
45
+ },
46
+ {
47
+ title: "Update privacy policy",
48
+ status: "done",
49
+ priority: "medium",
50
+ dueDate: "2026-10-20",
51
+ assignee: "Tomas Berg",
52
+ estimateHours: 4,
53
+ },
54
+ {
55
+ title: "Order replacement laptops",
56
+ status: "inProgress",
57
+ priority: "low",
58
+ dueDate: "2026-11-25",
59
+ assignee: "Mara Lindqvist",
60
+ estimateHours: 2,
61
+ },
62
+ {
63
+ title: "Prepare annual audit documents",
64
+ status: "open",
65
+ priority: "high",
66
+ dueDate: "2027-01-15",
67
+ assignee: "Ines Vogel",
68
+ estimateHours: 24,
69
+ },
70
+ {
71
+ title: "Fix mobile checkout layout",
72
+ status: "inProgress",
73
+ priority: "medium",
74
+ dueDate: "2026-11-05",
75
+ assignee: "Jonas Keller",
76
+ estimateHours: 8,
77
+ },
78
+ {
79
+ title: "Review supplier contracts",
80
+ status: "blocked",
81
+ priority: "medium",
82
+ dueDate: "2026-11-30",
83
+ assignee: "Tomas Berg",
84
+ estimateHours: 10,
85
+ },
86
+ {
87
+ title: "Archive 2025 customer files",
88
+ status: "done",
89
+ priority: "low",
90
+ dueDate: "2026-10-31",
91
+ assignee: "Jonas Keller",
92
+ estimateHours: 5,
93
+ },
94
+ {
95
+ title: "Plan team offsite",
96
+ status: "open",
97
+ priority: "low",
98
+ dueDate: "2027-02-20",
99
+ assignee: "Mara Lindqvist",
100
+ estimateHours: 7,
101
+ },
102
+ {
103
+ title: "Set up weekly sales report",
104
+ status: "inProgress",
105
+ priority: "medium",
106
+ dueDate: "2026-11-12",
107
+ assignee: "Jonas Keller",
108
+ estimateHours: 9,
109
+ },
110
+ ];
111
+
112
+ export const seedWorkboardTasks: SeedFn = async (stack) => {
113
+ for (const task of TASKS) {
114
+ const res = await stack.http.write("workboard:write:task:create", task, TestUsers.admin);
115
+ if (!res.ok) {
116
+ // biome-ignore lint/suspicious/noConsole: dev-seed surfaces failures in the runner log
117
+ console.error(`[seed] workboard task "${task.title}" failed:`, await res.text());
118
+ return;
119
+ }
120
+ }
121
+ };
@@ -0,0 +1,13 @@
1
+ import type {
2
+ ClientFeatureDefinition,
3
+ TranslationsByLocale,
4
+ } from "@cosmicdrift/kumiko-renderer/web";
5
+ import { toClientTranslations } from "../shared-i18n";
6
+ import { WORKBOARD_I18N } from "./i18n";
7
+
8
+ const translations: TranslationsByLocale = toClientTranslations(WORKBOARD_I18N);
9
+
10
+ export const workboardClient: ClientFeatureDefinition = {
11
+ name: "workboard",
12
+ translations,
13
+ };
@@ -50,6 +50,7 @@ export const rentalTranslations: TranslationsByLocale = {
50
50
  "rental.party.role.main": "Hauptmieter",
51
51
  "rental.party.role.co": "Mitmieterin",
52
52
  "rental.action.addParty": "Partei hinzufügen",
53
+ "rental.action.showTerminated": "Beendete Verträge",
53
54
 
54
55
  "rental.tab.parties": "Vertragsparteien",
55
56
  "rental.tab.positions": "Positionen",
@@ -214,6 +215,7 @@ export const rentalTranslations: TranslationsByLocale = {
214
215
  "rental.party.role.main": "Main tenant",
215
216
  "rental.party.role.co": "Co-tenant",
216
217
  "rental.action.addParty": "Add party",
218
+ "rental.action.showTerminated": "Terminated leases",
217
219
 
218
220
  "rental.tab.parties": "Parties",
219
221
  "rental.tab.positions": "Positions",
@@ -52,6 +52,15 @@ export const leaseListScreen: EntityListScreenDefinition = {
52
52
  pageSize: 25,
53
53
  createLabel: "rental.action.createLease",
54
54
  searchPlaceholder: "rental.search.placeholder",
55
+ toolbarActions: [
56
+ {
57
+ kind: "navigate",
58
+ id: "show-terminated",
59
+ label: "rental.action.showTerminated",
60
+ style: "primary",
61
+ screen: "lease-list-short",
62
+ },
63
+ ],
55
64
  rowActions: [openLeaseDetail],
56
65
  access: OPEN_ACCESS,
57
66
  };
@@ -8,6 +8,7 @@ import { InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
8
8
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
9
9
  import { composeEnvSchema } from "@cosmicdrift/kumiko-framework/env";
10
10
  import { runProdApp } from "@cosmicdrift/kumiko-server-runtime";
11
+ import { BUNDLED_LISTS } from "../src/bundled-lists";
11
12
  import {
12
13
  APP_FEATURES,
13
14
  AUTH_COMPOSE_OPTIONS,
@@ -27,6 +28,7 @@ await runProdApp({
27
28
  featureToggles: true,
28
29
  envSchema,
29
30
  validateBootOptions: VALIDATE_BOOT_OPTIONS,
31
+ bundledLists: BUNDLED_LISTS,
30
32
  // Smoke-only: exercises the hard PII boot gate + blind-index gate with
31
33
  // every bundled feature mounted. Real apps wire createPgKmsAdapter(...).
32
34
  kms: new InMemoryKmsAdapter(),
@@ -4,6 +4,7 @@
4
4
  // runs in a Playwright worker and must not pull the server import chain).
5
5
 
6
6
  export const GALLERY_PROVIDER = "gallery-mock-provider";
7
+ export const GALLERY_MOR_PROVIDER = "gallery-mock-mor";
7
8
  export const GALLERY_TERMS_SLUG = "gallery-terms";
8
9
  export const GALLERY_OPERATOR_EMAIL = "billing@gallery.example";
9
10
  export const GALLERY_PLAN_PATH = "/tenant-admin/billing-plans";
@@ -14,3 +15,7 @@ export const GALLERY_TENANT_HOST = "localhost";
14
15
  export const GALLERY_APEX_HOST = "127.0.0.1";
15
16
 
16
17
  export const GALLERY_PRICE_IDS = { starter: "price_gallery_starter", pro: "price_gallery_pro" };
18
+ export const GALLERY_MOR_PRICE_IDS = {
19
+ starter: "price_gallery_mor_starter",
20
+ pro: "price_gallery_mor_pro",
21
+ };
@@ -16,9 +16,12 @@ import {
16
16
  createChannelEmailFeature,
17
17
  createInMemoryTransport,
18
18
  } from "@cosmicdrift/kumiko-bundled-features/channel-email";
19
+ import type { ExtraRouteDefinition } from "@cosmicdrift/kumiko-framework/api";
19
20
  import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
20
21
  import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
21
22
  import {
23
+ GALLERY_MOR_PRICE_IDS,
24
+ GALLERY_MOR_PROVIDER,
22
25
  GALLERY_OPERATOR_EMAIL,
23
26
  GALLERY_PLAN_PATH,
24
27
  GALLERY_PRICE_IDS,
@@ -28,25 +31,44 @@ import {
28
31
 
29
32
  export const galleryMailTransport = createInMemoryTransport();
30
33
 
31
- const PRICES: readonly ProviderPrice[] = [
32
- {
33
- priceId: GALLERY_PRICE_IDS.starter,
34
- unitAmount: 900,
35
- currency: "eur",
36
- interval: "month",
37
- intervalCount: 1,
38
- active: true,
39
- metadata: {},
40
- },
41
- {
42
- priceId: GALLERY_PRICE_IDS.pro,
43
- unitAmount: 1900,
34
+ // The merchant-of-record mock is only offered to tenants the spec opts in, so
35
+ // the single-provider flows keep their original screens.
36
+ const morEnabledTenants = new Set<string>();
37
+
38
+ export const GALLERY_MOR_ENABLE_PATH = "/gallery/mor/:tenantId";
39
+
40
+ export function createGalleryMorEnableRoute(): ExtraRouteDefinition {
41
+ return {
42
+ method: "POST",
43
+ path: GALLERY_MOR_ENABLE_PATH,
44
+ entry: "anonymous",
45
+ handler: (c) => {
46
+ morEnabledTenants.add(c.req.param("tenantId") ?? "");
47
+ return c.body(null, 204);
48
+ },
49
+ };
50
+ }
51
+
52
+ function monthlyPrice(priceId: string, unitAmount: number): ProviderPrice {
53
+ return {
54
+ priceId,
55
+ unitAmount,
44
56
  currency: "eur",
45
57
  interval: "month",
46
58
  intervalCount: 1,
47
59
  active: true,
48
60
  metadata: {},
49
- },
61
+ };
62
+ }
63
+
64
+ const PRICES: readonly ProviderPrice[] = [
65
+ monthlyPrice(GALLERY_PRICE_IDS.starter, 900),
66
+ monthlyPrice(GALLERY_PRICE_IDS.pro, 1900),
67
+ ];
68
+
69
+ const MOR_PRICES: readonly ProviderPrice[] = [
70
+ monthlyPrice(GALLERY_MOR_PRICE_IDS.starter, 990),
71
+ monthlyPrice(GALLERY_MOR_PRICE_IDS.pro, 1990),
50
72
  ];
51
73
 
52
74
  async function resolveCurrentTier(db: TenantDb, tenantId: string): Promise<string> {
@@ -74,7 +96,6 @@ export function createGalleryBillingFeature(baseUrl: string): FeatureDefinition
74
96
  viewRoles: ["TenantAdmin", "SystemAdmin"],
75
97
  successPath: GALLERY_PLAN_PATH,
76
98
  cancelPath: GALLERY_PLAN_PATH,
77
- providerName: GALLERY_PROVIDER,
78
99
  contact: {
79
100
  labelKey: "gallery-screens:plan.enterprise.label",
80
101
  benefits: [{ labelKey: "gallery-screens:plan.enterprise.benefit" }],
@@ -85,6 +106,24 @@ export function createGalleryBillingFeature(baseUrl: string): FeatureDefinition
85
106
  consumerProtection: {
86
107
  termsTextBlock: GALLERY_TERMS_SLUG,
87
108
  vatNote: { de: "Preise inkl. USt.", en: "Prices include VAT." },
109
+ providerTexts: {
110
+ [GALLERY_PROVIDER]: {
111
+ checkoutNote: {
112
+ de: "Rechnung von Gallery Beispiel GmbH",
113
+ en: "Invoiced by Gallery Beispiel GmbH",
114
+ },
115
+ },
116
+ [GALLERY_MOR_PROVIDER]: {
117
+ checkoutNote: {
118
+ de: "Verkäufer ist Dodo Payments · Rechnung und USt von Dodo Payments",
119
+ en: "Sold by Dodo Payments · invoice and VAT from Dodo Payments",
120
+ },
121
+ vatNote: {
122
+ de: "Rechnung und Umsatzsteuer stellt Dodo Payments als Merchant of Record.",
123
+ en: "Dodo Payments, as merchant of record, issues the invoice and charges the VAT.",
124
+ },
125
+ },
126
+ },
88
127
  operatorEmail: GALLERY_OPERATOR_EMAIL,
89
128
  terminationScope: "platform",
90
129
  legalLinks: {
@@ -105,6 +144,7 @@ export function createGalleryEmailFeature(): FeatureDefinition {
105
144
  // The page URL carries the consent id so the spec can echo it on the webhook,
106
145
  // exactly as a real provider echoes the checkout metadata.
107
146
  const galleryProviderPlugin: SubscriptionProviderPlugin = {
147
+ displayName: "Stripe",
108
148
  verifyAndParseWebhook: async (rawBody) => JSON.parse(rawBody),
109
149
  priceToTier: {
110
150
  [GALLERY_PRICE_IDS.starter]: "starter",
@@ -119,12 +159,47 @@ const galleryProviderPlugin: SubscriptionProviderPlugin = {
119
159
  cancelSubscription: async () => {},
120
160
  };
121
161
 
162
+ // Merchant-of-record stand-in: switches plans in the app with fixed figures
163
+ // (Starter to Pro: 6.67 EUR due now, 1.06 EUR of it VAT). Applying without a
164
+ // consent id fails, so a successful switch in the spec proves the consent went
165
+ // through.
166
+ const galleryMorProviderPlugin: SubscriptionProviderPlugin = {
167
+ displayName: "Dodo Payments",
168
+ isBillingEnabled: async (ctx) => morEnabledTenants.has(ctx.user.tenantId),
169
+ verifyAndParseWebhook: async (rawBody) => JSON.parse(rawBody),
170
+ priceToTier: {
171
+ [GALLERY_MOR_PRICE_IDS.starter]: "starter",
172
+ [GALLERY_MOR_PRICE_IDS.pro]: "pro",
173
+ },
174
+ retrievePrices: async (_ctx, priceIds) => MOR_PRICES.filter((p) => priceIds.includes(p.priceId)),
175
+ createCheckoutSession: async (_ctx, options) => {
176
+ const url = new URL(options.successUrl);
177
+ url.searchParams.set("provider", "mor");
178
+ if (options.consentId !== undefined) url.searchParams.set("consentId", options.consentId);
179
+ return { url: url.toString() };
180
+ },
181
+ cancelSubscription: async () => {},
182
+ previewPlanSwitch: async () => ({
183
+ amountDueNow: 667,
184
+ tax: 106,
185
+ currency: "eur",
186
+ credit: 333,
187
+ effectiveAt: "2027-01-15T00:00:00Z",
188
+ }),
189
+ applyPlanSwitch: async (_ctx, options) => {
190
+ if (options.consentId === undefined) {
191
+ throw new Error("gallery MoR mock: plan switch applied without a consent id");
192
+ }
193
+ },
194
+ };
195
+
122
196
  export const galleryProviderFeature: FeatureDefinition = defineFeature(
123
197
  "gallery-mock-provider",
124
198
  (r) => {
125
199
  r.describe("E2E-only mock subscription provider for the consent/termination gallery.");
126
200
  r.requires({ features: ["billing-foundation"] });
127
201
  r.useExtension("subscriptionProvider", GALLERY_PROVIDER, galleryProviderPlugin);
202
+ r.useExtension("subscriptionProvider", GALLERY_MOR_PROVIDER, galleryMorProviderPlugin);
128
203
  },
129
204
  );
130
205