@cosmicdrift/kumiko-samples 0.387.0 → 0.389.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +8 -1
- package/packages/bundled-features/src/agent-tools/changes.json +12 -0
- package/packages/bundled-features/src/agent-tools/index.ts +1 -1
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +48 -32
- package/packages/bundled-features/src/audit/changes.json +12 -0
- package/packages/bundled-features/src/audit/feature.ts +1 -0
- package/packages/bundled-features/src/audit/handlers/audit-events.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +12 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +4 -0
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +8 -0
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +16 -3
- package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +8 -2
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +23 -21
- package/packages/bundled-features/src/auth-email-password/web/default-auth-pages-gate.tsx +6 -3
- package/packages/bundled-features/src/auth-email-password/web/forgot-password-screen.tsx +11 -1
- package/packages/bundled-features/src/auth-email-password/web/index.ts +5 -0
- package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +16 -5
- package/packages/bundled-features/src/auth-email-password/web/reset-password-screen.tsx +7 -1
- package/packages/bundled-features/src/auth-email-password/web/return-path.ts +35 -0
- package/packages/bundled-features/src/auth-email-password/web/session.tsx +6 -1
- package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +11 -3
- package/packages/bundled-features/src/auth-email-password/web/signup-screen.tsx +12 -2
- package/packages/bundled-features/src/auth-mfa/changes.json +12 -0
- package/packages/bundled-features/src/auth-mfa/config.ts +3 -7
- package/packages/bundled-features/src/auth-mfa/feature-exports.ts +2 -2
- package/packages/bundled-features/src/auth-mfa/feature.ts +6 -0
- package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +8 -5
- package/packages/bundled-features/src/auth-mfa/web/i18n.ts +6 -1
- package/packages/bundled-features/src/auth-mfa/web/mfa-code-or-recovery-field.tsx +68 -0
- package/packages/bundled-features/src/auth-mfa/web/mfa-disable-dialog.tsx +23 -13
- package/packages/bundled-features/src/auth-mfa/web/mfa-regenerate-recovery-dialog.tsx +23 -13
- package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +15 -6
- package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +65 -18
- package/packages/bundled-features/src/billing-foundation/changes.json +55 -0
- package/packages/bundled-features/src/billing-foundation/checkout-core.ts +32 -42
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +8 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +14 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +3 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +12 -6
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +35 -19
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +12 -8
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts +14 -8
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +5 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +8 -10
- package/packages/bundled-features/src/billing-foundation/events.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/feature.ts +3 -1
- package/packages/bundled-features/src/billing-foundation/handlers/billing-plans.query.ts +71 -6
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/handlers/plan-switch-preview.query.ts +65 -0
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +9 -2
- package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +44 -8
- package/packages/bundled-features/src/billing-foundation/handlers/switch-plan.write.ts +72 -82
- package/packages/bundled-features/src/billing-foundation/handlers/sync-subscription.write.ts +14 -1
- package/packages/bundled-features/src/billing-foundation/i18n.ts +22 -0
- package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +62 -27
- package/packages/bundled-features/src/billing-foundation/plan-switch.ts +84 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +80 -1
- package/packages/bundled-features/src/billing-foundation/validate-options.ts +19 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +119 -47
- package/packages/bundled-features/src/billing-foundation/web/checkout-consent-dialog.tsx +89 -42
- package/packages/bundled-features/src/billing-foundation/web/constants.ts +4 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-price.ts +55 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +224 -0
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +25 -8
- package/packages/bundled-features/src/cap-counter/changes.json +30 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +2 -1
- package/packages/bundled-features/src/cap-counter/feature.ts +1 -0
- package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +8 -1
- package/packages/bundled-features/src/cap-overview/feature.ts +1 -0
- package/packages/bundled-features/src/channel-email/changes.json +6 -0
- package/packages/bundled-features/src/channel-email/feature.ts +1 -1
- package/packages/bundled-features/src/config/changes.json +18 -0
- package/packages/bundled-features/src/config/handlers/cascade.query.ts +4 -2
- package/packages/bundled-features/src/config/handlers/set.write.ts +19 -2
- package/packages/bundled-features/src/config/handlers/values.query.ts +4 -2
- package/packages/bundled-features/src/config/write-helpers.ts +27 -8
- package/packages/bundled-features/src/crypto-shredding/changes.json +18 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +11 -10
- package/packages/bundled-features/src/custom-fields/lib/parse-serialized-field.ts +1 -1
- package/packages/bundled-features/src/delivery/changes.json +42 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +9 -3
- package/packages/bundled-features/src/delivery/db/queries/preferences.ts +2 -2
- package/packages/bundled-features/src/delivery/delivery-service.ts +35 -5
- package/packages/bundled-features/src/delivery/feature.ts +1 -0
- package/packages/bundled-features/src/delivery/handlers/preferences.query.ts +20 -1
- package/packages/bundled-features/src/delivery/handlers/unsubscribe-page-chrome.query.ts +1 -1
- package/packages/bundled-features/src/delivery/index.ts +5 -0
- package/packages/bundled-features/src/delivery/system-channel-context.ts +33 -0
- package/packages/bundled-features/src/delivery/unsubscribe-link.ts +3 -1
- package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +1 -0
- package/packages/bundled-features/src/delivery/unsubscribe-page.ts +6 -0
- package/packages/bundled-features/src/delivery/unsubscribe.ts +36 -15
- package/packages/bundled-features/src/document-ingest-foundation/append-missing-extract-event.ts +84 -0
- package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
- package/packages/bundled-features/src/document-ingest-foundation/events.ts +15 -0
- package/packages/bundled-features/src/document-ingest-foundation/index.ts +11 -0
- package/packages/bundled-features/src/feature-toggles/feature.ts +1 -0
- package/packages/bundled-features/src/file-foundation/index.ts +1 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +7 -1
- package/packages/bundled-features/src/file-provider-s3/env-named-feature.ts +0 -2
- package/packages/bundled-features/src/files/README.md +1 -1
- package/packages/bundled-features/src/form-draft/changes.json +6 -0
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +2 -2
- package/packages/bundled-features/src/form-draft/decrypt-draft.ts +4 -1
- package/packages/bundled-features/src/host-routing/README.md +1 -1
- package/packages/bundled-features/src/host-routing/changes.json +12 -0
- package/packages/bundled-features/src/host-routing/dispatch.ts +16 -17
- package/packages/bundled-features/src/host-routing/options.ts +2 -1
- package/packages/bundled-features/src/host-routing/validate-options.ts +5 -0
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +18 -0
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +17 -5
- package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +0 -1
- package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +0 -3
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -3
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +32 -16
- package/packages/bundled-features/src/jobs/changes.json +13 -0
- package/packages/bundled-features/src/jobs/feature.ts +15 -4
- package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +45 -40
- package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +63 -59
- package/packages/bundled-features/src/jobs/job-run-logger.ts +5 -4
- package/packages/bundled-features/src/ledger/web/index.ts +1 -1
- package/packages/bundled-features/src/legal-pages/README.md +1 -1
- package/packages/bundled-features/src/legal-pages/seed.ts +5 -2
- package/packages/bundled-features/src/mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/mail-foundation/feature.ts +5 -3
- package/packages/bundled-features/src/mail-transport-smtp/changes.json +6 -0
- package/packages/bundled-features/src/mail-transport-smtp/config-keys.ts +1 -1
- package/packages/bundled-features/src/mail-transport-smtp/feature.ts +9 -11
- package/packages/bundled-features/src/managed-pages/changes.json +6 -0
- package/packages/bundled-features/src/managed-pages/constants.ts +1 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +3 -1
- package/packages/bundled-features/src/managed-pages/index.ts +1 -0
- package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +5 -1
- package/packages/bundled-features/src/renderer-pdf/changes.json +6 -0
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +27 -10
- package/packages/bundled-features/src/secrets/changes.json +6 -0
- package/packages/bundled-features/src/secrets/write-gate.ts +9 -2
- package/packages/bundled-features/src/sessions/changes.json +7 -0
- package/packages/bundled-features/src/sessions/feature.ts +8 -2
- package/packages/bundled-features/src/sessions/index.ts +1 -1
- package/packages/bundled-features/src/sessions/session-callbacks.ts +1 -9
- package/packages/bundled-features/src/shared/decrypt-stored-pii.ts +10 -0
- package/packages/bundled-features/src/shared/index.ts +1 -1
- package/packages/bundled-features/src/shared/mail-template-body.ts +10 -3
- package/packages/bundled-features/src/shared/purge-files-reaching-subject.ts +24 -2
- package/packages/bundled-features/src/shared/run-in-sub-transaction.ts +5 -5
- package/packages/bundled-features/src/step-dispatcher/changes.json +12 -0
- package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +17 -3
- package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -0
- package/packages/bundled-features/src/step-dispatcher/mail-runner.ts +2 -1
- package/packages/bundled-features/src/subscription-dodo/changes.json +14 -0
- package/packages/bundled-features/src/subscription-dodo/client.ts +45 -0
- package/packages/bundled-features/src/subscription-dodo/constants.ts +24 -0
- package/packages/bundled-features/src/subscription-dodo/feature.ts +207 -0
- package/packages/bundled-features/src/subscription-dodo/index.ts +16 -0
- package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +345 -0
- package/packages/bundled-features/src/subscription-dodo/runtime.ts +126 -0
- package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +262 -0
- package/packages/bundled-features/src/subscription-stripe/changes.json +12 -0
- package/packages/bundled-features/src/subscription-stripe/feature.ts +1 -0
- package/packages/bundled-features/src/subscription-stripe/runtime.ts +7 -6
- package/packages/bundled-features/src/tags/feature.ts +1 -0
- package/packages/bundled-features/src/template-resolver/api.ts +31 -4
- package/packages/bundled-features/src/template-resolver/changes.json +84 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +1 -0
- package/packages/bundled-features/src/template-resolver/document-ops.ts +20 -1
- package/packages/bundled-features/src/template-resolver/feature.ts +4 -4
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +27 -5
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/document-base.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +36 -11
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +18 -19
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +45 -10
- package/packages/bundled-features/src/template-resolver/handlers/patch-draft.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/send-test-status.query.ts +22 -19
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-counts.query.ts +9 -5
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts +8 -6
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +44 -5
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +37 -26
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +7 -2
- package/packages/bundled-features/src/template-resolver/seeding.ts +42 -17
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +74 -12
- package/packages/bundled-features/src/template-resolver/template-delivery-policy.ts +0 -11
- package/packages/bundled-features/src/template-resolver/template-meta.ts +12 -4
- package/packages/bundled-features/src/template-resolver/template-preview.ts +11 -3
- package/packages/bundled-features/src/template-resolver/web/constants.ts +7 -5
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +6 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/mail-layout-constants.ts +8 -0
- package/packages/bundled-features/src/template-resolver/web/qualified-names.ts +1 -0
- package/packages/bundled-features/src/tenant/changes.json +6 -0
- package/packages/bundled-features/src/tenant/feature.ts +1 -0
- package/packages/bundled-features/src/tenant/handlers/member-directory.query.ts +9 -3
- package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -3
- package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +7 -4
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +12 -0
- package/packages/bundled-features/src/tenant-lifecycle/constants.ts +5 -0
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +5 -1
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +35 -1
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +4 -1
- package/packages/bundled-features/src/tier-engine/web/constants.ts +3 -3
- package/packages/bundled-features/src/user/feature.ts +1 -0
- package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
- package/packages/bundled-features/src/user-data-rights/feature.ts +1 -0
- package/packages/bundled-features/src/user-data-rights/index.ts +6 -1
- package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +3 -4
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +2 -1
- package/packages/bundled-features/src/user-data-rights/web/constants.ts +6 -0
- package/packages/bundled-features/src/user-data-rights/web/index.ts +1 -0
- package/packages/bundled-features/src/waitlist/changes.json +36 -0
- package/packages/bundled-features/src/waitlist/feature.ts +1 -0
- package/packages/bundled-features/src/waitlist/handlers/export-purge.write.ts +2 -1
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +28 -9
- package/packages/bundled-features/src/waitlist/import.ts +28 -3
- package/packages/bundled-features/src/waitlist/legacy-migrate.ts +24 -4
- package/packages/bundled-features/src/waitlist/link-on-signup.ts +20 -10
- package/packages/bundled-features/src/waitlist/options.ts +5 -1
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +1 -1
- package/samples/apps/styleguide/src/app/client.tsx +2 -0
- package/samples/apps/styleguide/src/app/seed.ts +6 -4
- package/samples/apps/styleguide/src/app/server.ts +4 -1
- package/samples/apps/styleguide/src/features/widgets/feature.ts +15 -4
- package/samples/apps/styleguide/src/features/widgets/i18n.ts +11 -0
- package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +24 -0
- package/samples/apps/styleguide/src/features/workboard/feature.ts +185 -0
- package/samples/apps/styleguide/src/features/workboard/i18n.ts +27 -0
- package/samples/apps/styleguide/src/features/workboard/seed.ts +121 -0
- package/samples/apps/styleguide/src/features/workboard/web.ts +13 -0
- package/samples/apps/ui-walkthrough/src/features/rental/i18n.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +9 -0
- package/samples/apps/use-all-bundled/bin/main.ts +2 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +5 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +90 -15
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +16 -6
- package/samples/apps/use-all-bundled/feature-manifest.json +136 -3
- package/samples/apps/use-all-bundled/kumiko/schema.ts +2 -0
- package/samples/apps/use-all-bundled/kumiko.config.ts +2 -0
- package/samples/apps/use-all-bundled/package.json +2 -1
- package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +2 -0
- package/samples/apps/use-all-bundled/src/app/notes-feature.ts +2 -0
- package/samples/apps/use-all-bundled/src/bundled-lists.ts +15 -0
- package/samples/apps/use-all-bundled/src/run-config.ts +9 -3
- package/samples/recipes/auth-mfa/README.md +5 -0
- package/samples/recipes/auth-mfa/package.json +5 -2
- package/samples/recipes/auth-mfa/src/web/confirmation-code-step.tsx +49 -0
- package/samples/recipes/auth-mfa/tsconfig.json +8 -0
- package/samples/recipes/env-composition/README.md +3 -1
- package/samples/recipes/env-composition/src/env.ts +2 -0
- package/samples/recipes/golden-path/src/feature.ts +2 -1
- package/samples/recipes/legal-pages/README.md +1 -1
- package/samples/recipes/lifecycle-hooks/README.md +4 -0
- package/samples/recipes/lifecycle-hooks/src/feature.ts +26 -1
- package/samples/recipes/list-table-options/README.md +50 -0
- package/samples/recipes/list-table-options/package.json +11 -0
- package/samples/recipes/list-table-options/src/feature.ts +221 -0
- package/samples/recipes/pdf-audit-export/src/feature.ts +2 -1
- package/samples/recipes/rate-limiting/README.md +18 -0
- package/samples/recipes/rate-limiting/src/feature.ts +12 -0
- package/samples/recipes/state-machine/src/events.ts +3 -2
- package/samples/recipes/tenant-isolation/README.md +1 -0
- package/samples/recipes/tenant-isolation/src/feature.ts +36 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/stored-user-email.ts +0 -12
package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts
CHANGED
|
@@ -14,11 +14,9 @@ import * as z from "zod";
|
|
|
14
14
|
import { subscriptionAggregateId } from "../aggregate-id.ts";
|
|
15
15
|
import {
|
|
16
16
|
CONTRACT_TERMINATION_DECLARED_EVENT_QN,
|
|
17
|
-
CONTRACT_TERMINATION_REQUESTED_EVENT_QN,
|
|
18
17
|
CONTRACT_TERMINATION_REVOKED_EVENT_QN,
|
|
19
18
|
type ContractTerminationRevokedPayload,
|
|
20
19
|
contractTerminationDeclaredPayloadSchema,
|
|
21
|
-
contractTerminationRequestedPayloadSchema,
|
|
22
20
|
SUBSCRIPTION_AGGREGATE_TYPE,
|
|
23
21
|
} from "../events.ts";
|
|
24
22
|
import { getSubscriptionForTenant } from "../get-subscription-for-tenant.ts";
|
|
@@ -26,7 +24,7 @@ import type { ConsumerProtectionOptions, ResolvedBillingFoundationOptions } from
|
|
|
26
24
|
import { SubscriptionFoundationHandlers } from "../web/constants.ts";
|
|
27
25
|
import type { ConsentLocale } from "./consent-text.ts";
|
|
28
26
|
import { notifyOperator } from "./termination-notify.ts";
|
|
29
|
-
import { hasRevocation, resumeAtProvider } from "./termination-record.ts";
|
|
27
|
+
import { findRecordedTermination, hasRevocation, resumeAtProvider } from "./termination-record.ts";
|
|
30
28
|
import type { OperatorNoticeReason } from "./termination-texts.ts";
|
|
31
29
|
|
|
32
30
|
export const REVOCATION_PATHS: Readonly<Record<ConsentLocale, string>> = {
|
|
@@ -152,10 +150,7 @@ export function createRecordContractTerminationRevocationHandler(
|
|
|
152
150
|
}
|
|
153
151
|
if (hasRevocation(stream.events, payload.requestId)) return outcome("already-revoked");
|
|
154
152
|
|
|
155
|
-
const requested = stream.events
|
|
156
|
-
.filter((e) => e.type === CONTRACT_TERMINATION_REQUESTED_EVENT_QN)
|
|
157
|
-
.map((e) => contractTerminationRequestedPayloadSchema.safeParse(e.payload).data)
|
|
158
|
-
.find((candidate) => candidate?.requestId === payload.requestId);
|
|
153
|
+
const requested = findRecordedTermination(stream.events, payload.requestId);
|
|
159
154
|
const subscription = await getSubscriptionForTenant(ctx, tenantId);
|
|
160
155
|
const effectiveAtIso = requested?.effectiveAtIso ?? subscription?.currentPeriodEnd.toString();
|
|
161
156
|
const now = options.now();
|
|
@@ -175,6 +170,17 @@ export function createRecordContractTerminationRevocationHandler(
|
|
|
175
170
|
aggregateId: subscriptionAggregateId(tenantId),
|
|
176
171
|
aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
|
|
177
172
|
});
|
|
173
|
+
// A cancel recorded between the first fetch and now was never resumed above.
|
|
174
|
+
const requestedMeanwhile =
|
|
175
|
+
requested === undefined
|
|
176
|
+
? findRecordedTermination(streamAfterProviderCall.events, payload.requestId)
|
|
177
|
+
: undefined;
|
|
178
|
+
if (requestedMeanwhile?.providerCancel === "period-end") {
|
|
179
|
+
reasons.push(
|
|
180
|
+
...(await resumeAtProvider(ctx, await getSubscriptionForTenant(ctx, tenantId))),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
const providerCancel = (requested ?? requestedMeanwhile)?.providerCancel;
|
|
178
184
|
const revoked: ContractTerminationRevokedPayload = {
|
|
179
185
|
requestId: payload.requestId,
|
|
180
186
|
revokedAtIso: now.toString(),
|
|
@@ -194,7 +200,7 @@ export function createRecordContractTerminationRevocationHandler(
|
|
|
194
200
|
},
|
|
195
201
|
reasons,
|
|
196
202
|
tenantId,
|
|
197
|
-
...(
|
|
203
|
+
...(providerCancel !== undefined && { providerCancel }),
|
|
198
204
|
});
|
|
199
205
|
return outcome("revoked");
|
|
200
206
|
},
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts
CHANGED
|
@@ -91,6 +91,7 @@ export const OPERATOR_NOTICE_REASONS = [
|
|
|
91
91
|
"recording_failed",
|
|
92
92
|
"withdrawal_period_expired",
|
|
93
93
|
"receipt_rate_limited",
|
|
94
|
+
"receipt_not_sent",
|
|
94
95
|
"revoked",
|
|
95
96
|
"revocation_resume_manual",
|
|
96
97
|
"revocation_resume_failed",
|
|
@@ -145,6 +146,8 @@ export const TERMINATION_TEXTS: Readonly<Record<ConsentLocale, TerminationTexts>
|
|
|
145
146
|
"Widerruf nach Ablauf der Widerrufsfrist: zum Periodenende gekündigt, Anspruch prüfen",
|
|
146
147
|
receipt_rate_limited:
|
|
147
148
|
"Keine Eingangsbestätigung versendet: Limit pro Empfängeradresse erreicht",
|
|
149
|
+
receipt_not_sent:
|
|
150
|
+
"Keine Eingangsbestätigung versendet: Limit-Prüfung nicht verfügbar (Redis-Fehler oder fehlende Konfiguration)",
|
|
148
151
|
revoked:
|
|
149
152
|
"Die Erklärung wurde vom Postfachinhaber über „Diese Kündigung war ich nicht“ zurückgenommen",
|
|
150
153
|
revocation_resume_manual:
|
|
@@ -256,6 +259,8 @@ export const TERMINATION_TEXTS: Readonly<Record<ConsentLocale, TerminationTexts>
|
|
|
256
259
|
withdrawal_period_expired:
|
|
257
260
|
"Withdrawal after the withdrawal period: cancelled at period end, review the claim",
|
|
258
261
|
receipt_rate_limited: "No receipt mail sent: the per-recipient limit was reached",
|
|
262
|
+
receipt_not_sent:
|
|
263
|
+
"No receipt mail sent: the limit check was unavailable (limiter error or missing configuration)",
|
|
259
264
|
revoked:
|
|
260
265
|
'The declaration was taken back by the mailbox owner ("I did not request this cancellation")',
|
|
261
266
|
revocation_resume_manual:
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts
CHANGED
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
2
|
-
import {
|
|
2
|
+
import { SUBSCRIPTION_CREATED_EVENT_QN } from "../events.ts";
|
|
3
3
|
|
|
4
4
|
// § 355 BGB: the withdrawal period is 14 days from conclusion of the contract.
|
|
5
5
|
export const WITHDRAWAL_PERIOD_DAYS = 14;
|
|
6
6
|
const HOURS_PER_DAY = 24;
|
|
7
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
8
|
type PeriodStartEvent = { readonly type: string; readonly createdAt: Temporal.Instant };
|
|
14
9
|
|
|
15
|
-
/** The period runs from the latest
|
|
16
|
-
*
|
|
10
|
+
/** The period runs from the latest subscription start on the stream. A checkout
|
|
11
|
+
* consent alone never restarts it: it is written before the buyer reaches the
|
|
12
|
+
* provider, so an abandoned plan-change checkout would otherwise reopen the
|
|
13
|
+
* period of a long-concluded contract. A stream without a subscription start
|
|
14
|
+
* has no provable start and counts as expired. */
|
|
17
15
|
export function isWithinWithdrawalPeriod(
|
|
18
16
|
events: readonly PeriodStartEvent[],
|
|
19
17
|
now: Temporal.Instant,
|
|
20
18
|
): boolean {
|
|
21
19
|
let latestStart: Temporal.Instant | undefined;
|
|
22
20
|
for (const event of events) {
|
|
23
|
-
if (
|
|
21
|
+
if (event.type !== SUBSCRIPTION_CREATED_EVENT_QN) continue;
|
|
24
22
|
if (
|
|
25
23
|
latestStart === undefined ||
|
|
26
24
|
event.createdAt.epochMilliseconds > latestStart.epochMilliseconds
|
|
@@ -28,7 +26,7 @@ export function isWithinWithdrawalPeriod(
|
|
|
28
26
|
latestStart = event.createdAt;
|
|
29
27
|
}
|
|
30
28
|
}
|
|
31
|
-
// skip: no
|
|
29
|
+
// skip: no subscription-created event, nothing proves a running period
|
|
32
30
|
if (latestStart === undefined) return false;
|
|
33
31
|
const periodEnd = latestStart.add({ hours: WITHDRAWAL_PERIOD_DAYS * HOURS_PER_DAY });
|
|
34
32
|
return now.epochMilliseconds <= periodEnd.epochMilliseconds;
|
|
@@ -141,6 +141,7 @@ export const checkoutConsentRecordedPayloadSchema = z.object({
|
|
|
141
141
|
termsTemplateVersion: z.number(),
|
|
142
142
|
locale: z.string().min(2).max(35),
|
|
143
143
|
actorUserId: z.string().min(1),
|
|
144
|
+
providerName: z.string().min(1).max(100).optional(),
|
|
144
145
|
itemLabelKey: z.string().min(1).max(200).optional(),
|
|
145
146
|
itemLabelParams: z.record(z.string(), z.union([z.string().max(100), z.number()])).optional(),
|
|
146
147
|
});
|
|
@@ -79,6 +79,7 @@ import { createBillingPlansQuery } from "./handlers/billing-plans.query.ts";
|
|
|
79
79
|
import { createCheckoutSessionHandler } from "./handlers/create-checkout-session.write.ts";
|
|
80
80
|
import { createPortalSessionHandler } from "./handlers/create-portal-session.write.ts";
|
|
81
81
|
import { listSubscriptionsQuery } from "./handlers/list-subscriptions.query.ts";
|
|
82
|
+
import { createPlanSwitchPreviewQuery } from "./handlers/plan-switch-preview.query.ts";
|
|
82
83
|
import { createProcessEventHandler } from "./handlers/process-event.write.ts";
|
|
83
84
|
import { processPaymentEventHandler } from "./handlers/process-payment-event.write.ts";
|
|
84
85
|
import { createStartPlanCheckoutHandler } from "./handlers/start-plan-checkout.write.ts";
|
|
@@ -137,7 +138,7 @@ export function createBillingFoundationFeature<TTier extends string = string>(
|
|
|
137
138
|
BILLING_FOUNDATION_FEATURE,
|
|
138
139
|
(r) => {
|
|
139
140
|
r.describe(
|
|
140
|
-
"Plugin host for subscription billing
|
|
141
|
+
"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.",
|
|
141
142
|
);
|
|
142
143
|
r.uiHints({
|
|
143
144
|
displayLabel: "Billing · Foundation",
|
|
@@ -239,6 +240,7 @@ export function createBillingFoundationFeature<TTier extends string = string>(
|
|
|
239
240
|
if (widened.catalog) {
|
|
240
241
|
const { catalog } = widened;
|
|
241
242
|
r.queryHandler(createBillingPlansQuery(widened, catalog));
|
|
243
|
+
r.queryHandler(createPlanSwitchPreviewQuery(catalog));
|
|
242
244
|
r.writeHandler(createStartPlanCheckoutHandler(widened, catalog));
|
|
243
245
|
r.writeHandler(createSwitchPlanHandler(widened, catalog));
|
|
244
246
|
r.screen(createBillingPlansScreen(catalog.viewRoles));
|
|
@@ -2,18 +2,29 @@
|
|
|
2
2
|
// benefits, the caller's current tier and per-plan action. Only registered
|
|
3
3
|
// when `createBillingFoundationFeature` gets a `catalog`.
|
|
4
4
|
|
|
5
|
-
import type { QueryHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
|
+
import type { HandlerContext, QueryHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
6
6
|
import * as z from "zod";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
filterOfferedProviders,
|
|
9
|
+
listCatalogProviders,
|
|
10
|
+
type ResolvedProvider,
|
|
11
|
+
} from "../checkout-core.ts";
|
|
8
12
|
import {
|
|
9
13
|
type CONSENT_LOCALES,
|
|
10
14
|
CONSENT_TEXTS,
|
|
11
15
|
consentTextVersion,
|
|
12
16
|
} from "../consumer-protection/consent-text.ts";
|
|
13
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
type BillingPlansBase,
|
|
19
|
+
buildProviderPlanViews,
|
|
20
|
+
loadBillingPlansBase,
|
|
21
|
+
type ProviderPlanViews,
|
|
22
|
+
toBillingPlansResult,
|
|
23
|
+
} from "../plan-catalog.ts";
|
|
14
24
|
import {
|
|
15
25
|
type BillingPlanCatalog,
|
|
16
26
|
type BillingPlansResult,
|
|
27
|
+
type BillingProviderView,
|
|
17
28
|
type ConsumerProtectionOptions,
|
|
18
29
|
isLegalLinkSet,
|
|
19
30
|
type LegalLinkSet,
|
|
@@ -29,12 +40,12 @@ export function createBillingPlansQuery(
|
|
|
29
40
|
return {
|
|
30
41
|
name: "billing-plans",
|
|
31
42
|
description:
|
|
32
|
-
"Lists the purchasable plans with live price, benefits, the caller's current tier and which action (checkout | switch | current | unavailable | paymentPending) each plan offers, plus the optional price-less contact card.",
|
|
43
|
+
"Lists the purchasable plans with live price, benefits, the caller's current tier and which action (checkout | switch | current | unavailable | paymentPending) each plan offers, plus the optional price-less contact card. With several live providers it also lists each provider's own plans under `providers`.",
|
|
33
44
|
schema: billingPlansSchema,
|
|
34
45
|
access: { roles: catalog.viewRoles },
|
|
35
46
|
handler: async (_query, ctx): Promise<BillingPlansResult> => {
|
|
36
|
-
const
|
|
37
|
-
const result = await
|
|
47
|
+
const base = await loadBillingPlansBase(ctx, catalog, options.now);
|
|
48
|
+
const result = await resolveProviderPlans(ctx, options, catalog, base);
|
|
38
49
|
const { consumerProtection } = options;
|
|
39
50
|
if (!consumerProtection) return result;
|
|
40
51
|
return {
|
|
@@ -51,6 +62,60 @@ export function createBillingPlansQuery(
|
|
|
51
62
|
};
|
|
52
63
|
}
|
|
53
64
|
|
|
65
|
+
// A tenant with a blocking subscription sees only the provider holding it
|
|
66
|
+
// (a second provider's prices would be a checkout it cannot start); otherwise
|
|
67
|
+
// every live provider is offered.
|
|
68
|
+
async function resolveProviderPlans(
|
|
69
|
+
ctx: HandlerContext,
|
|
70
|
+
options: ResolvedBillingFoundationOptions,
|
|
71
|
+
catalog: BillingPlanCatalog,
|
|
72
|
+
base: BillingPlansBase,
|
|
73
|
+
): Promise<BillingPlansResult> {
|
|
74
|
+
const candidates = listCatalogProviders(ctx, catalog);
|
|
75
|
+
if (base.blockingSubscription && base.subscriptionView) {
|
|
76
|
+
const holder = candidates.find((c) => c.name === base.subscriptionView?.providerName);
|
|
77
|
+
return toBillingPlansResult(
|
|
78
|
+
catalog,
|
|
79
|
+
base,
|
|
80
|
+
await buildProviderPlanViews(ctx, holder?.plugin ?? null, catalog, base),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const offered = await filterOfferedProviders(ctx, candidates);
|
|
85
|
+
const first = offered[0] ?? candidates[0] ?? null;
|
|
86
|
+
const firstViews = await buildProviderPlanViews(ctx, first?.plugin ?? null, catalog, base);
|
|
87
|
+
const result = toBillingPlansResult(catalog, base, firstViews);
|
|
88
|
+
|
|
89
|
+
const providerTexts = options.consumerProtection?.providerTexts;
|
|
90
|
+
const hasNote = first !== null && providerTexts?.[first.name]?.checkoutNote !== undefined;
|
|
91
|
+
if (offered.length <= 1 && !(offered.length === 1 && hasNote)) return result;
|
|
92
|
+
|
|
93
|
+
const providers = await Promise.all(
|
|
94
|
+
offered.map((provider, index) =>
|
|
95
|
+
providerView(ctx, catalog, base, provider, providerTexts, index === 0 ? firstViews : null),
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
return { ...result, providers };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function providerView(
|
|
102
|
+
ctx: HandlerContext,
|
|
103
|
+
catalog: BillingPlanCatalog,
|
|
104
|
+
base: BillingPlansBase,
|
|
105
|
+
provider: ResolvedProvider,
|
|
106
|
+
providerTexts: ConsumerProtectionOptions["providerTexts"],
|
|
107
|
+
known: ProviderPlanViews | null,
|
|
108
|
+
): Promise<BillingProviderView> {
|
|
109
|
+
const views = known ?? (await buildProviderPlanViews(ctx, provider.plugin, catalog, base));
|
|
110
|
+
const note = providerTexts?.[provider.name]?.checkoutNote;
|
|
111
|
+
return {
|
|
112
|
+
name: provider.name,
|
|
113
|
+
displayName: provider.plugin.displayName ?? provider.name,
|
|
114
|
+
...(note !== undefined && { note }),
|
|
115
|
+
plans: views.plans,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
54
119
|
function legalLinksPerLocale(
|
|
55
120
|
legalLinks: ConsumerProtectionOptions["legalLinks"],
|
|
56
121
|
): Readonly<Record<(typeof CONSENT_LOCALES)[number], LegalLinkSet>> {
|
package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts
CHANGED
|
@@ -121,6 +121,7 @@ export function createCheckoutSessionHandler(
|
|
|
121
121
|
tier: mode === "subscription" ? (plugin.priceToTier?.[payload.priceId] ?? null) : null,
|
|
122
122
|
priceId: payload.priceId,
|
|
123
123
|
price,
|
|
124
|
+
providerName: payload.providerName,
|
|
124
125
|
...(orderItem && { orderItem }),
|
|
125
126
|
});
|
|
126
127
|
|
package/packages/bundled-features/src/billing-foundation/handlers/plan-switch-preview.query.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// plan-switch-preview — what switching to a tier would cost right now, for
|
|
2
|
+
// providers that switch plans in-app (previewPlanSwitch + applyPlanSwitch).
|
|
3
|
+
// Only registered when `createBillingFoundationFeature` gets a `catalog`.
|
|
4
|
+
|
|
5
|
+
import type { QueryHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
+
import { UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
|
|
7
|
+
import * as z from "zod";
|
|
8
|
+
import { isNonEmptyStringArray } from "../checkout-core.ts";
|
|
9
|
+
import { purchaseRolesOf } from "../plan-catalog.ts";
|
|
10
|
+
import { preparePlanSwitch } from "../plan-switch.ts";
|
|
11
|
+
import type { BillingPlanCatalog, PlanSwitchPreview } from "../types.ts";
|
|
12
|
+
|
|
13
|
+
export type PlanSwitchPreviewResult = {
|
|
14
|
+
readonly preview: PlanSwitchPreview;
|
|
15
|
+
readonly target: {
|
|
16
|
+
readonly tier: string;
|
|
17
|
+
readonly unitAmount: number;
|
|
18
|
+
readonly currency: string;
|
|
19
|
+
readonly interval: "day" | "week" | "month" | "year" | null;
|
|
20
|
+
readonly intervalCount: number | null;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function createPlanSwitchPreviewQuery(catalog: BillingPlanCatalog): QueryHandlerDef {
|
|
25
|
+
const plans = catalog.plans;
|
|
26
|
+
if (!isNonEmptyStringArray(plans)) {
|
|
27
|
+
throw new Error("plan-switch-preview: catalog.plans must not be empty");
|
|
28
|
+
}
|
|
29
|
+
const schema = z.object({ tier: z.enum(plans) }).strict();
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
name: "plan-switch-preview",
|
|
33
|
+
description:
|
|
34
|
+
"Previews what switching the existing subscription to another plan tier costs right now (amount due, tax, credit, effective date) plus the target plan's recurring price. Only for providers that switch plans in-app.",
|
|
35
|
+
schema,
|
|
36
|
+
access: { roles: purchaseRolesOf(catalog) },
|
|
37
|
+
handler: async (query, ctx): Promise<PlanSwitchPreviewResult> => {
|
|
38
|
+
// @cast-boundary engine-payload — dispatcher-zod-validated payload
|
|
39
|
+
const { tier } = query.payload as { tier: string };
|
|
40
|
+
const prepared = await preparePlanSwitch(ctx, catalog, tier, "plan-switch-preview");
|
|
41
|
+
const { plugin } = prepared.provider;
|
|
42
|
+
if (!plugin.previewPlanSwitch || !plugin.applyPlanSwitch) {
|
|
43
|
+
throw new UnprocessableError("plan_switch_not_supported", {
|
|
44
|
+
i18nKey: "billing-foundation.errors.planSwitchNotSupported",
|
|
45
|
+
message: `subscription-foundation: provider "${prepared.provider.name}" switches plans on a hosted page and has no in-app preview`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const preview = await plugin.previewPlanSwitch(ctx, {
|
|
49
|
+
providerSubscriptionId: prepared.subscription.providerSubscriptionId,
|
|
50
|
+
targetPriceId: prepared.target.priceId,
|
|
51
|
+
});
|
|
52
|
+
const { price } = prepared.target;
|
|
53
|
+
return {
|
|
54
|
+
preview,
|
|
55
|
+
target: {
|
|
56
|
+
tier,
|
|
57
|
+
unitAmount: price.unitAmount ?? 0,
|
|
58
|
+
currency: price.currency,
|
|
59
|
+
interval: price.interval,
|
|
60
|
+
intervalCount: price.intervalCount,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -93,7 +93,7 @@ const STATE_EVENT_TYPES: ReadonlySet<string> = new Set([
|
|
|
93
93
|
SubscriptionEventTypes.canceled,
|
|
94
94
|
]);
|
|
95
95
|
|
|
96
|
-
function isLateStateEvent(
|
|
96
|
+
export function isLateStateEvent(
|
|
97
97
|
storedEvents: readonly {
|
|
98
98
|
readonly payload: unknown;
|
|
99
99
|
readonly metadata: { readonly headers?: Record<string, unknown> };
|
|
@@ -111,7 +111,14 @@ function isLateStateEvent(
|
|
|
111
111
|
// A stored value that does not parse can never prove a newer event; skip it instead of
|
|
112
112
|
// failing every later delivery of this tenant's stream.
|
|
113
113
|
try {
|
|
114
|
-
|
|
114
|
+
const order = Temporal.Instant.compare(Temporal.Instant.from(storedAt), incoming);
|
|
115
|
+
if (order !== 0) return order > 0;
|
|
116
|
+
// Provider timestamps have second resolution: on a tie a stored cancellation wins over
|
|
117
|
+
// a non-terminal state, so a reordered same-second `updated` cannot revive a canceled one.
|
|
118
|
+
return (
|
|
119
|
+
propertyOf(stored.payload, "status") === SubscriptionStatuses.canceled &&
|
|
120
|
+
payload.status !== SubscriptionStatuses.canceled
|
|
121
|
+
);
|
|
115
122
|
} catch {
|
|
116
123
|
return false;
|
|
117
124
|
}
|
package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts
CHANGED
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
// the tier's live price server-side and opens a hosted checkout for it.
|
|
4
4
|
// Only registered when `createBillingFoundationFeature` gets a `catalog`.
|
|
5
5
|
|
|
6
|
-
import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
-
import { UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
|
|
6
|
+
import type { HandlerContext, WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
import { FeatureDisabledError, UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
|
|
8
8
|
import * as z from "zod";
|
|
9
9
|
import {
|
|
10
10
|
assertBillingEnabled,
|
|
11
11
|
assertNoActiveSubscription,
|
|
12
12
|
isNonEmptyStringArray,
|
|
13
13
|
joinBaseUrl,
|
|
14
|
+
listOfferedCatalogProviders,
|
|
14
15
|
openCheckout,
|
|
15
|
-
|
|
16
|
+
type ResolvedProvider,
|
|
16
17
|
} from "../checkout-core.ts";
|
|
17
18
|
import {
|
|
18
19
|
type ConsentPayload,
|
|
@@ -23,6 +24,36 @@ import {
|
|
|
23
24
|
} from "../consumer-protection/consent-gate.ts";
|
|
24
25
|
import { purchaseRolesOf, resolvePlanPrices } from "../plan-catalog.ts";
|
|
25
26
|
import type { BillingPlanCatalog, ResolvedBillingFoundationOptions } from "../types.ts";
|
|
27
|
+
import { BILLING_FOUNDATION_FEATURE } from "../web/constants.ts";
|
|
28
|
+
|
|
29
|
+
/** The provider this checkout goes to: the named one if it is live, the only
|
|
30
|
+
* live one if the buyer had no choice, otherwise a client error. */
|
|
31
|
+
async function pickCheckoutProvider(
|
|
32
|
+
ctx: HandlerContext,
|
|
33
|
+
catalog: BillingPlanCatalog,
|
|
34
|
+
requestedName: string | undefined,
|
|
35
|
+
): Promise<ResolvedProvider> {
|
|
36
|
+
const offered = await listOfferedCatalogProviders(ctx, catalog);
|
|
37
|
+
if (requestedName !== undefined) {
|
|
38
|
+
const chosen = offered.find((provider) => provider.name === requestedName);
|
|
39
|
+
if (!chosen) {
|
|
40
|
+
throw new UnprocessableError("provider_not_offered", {
|
|
41
|
+
i18nKey: "billing-foundation.errors.providerNotOffered",
|
|
42
|
+
message: `subscription-foundation: provider "${requestedName}" is not offered by this catalog right now`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return chosen;
|
|
46
|
+
}
|
|
47
|
+
const [only, second] = offered;
|
|
48
|
+
if (!only) throw new FeatureDisabledError(BILLING_FOUNDATION_FEATURE, "start-plan-checkout");
|
|
49
|
+
if (second) {
|
|
50
|
+
throw new UnprocessableError("provider_required", {
|
|
51
|
+
i18nKey: "billing-foundation.errors.providerRequired",
|
|
52
|
+
message: "subscription-foundation: several providers are offered, name one as providerName",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return only;
|
|
56
|
+
}
|
|
26
57
|
|
|
27
58
|
export function createStartPlanCheckoutHandler(
|
|
28
59
|
options: ResolvedBillingFoundationOptions,
|
|
@@ -33,7 +64,7 @@ export function createStartPlanCheckoutHandler(
|
|
|
33
64
|
throw new Error("start-plan-checkout: catalog.plans must not be empty");
|
|
34
65
|
}
|
|
35
66
|
const consumerProtection = options.consumerProtection;
|
|
36
|
-
const tierSchema = z.object({ tier: z.enum(plans) });
|
|
67
|
+
const tierSchema = z.object({ tier: z.enum(plans), providerName: z.string().min(1).optional() });
|
|
37
68
|
const schema = (
|
|
38
69
|
consumerProtection
|
|
39
70
|
? tierSchema.extend({ consent: consentPayloadSchema.optional() })
|
|
@@ -43,16 +74,20 @@ export function createStartPlanCheckoutHandler(
|
|
|
43
74
|
return {
|
|
44
75
|
name: "start-plan-checkout",
|
|
45
76
|
description:
|
|
46
|
-
"Starts a hosted checkout for a plan tier when the tenant has no active subscription; the server picks price and redirect URLs.",
|
|
77
|
+
"Starts a hosted checkout for a plan tier when the tenant has no active subscription; the server picks price and redirect URLs. Name the provider with providerName when the catalog offers several.",
|
|
47
78
|
schema,
|
|
48
79
|
access: { roles: purchaseRolesOf(catalog) },
|
|
49
80
|
handler: async (event, ctx) => {
|
|
50
81
|
// @cast-boundary engine-payload — dispatcher-zod-validated payload
|
|
51
|
-
const payload = event.payload as {
|
|
52
|
-
|
|
82
|
+
const payload = event.payload as {
|
|
83
|
+
tier: string;
|
|
84
|
+
providerName?: string;
|
|
85
|
+
consent?: ConsentPayload;
|
|
86
|
+
};
|
|
87
|
+
const { name: providerName, plugin } = await pickCheckoutProvider(
|
|
53
88
|
ctx,
|
|
54
89
|
catalog,
|
|
55
|
-
|
|
90
|
+
payload.providerName,
|
|
56
91
|
);
|
|
57
92
|
await assertBillingEnabled(ctx, plugin, "start-plan-checkout");
|
|
58
93
|
|
|
@@ -98,6 +133,7 @@ export function createStartPlanCheckoutHandler(
|
|
|
98
133
|
tier: payload.tier,
|
|
99
134
|
priceId: resolved.priceId,
|
|
100
135
|
price: resolved.price,
|
|
136
|
+
providerName,
|
|
101
137
|
});
|
|
102
138
|
}
|
|
103
139
|
|