@cosmicdrift/kumiko-bundled-features 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 (320) hide show
  1. package/dist/agent-tools/index.d.ts +1 -1
  2. package/dist/agent-tools/index.js +1 -1
  3. package/dist/agent-tools/tool-dispatch.d.ts +12 -7
  4. package/dist/agent-tools/tool-dispatch.js +42 -31
  5. package/dist/audit/feature.js +1 -0
  6. package/dist/audit/handlers/audit-events.js +1 -1
  7. package/dist/auth-email-password/handlers/request-account-unlock.write.d.ts +1 -0
  8. package/dist/auth-email-password/handlers/request-account-unlock.write.js +1 -0
  9. package/dist/auth-email-password/handlers/request-email-verification.write.d.ts +1 -0
  10. package/dist/auth-email-password/handlers/request-email-verification.write.js +1 -0
  11. package/dist/auth-email-password/handlers/request-password-reset.write.d.ts +1 -0
  12. package/dist/auth-email-password/handlers/request-password-reset.write.js +1 -0
  13. package/dist/auth-email-password/handlers/signup-request.write.d.ts +1 -0
  14. package/dist/auth-email-password/handlers/signup-request.write.js +4 -0
  15. package/dist/auth-email-password/handlers/token-request-handler.d.ts +2 -0
  16. package/dist/auth-email-password/handlers/token-request-handler.js +5 -0
  17. package/dist/auth-email-password/magic-link-mail.d.ts +2 -0
  18. package/dist/auth-email-password/magic-link-mail.js +7 -3
  19. package/dist/auth-email-password/signup-activation.d.ts +1 -0
  20. package/dist/auth-email-password/signup-activation.js +1 -0
  21. package/dist/auth-email-password/web/auth-client.d.ts +2 -2
  22. package/dist/auth-email-password/web/auth-client.js +8 -4
  23. package/dist/auth-email-password/web/auth-redirect.d.ts +5 -1
  24. package/dist/auth-email-password/web/auth-redirect.js +20 -25
  25. package/dist/auth-email-password/web/default-auth-pages-gate.js +6 -3
  26. package/dist/auth-email-password/web/forgot-password-screen.d.ts +4 -1
  27. package/dist/auth-email-password/web/forgot-password-screen.js +7 -2
  28. package/dist/auth-email-password/web/index.d.ts +1 -0
  29. package/dist/auth-email-password/web/index.js +1 -0
  30. package/dist/auth-email-password/web/login-screen.js +11 -6
  31. package/dist/auth-email-password/web/reset-password-screen.js +7 -1
  32. package/dist/auth-email-password/web/return-path.d.ts +4 -0
  33. package/dist/auth-email-password/web/return-path.js +34 -0
  34. package/dist/auth-email-password/web/session.js +6 -1
  35. package/dist/auth-email-password/web/signup-complete-screen.js +11 -3
  36. package/dist/auth-email-password/web/signup-screen.d.ts +4 -1
  37. package/dist/auth-email-password/web/signup-screen.js +8 -3
  38. package/dist/auth-mfa/config.d.ts +1 -1
  39. package/dist/auth-mfa/config.js +3 -7
  40. package/dist/auth-mfa/feature-exports.d.ts +2 -2
  41. package/dist/auth-mfa/feature.js +8 -0
  42. package/dist/auth-mfa/mfa-status-checker.js +5 -3
  43. package/dist/auth-mfa/web/i18n.js +5 -1
  44. package/dist/auth-mfa/web/mfa-code-or-recovery-field.d.ts +11 -0
  45. package/dist/auth-mfa/web/mfa-code-or-recovery-field.js +12 -0
  46. package/dist/auth-mfa/web/mfa-disable-dialog.js +14 -3
  47. package/dist/auth-mfa/web/mfa-regenerate-recovery-dialog.js +14 -3
  48. package/dist/auth-mfa/web/mfa-setup-preauth-screen.js +10 -5
  49. package/dist/auth-mfa/web/mfa-verify-screen.js +26 -5
  50. package/dist/billing-foundation/checkout-core.d.ts +9 -14
  51. package/dist/billing-foundation/checkout-core.js +24 -39
  52. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +8 -2
  53. package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +1 -0
  54. package/dist/billing-foundation/consumer-protection/consent-gate.js +1 -0
  55. package/dist/billing-foundation/consumer-protection/issue-confirmation.js +9 -3
  56. package/dist/billing-foundation/consumer-protection/mail-notifications.js +3 -1
  57. package/dist/billing-foundation/consumer-protection/termination-receipt-limit.d.ts +4 -2
  58. package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +6 -5
  59. package/dist/billing-foundation/consumer-protection/termination-record.d.ts +5 -1
  60. package/dist/billing-foundation/consumer-protection/termination-record.js +27 -17
  61. package/dist/billing-foundation/consumer-protection/termination-request.js +12 -7
  62. package/dist/billing-foundation/consumer-protection/termination-revoke.js +12 -7
  63. package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -1
  64. package/dist/billing-foundation/consumer-protection/termination-texts.js +3 -0
  65. package/dist/billing-foundation/consumer-protection/withdrawal-period.d.ts +5 -2
  66. package/dist/billing-foundation/consumer-protection/withdrawal-period.js +8 -9
  67. package/dist/billing-foundation/events.d.ts +1 -0
  68. package/dist/billing-foundation/events.js +1 -0
  69. package/dist/billing-foundation/feature.js +3 -1
  70. package/dist/billing-foundation/handlers/billing-plans.query.js +35 -5
  71. package/dist/billing-foundation/handlers/create-checkout-session.write.js +1 -0
  72. package/dist/billing-foundation/handlers/plan-switch-preview.query.d.ts +13 -0
  73. package/dist/billing-foundation/handlers/plan-switch-preview.query.js +48 -0
  74. package/dist/billing-foundation/handlers/process-event.write.d.ts +6 -0
  75. package/dist/billing-foundation/handlers/process-event.write.js +8 -2
  76. package/dist/billing-foundation/handlers/start-plan-checkout.write.js +32 -5
  77. package/dist/billing-foundation/handlers/switch-plan.write.d.ts +2 -2
  78. package/dist/billing-foundation/handlers/switch-plan.write.js +56 -64
  79. package/dist/billing-foundation/handlers/sync-subscription.write.js +12 -1
  80. package/dist/billing-foundation/i18n.js +22 -0
  81. package/dist/billing-foundation/index.d.ts +1 -1
  82. package/dist/billing-foundation/plan-catalog.d.ts +21 -5
  83. package/dist/billing-foundation/plan-catalog.js +25 -16
  84. package/dist/billing-foundation/plan-switch.d.ts +12 -0
  85. package/dist/billing-foundation/plan-switch.js +60 -0
  86. package/dist/billing-foundation/types.d.ts +57 -1
  87. package/dist/billing-foundation/types.js +9 -0
  88. package/dist/billing-foundation/validate-options.js +17 -0
  89. package/dist/billing-foundation/web/billing-plans-panel.js +78 -32
  90. package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +17 -3
  91. package/dist/billing-foundation/web/checkout-consent-dialog.js +14 -10
  92. package/dist/billing-foundation/web/constants.d.ts +4 -0
  93. package/dist/billing-foundation/web/constants.js +4 -0
  94. package/dist/billing-foundation/web/plan-price.d.ts +15 -0
  95. package/dist/billing-foundation/web/plan-price.js +32 -0
  96. package/dist/billing-foundation/web/plan-switch-confirm-dialog.d.ts +15 -0
  97. package/dist/billing-foundation/web/plan-switch-confirm-dialog.js +66 -0
  98. package/dist/cap-counter/cap-reservation.js +20 -7
  99. package/dist/cap-counter/enforce-cap.js +2 -1
  100. package/dist/cap-counter/feature.js +1 -0
  101. package/dist/cap-counter/stock-cap-guard.js +7 -1
  102. package/dist/cap-overview/feature.js +1 -0
  103. package/dist/channel-email/feature.js +1 -1
  104. package/dist/config/handlers/cascade.query.d.ts +5 -0
  105. package/dist/config/handlers/cascade.query.js +4 -3
  106. package/dist/config/handlers/set.write.js +12 -2
  107. package/dist/config/handlers/values.query.d.ts +5 -0
  108. package/dist/config/handlers/values.query.js +4 -3
  109. package/dist/config/write-helpers.d.ts +1 -0
  110. package/dist/config/write-helpers.js +18 -9
  111. package/dist/crypto-shredding/handlers/forget-subject.write.js +14 -11
  112. package/dist/custom-fields/lib/parse-serialized-field.js +1 -1
  113. package/dist/delivery/channel-context.js +6 -3
  114. package/dist/delivery/db/queries/preferences.js +2 -2
  115. package/dist/delivery/delivery-service.js +22 -7
  116. package/dist/delivery/feature.js +1 -0
  117. package/dist/delivery/handlers/preferences.query.d.ts +1 -1
  118. package/dist/delivery/handlers/preferences.query.js +20 -1
  119. package/dist/delivery/handlers/unsubscribe-page-chrome.query.js +1 -1
  120. package/dist/delivery/index.d.ts +1 -0
  121. package/dist/delivery/index.js +1 -0
  122. package/dist/delivery/system-channel-context.d.ts +15 -0
  123. package/dist/delivery/system-channel-context.js +5 -0
  124. package/dist/delivery/unsubscribe-link.d.ts +3 -1
  125. package/dist/delivery/unsubscribe-page-strings.d.ts +1 -0
  126. package/dist/delivery/unsubscribe-page-strings.js +1 -0
  127. package/dist/delivery/unsubscribe-page.d.ts +2 -0
  128. package/dist/delivery/unsubscribe-page.js +5 -0
  129. package/dist/delivery/unsubscribe.js +25 -7
  130. package/dist/document-ingest-foundation/append-missing-extract-event.d.ts +21 -0
  131. package/dist/document-ingest-foundation/append-missing-extract-event.js +42 -0
  132. package/dist/document-ingest-foundation/events.d.ts +11 -0
  133. package/dist/document-ingest-foundation/events.js +11 -0
  134. package/dist/document-ingest-foundation/index.d.ts +2 -1
  135. package/dist/document-ingest-foundation/index.js +2 -1
  136. package/dist/feature-toggles/feature.js +1 -0
  137. package/dist/file-foundation/index.d.ts +1 -1
  138. package/dist/file-foundation/index.js +1 -1
  139. package/dist/file-foundation/read-file-ref-content.d.ts +3 -0
  140. package/dist/file-foundation/read-file-ref-content.js +6 -1
  141. package/dist/file-provider-s3/env-named-feature.d.ts +0 -2
  142. package/dist/form-draft/db/queries/cleanup.js +2 -2
  143. package/dist/form-draft/decrypt-draft.js +5 -1
  144. package/dist/host-routing/dispatch.js +11 -20
  145. package/dist/host-routing/validate-options.js +3 -0
  146. package/dist/inbound-mail-foundation/feature.js +1 -0
  147. package/dist/inbound-mail-foundation/handlers/update-account.write.d.ts +2 -2
  148. package/dist/inbound-mail-foundation/handlers/update-account.write.js +12 -4
  149. package/dist/inbound-mail-foundation/i18n.js +0 -1
  150. package/dist/inbound-mail-foundation/screens.js +0 -3
  151. package/dist/inbound-mail-foundation/types.d.ts +4 -3
  152. package/dist/inbound-mail-foundation/watch-supervisor.js +28 -11
  153. package/dist/jobs/feature.d.ts +1 -0
  154. package/dist/jobs/feature.js +9 -4
  155. package/dist/jobs/handlers/tenant-failures.query.d.ts +1 -1
  156. package/dist/jobs/handlers/tenant-failures.query.js +45 -41
  157. package/dist/jobs/handlers/tenant-runs.query.d.ts +1 -1
  158. package/dist/jobs/handlers/tenant-runs.query.js +54 -50
  159. package/dist/jobs/job-run-logger.js +5 -4
  160. package/dist/ledger/web/index.js +1 -1
  161. package/dist/legal-pages/seed.d.ts +3 -1
  162. package/dist/legal-pages/seed.js +2 -2
  163. package/dist/mail-foundation/feature.d.ts +3 -3
  164. package/dist/mail-foundation/feature.js +3 -0
  165. package/dist/mail-transport-smtp/config-keys.js +1 -1
  166. package/dist/mail-transport-smtp/feature.js +6 -5
  167. package/dist/managed-pages/constants.d.ts +1 -0
  168. package/dist/managed-pages/constants.js +1 -0
  169. package/dist/managed-pages/feature.js +3 -1
  170. package/dist/managed-pages/index.d.ts +1 -0
  171. package/dist/managed-pages/index.js +1 -0
  172. package/dist/notes-history/handlers/add-note.write.js +1 -1
  173. package/dist/personal-access-tokens/feature.d.ts +1 -1
  174. package/dist/personal-access-tokens/feature.js +5 -0
  175. package/dist/renderer-pdf/render-table-pdf.d.ts +1 -1
  176. package/dist/renderer-pdf/render-table-pdf.js +22 -10
  177. package/dist/secrets/write-gate.js +8 -2
  178. package/dist/sessions/feature.d.ts +2 -2
  179. package/dist/sessions/feature.js +5 -0
  180. package/dist/sessions/index.d.ts +1 -1
  181. package/dist/sessions/index.js +1 -1
  182. package/dist/sessions/session-callbacks.d.ts +0 -2
  183. package/dist/sessions/session-callbacks.js +1 -3
  184. package/dist/shared/decrypt-stored-pii.d.ts +1 -0
  185. package/dist/shared/decrypt-stored-pii.js +6 -1
  186. package/dist/shared/index.d.ts +1 -1
  187. package/dist/shared/index.js +1 -1
  188. package/dist/shared/mail-template-body.d.ts +6 -2
  189. package/dist/shared/mail-template-body.js +7 -4
  190. package/dist/shared/purge-files-reaching-subject.d.ts +1 -0
  191. package/dist/shared/purge-files-reaching-subject.js +19 -2
  192. package/dist/shared/run-in-sub-transaction.js +5 -5
  193. package/dist/step-dispatcher/deliver-dispatch-request.js +16 -3
  194. package/dist/step-dispatcher/feature.js +1 -0
  195. package/dist/step-dispatcher/mail-runner.d.ts +1 -0
  196. package/dist/subscription-dodo/client.d.ts +17 -0
  197. package/dist/subscription-dodo/client.js +31 -0
  198. package/dist/subscription-dodo/constants.d.ts +12 -0
  199. package/dist/subscription-dodo/constants.js +19 -0
  200. package/dist/subscription-dodo/feature.d.ts +37 -0
  201. package/dist/subscription-dodo/feature.js +144 -0
  202. package/dist/subscription-dodo/index.d.ts +2 -0
  203. package/dist/subscription-dodo/index.js +4 -0
  204. package/dist/subscription-dodo/plugin-methods.d.ts +23 -0
  205. package/dist/subscription-dodo/plugin-methods.js +260 -0
  206. package/dist/subscription-dodo/runtime.d.ts +27 -0
  207. package/dist/subscription-dodo/runtime.js +89 -0
  208. package/dist/subscription-dodo/verify-webhook.d.ts +37 -0
  209. package/dist/subscription-dodo/verify-webhook.js +189 -0
  210. package/dist/subscription-stripe/feature.js +1 -0
  211. package/dist/subscription-stripe/runtime.js +7 -6
  212. package/dist/tags/feature.js +1 -0
  213. package/dist/template-resolver/api.d.ts +3 -2
  214. package/dist/template-resolver/api.js +24 -2
  215. package/dist/template-resolver/compile-document-body.js +1 -0
  216. package/dist/template-resolver/document-ops.d.ts +5 -0
  217. package/dist/template-resolver/document-ops.js +18 -2
  218. package/dist/template-resolver/feature.js +4 -4
  219. package/dist/template-resolver/handlers/bundle-import.js +3 -3
  220. package/dist/template-resolver/handlers/copy-locale.write.d.ts +5 -1
  221. package/dist/template-resolver/handlers/copy-locale.write.js +22 -7
  222. package/dist/template-resolver/handlers/diff-to-system.query.js +3 -2
  223. package/dist/template-resolver/handlers/document-base.js +3 -2
  224. package/dist/template-resolver/handlers/find-by-id.query.js +3 -2
  225. package/dist/template-resolver/handlers/layout-source.query.d.ts +4 -0
  226. package/dist/template-resolver/handlers/layout-source.query.js +26 -6
  227. package/dist/template-resolver/handlers/list.query.js +5 -6
  228. package/dist/template-resolver/handlers/overview.query.d.ts +3 -0
  229. package/dist/template-resolver/handlers/overview.query.js +21 -10
  230. package/dist/template-resolver/handlers/patch-draft.write.js +1 -1
  231. package/dist/template-resolver/handlers/send-test-status.query.d.ts +2 -1
  232. package/dist/template-resolver/handlers/send-test-status.query.js +18 -15
  233. package/dist/template-resolver/handlers/tenant-override-counts.query.js +1 -3
  234. package/dist/template-resolver/handlers/tenant-override-selection.d.ts +2 -1
  235. package/dist/template-resolver/handlers/tenant-overrides.query.js +28 -5
  236. package/dist/template-resolver/handlers/usage.query.d.ts +2 -1
  237. package/dist/template-resolver/handlers/usage.query.js +26 -16
  238. package/dist/template-resolver/handlers/validate.query.js +7 -2
  239. package/dist/template-resolver/seeding.d.ts +1 -0
  240. package/dist/template-resolver/seeding.js +31 -12
  241. package/dist/template-resolver/store-template-resource.d.ts +1 -0
  242. package/dist/template-resolver/store-template-resource.js +65 -10
  243. package/dist/template-resolver/template-delivery-policy.d.ts +0 -5
  244. package/dist/template-resolver/template-delivery-policy.js +0 -6
  245. package/dist/template-resolver/template-meta.js +7 -4
  246. package/dist/template-resolver/template-preview.js +4 -3
  247. package/dist/template-resolver/web/constants.d.ts +2 -2
  248. package/dist/template-resolver/web/constants.js +3 -3
  249. package/dist/template-resolver/web/i18n.js +3 -0
  250. package/dist/template-resolver/web/index.d.ts +1 -0
  251. package/dist/template-resolver/web/index.js +1 -0
  252. package/dist/template-resolver/web/mail-layout-constants.d.ts +5 -0
  253. package/dist/template-resolver/web/mail-layout-constants.js +7 -0
  254. package/dist/template-resolver/web/qualified-names.d.ts +1 -0
  255. package/dist/template-resolver/web/qualified-names.js +1 -0
  256. package/dist/tenant/feature.js +1 -0
  257. package/dist/tenant/handlers/member-directory.query.d.ts +1 -0
  258. package/dist/tenant/handlers/member-directory.query.js +10 -3
  259. package/dist/tenant/handlers/team-list.query.js +3 -3
  260. package/dist/tenant/handlers/tenant-directory.query.d.ts +1 -0
  261. package/dist/tenant/handlers/tenant-directory.query.js +8 -4
  262. package/dist/tenant-lifecycle/constants.d.ts +1 -0
  263. package/dist/tenant-lifecycle/constants.js +4 -0
  264. package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -2
  265. package/dist/tenant-lifecycle/stages.js +22 -1
  266. package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +2 -3
  267. package/dist/tier-engine/handlers/set-tenant-tier.write.js +4 -1
  268. package/dist/tier-engine/web/constants.js +2 -2
  269. package/dist/user/feature.js +1 -0
  270. package/dist/user-data-rights/feature.js +1 -0
  271. package/dist/user-data-rights/index.d.ts +1 -1
  272. package/dist/user-data-rights/index.js +1 -1
  273. package/dist/user-data-rights/run-export-jobs.js +3 -4
  274. package/dist/user-data-rights/run-forget-cleanup.js +2 -1
  275. package/dist/user-data-rights/web/constants.d.ts +6 -0
  276. package/dist/user-data-rights/web/constants.js +6 -0
  277. package/dist/user-data-rights/web/index.d.ts +1 -0
  278. package/dist/user-data-rights/web/index.js +1 -0
  279. package/dist/waitlist/feature.js +1 -0
  280. package/dist/waitlist/handlers/export-purge.write.js +2 -1
  281. package/dist/waitlist/handlers/submit.write.js +14 -10
  282. package/dist/waitlist/import.d.ts +3 -1
  283. package/dist/waitlist/import.js +26 -2
  284. package/dist/waitlist/legacy-migrate.js +18 -7
  285. package/dist/waitlist/link-on-signup.js +18 -6
  286. package/dist/waitlist/options.d.ts +5 -1
  287. package/dist/workflow-runner/handlers/resume-run.write.js +1 -1
  288. package/package.json +19 -9
  289. package/src/agent-tools/changes.json +12 -0
  290. package/src/audit/changes.json +12 -0
  291. package/src/auth-email-password/changes.json +12 -0
  292. package/src/auth-mfa/changes.json +12 -0
  293. package/src/billing-foundation/changes.json +55 -0
  294. package/src/cap-counter/changes.json +30 -0
  295. package/src/channel-email/changes.json +6 -0
  296. package/src/config/changes.json +18 -0
  297. package/src/crypto-shredding/changes.json +18 -0
  298. package/src/delivery/changes.json +42 -0
  299. package/src/document-ingest-foundation/changes.json +6 -0
  300. package/src/form-draft/changes.json +6 -0
  301. package/src/host-routing/changes.json +12 -0
  302. package/src/inbound-mail-foundation/changes.json +18 -0
  303. package/src/jobs/changes.json +13 -0
  304. package/src/mail-foundation/changes.json +6 -0
  305. package/src/mail-transport-smtp/changes.json +6 -0
  306. package/src/managed-pages/changes.json +6 -0
  307. package/src/renderer-pdf/changes.json +6 -0
  308. package/src/secrets/changes.json +6 -0
  309. package/src/sessions/changes.json +7 -0
  310. package/src/step-dispatcher/changes.json +12 -0
  311. package/src/subscription-dodo/changes.json +14 -0
  312. package/src/subscription-stripe/changes.json +12 -0
  313. package/src/template-resolver/changes.json +84 -0
  314. package/src/tenant/changes.json +6 -0
  315. package/src/tenant-lifecycle/changes.json +12 -0
  316. package/src/tier-engine/changes.json +6 -0
  317. package/src/user-data-rights/changes.json +12 -0
  318. package/src/waitlist/changes.json +36 -0
  319. package/dist/billing-foundation/consumer-protection/stored-user-email.d.ts +0 -1
  320. package/dist/billing-foundation/consumer-protection/stored-user-email.js +0 -8
@@ -17,20 +17,15 @@ export type ResolvedProvider = {
17
17
  * entityName is registered. */
18
18
  export declare function findProviderPlugin(ctx: HandlerContext, providerName: string): ResolvedProvider | null;
19
19
  export declare function resolveProviderPlugin(ctx: HandlerContext, providerName: string): ResolvedProvider;
20
- /** Picks the catalog's provider — an explicit `catalog.providerName`, or the
21
- * single registered provider exposing `priceToTier` when there is exactly
22
- * one. Returns null when `providerName` is unset and no registered plugin
23
- * exposes `priceToTier` — a state `billing-plans` renders as
24
- * `enabled: false` instead of erroring. An explicit `providerName` that is
25
- * not registered, or more than one candidate, is a config bug and throws
26
- * `UnconfiguredError`. */
27
- export declare function findCatalogProvider(ctx: HandlerContext, catalog: BillingPlanCatalog): ResolvedProvider | null;
28
- /** `findCatalogProvider` for callers that require a provider to proceed —
29
- * start-plan-checkout and switch-plan. `FeatureDisabledError` (not
30
- * `UnconfiguredError`) when none is found: an unconfigured provider is
31
- * the same "billing isn't live yet" state as `isPluginBillingEnabled`
32
- * returning false, and the panel already renders both the same way. */
33
- export declare function resolveCatalogProvider(ctx: HandlerContext, catalog: BillingPlanCatalog, handlerName: string): ResolvedProvider;
20
+ /** The providers a catalog sells through, in registration order: an explicit
21
+ * `catalog.providerName` (only that one), otherwise every registered provider
22
+ * exposing `priceToTier`. Empty when none qualifies, a state `billing-plans`
23
+ * renders as `enabled: false` instead of erroring. An explicit `providerName`
24
+ * that is not registered is a config bug and throws `UnconfiguredError`. */
25
+ export declare function listCatalogProviders(ctx: HandlerContext, catalog: BillingPlanCatalog): readonly ResolvedProvider[];
26
+ export declare function filterOfferedProviders(ctx: HandlerContext, providers: readonly ResolvedProvider[]): Promise<readonly ResolvedProvider[]>;
27
+ /** The catalog providers that are live right now: what a buyer can choose from. */
28
+ export declare function listOfferedCatalogProviders(ctx: HandlerContext, catalog: BillingPlanCatalog): Promise<readonly ResolvedProvider[]>;
34
29
  /** `plugin.isBillingEnabled?.(ctx) ?? true` — the one place this fallback is
35
30
  * spelled out, so no call-site forgets to await the optional method before
36
31
  * applying the `?? true` default. */
@@ -15,14 +15,18 @@ import { BILLING_FOUNDATION_FEATURE, isSubscriptionBlockingCheckout, SUBSCRIPTIO
15
15
  export function isNonEmptyStringArray(arr) {
16
16
  return arr.length > 0;
17
17
  }
18
+ /** Every registered provider plugin, in registration order. */
19
+ function listProviderPlugins(ctx) {
20
+ return ctx.registry.getExtensionUsages(SUBSCRIPTION_PROVIDER_EXTENSION).map((usage) => ({
21
+ name: usage.entityName,
22
+ // @cast-boundary engine-payload — extension-usage carries unknown options
23
+ plugin: usage.options,
24
+ }));
25
+ }
18
26
  /** Non-throwing provider lookup by name — null when no plugin with that
19
27
  * entityName is registered. */
20
28
  export function findProviderPlugin(ctx, providerName) {
21
- const usage = ctx.registry
22
- .getExtensionUsages(SUBSCRIPTION_PROVIDER_EXTENSION)
23
- .find((u) => u.entityName === providerName);
24
- // @cast-boundary engine-payload — extension-usage carries unknown options
25
- return usage ? { name: providerName, plugin: usage.options } : null;
29
+ return listProviderPlugins(ctx).find((provider) => provider.name === providerName) ?? null;
26
30
  }
27
31
  export function resolveProviderPlugin(ctx, providerName) {
28
32
  const found = findProviderPlugin(ctx, providerName);
@@ -35,14 +39,12 @@ export function resolveProviderPlugin(ctx, providerName) {
35
39
  }
36
40
  return found;
37
41
  }
38
- /** Picks the catalog's provider — an explicit `catalog.providerName`, or the
39
- * single registered provider exposing `priceToTier` when there is exactly
40
- * one. Returns null when `providerName` is unset and no registered plugin
41
- * exposes `priceToTier` — a state `billing-plans` renders as
42
- * `enabled: false` instead of erroring. An explicit `providerName` that is
43
- * not registered, or more than one candidate, is a config bug and throws
44
- * `UnconfiguredError`. */
45
- export function findCatalogProvider(ctx, catalog) {
42
+ /** The providers a catalog sells through, in registration order: an explicit
43
+ * `catalog.providerName` (only that one), otherwise every registered provider
44
+ * exposing `priceToTier`. Empty when none qualifies, a state `billing-plans`
45
+ * renders as `enabled: false` instead of erroring. An explicit `providerName`
46
+ * that is not registered is a config bug and throws `UnconfiguredError`. */
47
+ export function listCatalogProviders(ctx, catalog) {
46
48
  if (catalog.providerName) {
47
49
  const found = findProviderPlugin(ctx, catalog.providerName);
48
50
  if (!found) {
@@ -52,34 +54,17 @@ export function findCatalogProvider(ctx, catalog) {
52
54
  hint: `provider "${catalog.providerName}" not registered`,
53
55
  });
54
56
  }
55
- return found;
56
- }
57
- const usages = ctx.registry.getExtensionUsages(SUBSCRIPTION_PROVIDER_EXTENSION);
58
- const withPriceCatalog = usages.filter((u) => u.options.priceToTier !== undefined);
59
- if (withPriceCatalog.length > 1) {
60
- throw new UnconfiguredError({
61
- feature: "billing-foundation",
62
- key: "catalog.providerName",
63
- hint: `ambiguous — ${withPriceCatalog.length} registered plugins expose priceToTier, set catalog.providerName explicitly`,
64
- });
57
+ return [found];
65
58
  }
66
- const usage = withPriceCatalog[0];
67
- if (!usage)
68
- return null;
69
- // @cast-boundary engine-payload — extension-usage carries unknown options
70
- return { name: usage.entityName, plugin: usage.options };
59
+ return listProviderPlugins(ctx).filter(({ plugin }) => plugin.priceToTier !== undefined);
71
60
  }
72
- /** `findCatalogProvider` for callers that require a provider to proceed —
73
- * start-plan-checkout and switch-plan. `FeatureDisabledError` (not
74
- * `UnconfiguredError`) when none is found: an unconfigured provider is
75
- * the same "billing isn't live yet" state as `isPluginBillingEnabled`
76
- * returning false, and the panel already renders both the same way. */
77
- export function resolveCatalogProvider(ctx, catalog, handlerName) {
78
- const found = findCatalogProvider(ctx, catalog);
79
- if (!found) {
80
- throw new FeatureDisabledError(BILLING_FOUNDATION_FEATURE, handlerName);
81
- }
82
- return found;
61
+ export async function filterOfferedProviders(ctx, providers) {
62
+ const enabledFlags = await Promise.all(providers.map(({ plugin }) => isPluginBillingEnabled(ctx, plugin)));
63
+ return providers.filter((_, index) => enabledFlags[index]);
64
+ }
65
+ /** The catalog providers that are live right now: what a buyer can choose from. */
66
+ export async function listOfferedCatalogProviders(ctx, catalog) {
67
+ return filterOfferedProviders(ctx, listCatalogProviders(ctx, catalog));
83
68
  }
84
69
  /** `plugin.isBillingEnabled?.(ctx) ?? true` — the one place this fallback is
85
70
  * spelled out, so no call-site forgets to await the optional method before
@@ -112,8 +112,14 @@ inlineHtmlMarkdown.use({
112
112
  blockquote({ tokens }) {
113
113
  return this.parser.parse(tokens);
114
114
  },
115
- list({ items }) {
116
- return items.map((item) => this.parser.parse(item.tokens)).join("");
115
+ list({ items, ordered, start }) {
116
+ const firstNumber = typeof start === "number" ? start : 1;
117
+ return items
118
+ .map((item, index) => {
119
+ const marker = ordered ? `${firstNumber + index}. ` : "";
120
+ return `${marker}${this.parser.parse(item.tokens)}`;
121
+ })
122
+ .join("");
117
123
  },
118
124
  hr: () => "",
119
125
  space: () => "",
@@ -30,6 +30,7 @@ export type ConsentPriceDetails = {
30
30
  readonly priceId: string;
31
31
  readonly price: Pick<ProviderPrice, "unitAmount" | "currency" | "interval" | "intervalCount"> | null;
32
32
  readonly orderItem?: OrderItem;
33
+ readonly providerName?: string;
33
34
  };
34
35
  export declare function prepareConsent(ctx: HandlerContext, options: ConsumerProtectionOptions, consent: ConsentPayload | undefined, mode: CheckoutMode): Promise<PreparedConsent>;
35
36
  export declare function consentCheckoutFields(prepared: PreparedConsent): {
@@ -104,6 +104,7 @@ export async function recordConsent(ctx, prepared, details) {
104
104
  termsTemplateVersion: prepared.termsTemplateVersion,
105
105
  locale: prepared.locale,
106
106
  actorUserId: String(ctx.user.id),
107
+ ...(details.providerName !== undefined && { providerName: details.providerName }),
107
108
  ...(details.orderItem && {
108
109
  itemLabelKey: details.orderItem.labelKey,
109
110
  ...(details.orderItem.params && { itemLabelParams: details.orderItem.params }),
@@ -6,6 +6,7 @@ import { InternalError, UnprocessableError } from "@cosmicdrift/kumiko-framework
6
6
  import { createI18n } from "@cosmicdrift/kumiko-framework/i18n";
7
7
  import { isValidIanaTimeZone } from "@cosmicdrift/kumiko-framework/time";
8
8
  import * as z from "zod";
9
+ import { decryptStoredUserEmail } from "../../shared/index.js";
9
10
  import { requireTemplateResolver, TEXT_BLOCK_KIND } from "../../template-resolver/index.js";
10
11
  import { UserQueries } from "../../user/index.js";
11
12
  import { paymentAggregateId, subscriptionAggregateId } from "../aggregate-id.js";
@@ -13,7 +14,6 @@ import { CHECKOUT_CONSENT_RECORDED_EVENT_QN, CONTRACT_CONFIRMATION_ISSUED_EVENT_
13
14
  import { CONTRACT_CONFIRMATION_NOTIFICATION_TYPE } from "../web/constants.js";
14
15
  import { contractConfirmationVariables } from "./confirmation-mail.js";
15
16
  import { resolveConsentLocale } from "./consent-text.js";
16
- import { openStoredUserEmail } from "./stored-user-email.js";
17
17
  export const issueContractConfirmationSchema = z
18
18
  .object({
19
19
  consentId: z.string().min(1).max(100),
@@ -31,6 +31,12 @@ const recipientRowSchema = z.object({
31
31
  function translatedOrUndefined(translation, key) {
32
32
  return translation === key ? undefined : translation;
33
33
  }
34
+ function vatNoteFor(consumerProtection, providerName) {
35
+ const providerNote = providerName === undefined
36
+ ? undefined
37
+ : consumerProtection.providerTexts?.[providerName]?.vatNote;
38
+ return providerNote ?? consumerProtection.vatNote;
39
+ }
34
40
  function pickTimeZone(userZone, tenantZone) {
35
41
  if (userZone && isValidIanaTimeZone(userZone))
36
42
  return userZone;
@@ -102,7 +108,7 @@ export function createIssueContractConfirmationHandler(options, consumerProtecti
102
108
  id: consent.actorUserId,
103
109
  }));
104
110
  const recipientEmail = recipient.success && recipient.data.isDeleted !== true
105
- ? await openStoredUserEmail(recipient.data.email, "billing-foundation:issue-contract-confirmation")
111
+ ? await decryptStoredUserEmail(recipient.data.email, "billing-foundation:issue-contract-confirmation")
106
112
  : null;
107
113
  if (!recipient.success || recipientEmail === null) {
108
114
  ctx.log?.warn(`[billing-foundation:issue-contract-confirmation] recipient of consent ${payload.consentId} not found; no mail sent`);
@@ -142,7 +148,7 @@ export function createIssueContractConfirmationHandler(options, consumerProtecti
142
148
  ...(payload.currentPeriodEndIso !== undefined && {
143
149
  currentPeriodEndIso: payload.currentPeriodEndIso,
144
150
  }),
145
- vatNote: consumerProtection.vatNote,
151
+ vatNote: vatNoteFor(consumerProtection, consent.providerName),
146
152
  operatorEmail: consumerProtection.operatorEmail,
147
153
  termsContent: terms.content,
148
154
  });
@@ -44,7 +44,9 @@ export function terminationReceiptTemplate() {
44
44
  mailRepeatedRows("details", "text"),
45
45
  mailParagraph(texts.receiptNextSteps),
46
46
  mailParagraph(texts.receiptRevoke(OPERATOR_EMAIL_SLOT)),
47
- mailParagraphWithLink(texts.receiptNotMe, texts.receiptNotMeLink, "revokeUrl").replace("<p>", '<p data-if="revokeUrl">'),
47
+ mailParagraphWithLink(texts.receiptNotMe, texts.receiptNotMeLink, "revokeUrl", {
48
+ showIf: "revokeUrl",
49
+ }),
48
50
  mailParagraph(`${texts.contact} {operatorEmail}`),
49
51
  ].join(""),
50
52
  };
@@ -1,6 +1,8 @@
1
1
  import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
2
2
  export declare const RECEIPT_MAILS_PER_RECIPIENT = 3;
3
3
  export declare const RECEIPT_WINDOW_SECONDS: number;
4
+ export type ReceiptMailVerdict = "allowed" | "limited" | "unavailable";
4
5
  /** Consumes one receipt token for the recipient. Fails closed when the limiter
5
- * errors or no digest key is wired: the declaration is already recorded, a missing receipt is the lesser harm. */
6
- export declare function mayMailReceipt(ctx: HandlerContext, email: string): Promise<boolean>;
6
+ * errors or no digest key is wired ("unavailable"): the declaration is already
7
+ * recorded, a missing receipt is the lesser harm. */
8
+ export declare function mayMailReceipt(ctx: HandlerContext, email: string): Promise<ReceiptMailVerdict>;
@@ -10,25 +10,26 @@ function receiptBucketKey(ctx, email) {
10
10
  return digest === undefined ? undefined : `${RECEIPT_BUCKET_PREFIX}:${digest}`;
11
11
  }
12
12
  /** Consumes one receipt token for the recipient. Fails closed when the limiter
13
- * errors or no digest key is wired: the declaration is already recorded, a missing receipt is the lesser harm. */
13
+ * errors or no digest key is wired ("unavailable"): the declaration is already
14
+ * recorded, a missing receipt is the lesser harm. */
14
15
  export async function mayMailReceipt(ctx, email) {
15
16
  // skip: no rate limiter wired, nothing to count against
16
17
  if (!ctx.rateLimit)
17
- return true;
18
+ return "allowed";
18
19
  try {
19
20
  const bucketKey = receiptBucketKey(ctx, email);
20
21
  if (bucketKey === undefined) {
21
22
  ctx.log?.warn("[billing-foundation:termination] no rate-limit digest available, receipt mail withheld");
22
- return false;
23
+ return "unavailable";
23
24
  }
24
25
  const decision = await ctx.rateLimit.check(bucketKey, {
25
26
  limit: RECEIPT_MAILS_PER_RECIPIENT,
26
27
  windowSeconds: RECEIPT_WINDOW_SECONDS,
27
28
  });
28
- return decision.allowed;
29
+ return decision.allowed ? "allowed" : "limited";
29
30
  }
30
31
  catch (error) {
31
32
  ctx.log?.warn(`[billing-foundation:termination] receipt rate limiter failed: ${error instanceof Error ? error.message : String(error)}`);
32
- return false;
33
+ return "unavailable";
33
34
  }
34
35
  }
@@ -1,6 +1,6 @@
1
1
  import { type HandlerContext, type QueryHandlerDef, type WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import * as z from "zod";
3
- import { type ProviderCancelOutcome } from "../events.ts";
3
+ import { type ContractTerminationRequestedPayload, type ProviderCancelOutcome } from "../events.ts";
4
4
  import { type SubscriptionView } from "../get-subscription-for-tenant.ts";
5
5
  import type { ConsumerProtectionOptions, ResolvedBillingFoundationOptions } from "../types.ts";
6
6
  import type { OperatorNoticeReason } from "./termination-texts.ts";
@@ -74,6 +74,10 @@ export declare const terminableSubscriptionSchema: z.ZodObject<{}, z.core.$stric
74
74
  export declare function createTerminableSubscriptionQuery(): QueryHandlerDef;
75
75
  export declare function createDeclareContractTerminationHandler(): WriteHandlerDef;
76
76
  export declare function createRecordContractTerminationHandler(options: ResolvedBillingFoundationOptions, consumerProtection: ConsumerProtectionOptions): WriteHandlerDef;
77
+ export declare function findRecordedTermination(events: readonly {
78
+ type: string;
79
+ payload: unknown;
80
+ }[], requestId: string): ContractTerminationRequestedPayload | undefined;
77
81
  export declare function hasRevocation(events: readonly {
78
82
  type: string;
79
83
  payload: unknown;
@@ -173,23 +173,9 @@ export function createRecordContractTerminationHandler(options, consumerProtecti
173
173
  });
174
174
  // The public job runs at least once: a second run must not call the
175
175
  // provider or append again.
176
- const alreadyRecorded = stream.events
177
- .map((e) => e.type === CONTRACT_TERMINATION_REQUESTED_EVENT_QN
178
- ? contractTerminationRequestedPayloadSchema.safeParse(e.payload).data
179
- : undefined)
180
- .find((recordedPayload) => recordedPayload?.requestId === payload.requestId);
181
- if (alreadyRecorded) {
182
- return {
183
- isSuccess: true,
184
- data: {
185
- receivedAtIso: alreadyRecorded.receivedAtIso,
186
- effectiveAtIso: alreadyRecorded.effectiveAtIso,
187
- providerCancel: alreadyRecorded.providerCancel,
188
- noticeReason: null,
189
- withdrawalPeriodExpired: false,
190
- },
191
- };
192
- }
176
+ const alreadyRecorded = findRecordedTermination(stream.events, payload.requestId);
177
+ if (alreadyRecorded)
178
+ return recordedTerminationResult(alreadyRecorded);
193
179
  // A revoked public declaration never reaches the provider.
194
180
  if (hasRevocation(stream.events, payload.requestId)) {
195
181
  return revokedRecordResult(receivedAtIso);
@@ -214,6 +200,11 @@ export function createRecordContractTerminationHandler(options, consumerProtecti
214
200
  aggregateId: subscriptionAggregateId(tenantId),
215
201
  aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
216
202
  });
203
+ // A concurrent run with the same requestId may have recorded while we were
204
+ // at the provider; a second append would duplicate the event and the notice.
205
+ const recordedMeanwhile = findRecordedTermination(streamAfterProviderCall.events, payload.requestId);
206
+ if (recordedMeanwhile)
207
+ return recordedTerminationResult(recordedMeanwhile);
217
208
  // A revocation that landed during the provider call found no cancel to take back.
218
209
  if (hasRevocation(streamAfterProviderCall.events, payload.requestId)) {
219
210
  await takeBackLateRevokedCancel(ctx, consumerProtection, {
@@ -258,6 +249,25 @@ export function createRecordContractTerminationHandler(options, consumerProtecti
258
249
  },
259
250
  };
260
251
  }
252
+ export function findRecordedTermination(events, requestId) {
253
+ return events
254
+ .map((e) => e.type === CONTRACT_TERMINATION_REQUESTED_EVENT_QN
255
+ ? contractTerminationRequestedPayloadSchema.safeParse(e.payload).data
256
+ : undefined)
257
+ .find((recordedPayload) => recordedPayload?.requestId === requestId);
258
+ }
259
+ function recordedTerminationResult(recorded) {
260
+ return {
261
+ isSuccess: true,
262
+ data: {
263
+ receivedAtIso: recorded.receivedAtIso,
264
+ effectiveAtIso: recorded.effectiveAtIso,
265
+ providerCancel: recorded.providerCancel,
266
+ noticeReason: null,
267
+ withdrawalPeriodExpired: false,
268
+ },
269
+ };
270
+ }
261
271
  export function hasRevocation(events, requestId) {
262
272
  return events.some((e) => e.type === CONTRACT_TERMINATION_REVOKED_EVENT_QN &&
263
273
  contractTerminationRevokedPayloadSchema.safeParse(e.payload).data?.requestId === requestId);
@@ -6,13 +6,13 @@ import { createSystemUser, declareEscapeHatch, SYSTEM_TENANT_ID, } from "@cosmic
6
6
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
7
7
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
8
8
  import * as z from "zod";
9
+ import { decryptStoredUserEmail } from "../../shared/index.js";
9
10
  import { TenantQueries } from "../../tenant/index.js";
10
11
  import { UserQueries } from "../../user/index.js";
11
12
  import { CONTRACT_TERMINATION_DECLARATION_TYPES, CONTRACT_TERMINATION_KINDS, } from "../events.js";
12
13
  import { purchaseRolesOf } from "../plan-catalog.js";
13
14
  import { CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE, SubscriptionCancelTimings, SubscriptionFoundationHandlers, SubscriptionFoundationQueries, } from "../web/constants.js";
14
15
  import { resolveConsentLocale } from "./consent-text.js";
15
- import { openStoredUserEmail } from "./stored-user-email.js";
16
16
  import { terminationReceiptVariables } from "./termination-mail.js";
17
17
  import { notifyOperator, requireNotify } from "./termination-notify.js";
18
18
  import { mayMailReceipt } from "./termination-receipt-limit.js";
@@ -54,9 +54,16 @@ function needsOperatorNotice(declaration) {
54
54
  ...(declaration.terminationKind === "extraordinary" ? ["extraordinary"] : []),
55
55
  ];
56
56
  }
57
+ function receiptWithheldReasons(verdict) {
58
+ if (verdict === "limited")
59
+ return ["receipt_rate_limited"];
60
+ if (verdict === "unavailable")
61
+ return ["receipt_not_sent"];
62
+ return [];
63
+ }
57
64
  async function sendTerminationMails(ctx, consumerProtection, plan) {
58
- const receiptAllowed = await mayMailReceipt(ctx, plan.declaration.email);
59
- if (receiptAllowed) {
65
+ const receiptVerdict = await mayMailReceipt(ctx, plan.declaration.email);
66
+ if (receiptVerdict === "allowed") {
60
67
  await requireNotify(ctx)(CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE, {
61
68
  route: { email: plan.declaration.email },
62
69
  data: terminationReceiptVariables({
@@ -74,9 +81,7 @@ async function sendTerminationMails(ctx, consumerProtection, plan) {
74
81
  priority: "critical",
75
82
  });
76
83
  }
77
- const reasons = receiptAllowed
78
- ? plan.reasons
79
- : [...plan.reasons, "receipt_rate_limited"];
84
+ const reasons = [...plan.reasons, ...receiptWithheldReasons(receiptVerdict)];
80
85
  // skip: no operator notice needed for this declaration
81
86
  if (reasons.length === 0)
82
87
  return;
@@ -217,7 +222,7 @@ export function createTerminateContractHandler(options, consumerProtection) {
217
222
  id: event.user.id,
218
223
  }));
219
224
  const callerEmail = caller.success && caller.data.email && caller.data.isDeleted !== true
220
- ? await openStoredUserEmail(caller.data.email, "billing-foundation:terminate-contract")
225
+ ? await decryptStoredUserEmail(caller.data.email, "billing-foundation:terminate-contract")
221
226
  : null;
222
227
  if (callerEmail === null) {
223
228
  throw new InternalError({
@@ -7,11 +7,11 @@ import { access, createSystemUser, } from "@cosmicdrift/kumiko-framework/engine"
7
7
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
8
8
  import * as z from "zod";
9
9
  import { subscriptionAggregateId } from "../aggregate-id.js";
10
- import { CONTRACT_TERMINATION_DECLARED_EVENT_QN, CONTRACT_TERMINATION_REQUESTED_EVENT_QN, CONTRACT_TERMINATION_REVOKED_EVENT_QN, contractTerminationDeclaredPayloadSchema, contractTerminationRequestedPayloadSchema, SUBSCRIPTION_AGGREGATE_TYPE, } from "../events.js";
10
+ import { CONTRACT_TERMINATION_DECLARED_EVENT_QN, CONTRACT_TERMINATION_REVOKED_EVENT_QN, contractTerminationDeclaredPayloadSchema, SUBSCRIPTION_AGGREGATE_TYPE, } from "../events.js";
11
11
  import { getSubscriptionForTenant } from "../get-subscription-for-tenant.js";
12
12
  import { SubscriptionFoundationHandlers } from "../web/constants.js";
13
13
  import { notifyOperator } from "./termination-notify.js";
14
- import { hasRevocation, resumeAtProvider } from "./termination-record.js";
14
+ import { findRecordedTermination, hasRevocation, resumeAtProvider } from "./termination-record.js";
15
15
  export const REVOCATION_PATHS = {
16
16
  de: "/legal/kuendigen/zuruecknehmen",
17
17
  en: "/legal/cancel/revoke",
@@ -102,10 +102,7 @@ export function createRecordContractTerminationRevocationHandler(options, consum
102
102
  }
103
103
  if (hasRevocation(stream.events, payload.requestId))
104
104
  return outcome("already-revoked");
105
- const requested = stream.events
106
- .filter((e) => e.type === CONTRACT_TERMINATION_REQUESTED_EVENT_QN)
107
- .map((e) => contractTerminationRequestedPayloadSchema.safeParse(e.payload).data)
108
- .find((candidate) => candidate?.requestId === payload.requestId);
105
+ const requested = findRecordedTermination(stream.events, payload.requestId);
109
106
  const subscription = await getSubscriptionForTenant(ctx, tenantId);
110
107
  const effectiveAtIso = requested?.effectiveAtIso ?? subscription?.currentPeriodEnd.toString();
111
108
  const now = options.now();
@@ -122,6 +119,14 @@ export function createRecordContractTerminationRevocationHandler(options, consum
122
119
  aggregateId: subscriptionAggregateId(tenantId),
123
120
  aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
124
121
  });
122
+ // A cancel recorded between the first fetch and now was never resumed above.
123
+ const requestedMeanwhile = requested === undefined
124
+ ? findRecordedTermination(streamAfterProviderCall.events, payload.requestId)
125
+ : undefined;
126
+ if (requestedMeanwhile?.providerCancel === "period-end") {
127
+ reasons.push(...(await resumeAtProvider(ctx, await getSubscriptionForTenant(ctx, tenantId))));
128
+ }
129
+ const providerCancel = (requested ?? requestedMeanwhile)?.providerCancel;
125
130
  const revoked = {
126
131
  requestId: payload.requestId,
127
132
  revokedAtIso: now.toString(),
@@ -141,7 +146,7 @@ export function createRecordContractTerminationRevocationHandler(options, consum
141
146
  },
142
147
  reasons,
143
148
  tenantId,
144
- ...(requested !== undefined && { providerCancel: requested.providerCancel }),
149
+ ...(providerCancel !== undefined && { providerCancel }),
145
150
  });
146
151
  return outcome("revoked");
147
152
  },
@@ -74,6 +74,6 @@ export type RevokePageTexts = {
74
74
  readonly invalidTitle: string;
75
75
  readonly invalidBody: string;
76
76
  };
77
- export declare const OPERATOR_NOTICE_REASONS: readonly ["unmatched", "ambiguous", "withdrawal", "extraordinary", "provider_cannot_cancel", "provider_error", "no_active_subscription", "recording_failed", "withdrawal_period_expired", "receipt_rate_limited", "revoked", "revocation_resume_manual", "revocation_resume_failed"];
77
+ export declare const OPERATOR_NOTICE_REASONS: readonly ["unmatched", "ambiguous", "withdrawal", "extraordinary", "provider_cannot_cancel", "provider_error", "no_active_subscription", "recording_failed", "withdrawal_period_expired", "receipt_rate_limited", "receipt_not_sent", "revoked", "revocation_resume_manual", "revocation_resume_failed"];
78
78
  export type OperatorNoticeReason = (typeof OPERATOR_NOTICE_REASONS)[number];
79
79
  export declare const TERMINATION_TEXTS: Readonly<Record<ConsentLocale, TerminationTexts>>;
@@ -9,6 +9,7 @@ export const OPERATOR_NOTICE_REASONS = [
9
9
  "recording_failed",
10
10
  "withdrawal_period_expired",
11
11
  "receipt_rate_limited",
12
+ "receipt_not_sent",
12
13
  "revoked",
13
14
  "revocation_resume_manual",
14
15
  "revocation_resume_failed",
@@ -55,6 +56,7 @@ export const TERMINATION_TEXTS = {
55
56
  recording_failed: "Die Erklärung konnte nicht gespeichert werden",
56
57
  withdrawal_period_expired: "Widerruf nach Ablauf der Widerrufsfrist: zum Periodenende gekündigt, Anspruch prüfen",
57
58
  receipt_rate_limited: "Keine Eingangsbestätigung versendet: Limit pro Empfängeradresse erreicht",
59
+ receipt_not_sent: "Keine Eingangsbestätigung versendet: Limit-Prüfung nicht verfügbar (Redis-Fehler oder fehlende Konfiguration)",
58
60
  revoked: "Die Erklärung wurde vom Postfachinhaber über „Diese Kündigung war ich nicht“ zurückgenommen",
59
61
  revocation_resume_manual: "Die Kündigung beim Zahlungsanbieter bitte manuell zurücknehmen (keine Schnittstelle dafür)",
60
62
  revocation_resume_failed: "Die Kündigung beim Zahlungsanbieter konnte nicht zurückgenommen werden, bitte manuell zurücknehmen",
@@ -154,6 +156,7 @@ export const TERMINATION_TEXTS = {
154
156
  recording_failed: "The declaration could not be stored",
155
157
  withdrawal_period_expired: "Withdrawal after the withdrawal period: cancelled at period end, review the claim",
156
158
  receipt_rate_limited: "No receipt mail sent: the per-recipient limit was reached",
159
+ receipt_not_sent: "No receipt mail sent: the limit check was unavailable (limiter error or missing configuration)",
157
160
  revoked: 'The declaration was taken back by the mailbox owner ("I did not request this cancellation")',
158
161
  revocation_resume_manual: "Undo the cancellation at the payment provider manually (no API for it)",
159
162
  revocation_resume_failed: "Undoing the cancellation at the payment provider failed, undo it manually",
@@ -4,7 +4,10 @@ type PeriodStartEvent = {
4
4
  readonly type: string;
5
5
  readonly createdAt: Temporal.Instant;
6
6
  };
7
- /** The period runs from the latest consent or subscription start on the stream.
8
- * A stream with neither has no provable start and counts as expired. */
7
+ /** The period runs from the latest subscription start on the stream. A checkout
8
+ * consent alone never restarts it: it is written before the buyer reaches the
9
+ * provider, so an abandoned plan-change checkout would otherwise reopen the
10
+ * period of a long-concluded contract. A stream without a subscription start
11
+ * has no provable start and counts as expired. */
9
12
  export declare function isWithinWithdrawalPeriod(events: readonly PeriodStartEvent[], now: Temporal.Instant): boolean;
10
13
  export {};
@@ -1,24 +1,23 @@
1
- import { CHECKOUT_CONSENT_RECORDED_EVENT_QN, SUBSCRIPTION_CREATED_EVENT_QN } from "../events.js";
1
+ import { SUBSCRIPTION_CREATED_EVENT_QN } from "../events.js";
2
2
  // § 355 BGB: the withdrawal period is 14 days from conclusion of the contract.
3
3
  export const WITHDRAWAL_PERIOD_DAYS = 14;
4
4
  const HOURS_PER_DAY = 24;
5
- const PERIOD_START_EVENT_TYPES = new Set([
6
- CHECKOUT_CONSENT_RECORDED_EVENT_QN,
7
- SUBSCRIPTION_CREATED_EVENT_QN,
8
- ]);
9
- /** The period runs from the latest consent or subscription start on the stream.
10
- * A stream with neither has no provable start and counts as expired. */
5
+ /** The period runs from the latest subscription start on the stream. A checkout
6
+ * consent alone never restarts it: it is written before the buyer reaches the
7
+ * provider, so an abandoned plan-change checkout would otherwise reopen the
8
+ * period of a long-concluded contract. A stream without a subscription start
9
+ * has no provable start and counts as expired. */
11
10
  export function isWithinWithdrawalPeriod(events, now) {
12
11
  let latestStart;
13
12
  for (const event of events) {
14
- if (!PERIOD_START_EVENT_TYPES.has(event.type))
13
+ if (event.type !== SUBSCRIPTION_CREATED_EVENT_QN)
15
14
  continue;
16
15
  if (latestStart === undefined ||
17
16
  event.createdAt.epochMilliseconds > latestStart.epochMilliseconds) {
18
17
  latestStart = event.createdAt;
19
18
  }
20
19
  }
21
- // skip: no consent or subscription-created event, nothing proves a running period
20
+ // skip: no subscription-created event, nothing proves a running period
22
21
  if (latestStart === undefined)
23
22
  return false;
24
23
  const periodEnd = latestStart.add({ hours: WITHDRAWAL_PERIOD_DAYS * HOURS_PER_DAY });
@@ -66,6 +66,7 @@ export declare const checkoutConsentRecordedPayloadSchema: z.ZodObject<{
66
66
  termsTemplateVersion: z.ZodNumber;
67
67
  locale: z.ZodString;
68
68
  actorUserId: z.ZodString;
69
+ providerName: z.ZodOptional<z.ZodString>;
69
70
  itemLabelKey: z.ZodOptional<z.ZodString>;
70
71
  itemLabelParams: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
71
72
  }, z.core.$strip>;
@@ -104,6 +104,7 @@ export const checkoutConsentRecordedPayloadSchema = z.object({
104
104
  termsTemplateVersion: z.number(),
105
105
  locale: z.string().min(2).max(35),
106
106
  actorUserId: z.string().min(1),
107
+ providerName: z.string().min(1).max(100).optional(),
107
108
  itemLabelKey: z.string().min(1).max(200).optional(),
108
109
  itemLabelParams: z.record(z.string(), z.union([z.string().max(100), z.number()])).optional(),
109
110
  });
@@ -57,6 +57,7 @@ import { createBillingPlansQuery } from "./handlers/billing-plans.query.js";
57
57
  import { createCheckoutSessionHandler } from "./handlers/create-checkout-session.write.js";
58
58
  import { createPortalSessionHandler } from "./handlers/create-portal-session.write.js";
59
59
  import { listSubscriptionsQuery } from "./handlers/list-subscriptions.query.js";
60
+ import { createPlanSwitchPreviewQuery } from "./handlers/plan-switch-preview.query.js";
60
61
  import { createProcessEventHandler } from "./handlers/process-event.write.js";
61
62
  import { processPaymentEventHandler } from "./handlers/process-payment-event.write.js";
62
63
  import { createStartPlanCheckoutHandler } from "./handlers/start-plan-checkout.write.js";
@@ -89,7 +90,7 @@ export function createBillingFoundationFeature(options = {}) {
89
90
  }),
90
91
  };
91
92
  return defineFeature(BILLING_FOUNDATION_FEATURE, (r) => {
92
- r.describe("Plugin host for subscription billing — manages the `read_subscriptions` projection table and exposes 5 domain events (subscription created/updated/canceled, invoice paid/failed) appended by the foundation's own `billing-foundation:write:process-event` write-handler after provider plugins verify and normalize each webhook. Also manages a separate `read_payments` projection table (one row per one-off-payment) fed by its own `payment-received` event and `billing-foundation:write:process-payment-event` write-handler. Also ships `billing-foundation:write:create-checkout-session` and `billing-foundation:write:create-portal-session` write-handlers, a `billing-foundation:query:subscription:list` query handler, and a `createSubscriptionWebhookRoute` factory for the `/api/subscription/webhook/:providerName` extraRoute. `createBillingFoundationFeature({ baseUrl, catalog })` additionally derives a `billing-foundation:query:billing-plans` query, `start-plan-checkout`/`switch-plan` write-handlers and a dormant billing-plans dashboard screen/panel from the catalog. Also ships `billing-foundation:write:sync-subscription` (pulls a provider plugin's live subscription state via `retrieveSubscription` and appends drift as a `subscription.updated` event — catches changes made on the provider's own dashboard that never reached us as a webhook) and the `sync-subscriptions` job (manual-trigger + runOnBoot, perTenant) that dispatches it. Low-level building block — use `subscription-stripe` or `subscription-mollie` unless you are writing a new payment provider.");
93
+ r.describe("Plugin host for subscription billing. Manages the `read_subscriptions` projection table and exposes 5 domain events (subscription created/updated/canceled, invoice paid/failed) appended by the foundation's own `billing-foundation:write:process-event` write-handler after provider plugins verify and normalize each webhook. Also manages a separate `read_payments` projection table (one row per one-off-payment) fed by its own `payment-received` event and `billing-foundation:write:process-payment-event` write-handler. Also ships `billing-foundation:write:create-checkout-session` and `billing-foundation:write:create-portal-session` write-handlers, a `billing-foundation:query:subscription:list` query handler, and a `createSubscriptionWebhookRoute` factory for the `/api/subscription/webhook/:providerName` extraRoute. `createBillingFoundationFeature({ baseUrl, catalog })` additionally derives a `billing-foundation:query:billing-plans` query, `start-plan-checkout`/`switch-plan` write-handlers, a `plan-switch-preview` query and a dormant billing-plans dashboard screen/panel from the catalog. Several providers can be mounted at once: new checkouts are offered by every enabled provider (enabled means keys set and billingLive on, or simply mounted for plugins without `isBillingEnabled`), the customer picks one in the plan panel and `start-plan-checkout` takes a `providerName`. An existing subscription stays with the provider that stored it, so the plan list and every switch follow that provider. A plan switch is either hosted (`createPlanSwitchSession` answers a url) or in-app (`previewPlanSwitch` shows the proration, `applyPlanSwitch` runs after the customer confirms; `switch-plan` then answers `{ applied: true }` and records the consent). `consumerProtection.providerTexts` overrides the checkout note and the VAT note per provider. Also ships `billing-foundation:write:sync-subscription` (pulls a provider plugin's live subscription state via `retrieveSubscription` and appends drift as a `subscription.updated` event, which catches changes made on the provider's own dashboard that never reached us as a webhook) and the `sync-subscriptions` job (manual-trigger + runOnBoot, perTenant) that dispatches it. Low-level building block: use `subscription-stripe`, `subscription-mollie` or `subscription-dodo` unless you are writing a new payment provider.");
93
94
  r.uiHints({
94
95
  displayLabel: "Billing · Foundation",
95
96
  category: "billing",
@@ -183,6 +184,7 @@ export function createBillingFoundationFeature(options = {}) {
183
184
  if (widened.catalog) {
184
185
  const { catalog } = widened;
185
186
  r.queryHandler(createBillingPlansQuery(widened, catalog));
187
+ r.queryHandler(createPlanSwitchPreviewQuery(catalog));
186
188
  r.writeHandler(createStartPlanCheckoutHandler(widened, catalog));
187
189
  r.writeHandler(createSwitchPlanHandler(widened, catalog));
188
190
  r.screen(createBillingPlansScreen(catalog.viewRoles));