@cosmicdrift/kumiko-samples 0.387.0 → 0.388.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 +6 -0
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +9 -9
- package/packages/bundled-features/src/audit/changes.json +12 -0
- package/packages/bundled-features/src/audit/handlers/audit-events.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/changes.json +6 -0
- 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/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 +5 -1
- package/packages/bundled-features/src/cap-counter/changes.json +18 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +2 -1
- package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +8 -1
- 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 +12 -0
- package/packages/bundled-features/src/config/handlers/cascade.query.ts +4 -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 +6 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +10 -9
- package/packages/bundled-features/src/delivery/changes.json +30 -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/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/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/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 +6 -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/inbound-mail-foundation/changes.json +18 -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 +14 -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/legal-pages/README.md +1 -1
- 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/personal-access-tokens/feature.ts +4 -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/sessions/feature.ts +7 -2
- 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/step-dispatcher/changes.json +6 -0
- package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -0
- 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/template-resolver/api.ts +31 -4
- package/packages/bundled-features/src/template-resolver/changes.json +48 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +1 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +2 -2
- 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/list.query.ts +18 -19
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +15 -5
- 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 +32 -14
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +22 -4
- 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/index.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/mail-layout-constants.ts +8 -0
- package/packages/bundled-features/src/tenant/changes.json +6 -0
- package/packages/bundled-features/src/tenant/handlers/member-directory.query.ts +9 -3
- package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +7 -4
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +6 -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/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-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/waitlist/changes.json +18 -0
- package/packages/bundled-features/src/waitlist/handlers/export-purge.write.ts +2 -1
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +5 -5
- package/packages/bundled-features/src/waitlist/import.ts +5 -0
- package/packages/bundled-features/src/waitlist/legacy-migrate.ts +22 -3
- package/packages/bundled-features/src/waitlist/link-on-signup.ts +20 -10
- package/packages/bundled-features/src/waitlist/options.ts +3 -0
- 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/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 +120 -3
- 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/legal-pages/README.md +1 -1
- package/samples/recipes/rate-limiting/README.md +18 -0
- package/samples/recipes/rate-limiting/src/feature.ts +12 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/stored-user-email.ts +0 -12
|
@@ -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
|
|
|
@@ -1,109 +1,99 @@
|
|
|
1
1
|
// switch-plan — a tenant-admin/purchase-role switches an existing,
|
|
2
|
-
// non-terminal subscription to a different plan tier
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// non-terminal subscription to a different plan tier. A provider with a hosted
|
|
3
|
+
// switch session answers `{ url }`; a provider that switches in-app (preview +
|
|
4
|
+
// apply) applies the switch right here and answers `{ applied: true }`.
|
|
5
|
+
// Only registered when `createBillingFoundationFeature` gets a `catalog`.
|
|
5
6
|
|
|
6
|
-
import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
-
import {
|
|
7
|
+
import type { HandlerContext, WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
8
|
+
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
8
9
|
import * as z from "zod";
|
|
10
|
+
import { assertRedirectOrigins, isNonEmptyStringArray, joinBaseUrl } from "../checkout-core.ts";
|
|
9
11
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from "../
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import type { BillingFoundationOptions, BillingPlanCatalog } from "../types.ts";
|
|
19
|
-
import { isSwitchableSubscriptionStatus } from "../web/constants.ts";
|
|
12
|
+
type ConsentPayload,
|
|
13
|
+
consentPayloadSchema,
|
|
14
|
+
prepareConsent,
|
|
15
|
+
recordConsent,
|
|
16
|
+
} from "../consumer-protection/consent-gate.ts";
|
|
17
|
+
import { purchaseRolesOf } from "../plan-catalog.ts";
|
|
18
|
+
import { type PreparedPlanSwitch, preparePlanSwitch } from "../plan-switch.ts";
|
|
19
|
+
import type { BillingPlanCatalog, ResolvedBillingFoundationOptions } from "../types.ts";
|
|
20
20
|
|
|
21
21
|
export function createSwitchPlanHandler(
|
|
22
|
-
options:
|
|
22
|
+
options: ResolvedBillingFoundationOptions,
|
|
23
23
|
catalog: BillingPlanCatalog,
|
|
24
24
|
): WriteHandlerDef {
|
|
25
25
|
const plans = catalog.plans;
|
|
26
26
|
if (!isNonEmptyStringArray(plans)) {
|
|
27
27
|
throw new Error("switch-plan: catalog.plans must not be empty");
|
|
28
28
|
}
|
|
29
|
-
const
|
|
29
|
+
const consumerProtection = options.consumerProtection;
|
|
30
|
+
const tierSchema = z.object({ tier: z.enum(plans) });
|
|
31
|
+
const schema = (
|
|
32
|
+
consumerProtection
|
|
33
|
+
? tierSchema.extend({ consent: consentPayloadSchema.optional() })
|
|
34
|
+
: tierSchema
|
|
35
|
+
).strict();
|
|
30
36
|
|
|
31
37
|
return {
|
|
32
38
|
name: "switch-plan",
|
|
33
39
|
description:
|
|
34
|
-
"
|
|
40
|
+
"Switches the existing subscription to another plan tier: opens the provider's confirmation page (answers { url }), or applies the switch in-app for providers that preview and apply themselves (answers { applied: true }; send consent when consumer protection is configured).",
|
|
35
41
|
schema,
|
|
36
42
|
access: { roles: purchaseRolesOf(catalog) },
|
|
37
43
|
handler: async (event, ctx) => {
|
|
38
44
|
// @cast-boundary engine-payload — dispatcher-zod-validated payload
|
|
39
|
-
const payload = event.payload as { tier: string };
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
catalog,
|
|
43
|
-
"switch-plan",
|
|
44
|
-
);
|
|
45
|
-
await assertBillingEnabled(ctx, plugin, "switch-plan");
|
|
45
|
+
const payload = event.payload as { tier: string; consent?: ConsentPayload };
|
|
46
|
+
const prepared = await preparePlanSwitch(ctx, catalog, payload.tier, "switch-plan");
|
|
47
|
+
const { plugin } = prepared.provider;
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
throw new ConflictError({
|
|
57
|
-
i18nKey: "billing-foundation.errors.cancellationScheduled",
|
|
58
|
-
message:
|
|
59
|
-
"subscription has a scheduled cancellation; reactivate it via billing-foundation:write:create-portal-session before switching plans",
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
if (payload.tier === sub.tier) {
|
|
63
|
-
throw new ConflictError({
|
|
64
|
-
i18nKey: "billing-foundation.errors.alreadyOnPlan",
|
|
65
|
-
message: `tenant is already on plan "${payload.tier}"`,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
// providerSubscriptionId only exists at the subscription's own provider; after a
|
|
69
|
-
// provider migration the catalog provider can differ, which is not a plan switch.
|
|
70
|
-
if (sub.providerName !== catalogProviderName) {
|
|
71
|
-
throw new ConflictError({
|
|
72
|
-
i18nKey: "billing-foundation.errors.providerMismatch",
|
|
73
|
-
message: `tenant's active subscription is on provider "${sub.providerName}", the catalog resolves provider "${catalogProviderName}" — switch-plan cannot switch across providers`,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
if (!plugin.createPlanSwitchSession) {
|
|
77
|
-
throw new UnprocessableError("plan_switch_not_supported", {
|
|
78
|
-
i18nKey: "billing-foundation.errors.planSwitchNotSupported",
|
|
79
|
-
message: `subscription-foundation: provider "${sub.providerName}" has no createPlanSwitchSession-method`,
|
|
49
|
+
if (plugin.createPlanSwitchSession) {
|
|
50
|
+
const baseUrl = options.baseUrl ?? "";
|
|
51
|
+
const returnUrl = joinBaseUrl(baseUrl, catalog.returnPath ?? catalog.successPath);
|
|
52
|
+
assertRedirectOrigins([returnUrl], options.baseUrl);
|
|
53
|
+
const result = await plugin.createPlanSwitchSession(ctx, {
|
|
54
|
+
providerSubscriptionId: prepared.subscription.providerSubscriptionId,
|
|
55
|
+
targetPriceId: prepared.target.priceId,
|
|
56
|
+
allowedPriceIds: prepared.allowedPriceIds,
|
|
57
|
+
returnUrl,
|
|
80
58
|
});
|
|
59
|
+
return { isSuccess: true as const, data: { url: result.url } };
|
|
81
60
|
}
|
|
82
61
|
|
|
83
|
-
|
|
84
|
-
const target = prices.get(payload.tier);
|
|
85
|
-
if (!target) {
|
|
86
|
-
throw new UnprocessableError("price_unavailable", {
|
|
87
|
-
i18nKey: "billing-foundation.errors.priceUnavailable",
|
|
88
|
-
message: `subscription-foundation: no live price resolved for tier "${payload.tier}"`,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
const allowedPriceIds = [...prices.values()]
|
|
92
|
-
.filter((p): p is NonNullable<typeof p> => p !== null)
|
|
93
|
-
.map((p) => p.priceId);
|
|
94
|
-
|
|
95
|
-
const baseUrl = options.baseUrl ?? "";
|
|
96
|
-
const returnUrl = joinBaseUrl(baseUrl, catalog.returnPath ?? catalog.successPath);
|
|
97
|
-
assertRedirectOrigins([returnUrl], options.baseUrl);
|
|
98
|
-
|
|
99
|
-
const result = await plugin.createPlanSwitchSession(ctx, {
|
|
100
|
-
providerSubscriptionId: sub.providerSubscriptionId,
|
|
101
|
-
targetPriceId: target.priceId,
|
|
102
|
-
allowedPriceIds,
|
|
103
|
-
returnUrl,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
return { isSuccess: true as const, data: { url: result.url } };
|
|
62
|
+
return applyInApp(prepared, payload, ctx, options);
|
|
107
63
|
},
|
|
108
64
|
};
|
|
109
65
|
}
|
|
66
|
+
|
|
67
|
+
// Consent is recorded before the provider call: the apply carries the consentId
|
|
68
|
+
// as provider metadata, and a failing apply rolls the whole handler back.
|
|
69
|
+
async function applyInApp(
|
|
70
|
+
prepared: PreparedPlanSwitch,
|
|
71
|
+
payload: { readonly tier: string; readonly consent?: ConsentPayload },
|
|
72
|
+
ctx: HandlerContext,
|
|
73
|
+
options: ResolvedBillingFoundationOptions,
|
|
74
|
+
) {
|
|
75
|
+
const { plugin, name: providerName } = prepared.provider;
|
|
76
|
+
if (!plugin.applyPlanSwitch) {
|
|
77
|
+
throw new InternalError({
|
|
78
|
+
message: `billing-foundation: provider "${providerName}" has neither createPlanSwitchSession nor applyPlanSwitch`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const consent = options.consumerProtection
|
|
82
|
+
? await prepareConsent(ctx, options.consumerProtection, payload.consent, "subscription")
|
|
83
|
+
: undefined;
|
|
84
|
+
if (consent) {
|
|
85
|
+
await recordConsent(ctx, consent, {
|
|
86
|
+
tier: payload.tier,
|
|
87
|
+
priceId: prepared.target.priceId,
|
|
88
|
+
price: prepared.target.price,
|
|
89
|
+
providerName,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
await plugin.applyPlanSwitch(ctx, {
|
|
93
|
+
providerSubscriptionId: prepared.subscription.providerSubscriptionId,
|
|
94
|
+
targetPriceId: prepared.target.priceId,
|
|
95
|
+
tenantId: ctx.user.tenantId,
|
|
96
|
+
...(consent && { consentId: consent.consentId }),
|
|
97
|
+
});
|
|
98
|
+
return { isSuccess: true as const, data: { applied: true as const } };
|
|
99
|
+
}
|
package/packages/bundled-features/src/billing-foundation/handlers/sync-subscription.write.ts
CHANGED
|
@@ -72,6 +72,18 @@ function syncProviderEventId(
|
|
|
72
72
|
return `sync:${createHash("sha256").update(hashInput).digest("hex")}`;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// The retrieval time stands in for the provider's event time, so a webhook emitted before
|
|
76
|
+
// this snapshot cannot roll the synced state back later. Provider webhook times are
|
|
77
|
+
// whole-second and come from the provider's clock, so the stamp is floored and shifted back:
|
|
78
|
+
// a genuine webhook right after the sync must not look older than the snapshot.
|
|
79
|
+
const SYNC_SNAPSHOT_EVENT_TIME_MARGIN_SECONDS = 5;
|
|
80
|
+
|
|
81
|
+
function snapshotEventTime(): Temporal.Instant {
|
|
82
|
+
return Temporal.Now.instant()
|
|
83
|
+
.round({ smallestUnit: "second", roundingMode: "floor" })
|
|
84
|
+
.subtract({ seconds: SYNC_SNAPSHOT_EVENT_TIME_MARGIN_SECONDS });
|
|
85
|
+
}
|
|
86
|
+
|
|
75
87
|
export const syncSubscriptionHandler: WriteHandlerDef = {
|
|
76
88
|
name: "sync-subscription",
|
|
77
89
|
agent: { expose: false },
|
|
@@ -143,11 +155,12 @@ export const syncSubscriptionHandler: WriteHandlerDef = {
|
|
|
143
155
|
tier: snapshot.tier,
|
|
144
156
|
currentPeriodEndIso: snapshot.currentPeriodEnd,
|
|
145
157
|
cancelAtIso: snapshot.cancelAt,
|
|
158
|
+
providerEventAtIso: snapshotEventTime().toString(),
|
|
146
159
|
});
|
|
147
160
|
|
|
148
161
|
return {
|
|
149
162
|
isSuccess: true as const,
|
|
150
|
-
data: (appendResult.duplicate
|
|
163
|
+
data: (appendResult.duplicate || appendResult.stale
|
|
151
164
|
? { synced: false, reason: "unchanged" }
|
|
152
165
|
: { synced: true }) satisfies SyncSubscriptionResult,
|
|
153
166
|
};
|
|
@@ -25,6 +25,20 @@ export const BILLING_FOUNDATION_I18N: Readonly<Record<string, LocalizedString>>
|
|
|
25
25
|
"billing-foundation.plans.everyInterval.year": { en: "every {count} years" },
|
|
26
26
|
"billing-foundation.plans.choose": { en: "Choose {plan}" },
|
|
27
27
|
"billing-foundation.plans.switch": { en: "Switch to {plan}" },
|
|
28
|
+
"billing-foundation.plans.planSwitchRequested": {
|
|
29
|
+
en: "Plan switch requested. It becomes active once the payment is confirmed.",
|
|
30
|
+
},
|
|
31
|
+
"billing-foundation.planSwitch.title": { en: "Confirm plan switch" },
|
|
32
|
+
"billing-foundation.planSwitch.summary": { en: "{from} to {to}" },
|
|
33
|
+
"billing-foundation.planSwitch.dueNow": { en: "Due now: {amount}" },
|
|
34
|
+
"billing-foundation.planSwitch.includesTax": { en: "including {tax} VAT" },
|
|
35
|
+
"billing-foundation.planSwitch.credit": { en: "Credit for the unused time: {amount}" },
|
|
36
|
+
"billing-foundation.planSwitch.recurring": { en: "New recurring price: {amount}" },
|
|
37
|
+
"billing-foundation.planSwitch.effective": { en: "Effective immediately" },
|
|
38
|
+
"billing-foundation.planSwitch.pendingNote": {
|
|
39
|
+
en: "The switch becomes active after payment confirmation.",
|
|
40
|
+
},
|
|
41
|
+
"billing-foundation.planSwitch.order": { en: "Switch with obligation to pay" },
|
|
28
42
|
"billing-foundation.plans.manage": { en: "Manage subscription" },
|
|
29
43
|
"billing-foundation.plans.reactivate": { en: "Reactivate subscription" },
|
|
30
44
|
"billing-foundation.plans.cancelScheduled": {
|
|
@@ -70,6 +84,8 @@ export const BILLING_FOUNDATION_I18N: Readonly<Record<string, LocalizedString>>
|
|
|
70
84
|
"billing-foundation.consent.link.withdrawal": { en: "Withdrawal policy" },
|
|
71
85
|
"billing-foundation.consent.link.privacy": { en: "Privacy policy" },
|
|
72
86
|
"billing-foundation.consent.order": { en: "Order with obligation to pay" },
|
|
87
|
+
"billing-foundation.plans.priceFrom": { en: "from" },
|
|
88
|
+
"billing-foundation.consent.chooseProvider": { en: "Payment provider" },
|
|
73
89
|
"billing-foundation.consent.back": { en: "Back" },
|
|
74
90
|
"billing-foundation.cancel.open": { en: "Cancel contract here" },
|
|
75
91
|
"billing-foundation.cancel.title": { en: "Cancel contract" },
|
|
@@ -130,6 +146,12 @@ export const BILLING_FOUNDATION_I18N: Readonly<Record<string, LocalizedString>>
|
|
|
130
146
|
"billing-foundation.errors.providerMismatch": {
|
|
131
147
|
en: "This tenant's subscription is on a different provider than the plan catalog. Contact support to migrate.",
|
|
132
148
|
},
|
|
149
|
+
"billing-foundation.errors.providerNotOffered": {
|
|
150
|
+
en: "This payment provider is not available right now.",
|
|
151
|
+
},
|
|
152
|
+
"billing-foundation.errors.providerRequired": {
|
|
153
|
+
en: "Choose a payment provider to continue.",
|
|
154
|
+
},
|
|
133
155
|
"billing-foundation.errors.priceUnavailable": {
|
|
134
156
|
en: "This plan's price is temporarily unavailable.",
|
|
135
157
|
},
|
|
@@ -62,10 +62,12 @@ export {
|
|
|
62
62
|
type BillingPlanPrice,
|
|
63
63
|
type BillingPlansResult,
|
|
64
64
|
type BillingPlanView,
|
|
65
|
+
type BillingProviderView,
|
|
65
66
|
type ConsumerProtectionOptions,
|
|
66
67
|
KNOWN_RECURRING_INTERVALS,
|
|
67
68
|
type LegalLinkSet,
|
|
68
69
|
type PaymentEvent,
|
|
70
|
+
type PlanSwitchPreview,
|
|
69
71
|
type ProviderPrice,
|
|
70
72
|
type ProviderSubscriptionSnapshot,
|
|
71
73
|
type RecurringInterval,
|