@cosmicdrift/kumiko-samples 0.362.0 → 0.364.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.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +23 -6
- package/packages/bundled-features/src/admin-shell/changes.json +6 -0
- package/packages/bundled-features/src/admin-shell/feature.ts +8 -1
- package/packages/bundled-features/src/agent-tools/agent-manifest.ts +3 -4
- package/packages/bundled-features/src/agent-tools/filter-ops.ts +1 -3
- package/packages/bundled-features/src/agent-tools/tool-catalog.ts +1 -3
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +2 -1
- package/packages/bundled-features/src/audit/changes.json +6 -0
- package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +29 -8
- package/packages/bundled-features/src/auth-email-password/changes.json +51 -0
- package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
- package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
- package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
- package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
- package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
- package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +5 -13
- package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
- package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
- package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
- package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
- package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
- package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
- package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
- package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
- package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
- package/packages/bundled-features/src/billing-foundation/changes.json +43 -0
- package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +90 -44
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +118 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +39 -33
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +10 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +36 -20
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
- package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
- package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
- package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +12 -6
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +20 -11
- package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
- package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
- package/packages/bundled-features/src/cap-counter/changes.json +6 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
- package/packages/bundled-features/src/cap-counter/index.ts +1 -0
- package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
- package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
- package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
- package/packages/bundled-features/src/channel-email/changes.json +19 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +135 -29
- package/packages/bundled-features/src/channel-email/feature.ts +6 -3
- package/packages/bundled-features/src/channel-email/index.ts +7 -1
- package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
- package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
- package/packages/bundled-features/src/channel-email/types.ts +23 -0
- package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
- package/packages/bundled-features/src/config/changes.json +6 -0
- package/packages/bundled-features/src/config/handlers/set.write.ts +3 -7
- package/packages/bundled-features/src/config/resolver.ts +15 -12
- package/packages/bundled-features/src/config/write-helpers.ts +23 -0
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +48 -27
- package/packages/bundled-features/src/custom-fields/changes.json +6 -0
- package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
- package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
- package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
- package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
- package/packages/bundled-features/src/data-retention/changes.json +6 -0
- package/packages/bundled-features/src/data-retention/index.ts +1 -0
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
- package/packages/bundled-features/src/delivery/attachment-ref.ts +105 -0
- package/packages/bundled-features/src/delivery/changes.json +66 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +152 -4
- package/packages/bundled-features/src/delivery/constants.ts +2 -0
- package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
- package/packages/bundled-features/src/delivery/feature.ts +1 -1
- package/packages/bundled-features/src/delivery/index.ts +19 -2
- package/packages/bundled-features/src/delivery/jobs.ts +97 -17
- package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
- package/packages/bundled-features/src/delivery/testing.ts +9 -1
- package/packages/bundled-features/src/delivery/types.ts +34 -18
- package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
- package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
- package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
- package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
- package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
- package/packages/bundled-features/src/file-foundation/changes.json +6 -0
- package/packages/bundled-features/src/file-foundation/index.ts +7 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +89 -0
- package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
- package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
- package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
- package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
- package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
- package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
- package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +42 -25
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
- package/packages/bundled-features/src/jobs/changes.json +19 -0
- package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
- package/packages/bundled-features/src/jobs/feature.ts +7 -4
- package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
- package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
- package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
- package/packages/bundled-features/src/jobs/job-run-logger.ts +52 -17
- package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
- package/packages/bundled-features/src/ledger/changes.json +7 -0
- package/packages/bundled-features/src/ledger/entity.ts +3 -1
- package/packages/bundled-features/src/ledger/recurring.ts +1 -3
- package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
- package/packages/bundled-features/src/managed-pages/changes.json +6 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
- package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
- package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
- package/packages/bundled-features/src/page-render/branding.ts +3 -0
- package/packages/bundled-features/src/page-render/markdown.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
- package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
- package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
- package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
- package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
- package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
- package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
- package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
- package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
- package/packages/bundled-features/src/renderer-foundation/types.ts +134 -35
- package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
- package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
- package/packages/bundled-features/src/secrets/changes.json +6 -0
- package/packages/bundled-features/src/secrets/feature.ts +4 -3
- package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
- package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
- package/packages/bundled-features/src/sessions/feature.ts +2 -4
- package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
- package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
- package/packages/bundled-features/src/shared/changes.json +6 -0
- package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
- package/packages/bundled-features/src/shared/index.ts +6 -0
- package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
- package/packages/bundled-features/src/shared/mail-template-body.ts +42 -0
- package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
- package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
- package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
- package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
- package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
- package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
- package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
- package/packages/bundled-features/src/template-resolver/README.md +66 -8
- package/packages/bundled-features/src/template-resolver/api.ts +85 -9
- package/packages/bundled-features/src/template-resolver/changes.json +186 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +278 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +49 -11
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +345 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +59 -6
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +160 -0
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +33 -0
- package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +138 -0
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +9 -6
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +151 -0
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +19 -18
- package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +323 -0
- package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
- package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
- package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +135 -0
- package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
- package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
- package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +196 -0
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +233 -0
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +137 -0
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +91 -17
- package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
- package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +16 -77
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +31 -0
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
- package/packages/bundled-features/src/template-resolver/index.ts +111 -0
- package/packages/bundled-features/src/template-resolver/layout-appearance.ts +40 -0
- package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
- package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
- package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
- package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
- package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +34 -0
- package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
- package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
- package/packages/bundled-features/src/template-resolver/seeding.ts +171 -20
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
- package/packages/bundled-features/src/template-resolver/table.ts +27 -1
- package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
- package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
- package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
- package/packages/bundled-features/src/template-resolver/template-meta.ts +192 -0
- package/packages/bundled-features/src/template-resolver/template-preview.ts +676 -0
- package/packages/bundled-features/src/template-resolver/template-resources.ts +114 -0
- package/packages/bundled-features/src/template-resolver/template-usage.ts +68 -0
- package/packages/bundled-features/src/template-resolver/template-validation.ts +230 -0
- package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +20 -13
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +215 -0
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +37 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +1065 -0
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +72 -0
- package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +121 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +26 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +176 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
- package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
- package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
- package/packages/bundled-features/src/tenant/seeding.ts +41 -7
- package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
- package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
- package/packages/bundled-features/src/user/changes.json +6 -0
- package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
- package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
- package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
- package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
- package/packages/bundled-features/src/user/seeding.ts +19 -5
- package/packages/bundled-features/src/user-data-rights/changes.json +13 -0
- package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
- package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
- package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
- package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
- package/packages/bundled-features/src/user-profile/changes.json +1 -1
- package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
- package/packages/bundled-features/src/waitlist/changes.json +7 -0
- package/packages/bundled-features/src/waitlist/feature.ts +17 -0
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
- package/packages/bundled-features/src/waitlist/mail.ts +69 -18
- package/packages/bundled-features/src/waitlist/options.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
- package/packages/bundled-features/src/workflow-runner/event-trigger.ts +3 -3
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
- package/samples/apps/ui-walkthrough/package.json +11 -0
- package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
- package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
- package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
- package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +239 -0
- package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
- package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +570 -0
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +72 -0
- package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
- package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
- package/samples/apps/ui-walkthrough/src/run-config.ts +41 -1
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +118 -21
- package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/package.json +2 -0
- package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
- package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
- package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
- package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
- package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
- package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
- package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
- package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
- package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
- package/samples/recipes/delivery-notifications/README.md +26 -25
- package/samples/recipes/delivery-notifications/package.json +2 -1
- package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
- package/samples/recipes/seed-migration/README.md +1 -1
- package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
- package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
- package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
- package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
- package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
- package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "../events.js";
|
|
14
14
|
import type { ConsumerProtectionOptions, ResolvedBillingFoundationOptions } from "../types.js";
|
|
15
15
|
import { createIssueContractConfirmationHandler } from "./issue-confirmation.js";
|
|
16
|
+
import { registerConsumerProtectionMails } from "./mail-notifications.js";
|
|
16
17
|
import { registerContractTermination } from "./register-termination.js";
|
|
17
18
|
|
|
18
19
|
type ConfirmationTrigger = {
|
|
@@ -46,6 +47,7 @@ export function registerConsumerProtection(
|
|
|
46
47
|
consumerProtection: ConsumerProtectionOptions,
|
|
47
48
|
): void {
|
|
48
49
|
r.requires("template-resolver", "delivery", "user", "tenant");
|
|
50
|
+
registerConsumerProtectionMails(r);
|
|
49
51
|
registerContractTermination(r, options, consumerProtection);
|
|
50
52
|
r.defineEvent(CHECKOUT_CONSENT_RECORDED_EVENT_SHORT, checkoutConsentRecordedPayloadSchema, {
|
|
51
53
|
piiFields: "none",
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts
CHANGED
|
@@ -52,9 +52,19 @@ export function registerContractTermination(
|
|
|
52
52
|
// request: matched and unmatched requests then do the same work and the
|
|
53
53
|
// response time does not tell whether the email belongs to a customer.
|
|
54
54
|
// The tenant comes from the stored trigger event (the job's system user).
|
|
55
|
+
// Every declaration appends to the same subscription stream, so parallel runs
|
|
56
|
+
// and a declaration arriving mid-run hit version conflicts. Sequential runs
|
|
57
|
+
// rule out the first; a retry re-reads the stream and the requestId check in
|
|
58
|
+
// record-contract-termination keeps it from recording twice.
|
|
55
59
|
r.job(
|
|
56
60
|
"cancel-on-public-termination-declared",
|
|
57
|
-
{
|
|
61
|
+
{
|
|
62
|
+
trigger: { on: CONTRACT_TERMINATION_DECLARED_EVENT_QN },
|
|
63
|
+
runIn: "worker",
|
|
64
|
+
concurrency: "sequential",
|
|
65
|
+
retries: 3,
|
|
66
|
+
backoff: "exponential",
|
|
67
|
+
},
|
|
58
68
|
async (payload, ctx) => {
|
|
59
69
|
const declared = contractTerminationDeclaredPayloadSchema.safeParse(payload);
|
|
60
70
|
// skip: trigger event carries no usable declaration
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts
CHANGED
|
@@ -7,11 +7,22 @@ import type {
|
|
|
7
7
|
import type { ConsentLocale } from "./consent-text.js";
|
|
8
8
|
import { type OperatorNoticeReason, TERMINATION_TEXTS } from "./termination-texts.js";
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
type TextRow = { readonly text: string };
|
|
11
|
+
|
|
12
|
+
/** Variables of the termination receipt template. */
|
|
13
|
+
export type TerminationReceiptVariables = {
|
|
11
14
|
readonly subject: string;
|
|
12
|
-
readonly
|
|
13
|
-
readonly
|
|
14
|
-
readonly
|
|
15
|
+
readonly receivedAt: string;
|
|
16
|
+
readonly requestId: string;
|
|
17
|
+
readonly details: readonly TextRow[];
|
|
18
|
+
readonly operatorEmail: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Variables of the operator notice template. */
|
|
22
|
+
export type OperatorNoticeVariables = {
|
|
23
|
+
readonly subject: string;
|
|
24
|
+
readonly lines: readonly TextRow[];
|
|
25
|
+
readonly operatorEmail: string;
|
|
15
26
|
};
|
|
16
27
|
|
|
17
28
|
/** What the declarant entered; the only input of the receipt. */
|
|
@@ -53,7 +64,7 @@ export function formatReceivedAt(iso: string, locale: ConsentLocale): string {
|
|
|
53
64
|
function declarationLines(
|
|
54
65
|
declaration: OperatorNoticeDeclaration,
|
|
55
66
|
locale: ConsentLocale,
|
|
56
|
-
): readonly
|
|
67
|
+
): readonly TextRow[] {
|
|
57
68
|
const texts = TERMINATION_TEXTS[locale];
|
|
58
69
|
const lines = [
|
|
59
70
|
`${texts.declarationType}: ${texts.declarationTypeLabel[declaration.declarationType]}`,
|
|
@@ -68,38 +79,36 @@ function declarationLines(
|
|
|
68
79
|
return lines.map((text) => ({ text }));
|
|
69
80
|
}
|
|
70
81
|
|
|
71
|
-
|
|
82
|
+
/** The receipt carries enum fields only: free text from an anonymous caller
|
|
83
|
+
* must not be echoed to an arbitrary address. */
|
|
84
|
+
export type ReceiptDeclaration = Pick<
|
|
85
|
+
TerminationDeclaration,
|
|
86
|
+
"declarationType" | "terminationKind"
|
|
87
|
+
>;
|
|
88
|
+
|
|
89
|
+
export type TerminationReceiptArgs = {
|
|
72
90
|
readonly locale: ConsentLocale;
|
|
73
91
|
readonly requestId: string;
|
|
74
92
|
readonly receivedAtIso: string;
|
|
75
|
-
readonly declaration:
|
|
93
|
+
readonly declaration: ReceiptDeclaration;
|
|
76
94
|
readonly operatorEmail: string;
|
|
77
95
|
};
|
|
78
96
|
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
): TerminationMailContent {
|
|
97
|
+
/** The content must not depend on whether (or how) the declaration matched a contract. */
|
|
98
|
+
export function terminationReceiptVariables(
|
|
99
|
+
args: TerminationReceiptArgs,
|
|
100
|
+
): TerminationReceiptVariables {
|
|
84
101
|
const texts = TERMINATION_TEXTS[args.locale];
|
|
85
|
-
const subject = texts.receiptSubject[args.declaration.declarationType];
|
|
86
102
|
return {
|
|
87
|
-
subject,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
{ text: `${texts.receivedAt}: ${formatReceivedAt(args.receivedAtIso, args.locale)}` },
|
|
93
|
-
{ text: `${texts.requestId}: ${args.requestId}` },
|
|
94
|
-
...declarationLines(args.declaration, args.locale),
|
|
95
|
-
{ text: texts.receiptNextSteps },
|
|
96
|
-
{ text: texts.receiptRevoke(args.operatorEmail) },
|
|
97
|
-
],
|
|
98
|
-
footer: `${texts.contact} ${args.operatorEmail}`,
|
|
103
|
+
subject: texts.receiptSubject[args.declaration.declarationType],
|
|
104
|
+
receivedAt: formatReceivedAt(args.receivedAtIso, args.locale),
|
|
105
|
+
requestId: args.requestId,
|
|
106
|
+
details: declarationLines(args.declaration, args.locale),
|
|
107
|
+
operatorEmail: args.operatorEmail,
|
|
99
108
|
};
|
|
100
109
|
}
|
|
101
110
|
|
|
102
|
-
export type
|
|
111
|
+
export type OperatorNoticeArgs = {
|
|
103
112
|
readonly locale: ConsentLocale;
|
|
104
113
|
readonly requestId: string;
|
|
105
114
|
readonly receivedAtIso: string;
|
|
@@ -111,14 +120,11 @@ export type RenderOperatorNoticeArgs = {
|
|
|
111
120
|
readonly operatorEmail: string;
|
|
112
121
|
};
|
|
113
122
|
|
|
114
|
-
export function
|
|
123
|
+
export function operatorNoticeVariables(args: OperatorNoticeArgs): OperatorNoticeVariables {
|
|
115
124
|
const texts = TERMINATION_TEXTS[args.locale];
|
|
116
|
-
const subject = texts.operatorSubject[args.declaration.declarationType];
|
|
117
125
|
return {
|
|
118
|
-
subject,
|
|
119
|
-
|
|
120
|
-
sections: [
|
|
121
|
-
{ text: texts.operatorIntro },
|
|
126
|
+
subject: texts.operatorSubject[args.declaration.declarationType],
|
|
127
|
+
lines: [
|
|
122
128
|
...args.reasons.map((reason) => ({
|
|
123
129
|
text: `${texts.noticeReasons}: ${texts.operatorReasons[reason]}`,
|
|
124
130
|
})),
|
|
@@ -131,6 +137,6 @@ export function renderOperatorNotice(args: RenderOperatorNoticeArgs): Terminatio
|
|
|
131
137
|
: []),
|
|
132
138
|
...declarationLines(args.declaration, args.locale),
|
|
133
139
|
],
|
|
134
|
-
|
|
140
|
+
operatorEmail: args.operatorEmail,
|
|
135
141
|
};
|
|
136
142
|
}
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { CONTRACT_TERMINATION_OPERATOR_NOTIFICATION_TYPE } from "../constants.js
|
|
|
4
4
|
import type { ProviderCancelOutcome } from "../events.js";
|
|
5
5
|
import type { ConsumerProtectionOptions } from "../types.js";
|
|
6
6
|
import type { ConsentLocale } from "./consent-text.js";
|
|
7
|
-
import { type OperatorNoticeDeclaration,
|
|
7
|
+
import { type OperatorNoticeDeclaration, operatorNoticeVariables } from "./termination-mail.js";
|
|
8
8
|
import type { OperatorNoticeReason } from "./termination-texts.js";
|
|
9
9
|
|
|
10
10
|
export type OperatorNoticePlan = {
|
|
@@ -35,7 +35,7 @@ export async function notifyOperator(
|
|
|
35
35
|
): Promise<void> {
|
|
36
36
|
await requireNotify(ctx)(CONTRACT_TERMINATION_OPERATOR_NOTIFICATION_TYPE, {
|
|
37
37
|
route: { email: consumerProtection.operatorEmail },
|
|
38
|
-
data:
|
|
38
|
+
data: operatorNoticeVariables({
|
|
39
39
|
...plan,
|
|
40
40
|
operatorEmail: consumerProtection.operatorEmail,
|
|
41
41
|
}),
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { type AnonymousExtraRoute, getSessionCookies } from "@cosmicdrift/kumiko-framework/api";
|
|
5
5
|
import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
6
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
6
7
|
import type { Context } from "hono";
|
|
7
8
|
import { getCookie } from "hono/cookie";
|
|
8
9
|
import {
|
|
@@ -98,10 +99,6 @@ function htmlResponse(
|
|
|
98
99
|
);
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
function isOneOf<T extends string>(options: readonly T[], value: string): value is T {
|
|
102
|
-
return (options as readonly string[]).includes(value);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
102
|
async function readForm(c: Context): Promise<{ step: string; values: FormValues }> {
|
|
106
103
|
const body = await c.req.parseBody();
|
|
107
104
|
const text = (key: string): string => {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { normalizePayloadBucketValue } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
3
|
+
|
|
4
|
+
// The declaration itself is a statutory duty and is never refused. Only the
|
|
5
|
+
// receipt mail is capped, so the endpoint cannot be used to mail one inbox
|
|
6
|
+
// from the operator's sender domain.
|
|
7
|
+
export const RECEIPT_MAILS_PER_RECIPIENT = 3;
|
|
8
|
+
export const RECEIPT_WINDOW_SECONDS = 24 * 60 * 60;
|
|
9
|
+
|
|
10
|
+
const RECEIPT_BUCKET_PREFIX = "termination-receipt";
|
|
11
|
+
|
|
12
|
+
function receiptBucketKey(ctx: HandlerContext, email: string): string | undefined {
|
|
13
|
+
const digest = ctx.rateLimitDigest?.(normalizePayloadBucketValue(email, "email"));
|
|
14
|
+
return digest === undefined ? undefined : `${RECEIPT_BUCKET_PREFIX}:${digest}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Consumes one receipt token for the recipient. Fails closed when the limiter
|
|
18
|
+
* errors or no digest key is wired: the declaration is already recorded, a missing receipt is the lesser harm. */
|
|
19
|
+
export async function mayMailReceipt(ctx: HandlerContext, email: string): Promise<boolean> {
|
|
20
|
+
// skip: no rate limiter wired, nothing to count against
|
|
21
|
+
if (!ctx.rateLimit) return true;
|
|
22
|
+
try {
|
|
23
|
+
const bucketKey = receiptBucketKey(ctx, email);
|
|
24
|
+
if (bucketKey === undefined) {
|
|
25
|
+
ctx.log?.warn(
|
|
26
|
+
"[billing-foundation:termination] no rate-limit digest available, receipt mail withheld",
|
|
27
|
+
);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const decision = await ctx.rateLimit.check(bucketKey, {
|
|
31
|
+
limit: RECEIPT_MAILS_PER_RECIPIENT,
|
|
32
|
+
windowSeconds: RECEIPT_WINDOW_SECONDS,
|
|
33
|
+
});
|
|
34
|
+
return decision.allowed;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
ctx.log?.warn(
|
|
37
|
+
`[billing-foundation:termination] receipt rate limiter failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
38
|
+
);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts
CHANGED
|
@@ -35,6 +35,7 @@ import type { ConsumerProtectionOptions, ResolvedBillingFoundationOptions } from
|
|
|
35
35
|
import { CONSENT_LOCALES, FALLBACK_CONSENT_LOCALE } from "./consent-locale.js";
|
|
36
36
|
import { notifyOperator } from "./termination-notify.js";
|
|
37
37
|
import type { OperatorNoticeReason } from "./termination-texts.js";
|
|
38
|
+
import { isWithinWithdrawalPeriod } from "./withdrawal-period.js";
|
|
38
39
|
|
|
39
40
|
const REQUESTED_CANCEL_TIMINGS = [
|
|
40
41
|
SubscriptionCancelTimings.periodEnd,
|
|
@@ -74,6 +75,8 @@ export type RecordContractTerminationResult = {
|
|
|
74
75
|
OperatorNoticeReason,
|
|
75
76
|
"provider_cannot_cancel" | "provider_error" | "no_active_subscription"
|
|
76
77
|
> | null;
|
|
78
|
+
/** An account withdrawal arrived after the period: cancelled at period end instead. */
|
|
79
|
+
readonly withdrawalPeriodExpired: boolean;
|
|
77
80
|
};
|
|
78
81
|
|
|
79
82
|
export const recordUnmatchedContractTerminationSchema = z
|
|
@@ -210,14 +213,19 @@ export function createRecordContractTerminationHandler(
|
|
|
210
213
|
effectiveAtIso: alreadyRecorded.effectiveAtIso,
|
|
211
214
|
providerCancel: alreadyRecorded.providerCancel,
|
|
212
215
|
noticeReason: null,
|
|
216
|
+
withdrawalPeriodExpired: false,
|
|
213
217
|
} satisfies RecordContractTerminationResult,
|
|
214
218
|
};
|
|
215
219
|
}
|
|
216
220
|
|
|
221
|
+
const withdrawalPeriodExpired =
|
|
222
|
+
payload.declarationType === "withdrawal" &&
|
|
223
|
+
payload.when === SubscriptionCancelTimings.immediately &&
|
|
224
|
+
!isWithinWithdrawalPeriod(stream.events, options.now());
|
|
217
225
|
const { providerCancel, effectiveAtIso, noticeReason } = await cancelAtProvider(
|
|
218
226
|
ctx,
|
|
219
227
|
tenantId,
|
|
220
|
-
payload.when,
|
|
228
|
+
withdrawalPeriodExpired ? SubscriptionCancelTimings.periodEnd : payload.when,
|
|
221
229
|
receivedAtIso,
|
|
222
230
|
);
|
|
223
231
|
|
|
@@ -260,6 +268,7 @@ export function createRecordContractTerminationHandler(
|
|
|
260
268
|
effectiveAtIso,
|
|
261
269
|
providerCancel,
|
|
262
270
|
noticeReason,
|
|
271
|
+
withdrawalPeriodExpired,
|
|
263
272
|
} satisfies RecordContractTerminationResult,
|
|
264
273
|
};
|
|
265
274
|
},
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts
CHANGED
|
@@ -31,12 +31,12 @@ import {
|
|
|
31
31
|
import { purchaseRolesOf } from "../plan-catalog.js";
|
|
32
32
|
import type { ConsumerProtectionOptions, ResolvedBillingFoundationOptions } from "../types.js";
|
|
33
33
|
import { resolveConsentLocale } from "./consent-text.js";
|
|
34
|
-
import {
|
|
34
|
+
import { type TerminationDeclaration, terminationReceiptVariables } from "./termination-mail.js";
|
|
35
35
|
import { notifyOperator, type OperatorNoticePlan, requireNotify } from "./termination-notify.js";
|
|
36
|
+
import { mayMailReceipt } from "./termination-receipt-limit.js";
|
|
36
37
|
import type { RecordContractTerminationResult } from "./termination-record.js";
|
|
37
38
|
import type { OperatorNoticeReason } from "./termination-texts.js";
|
|
38
39
|
|
|
39
|
-
const TENANT_ADMIN_ROLE = "TenantAdmin";
|
|
40
40
|
const EXTRAORDINARY_REASON_MESSAGE = "reason is required for an extraordinary termination";
|
|
41
41
|
|
|
42
42
|
const authUserRowSchema = z.object({
|
|
@@ -109,28 +109,38 @@ async function sendTerminationMails(
|
|
|
109
109
|
consumerProtection: ConsumerProtectionOptions,
|
|
110
110
|
plan: TerminationMailPlan,
|
|
111
111
|
): Promise<void> {
|
|
112
|
-
await
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
const receiptAllowed = await mayMailReceipt(ctx, plan.declaration.email);
|
|
113
|
+
if (receiptAllowed) {
|
|
114
|
+
await requireNotify(ctx)(CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE, {
|
|
115
|
+
route: { email: plan.declaration.email },
|
|
116
|
+
data: terminationReceiptVariables({
|
|
117
|
+
locale: plan.locale,
|
|
118
|
+
requestId: plan.requestId,
|
|
119
|
+
receivedAtIso: plan.receivedAtIso,
|
|
120
|
+
declaration: {
|
|
121
|
+
declarationType: plan.declaration.declarationType,
|
|
122
|
+
terminationKind: plan.declaration.terminationKind,
|
|
123
|
+
},
|
|
124
|
+
operatorEmail: consumerProtection.operatorEmail,
|
|
125
|
+
}),
|
|
115
126
|
locale: plan.locale,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
priority: "critical",
|
|
123
|
-
});
|
|
127
|
+
priority: "critical",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const reasons = receiptAllowed
|
|
131
|
+
? plan.reasons
|
|
132
|
+
: [...plan.reasons, "receipt_rate_limited" as const];
|
|
124
133
|
// skip: no operator notice needed for this declaration
|
|
125
|
-
if (
|
|
126
|
-
await notifyOperator(ctx, consumerProtection, plan);
|
|
134
|
+
if (reasons.length === 0) return;
|
|
135
|
+
await notifyOperator(ctx, consumerProtection, { ...plan, reasons });
|
|
127
136
|
}
|
|
128
137
|
|
|
129
138
|
// Tenants the entered email can terminate a contract for: tenants where the
|
|
130
|
-
// user
|
|
139
|
+
// user holds a purchase role and the subscription is not terminal. Every identity
|
|
131
140
|
// below comes from the trusted lookups, never from the payload.
|
|
132
141
|
async function findTerminableTenantIds(
|
|
133
142
|
ctx: HandlerContext,
|
|
143
|
+
options: ResolvedBillingFoundationOptions,
|
|
134
144
|
requestTenantId: string,
|
|
135
145
|
email: string,
|
|
136
146
|
): Promise<readonly string[]> {
|
|
@@ -148,10 +158,11 @@ async function findTerminableTenantIds(
|
|
|
148
158
|
}),
|
|
149
159
|
);
|
|
150
160
|
if (!memberships.success) return [];
|
|
161
|
+
const purchaseRoles = purchaseRolesOf(options.catalog);
|
|
151
162
|
const adminTenantIds = [
|
|
152
163
|
...new Set(
|
|
153
164
|
memberships.data
|
|
154
|
-
.filter((membership) => membership.roles.includes(
|
|
165
|
+
.filter((membership) => membership.roles.some((role) => purchaseRoles.includes(role)))
|
|
155
166
|
.map((membership) => membership.tenantId),
|
|
156
167
|
),
|
|
157
168
|
];
|
|
@@ -180,7 +191,6 @@ export function createRequestContractTerminationHandler(
|
|
|
180
191
|
schema: requestContractTerminationSchema,
|
|
181
192
|
access: { roles: ["anonymous"] },
|
|
182
193
|
rateLimit: { per: "ip+handler", limit: 5, windowSeconds: 600 },
|
|
183
|
-
additionalRateLimits: [{ per: { payloadField: "email" }, limit: 3, windowSeconds: 86400 }],
|
|
184
194
|
...(consumerProtection.terminationScope === "platform" && { tenantlessAnonymous: true }),
|
|
185
195
|
escapeHatch: {
|
|
186
196
|
reason:
|
|
@@ -196,7 +206,12 @@ export function createRequestContractTerminationHandler(
|
|
|
196
206
|
const locale = resolveConsentLocale(payload.locale);
|
|
197
207
|
let matchedTenantId: string | undefined;
|
|
198
208
|
|
|
199
|
-
const candidates = await findTerminableTenantIds(
|
|
209
|
+
const candidates = await findTerminableTenantIds(
|
|
210
|
+
ctx,
|
|
211
|
+
options,
|
|
212
|
+
event.user.tenantId,
|
|
213
|
+
payload.email,
|
|
214
|
+
);
|
|
200
215
|
const [onlyCandidate] = candidates;
|
|
201
216
|
if (candidates.length === 1 && onlyCandidate !== undefined) {
|
|
202
217
|
matchedTenantId = onlyCandidate;
|
|
@@ -257,7 +272,7 @@ export function createTerminateContractHandler(
|
|
|
257
272
|
return {
|
|
258
273
|
name: "terminate-contract",
|
|
259
274
|
description:
|
|
260
|
-
"Terminates the tenant's subscription at the end of the billing period, or withdraws from the contract and cancels it immediately; sends a receipt to the caller's email.",
|
|
275
|
+
"Terminates the tenant's subscription at the end of the billing period, or withdraws from the contract and cancels it immediately within the 14-day withdrawal period (at period end after it); sends a receipt to the caller's email.",
|
|
261
276
|
schema: terminateContractSchema,
|
|
262
277
|
access: { roles: purchaseRolesOf(options.catalog) },
|
|
263
278
|
escapeHatch: {
|
|
@@ -313,6 +328,7 @@ export function createTerminateContractHandler(
|
|
|
313
328
|
},
|
|
314
329
|
reasons: [
|
|
315
330
|
...(data.noticeReason !== null ? [data.noticeReason] : []),
|
|
331
|
+
...(data.withdrawalPeriodExpired ? (["withdrawal_period_expired"] as const) : []),
|
|
316
332
|
...needsOperatorNotice(payload),
|
|
317
333
|
],
|
|
318
334
|
tenantId,
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts
CHANGED
|
@@ -72,6 +72,8 @@ export const OPERATOR_NOTICE_REASONS = [
|
|
|
72
72
|
"provider_error",
|
|
73
73
|
"no_active_subscription",
|
|
74
74
|
"recording_failed",
|
|
75
|
+
"withdrawal_period_expired",
|
|
76
|
+
"receipt_rate_limited",
|
|
75
77
|
] as const;
|
|
76
78
|
export type OperatorNoticeReason = (typeof OPERATOR_NOTICE_REASONS)[number];
|
|
77
79
|
|
|
@@ -116,6 +118,10 @@ export const TERMINATION_TEXTS: Readonly<Record<ConsentLocale, TerminationTexts>
|
|
|
116
118
|
provider_error: "Die Kündigung beim Zahlungsanbieter ist fehlgeschlagen",
|
|
117
119
|
no_active_subscription: "Keine laufende Subscription beim Anbieter",
|
|
118
120
|
recording_failed: "Die Erklärung konnte nicht gespeichert werden",
|
|
121
|
+
withdrawal_period_expired:
|
|
122
|
+
"Widerruf nach Ablauf der Widerrufsfrist: zum Periodenende gekündigt, Anspruch prüfen",
|
|
123
|
+
receipt_rate_limited:
|
|
124
|
+
"Keine Eingangsbestätigung versendet: Limit pro Empfängeradresse erreicht",
|
|
119
125
|
},
|
|
120
126
|
noticeReasons: "Grund der Benachrichtigung",
|
|
121
127
|
channel: "Kanal",
|
|
@@ -199,6 +205,9 @@ export const TERMINATION_TEXTS: Readonly<Record<ConsentLocale, TerminationTexts>
|
|
|
199
205
|
provider_error: "Cancelling at the payment provider failed",
|
|
200
206
|
no_active_subscription: "No running subscription at the provider",
|
|
201
207
|
recording_failed: "The declaration could not be stored",
|
|
208
|
+
withdrawal_period_expired:
|
|
209
|
+
"Withdrawal after the withdrawal period: cancelled at period end, review the claim",
|
|
210
|
+
receipt_rate_limited: "No receipt mail sent: the per-recipient limit was reached",
|
|
202
211
|
},
|
|
203
212
|
noticeReasons: "Reason for this notice",
|
|
204
213
|
channel: "Channel",
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
2
|
+
import { CHECKOUT_CONSENT_RECORDED_EVENT_QN, SUBSCRIPTION_CREATED_EVENT_QN } from "../events.js";
|
|
3
|
+
|
|
4
|
+
// § 355 BGB: the withdrawal period is 14 days from conclusion of the contract.
|
|
5
|
+
export const WITHDRAWAL_PERIOD_DAYS = 14;
|
|
6
|
+
const HOURS_PER_DAY = 24;
|
|
7
|
+
|
|
8
|
+
const PERIOD_START_EVENT_TYPES: ReadonlySet<string> = new Set([
|
|
9
|
+
CHECKOUT_CONSENT_RECORDED_EVENT_QN,
|
|
10
|
+
SUBSCRIPTION_CREATED_EVENT_QN,
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
type PeriodStartEvent = { readonly type: string; readonly createdAt: Temporal.Instant };
|
|
14
|
+
|
|
15
|
+
/** The period runs from the latest consent or subscription start on the stream.
|
|
16
|
+
* A stream with neither has no provable start and counts as expired. */
|
|
17
|
+
export function isWithinWithdrawalPeriod(
|
|
18
|
+
events: readonly PeriodStartEvent[],
|
|
19
|
+
now: Temporal.Instant,
|
|
20
|
+
): boolean {
|
|
21
|
+
let latestStart: Temporal.Instant | undefined;
|
|
22
|
+
for (const event of events) {
|
|
23
|
+
if (!PERIOD_START_EVENT_TYPES.has(event.type)) continue;
|
|
24
|
+
if (
|
|
25
|
+
latestStart === undefined ||
|
|
26
|
+
event.createdAt.epochMilliseconds > latestStart.epochMilliseconds
|
|
27
|
+
) {
|
|
28
|
+
latestStart = event.createdAt;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// skip: no consent or subscription-created event, nothing proves a running period
|
|
32
|
+
if (latestStart === undefined) return false;
|
|
33
|
+
const periodEnd = latestStart.add({ hours: WITHDRAWAL_PERIOD_DAYS * HOURS_PER_DAY });
|
|
34
|
+
return now.epochMilliseconds <= periodEnd.epochMilliseconds;
|
|
35
|
+
}
|
|
@@ -40,10 +40,10 @@ export async function getSubscriptionForTenant(
|
|
|
40
40
|
if (!row) return null;
|
|
41
41
|
const piiKms = configuredPiiSubjectKms();
|
|
42
42
|
const decrypted = piiKms
|
|
43
|
-
? await decryptPiiFieldValues(row
|
|
43
|
+
? await decryptPiiFieldValues(row, SUBSCRIPTION_PII_FIELDS, piiKms, {
|
|
44
44
|
requestId: `billing-foundation:get-subscription:${tenantId}`,
|
|
45
45
|
})
|
|
46
|
-
:
|
|
46
|
+
: row;
|
|
47
47
|
// @cast-boundary db-row — drizzle-row carries column-as-unknown
|
|
48
48
|
return {
|
|
49
49
|
tier: decrypted["tier"] as string,
|
package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// hand-picked priceId.
|
|
24
24
|
|
|
25
25
|
import type { HandlerContext, WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
26
|
-
import { UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
|
|
26
|
+
import { InternalError, UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
|
|
27
27
|
import * as z from "zod";
|
|
28
28
|
import {
|
|
29
29
|
assertCheckoutAllowed,
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
startProviderCheckout,
|
|
33
33
|
} from "../checkout-core.js";
|
|
34
34
|
import {
|
|
35
|
+
assertValidOrderItem,
|
|
35
36
|
type ConsentPayload,
|
|
36
37
|
consentCheckoutFields,
|
|
37
38
|
consentPayloadSchema,
|
|
@@ -104,11 +105,12 @@ export function createCheckoutSessionHandler(
|
|
|
104
105
|
|
|
105
106
|
const mode = payload.mode ?? "subscription";
|
|
106
107
|
const plugin = await assertCheckoutAllowed(ctx, openOptions, checkoutInput);
|
|
107
|
-
const price = await lookupPrice(ctx, plugin, payload.priceId);
|
|
108
|
+
const price = await lookupPrice(ctx, plugin, payload.providerName, payload.priceId);
|
|
108
109
|
const orderItem =
|
|
109
110
|
mode === "payment"
|
|
110
111
|
? await consumerProtection.oneOffItemLabel?.(ctx, payload.priceId)
|
|
111
112
|
: undefined;
|
|
113
|
+
assertValidOrderItem(orderItem, payload.priceId);
|
|
112
114
|
const consent = await prepareConsent(ctx, consumerProtection, payload.consent, mode);
|
|
113
115
|
const result = await startProviderCheckout(ctx, plugin, {
|
|
114
116
|
...checkoutInput,
|
|
@@ -134,11 +136,17 @@ export function createCheckoutSessionHandler(
|
|
|
134
136
|
async function lookupPrice(
|
|
135
137
|
ctx: HandlerContext,
|
|
136
138
|
plugin: SubscriptionProviderPlugin,
|
|
139
|
+
providerName: string,
|
|
137
140
|
priceId: string,
|
|
138
141
|
): Promise<ProviderPrice> {
|
|
142
|
+
if (!plugin.retrievePrices) {
|
|
143
|
+
throw new InternalError({
|
|
144
|
+
message: `billing-foundation: provider "${providerName}" lacks retrievePrices, required by consumerProtection`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
139
147
|
let price: ProviderPrice | undefined;
|
|
140
148
|
try {
|
|
141
|
-
const prices =
|
|
149
|
+
const prices = await plugin.retrievePrices(ctx, [priceId]);
|
|
142
150
|
price = prices.find((p) => p.priceId === priceId);
|
|
143
151
|
} catch (cause) {
|
|
144
152
|
throw priceUnavailable(priceId, cause);
|
package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts
CHANGED
|
@@ -52,15 +52,10 @@ export function createPortalSessionHandler(options: BillingFoundationOptions): W
|
|
|
52
52
|
}
|
|
53
53
|
const piiKms = configuredPiiSubjectKms();
|
|
54
54
|
const decrypted = piiKms
|
|
55
|
-
? await decryptPiiFieldValues(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
{
|
|
60
|
-
requestId: `billing-foundation:create-portal-session:${tenantId}`,
|
|
61
|
-
},
|
|
62
|
-
)
|
|
63
|
-
: (row as Record<string, unknown>);
|
|
55
|
+
? await decryptPiiFieldValues(row, SUBSCRIPTION_PII_FIELDS, piiKms, {
|
|
56
|
+
requestId: `billing-foundation:create-portal-session:${tenantId}`,
|
|
57
|
+
})
|
|
58
|
+
: row;
|
|
64
59
|
const providerName = row["providerName"] as string; // @cast-boundary db-row
|
|
65
60
|
const providerCustomerId = decrypted["providerCustomerId"] as string; // @cast-boundary db-row
|
|
66
61
|
|
package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts
CHANGED
|
@@ -27,7 +27,7 @@ export const listSubscriptionsQuery: QueryHandlerDef = {
|
|
|
27
27
|
if (!piiKms) return { rows };
|
|
28
28
|
const decryptedRows = await Promise.all(
|
|
29
29
|
rows.map((row) =>
|
|
30
|
-
decryptPiiFieldValues(row
|
|
30
|
+
decryptPiiFieldValues(row, SUBSCRIPTION_PII_FIELDS, piiKms, {
|
|
31
31
|
requestId: `billing-foundation:list-subscriptions:${ctx.user.tenantId}`,
|
|
32
32
|
}),
|
|
33
33
|
),
|
|
@@ -21,6 +21,7 @@ import * as z from "zod";
|
|
|
21
21
|
import { subscriptionAggregateId } from "../aggregate-id.js";
|
|
22
22
|
import {
|
|
23
23
|
isSubscriptionBlockingCheckout,
|
|
24
|
+
isSwitchableSubscriptionStatus,
|
|
24
25
|
SubscriptionEventTypes,
|
|
25
26
|
SubscriptionStatuses,
|
|
26
27
|
} from "../constants.js";
|
|
@@ -167,8 +168,8 @@ export async function appendSubscriptionEvent(
|
|
|
167
168
|
// ---------------------------------------------------------------
|
|
168
169
|
const eventPayload: SubscriptionEventPayload = {
|
|
169
170
|
providerName: payload.providerName,
|
|
170
|
-
providerCustomerId: encryptedFields
|
|
171
|
-
providerSubscriptionId: encryptedFields
|
|
171
|
+
providerCustomerId: encryptedFields.providerCustomerId,
|
|
172
|
+
providerSubscriptionId: encryptedFields.providerSubscriptionId,
|
|
172
173
|
status: payload.status,
|
|
173
174
|
tier: payload.tier,
|
|
174
175
|
currentPeriodEndIso: payload.currentPeriodEndIso,
|
|
@@ -199,19 +200,24 @@ export async function appendSubscriptionEvent(
|
|
|
199
200
|
// Tenant-Admin direkt.
|
|
200
201
|
// One subscription aggregate exists per tenant, but a tenant can legitimately hold two provider
|
|
201
202
|
// subscriptions over time (a stale `incomplete` one superseded by a fresh checkout). A late or
|
|
202
|
-
// retried event of the old subscription must not overwrite the row of the live one, so
|
|
203
|
-
//
|
|
203
|
+
// retried event of the old subscription must not overwrite the row of the live one, so an event of a
|
|
204
|
+
// different provider subscription only replaces a row that no longer blocks a checkout. The exception
|
|
205
|
+
// is a `created` of a subscription that is itself live (a provider switch); a late `created` of a
|
|
206
|
+
// never-completed `incomplete` one stays stale, since delivery order is not guaranteed.
|
|
204
207
|
function isStaleSubscriptionEvent(
|
|
205
208
|
current: SubscriptionView | null,
|
|
206
209
|
payload: ProcessEventPayload,
|
|
207
210
|
now: Temporal.Instant,
|
|
208
211
|
): boolean {
|
|
209
212
|
if (current === null) return false;
|
|
210
|
-
if (payload.type === SubscriptionEventTypes.created) return false;
|
|
211
213
|
const sameSubscription =
|
|
212
214
|
current.providerName === payload.providerName &&
|
|
213
215
|
current.providerSubscriptionId === payload.providerSubscriptionId;
|
|
214
|
-
|
|
216
|
+
if (sameSubscription || !isSubscriptionBlockingCheckout(current, now)) return false;
|
|
217
|
+
const supersedesCurrent =
|
|
218
|
+
payload.type === SubscriptionEventTypes.created &&
|
|
219
|
+
isSwitchableSubscriptionStatus(payload.status);
|
|
220
|
+
return !supersedesCurrent;
|
|
215
221
|
}
|
|
216
222
|
|
|
217
223
|
export function createProcessEventHandler(now: () => Temporal.Instant): WriteHandlerDef {
|
package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts
CHANGED
|
@@ -95,7 +95,7 @@ export const processPaymentEventHandler: WriteHandlerDef = {
|
|
|
95
95
|
// ---------------------------------------------------------------
|
|
96
96
|
const eventPayload: PaymentEventPayload = {
|
|
97
97
|
providerName: payload.providerName,
|
|
98
|
-
providerCustomerId: encryptedFields
|
|
98
|
+
providerCustomerId: encryptedFields.providerCustomerId,
|
|
99
99
|
priceId: payload.priceId,
|
|
100
100
|
...(payload.consentId !== undefined && { consentId: payload.consentId }),
|
|
101
101
|
};
|
|
@@ -95,6 +95,9 @@ export const BILLING_FOUNDATION_I18N: Readonly<Record<string, LocalizedString>>
|
|
|
95
95
|
"billing-foundation.errors.redirectOriginNotAllowed": {
|
|
96
96
|
en: "This redirect URL is not allowed for this app.",
|
|
97
97
|
},
|
|
98
|
+
"billing-foundation.errors.providerCustomerUnknown": {
|
|
99
|
+
en: "The billing account for this subscription no longer exists. Subscribe again to continue.",
|
|
100
|
+
},
|
|
98
101
|
"billing-foundation.errors.providerHasNoPriceCatalog": {
|
|
99
102
|
en: "This provider has no known prices configured.",
|
|
100
103
|
},
|