@cosmicdrift/kumiko-bundled-features 0.338.0 → 0.340.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/dist/auth-email-password/handlers/invite-accept-with-login.write.js +2 -2
- package/dist/auth-email-password/handlers/invite-accept.write.js +2 -4
- package/dist/auth-email-password/handlers/invite-info.query.js +3 -1
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +2 -4
- package/dist/auth-email-password/handlers/signup-confirm.write.js +1 -1
- package/dist/auth-email-password/handlers/signup-request.write.js +1 -1
- package/dist/auth-email-password/web/auth-gate.d.ts +12 -2
- package/dist/auth-email-password/web/auth-gate.js +25 -1
- package/dist/auth-email-password/web/auth-redirect.d.ts +5 -0
- package/dist/auth-email-password/web/auth-redirect.js +58 -0
- package/dist/auth-email-password/web/client-plugin.d.ts +8 -1
- package/dist/auth-email-password/web/client-plugin.js +2 -0
- package/dist/auth-email-password/web/index.d.ts +2 -1
- package/dist/auth-email-password/web/index.js +1 -0
- package/dist/auth-email-password/web/session.d.ts +3 -1
- package/dist/auth-email-password/web/session.js +8 -2
- package/dist/auth-mfa/feature.js +1 -1
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +1 -1
- package/dist/auth-mfa/handlers/enable-start-preauth.write.js +1 -1
- package/dist/auth-mfa/handlers/verify.write.js +1 -1
- package/dist/auth-mfa/mfa-code-verifier.js +1 -1
- package/dist/auth-mfa/mfa-status-checker.js +1 -1
- package/dist/billing-foundation/constants.d.ts +7 -0
- package/dist/billing-foundation/constants.js +8 -0
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +3 -3
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +3 -3
- package/dist/billing-foundation/consumer-protection/consent-gate.js +10 -2
- package/dist/billing-foundation/consumer-protection/consent-text.js +2 -2
- package/dist/billing-foundation/consumer-protection/register-termination.d.ts +1 -1
- package/dist/billing-foundation/consumer-protection/register-termination.js +31 -3
- package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +3 -1
- package/dist/billing-foundation/consumer-protection/termination-mail.js +1 -1
- package/dist/billing-foundation/consumer-protection/termination-notify.d.ts +18 -0
- package/dist/billing-foundation/consumer-protection/termination-notify.js +21 -0
- package/dist/billing-foundation/consumer-protection/termination-pages.js +31 -2
- package/dist/billing-foundation/consumer-protection/termination-record.d.ts +24 -2
- package/dist/billing-foundation/consumer-protection/termination-record.js +102 -34
- package/dist/billing-foundation/consumer-protection/termination-request.js +10 -42
- package/dist/billing-foundation/consumer-protection/termination-texts.js +20 -20
- package/dist/billing-foundation/events.d.ts +19 -0
- package/dist/billing-foundation/events.js +12 -0
- package/dist/billing-foundation/handlers/create-checkout-session.write.js +2 -2
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +4 -2
- package/dist/billing-foundation/i18n.js +3 -0
- package/dist/billing-foundation/projection.js +23 -40
- package/dist/billing-foundation/tenant-destroy-hook.js +4 -4
- package/dist/billing-foundation/types.d.ts +6 -1
- package/dist/billing-foundation/validate-options.js +4 -0
- package/dist/channel-discord/discord-channel.d.ts +6 -0
- package/dist/channel-discord/discord-channel.js +19 -0
- package/dist/channel-discord/feature.d.ts +3 -0
- package/dist/channel-discord/feature.js +30 -0
- package/dist/channel-discord/index.d.ts +2 -0
- package/dist/channel-discord/index.js +2 -0
- package/dist/channel-slack/feature.d.ts +3 -0
- package/dist/channel-slack/feature.js +30 -0
- package/dist/channel-slack/index.d.ts +2 -0
- package/dist/channel-slack/index.js +2 -0
- package/dist/channel-slack/slack-channel.d.ts +5 -0
- package/dist/channel-slack/slack-channel.js +16 -0
- package/dist/channel-teams/feature.d.ts +3 -0
- package/dist/channel-teams/feature.js +34 -0
- package/dist/channel-teams/index.d.ts +2 -0
- package/dist/channel-teams/index.js +2 -0
- package/dist/channel-teams/teams-channel.d.ts +5 -0
- package/dist/channel-teams/teams-channel.js +38 -0
- package/dist/channel-telegram/feature.d.ts +3 -0
- package/dist/channel-telegram/feature.js +28 -0
- package/dist/channel-telegram/index.d.ts +2 -0
- package/dist/channel-telegram/index.js +2 -0
- package/dist/channel-telegram/telegram-channel.d.ts +8 -0
- package/dist/channel-telegram/telegram-channel.js +54 -0
- package/dist/crypto-shredding/handlers/forget-subject.write.js +6 -6
- package/dist/custom-fields/handlers/clear-custom-field.write.js +1 -1
- package/dist/custom-fields/handlers/define-tenant-field.write.js +1 -1
- package/dist/custom-fields/handlers/set-custom-field.write.js +1 -1
- package/dist/custom-fields/wire-user-data-rights.js +1 -1
- package/dist/data-retention/feature.js +1 -1
- package/dist/delivery/channel-context.d.ts +2 -1
- package/dist/delivery/channel-context.js +2 -2
- package/dist/delivery/chat-webhook-channel.d.ts +19 -0
- package/dist/delivery/chat-webhook-channel.js +48 -0
- package/dist/delivery/chat-webhook-sender.d.ts +21 -0
- package/dist/delivery/chat-webhook-sender.js +102 -0
- package/dist/delivery/delivery-service.js +3 -0
- package/dist/delivery/index.d.ts +2 -0
- package/dist/delivery/index.js +2 -0
- package/dist/delivery/jobs.js +1 -1
- package/dist/delivery/testing.d.ts +2 -0
- package/dist/delivery/types.d.ts +3 -1
- package/dist/delivery/types.js +1 -2
- package/dist/file-derivatives/handlers/public-variant-by-file-ref.query.js +1 -1
- package/dist/files-tenant-data/index.js +1 -1
- package/dist/form-draft/feature.js +1 -1
- package/dist/form-draft/handlers/discard.write.js +1 -1
- package/dist/form-draft/handlers/save.write.js +1 -1
- package/dist/inbound-mail-foundation/connect-routes.d.ts +7 -0
- package/dist/inbound-mail-foundation/connect-routes.js +13 -8
- package/dist/inbound-mail-foundation/feature.js +1 -1
- package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +1 -1
- package/dist/inbound-mail-foundation/index.d.ts +1 -0
- package/dist/inbound-mail-foundation/index.js +1 -0
- package/dist/inbound-mail-foundation/oauth-access-token.d.ts +28 -0
- package/dist/inbound-mail-foundation/oauth-access-token.js +131 -0
- package/dist/inbound-mail-foundation/projection.js +37 -45
- package/dist/inbound-mail-foundation/tenant-destroy-hook.js +1 -1
- package/dist/inbound-mail-foundation/types.d.ts +18 -1
- package/dist/inbound-mail-foundation/watch-supervisor.d.ts +5 -0
- package/dist/inbound-mail-foundation/watch-supervisor.js +42 -14
- package/dist/managed-pages/handlers/set.write.js +1 -1
- package/dist/notes-history/handlers/add-note.write.js +1 -1
- package/dist/renderer-simple/feature.d.ts +8 -2
- package/dist/renderer-simple/feature.js +8 -5
- package/dist/renderer-simple/index.d.ts +2 -2
- package/dist/renderer-simple/index.js +1 -1
- package/dist/renderer-simple/simple-renderer.d.ts +15 -0
- package/dist/renderer-simple/simple-renderer.js +74 -27
- package/dist/secrets/feature.js +1 -1
- package/dist/sessions/feature.js +1 -1
- package/dist/sessions/handlers/revoke-all-for-user.write.js +1 -1
- package/dist/tenant-handover/handlers/claim.write.js +1 -2
- package/dist/tenant-lifecycle/feature.js +1 -1
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +3 -1
- package/dist/tenant-lifecycle/handlers/request-destruction.write.js +1 -1
- package/dist/tier-engine/feature.js +1 -1
- package/dist/tier-engine/handlers/get-tenant-tier.query.js +1 -1
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +1 -1
- package/dist/user/handlers/update-roles.js +7 -18
- package/dist/user-data-rights/feature.js +2 -2
- package/dist/user-data-rights/handlers/cancel-deletion.write.js +1 -1
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +1 -1
- package/dist/user-data-rights/handlers/download-by-job.query.js +1 -1
- package/dist/user-data-rights/handlers/download-by-token.query.js +1 -1
- package/dist/user-data-rights/handlers/export-status.query.js +1 -1
- package/dist/user-data-rights/handlers/lift-restriction.write.js +2 -4
- package/dist/user-data-rights/handlers/my-audit-log.query.js +1 -1
- package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +1 -1
- package/dist/user-data-rights/handlers/request-deletion.write.js +1 -1
- package/dist/user-data-rights/handlers/request-export.write.js +1 -1
- package/dist/user-data-rights/handlers/restrict-account.write.js +2 -2
- package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +1 -1
- package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
- package/dist/workflow-runner/db/queries/due-runs.d.ts +4 -5
- package/dist/workflow-runner/db/queries/due-runs.js +5 -4
- package/dist/workflow-runner/event-subscriber.js +6 -2
- package/dist/workflow-runner/event-trigger.js +5 -0
- package/dist/workflow-runner/feature.js +4 -6
- package/dist/workflow-runner/handlers/resume-run.write.js +147 -59
- package/dist/workflow-runner/pending-projection.js +1 -0
- package/dist/workflow-runner/runner.d.ts +7 -1
- package/dist/workflow-runner/runner.js +1 -1
- package/dist/workflow-runner/tables.js +8 -1
- package/package.json +41 -9
- package/src/auth-email-password/changes.json +7 -0
- package/src/billing-foundation/changes.json +41 -0
- package/src/delivery/changes.json +7 -0
- package/src/inbound-mail-foundation/changes.json +14 -0
- package/src/renderer-simple/changes.json +9 -1
- package/src/user/changes.json +13 -6
- package/src/workflow-runner/changes.json +14 -0
- package/dist/billing-foundation/db/queries/payment-projection.d.ts +0 -2
- package/dist/billing-foundation/db/queries/payment-projection.js +0 -13
- package/dist/billing-foundation/db/queries/subscription-projection.d.ts +0 -2
- package/dist/billing-foundation/db/queries/subscription-projection.js +0 -7
- package/dist/inbound-mail-foundation/db/queries/inbound-projections.d.ts +0 -7
- package/dist/inbound-mail-foundation/db/queries/inbound-projections.js +0 -24
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import type { ProviderCancelOutcome } from "../events.js";
|
|
3
|
+
import type { ConsumerProtectionOptions } from "../types.js";
|
|
4
|
+
import type { ConsentLocale } from "./consent-text.js";
|
|
5
|
+
import { type OperatorNoticeDeclaration } from "./termination-mail.js";
|
|
6
|
+
import type { OperatorNoticeReason } from "./termination-texts.js";
|
|
7
|
+
export type OperatorNoticePlan = {
|
|
8
|
+
readonly locale: ConsentLocale;
|
|
9
|
+
readonly requestId: string;
|
|
10
|
+
readonly receivedAtIso: string;
|
|
11
|
+
readonly channel: "public" | "account";
|
|
12
|
+
readonly declaration: OperatorNoticeDeclaration;
|
|
13
|
+
readonly reasons: readonly OperatorNoticeReason[];
|
|
14
|
+
readonly tenantId?: string;
|
|
15
|
+
readonly providerCancel?: ProviderCancelOutcome;
|
|
16
|
+
};
|
|
17
|
+
export declare function requireNotify(ctx: HandlerContext): NonNullable<HandlerContext["notify"]>;
|
|
18
|
+
export declare function notifyOperator(ctx: HandlerContext, consumerProtection: ConsumerProtectionOptions, plan: OperatorNoticePlan): Promise<void>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
2
|
+
import { CONTRACT_TERMINATION_OPERATOR_NOTIFICATION_TYPE } from "../constants.js";
|
|
3
|
+
import { renderOperatorNotice } from "./termination-mail.js";
|
|
4
|
+
export function requireNotify(ctx) {
|
|
5
|
+
if (!ctx.notify) {
|
|
6
|
+
throw new InternalError({
|
|
7
|
+
message: "billing-foundation:termination: ctx.notify unavailable — the delivery feature must be mounted",
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
return ctx.notify;
|
|
11
|
+
}
|
|
12
|
+
export async function notifyOperator(ctx, consumerProtection, plan) {
|
|
13
|
+
await requireNotify(ctx)(CONTRACT_TERMINATION_OPERATOR_NOTIFICATION_TYPE, {
|
|
14
|
+
route: { email: consumerProtection.operatorEmail },
|
|
15
|
+
data: renderOperatorNotice({
|
|
16
|
+
...plan,
|
|
17
|
+
operatorEmail: consumerProtection.operatorEmail,
|
|
18
|
+
}),
|
|
19
|
+
priority: "critical",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Public § 312k pages (/legal/kuendigen, /legal/cancel): form → review → confirm,
|
|
2
2
|
// no JavaScript. Every echoed value goes through escapeHtml/escapeHtmlAttr.
|
|
3
3
|
import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
4
|
+
import { getCookie } from "hono/cookie";
|
|
4
5
|
import { securePageHeaders, wrapInLayout } from "../../page-render/index.js";
|
|
5
6
|
import { SubscriptionFoundationHandlers } from "../constants.js";
|
|
6
7
|
import { CONTRACT_TERMINATION_DECLARATION_TYPES, CONTRACT_TERMINATION_KINDS } from "../events.js";
|
|
@@ -171,14 +172,42 @@ function resultPage(locale, receivedAtIso, requestId) {
|
|
|
171
172
|
return htmlResponse(locale, page.resultTitle, body, 200);
|
|
172
173
|
}
|
|
173
174
|
const RATE_LIMITED_CODE = "rate_limited";
|
|
175
|
+
const TENANT_HEADER = "x-tenant";
|
|
176
|
+
const TENANT_COOKIE = "kumiko_tenant";
|
|
177
|
+
// Exactly what the anonymous tenant resolution reads. The resolver and its
|
|
178
|
+
// trust rules still decide; the re-entry only sees what a direct /api/write
|
|
179
|
+
// from this client would see. Never the session cookie or Authorization (the
|
|
180
|
+
// handler is anonymous) and never X-Forwarded-For (the resolved clientIp
|
|
181
|
+
// travels as env).
|
|
182
|
+
const FORWARDED_TENANT_HEADERS = [
|
|
183
|
+
"host",
|
|
184
|
+
"x-forwarded-host",
|
|
185
|
+
"x-forwarded-proto",
|
|
186
|
+
TENANT_HEADER,
|
|
187
|
+
];
|
|
188
|
+
function reentryHeaders(c) {
|
|
189
|
+
const headers = new Headers({ "content-type": "application/json" });
|
|
190
|
+
for (const name of FORWARDED_TENANT_HEADERS) {
|
|
191
|
+
const value = c.req.header(name);
|
|
192
|
+
if (value !== undefined)
|
|
193
|
+
headers.set(name, value);
|
|
194
|
+
}
|
|
195
|
+
const tenantCookie = getCookie(c, TENANT_COOKIE);
|
|
196
|
+
if (tenantCookie !== undefined) {
|
|
197
|
+
headers.set("cookie", `${TENANT_COOKIE}=${encodeURIComponent(tenantCookie)}`);
|
|
198
|
+
}
|
|
199
|
+
return headers;
|
|
200
|
+
}
|
|
174
201
|
async function submitDeclaration(c, app, clientIp, values, locale) {
|
|
175
202
|
// deps.write only resolves a session user under /api/*; this page lives
|
|
176
203
|
// outside it, so it re-enters the dispatcher through the app like the
|
|
177
204
|
// user-export by-token route. The resolved clientIp as env keeps the
|
|
178
|
-
// handler's per-IP rate limit per visitor instead of one shared bucket
|
|
205
|
+
// handler's per-IP rate limit per visitor instead of one shared bucket;
|
|
206
|
+
// the forwarded host headers let a host-based tenant resolver see this
|
|
207
|
+
// visitor's host.
|
|
179
208
|
const response = await app.fetch(new Request(`${new URL(c.req.url).origin}/api/write`, {
|
|
180
209
|
method: "POST",
|
|
181
|
-
headers:
|
|
210
|
+
headers: reentryHeaders(c),
|
|
182
211
|
body: JSON.stringify({
|
|
183
212
|
type: SubscriptionFoundationHandlers.requestContractTermination,
|
|
184
213
|
payload: payloadOf(values, locale),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type QueryHandlerDef, type WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import { type ProviderCancelOutcome } from "../events.js";
|
|
4
|
-
import type { ResolvedBillingFoundationOptions } from "../types.js";
|
|
4
|
+
import type { ConsumerProtectionOptions, ResolvedBillingFoundationOptions } from "../types.js";
|
|
5
5
|
import type { OperatorNoticeReason } from "./termination-texts.js";
|
|
6
6
|
export declare const recordContractTerminationSchema: z.ZodObject<{
|
|
7
7
|
requestId: z.ZodString;
|
|
@@ -22,6 +22,27 @@ export declare const recordContractTerminationSchema: z.ZodObject<{
|
|
|
22
22
|
none: "none";
|
|
23
23
|
"period-end": "period-end";
|
|
24
24
|
}>;
|
|
25
|
+
receivedAtIso: z.ZodOptional<z.ZodString>;
|
|
26
|
+
locale: z.ZodOptional<z.ZodEnum<{
|
|
27
|
+
de: "de";
|
|
28
|
+
en: "en";
|
|
29
|
+
}>>;
|
|
30
|
+
}, z.core.$strict>;
|
|
31
|
+
export declare const declareContractTerminationSchema: z.ZodObject<{
|
|
32
|
+
requestId: z.ZodString;
|
|
33
|
+
declarationType: z.ZodEnum<{
|
|
34
|
+
termination: "termination";
|
|
35
|
+
withdrawal: "withdrawal";
|
|
36
|
+
}>;
|
|
37
|
+
terminationKind: z.ZodEnum<{
|
|
38
|
+
extraordinary: "extraordinary";
|
|
39
|
+
ordinary: "ordinary";
|
|
40
|
+
}>;
|
|
41
|
+
receivedAtIso: z.ZodString;
|
|
42
|
+
locale: z.ZodEnum<{
|
|
43
|
+
de: "de";
|
|
44
|
+
en: "en";
|
|
45
|
+
}>;
|
|
25
46
|
}, z.core.$strict>;
|
|
26
47
|
export type RecordContractTerminationResult = {
|
|
27
48
|
readonly receivedAtIso: string;
|
|
@@ -47,5 +68,6 @@ export declare const recordUnmatchedContractTerminationSchema: z.ZodObject<{
|
|
|
47
68
|
}, z.core.$strict>;
|
|
48
69
|
export declare const terminableSubscriptionSchema: z.ZodObject<{}, z.core.$strict>;
|
|
49
70
|
export declare function createTerminableSubscriptionQuery(): QueryHandlerDef;
|
|
50
|
-
export declare function
|
|
71
|
+
export declare function createDeclareContractTerminationHandler(): WriteHandlerDef;
|
|
72
|
+
export declare function createRecordContractTerminationHandler(options: ResolvedBillingFoundationOptions, consumerProtection: ConsumerProtectionOptions): WriteHandlerDef;
|
|
51
73
|
export declare function createRecordUnmatchedContractTerminationHandler(): WriteHandlerDef;
|
|
@@ -8,8 +8,10 @@ import * as z from "zod";
|
|
|
8
8
|
import { subscriptionAggregateId, terminationUnmatchedAggregateId } from "../aggregate-id.js";
|
|
9
9
|
import { resolveProviderPlugin } from "../checkout-core.js";
|
|
10
10
|
import { isTerminalSubscriptionStatus, SubscriptionCancelTimings } from "../constants.js";
|
|
11
|
-
import { CONTRACT_TERMINATION_CHANNELS, CONTRACT_TERMINATION_DECLARATION_TYPES, CONTRACT_TERMINATION_KINDS, CONTRACT_TERMINATION_REQUESTED_EVENT_QN, CONTRACT_TERMINATION_UNMATCHED_AGGREGATE_TYPE, CONTRACT_TERMINATION_UNMATCHED_EVENT_QN, SUBSCRIPTION_AGGREGATE_TYPE, } from "../events.js";
|
|
11
|
+
import { CONTRACT_TERMINATION_CHANNELS, CONTRACT_TERMINATION_DECLARATION_TYPES, CONTRACT_TERMINATION_DECLARED_EVENT_QN, CONTRACT_TERMINATION_KINDS, CONTRACT_TERMINATION_REQUESTED_EVENT_QN, CONTRACT_TERMINATION_UNMATCHED_AGGREGATE_TYPE, CONTRACT_TERMINATION_UNMATCHED_EVENT_QN, contractTerminationRequestedPayloadSchema, SUBSCRIPTION_AGGREGATE_TYPE, } from "../events.js";
|
|
12
12
|
import { getSubscriptionForTenant } from "../get-subscription-for-tenant.js";
|
|
13
|
+
import { CONSENT_LOCALES, FALLBACK_CONSENT_LOCALE } from "./consent-locale.js";
|
|
14
|
+
import { notifyOperator } from "./termination-notify.js";
|
|
13
15
|
const REQUESTED_CANCEL_TIMINGS = [
|
|
14
16
|
SubscriptionCancelTimings.periodEnd,
|
|
15
17
|
SubscriptionCancelTimings.immediately,
|
|
@@ -22,6 +24,19 @@ export const recordContractTerminationSchema = z
|
|
|
22
24
|
terminationKind: z.enum(CONTRACT_TERMINATION_KINDS),
|
|
23
25
|
channel: z.enum(CONTRACT_TERMINATION_CHANNELS),
|
|
24
26
|
when: z.enum(REQUESTED_CANCEL_TIMINGS),
|
|
27
|
+
// The public job passes the time the declaration arrived and the declarant's
|
|
28
|
+
// locale, so the record and a provider-problem notice do not take the job's.
|
|
29
|
+
receivedAtIso: z.string().min(1).optional(),
|
|
30
|
+
locale: z.enum(CONSENT_LOCALES).optional(),
|
|
31
|
+
})
|
|
32
|
+
.strict();
|
|
33
|
+
export const declareContractTerminationSchema = z
|
|
34
|
+
.object({
|
|
35
|
+
requestId: z.string().min(1).max(100),
|
|
36
|
+
declarationType: z.enum(CONTRACT_TERMINATION_DECLARATION_TYPES),
|
|
37
|
+
terminationKind: z.enum(CONTRACT_TERMINATION_KINDS),
|
|
38
|
+
receivedAtIso: z.string().min(1),
|
|
39
|
+
locale: z.enum(CONSENT_LOCALES),
|
|
25
40
|
})
|
|
26
41
|
.strict();
|
|
27
42
|
export const recordUnmatchedContractTerminationSchema = z
|
|
@@ -49,7 +64,56 @@ export function createTerminableSubscriptionQuery() {
|
|
|
49
64
|
},
|
|
50
65
|
};
|
|
51
66
|
}
|
|
52
|
-
|
|
67
|
+
async function cancelAtProvider(ctx, tenantId, when, receivedAtIso) {
|
|
68
|
+
// skip: a withdrawal over the public channel makes no provider call
|
|
69
|
+
if (when === "none")
|
|
70
|
+
return { providerCancel: "none", effectiveAtIso: null, noticeReason: null };
|
|
71
|
+
const subscription = await getSubscriptionForTenant(ctx, tenantId);
|
|
72
|
+
if (subscription === null || isTerminalSubscriptionStatus(subscription.status)) {
|
|
73
|
+
return { providerCancel: "none", effectiveAtIso: null, noticeReason: "no_active_subscription" };
|
|
74
|
+
}
|
|
75
|
+
const plugin = findPlugin(ctx, subscription.providerName);
|
|
76
|
+
if (!plugin?.cancelSubscription) {
|
|
77
|
+
return { providerCancel: "none", effectiveAtIso: null, noticeReason: "provider_cannot_cancel" };
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
await plugin.cancelSubscription(ctx, {
|
|
81
|
+
providerSubscriptionId: subscription.providerSubscriptionId,
|
|
82
|
+
when,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
ctx.log?.warn(`[billing-foundation:record-contract-termination] provider cancel failed for tenant ${tenantId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
87
|
+
return { providerCancel: "none", effectiveAtIso: null, noticeReason: "provider_error" };
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
providerCancel: when,
|
|
91
|
+
effectiveAtIso: when === SubscriptionCancelTimings.immediately
|
|
92
|
+
? receivedAtIso
|
|
93
|
+
: subscription.currentPeriodEnd.toString(),
|
|
94
|
+
noticeReason: null,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export function createDeclareContractTerminationHandler() {
|
|
98
|
+
return {
|
|
99
|
+
name: "declare-contract-termination",
|
|
100
|
+
agent: { expose: false },
|
|
101
|
+
schema: declareContractTerminationSchema,
|
|
102
|
+
access: { roles: access.system },
|
|
103
|
+
handler: async (event, ctx) => {
|
|
104
|
+
// @cast-boundary engine-payload — dispatcher-zod-validated payload
|
|
105
|
+
const payload = event.payload;
|
|
106
|
+
const stream = await ctx.fetchForWriting({
|
|
107
|
+
aggregateId: subscriptionAggregateId(event.user.tenantId),
|
|
108
|
+
aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
|
|
109
|
+
});
|
|
110
|
+
const declared = payload;
|
|
111
|
+
await stream.appendOne({ type: CONTRACT_TERMINATION_DECLARED_EVENT_QN, payload: declared });
|
|
112
|
+
return { isSuccess: true, data: { declared: true } };
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export function createRecordContractTerminationHandler(options, consumerProtection) {
|
|
53
117
|
return {
|
|
54
118
|
name: "record-contract-termination",
|
|
55
119
|
agent: { expose: false },
|
|
@@ -64,43 +128,30 @@ export function createRecordContractTerminationHandler(options) {
|
|
|
64
128
|
message: "billing-foundation: the public channel never cancels at the provider immediately",
|
|
65
129
|
});
|
|
66
130
|
}
|
|
67
|
-
const receivedAtIso = options.now().toString();
|
|
131
|
+
const receivedAtIso = payload.receivedAtIso ?? options.now().toString();
|
|
68
132
|
const stream = await ctx.fetchForWriting({
|
|
69
133
|
aggregateId: subscriptionAggregateId(tenantId),
|
|
70
134
|
aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
|
|
71
135
|
});
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
when: payload.when,
|
|
90
|
-
});
|
|
91
|
-
providerCancel = payload.when;
|
|
92
|
-
effectiveAtIso =
|
|
93
|
-
payload.when === SubscriptionCancelTimings.immediately
|
|
94
|
-
? receivedAtIso
|
|
95
|
-
: subscription.currentPeriodEnd.toString();
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
ctx.log?.warn(`[billing-foundation:record-contract-termination] provider cancel failed for tenant ${tenantId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
99
|
-
noticeReason = "provider_error";
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
136
|
+
// The public job runs at least once: a second run must not call the
|
|
137
|
+
// provider or append again.
|
|
138
|
+
const alreadyRecorded = stream.events
|
|
139
|
+
.map((e) => e.type === CONTRACT_TERMINATION_REQUESTED_EVENT_QN
|
|
140
|
+
? contractTerminationRequestedPayloadSchema.safeParse(e.payload).data
|
|
141
|
+
: undefined)
|
|
142
|
+
.find((recordedPayload) => recordedPayload?.requestId === payload.requestId);
|
|
143
|
+
if (alreadyRecorded) {
|
|
144
|
+
return {
|
|
145
|
+
isSuccess: true,
|
|
146
|
+
data: {
|
|
147
|
+
receivedAtIso: alreadyRecorded.receivedAtIso,
|
|
148
|
+
effectiveAtIso: alreadyRecorded.effectiveAtIso,
|
|
149
|
+
providerCancel: alreadyRecorded.providerCancel,
|
|
150
|
+
noticeReason: null,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
103
153
|
}
|
|
154
|
+
const { providerCancel, effectiveAtIso, noticeReason } = await cancelAtProvider(ctx, tenantId, payload.when, receivedAtIso);
|
|
104
155
|
const recorded = {
|
|
105
156
|
requestId: payload.requestId,
|
|
106
157
|
declarationType: payload.declarationType,
|
|
@@ -114,6 +165,23 @@ export function createRecordContractTerminationHandler(options) {
|
|
|
114
165
|
type: CONTRACT_TERMINATION_REQUESTED_EVENT_QN,
|
|
115
166
|
payload: recorded,
|
|
116
167
|
});
|
|
168
|
+
// The account path reports the provider outcome to its caller and the
|
|
169
|
+
// operator in its own mail; the public path has nobody else to tell.
|
|
170
|
+
if (payload.channel === "public" && noticeReason !== null) {
|
|
171
|
+
await notifyOperator(ctx, consumerProtection, {
|
|
172
|
+
locale: payload.locale ?? FALLBACK_CONSENT_LOCALE,
|
|
173
|
+
requestId: payload.requestId,
|
|
174
|
+
receivedAtIso,
|
|
175
|
+
channel: "public",
|
|
176
|
+
declaration: {
|
|
177
|
+
declarationType: payload.declarationType,
|
|
178
|
+
terminationKind: payload.terminationKind,
|
|
179
|
+
},
|
|
180
|
+
reasons: [noticeReason],
|
|
181
|
+
tenantId,
|
|
182
|
+
providerCancel,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
117
185
|
return {
|
|
118
186
|
isSuccess: true,
|
|
119
187
|
data: {
|
|
@@ -8,11 +8,12 @@ import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
|
8
8
|
import * as z from "zod";
|
|
9
9
|
import { TenantQueries } from "../../tenant/index.js";
|
|
10
10
|
import { UserQueries } from "../../user/index.js";
|
|
11
|
-
import {
|
|
11
|
+
import { CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE, SubscriptionCancelTimings, SubscriptionFoundationHandlers, SubscriptionFoundationQueries, } from "../constants.js";
|
|
12
12
|
import { CONTRACT_TERMINATION_DECLARATION_TYPES, CONTRACT_TERMINATION_KINDS, } from "../events.js";
|
|
13
13
|
import { purchaseRolesOf } from "../plan-catalog.js";
|
|
14
14
|
import { resolveConsentLocale } from "./consent-text.js";
|
|
15
|
-
import {
|
|
15
|
+
import { renderTerminationReceipt } from "./termination-mail.js";
|
|
16
|
+
import { notifyOperator, requireNotify } from "./termination-notify.js";
|
|
16
17
|
const TENANT_ADMIN_ROLE = "TenantAdmin";
|
|
17
18
|
const EXTRAORDINARY_REASON_MESSAGE = "reason is required for an extraordinary termination";
|
|
18
19
|
const authUserRowSchema = z.object({
|
|
@@ -52,12 +53,7 @@ function needsOperatorNotice(declaration) {
|
|
|
52
53
|
];
|
|
53
54
|
}
|
|
54
55
|
async function sendTerminationMails(ctx, consumerProtection, plan) {
|
|
55
|
-
|
|
56
|
-
throw new InternalError({
|
|
57
|
-
message: "billing-foundation:termination: ctx.notify unavailable — the delivery feature must be mounted",
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
await ctx.notify(CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE, {
|
|
56
|
+
await requireNotify(ctx)(CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE, {
|
|
61
57
|
route: { email: plan.declaration.email },
|
|
62
58
|
data: renderTerminationReceipt({
|
|
63
59
|
locale: plan.locale,
|
|
@@ -71,21 +67,7 @@ async function sendTerminationMails(ctx, consumerProtection, plan) {
|
|
|
71
67
|
// skip: no operator notice needed for this declaration
|
|
72
68
|
if (plan.reasons.length === 0)
|
|
73
69
|
return;
|
|
74
|
-
await ctx
|
|
75
|
-
route: { email: consumerProtection.operatorEmail },
|
|
76
|
-
data: renderOperatorNotice({
|
|
77
|
-
locale: plan.locale,
|
|
78
|
-
requestId: plan.requestId,
|
|
79
|
-
receivedAtIso: plan.receivedAtIso,
|
|
80
|
-
channel: plan.channel,
|
|
81
|
-
declaration: plan.declaration,
|
|
82
|
-
reasons: plan.reasons,
|
|
83
|
-
operatorEmail: consumerProtection.operatorEmail,
|
|
84
|
-
...(plan.tenantId !== undefined && { tenantId: plan.tenantId }),
|
|
85
|
-
...(plan.providerCancel !== undefined && { providerCancel: plan.providerCancel }),
|
|
86
|
-
}),
|
|
87
|
-
priority: "critical",
|
|
88
|
-
});
|
|
70
|
+
await notifyOperator(ctx, consumerProtection, plan);
|
|
89
71
|
}
|
|
90
72
|
// Tenants the entered email can terminate a contract for: tenants where the
|
|
91
73
|
// user is TenantAdmin and the subscription is not terminal. Every identity
|
|
@@ -122,6 +104,7 @@ export function createRequestContractTerminationHandler(options, consumerProtect
|
|
|
122
104
|
schema: requestContractTerminationSchema,
|
|
123
105
|
access: { roles: ["anonymous"] },
|
|
124
106
|
rateLimit: { per: "ip+handler", limit: 5, windowSeconds: 600 },
|
|
107
|
+
...(consumerProtection.terminationScope === "platform" && { tenantlessAnonymous: true }),
|
|
125
108
|
escapeHatch: {
|
|
126
109
|
reason: "Anonymous declarant has no session — finds the contract by email via ctx.queryAs(system, user:find-for-auth) and tenant:memberships; the tenant written to comes only from those lookups.",
|
|
127
110
|
},
|
|
@@ -132,29 +115,15 @@ export function createRequestContractTerminationHandler(options, consumerProtect
|
|
|
132
115
|
const receivedAtIso = options.now().toString();
|
|
133
116
|
const { declarationType, terminationKind } = payload;
|
|
134
117
|
const reasons = [];
|
|
118
|
+
const locale = resolveConsentLocale(payload.locale);
|
|
135
119
|
let matchedTenantId;
|
|
136
|
-
let providerCancel;
|
|
137
120
|
const candidates = await findTerminableTenantIds(ctx, event.user.tenantId, payload.email);
|
|
138
121
|
const [onlyCandidate] = candidates;
|
|
139
122
|
if (candidates.length === 1 && onlyCandidate !== undefined) {
|
|
140
123
|
matchedTenantId = onlyCandidate;
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
declarationType,
|
|
144
|
-
terminationKind,
|
|
145
|
-
channel: "public",
|
|
146
|
-
when: declarationType === "withdrawal" ? "none" : SubscriptionCancelTimings.periodEnd,
|
|
147
|
-
});
|
|
148
|
-
if (recorded.isSuccess) {
|
|
149
|
-
// @cast-boundary engine-payload — our own record handler's return shape
|
|
150
|
-
const data = recorded.data;
|
|
151
|
-
providerCancel = data.providerCancel;
|
|
152
|
-
if (data.noticeReason !== null)
|
|
153
|
-
reasons.push(data.noticeReason);
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
124
|
+
const declared = await ctx.writeAs(createSystemUser(onlyCandidate), SubscriptionFoundationHandlers.declareContractTermination, { requestId, declarationType, terminationKind, receivedAtIso, locale });
|
|
125
|
+
if (!declared.isSuccess)
|
|
156
126
|
reasons.push("recording_failed");
|
|
157
|
-
}
|
|
158
127
|
}
|
|
159
128
|
else {
|
|
160
129
|
reasons.push(candidates.length === 0 ? "unmatched" : "ambiguous");
|
|
@@ -170,7 +139,7 @@ export function createRequestContractTerminationHandler(options, consumerProtect
|
|
|
170
139
|
}
|
|
171
140
|
reasons.push(...needsOperatorNotice(payload));
|
|
172
141
|
await sendTerminationMails(ctx, consumerProtection, {
|
|
173
|
-
locale
|
|
142
|
+
locale,
|
|
174
143
|
requestId,
|
|
175
144
|
receivedAtIso,
|
|
176
145
|
channel: "public",
|
|
@@ -186,7 +155,6 @@ export function createRequestContractTerminationHandler(options, consumerProtect
|
|
|
186
155
|
},
|
|
187
156
|
reasons,
|
|
188
157
|
...(matchedTenantId !== undefined && { tenantId: matchedTenantId }),
|
|
189
|
-
...(providerCancel !== undefined && { providerCancel }),
|
|
190
158
|
});
|
|
191
159
|
return {
|
|
192
160
|
isSuccess: true,
|
|
@@ -24,18 +24,18 @@ export const TERMINATION_TEXTS = {
|
|
|
24
24
|
customerReference: "Kundennummer / Referenz",
|
|
25
25
|
reasonFieldLabel: "Begründung",
|
|
26
26
|
receiptSubject: {
|
|
27
|
-
termination: "Eingangsbestätigung
|
|
28
|
-
withdrawal: "Eingangsbestätigung
|
|
27
|
+
termination: "Eingangsbestätigung deiner Kündigung",
|
|
28
|
+
withdrawal: "Eingangsbestätigung deines Widerrufs",
|
|
29
29
|
},
|
|
30
|
-
receiptIntro: "wir bestätigen den Eingang
|
|
31
|
-
receiptNextSteps: "Wir prüfen
|
|
32
|
-
receiptRevoke: (operatorEmail) => `
|
|
33
|
-
contact: "Fragen
|
|
30
|
+
receiptIntro: "wir bestätigen den Eingang deiner Erklärung mit folgendem Inhalt. Diese E-Mail ist eine Eingangsbestätigung, keine Bestätigung der Wirksamkeit.",
|
|
31
|
+
receiptNextSteps: "Wir prüfen deine Erklärung und melden uns, sobald sie bearbeitet ist oder wir Rückfragen haben.",
|
|
32
|
+
receiptRevoke: (operatorEmail) => `Hast du diese Erklärung nicht abgegeben oder möchtest du sie zurücknehmen? Schreib uns bitte umgehend an ${operatorEmail}.`,
|
|
33
|
+
contact: "Fragen richtest du bitte an",
|
|
34
34
|
operatorSubject: {
|
|
35
35
|
termination: "Kündigung: Bearbeitung nötig",
|
|
36
36
|
withdrawal: "Widerruf: Bearbeitung nötig",
|
|
37
37
|
},
|
|
38
|
-
operatorIntro: "Es ist eine Erklärung eingegangen, die
|
|
38
|
+
operatorIntro: "Es ist eine Erklärung eingegangen, die bearbeitet werden muss.",
|
|
39
39
|
operatorReasons: {
|
|
40
40
|
unmatched: "Kein passender Vertrag zur angegebenen E-Mail gefunden",
|
|
41
41
|
ambiguous: "Die angegebene E-Mail passt zu mehreren Verträgen",
|
|
@@ -58,8 +58,8 @@ export const TERMINATION_TEXTS = {
|
|
|
58
58
|
},
|
|
59
59
|
page: {
|
|
60
60
|
title: "Vertrag kündigen oder widerrufen",
|
|
61
|
-
lead: "Mit diesem Formular
|
|
62
|
-
declarationTypeHeading: "Was
|
|
61
|
+
lead: "Mit diesem Formular erklärst du die Kündigung oder den Widerruf deines Vertrags. Du brauchst dafür kein Kundenkonto.",
|
|
62
|
+
declarationTypeHeading: "Was möchtest du erklären?",
|
|
63
63
|
kindHeading: "Art der Kündigung",
|
|
64
64
|
reasonHint: "Pflicht bei außerordentlicher Kündigung.",
|
|
65
65
|
earliestDate: "Die Kündigung gilt zum nächstmöglichen Zeitpunkt.",
|
|
@@ -68,23 +68,23 @@ export const TERMINATION_TEXTS = {
|
|
|
68
68
|
customerReferenceLabel: "Kundennummer (optional)",
|
|
69
69
|
reasonLabel: "Begründung",
|
|
70
70
|
reviewButton: "Weiter zur Prüfung",
|
|
71
|
-
reviewTitle: "Bitte
|
|
72
|
-
reviewLead: "Erst mit dem Klick auf den Button unten wird
|
|
71
|
+
reviewTitle: "Bitte prüfe deine Angaben",
|
|
72
|
+
reviewLead: "Erst mit dem Klick auf den Button unten wird deine Erklärung abgegeben.",
|
|
73
73
|
confirmButton: { termination: "Jetzt kündigen", withdrawal: "Jetzt widerrufen" },
|
|
74
74
|
backButton: "Angaben ändern",
|
|
75
|
-
resultTitle: "
|
|
76
|
-
resultLead: "Wir haben
|
|
75
|
+
resultTitle: "Deine Erklärung ist eingegangen",
|
|
76
|
+
resultLead: "Wir haben deine Erklärung erhalten und senden dir eine Eingangsbestätigung.",
|
|
77
77
|
resultReceivedAt: "Eingegangen am",
|
|
78
78
|
resultRequestId: "Vorgangsnummer",
|
|
79
|
-
resultMailNote: "Die Eingangsbestätigung geht an die von
|
|
80
|
-
errorsHeading: "Bitte
|
|
81
|
-
errorName: "Bitte
|
|
82
|
-
errorEmail: "Bitte
|
|
79
|
+
resultMailNote: "Die Eingangsbestätigung geht an die von dir angegebene E-Mail-Adresse.",
|
|
80
|
+
errorsHeading: "Bitte korrigiere deine Angaben",
|
|
81
|
+
errorName: "Bitte gib deinen Namen an.",
|
|
82
|
+
errorEmail: "Bitte gib eine gültige E-Mail-Adresse an.",
|
|
83
83
|
errorReason: "Bei einer außerordentlichen Kündigung ist eine Begründung nötig.",
|
|
84
|
-
errorChoice: "Bitte
|
|
85
|
-
errorGeneric: "
|
|
84
|
+
errorChoice: "Bitte wähle eine Erklärung und die Art der Kündigung.",
|
|
85
|
+
errorGeneric: "Deine Erklärung konnte nicht verarbeitet werden. Bitte versuch es erneut.",
|
|
86
86
|
rateLimitedTitle: "Zu viele Anfragen",
|
|
87
|
-
rateLimitedBody: "Von
|
|
87
|
+
rateLimitedBody: "Von deiner Adresse kamen zu viele Anfragen. Bitte versuch es später erneut.",
|
|
88
88
|
},
|
|
89
89
|
},
|
|
90
90
|
en: {
|
|
@@ -108,6 +108,25 @@ export declare const contractTerminationRequestedPayloadSchema: z.ZodObject<{
|
|
|
108
108
|
}>;
|
|
109
109
|
}, z.core.$strip>;
|
|
110
110
|
export type ContractTerminationRequestedPayload = z.infer<typeof contractTerminationRequestedPayloadSchema>;
|
|
111
|
+
export declare const CONTRACT_TERMINATION_DECLARED_EVENT_SHORT: "contract-termination-declared";
|
|
112
|
+
export declare const CONTRACT_TERMINATION_DECLARED_EVENT_QN: "billing-foundation:event:contract-termination-declared";
|
|
113
|
+
export declare const contractTerminationDeclaredPayloadSchema: z.ZodObject<{
|
|
114
|
+
requestId: z.ZodString;
|
|
115
|
+
declarationType: z.ZodEnum<{
|
|
116
|
+
termination: "termination";
|
|
117
|
+
withdrawal: "withdrawal";
|
|
118
|
+
}>;
|
|
119
|
+
terminationKind: z.ZodEnum<{
|
|
120
|
+
extraordinary: "extraordinary";
|
|
121
|
+
ordinary: "ordinary";
|
|
122
|
+
}>;
|
|
123
|
+
receivedAtIso: z.ZodString;
|
|
124
|
+
locale: z.ZodEnum<{
|
|
125
|
+
de: "de";
|
|
126
|
+
en: "en";
|
|
127
|
+
}>;
|
|
128
|
+
}, z.core.$strip>;
|
|
129
|
+
export type ContractTerminationDeclaredPayload = z.infer<typeof contractTerminationDeclaredPayloadSchema>;
|
|
111
130
|
export declare const CONTRACT_TERMINATION_UNMATCHED_AGGREGATE_TYPE: "contract-termination-unmatched";
|
|
112
131
|
export declare const CONTRACT_TERMINATION_UNMATCHED_EVENT_SHORT: "contract-termination-unmatched";
|
|
113
132
|
export declare const CONTRACT_TERMINATION_UNMATCHED_EVENT_QN: "billing-foundation:event:contract-termination-unmatched";
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// type ohne payload-discriminator.
|
|
12
12
|
import * as z from "zod";
|
|
13
13
|
import { BILLING_FOUNDATION_FEATURE, SubscriptionStatuses } from "./constants.js";
|
|
14
|
+
import { CONSENT_LOCALES } from "./consumer-protection/consent-locale.js";
|
|
14
15
|
// Aggregate-type für den event-store. Eine subscription pro Tenant ist
|
|
15
16
|
// ein stream; der subscriptionAggregateId-helper liefert die stream-id.
|
|
16
17
|
export const SUBSCRIPTION_AGGREGATE_TYPE = "subscription";
|
|
@@ -138,6 +139,17 @@ export const contractTerminationRequestedPayloadSchema = z.object({
|
|
|
138
139
|
effectiveAtIso: z.string().min(1).nullable(),
|
|
139
140
|
providerCancel: z.enum(PROVIDER_CANCEL_OUTCOMES),
|
|
140
141
|
});
|
|
142
|
+
// A public declaration matched to this tenant. The provider cancel runs in a
|
|
143
|
+
// job triggered by this event, so the request itself only looks up and appends.
|
|
144
|
+
export const CONTRACT_TERMINATION_DECLARED_EVENT_SHORT = "contract-termination-declared";
|
|
145
|
+
export const CONTRACT_TERMINATION_DECLARED_EVENT_QN = `${BILLING_FOUNDATION_FEATURE}:event:${CONTRACT_TERMINATION_DECLARED_EVENT_SHORT}`;
|
|
146
|
+
export const contractTerminationDeclaredPayloadSchema = z.object({
|
|
147
|
+
requestId: z.string().min(1).max(100),
|
|
148
|
+
declarationType: z.enum(CONTRACT_TERMINATION_DECLARATION_TYPES),
|
|
149
|
+
terminationKind: z.enum(CONTRACT_TERMINATION_KINDS),
|
|
150
|
+
receivedAtIso: z.string().min(1),
|
|
151
|
+
locale: z.enum(CONSENT_LOCALES),
|
|
152
|
+
});
|
|
141
153
|
export const CONTRACT_TERMINATION_UNMATCHED_AGGREGATE_TYPE = "contract-termination-unmatched";
|
|
142
154
|
export const CONTRACT_TERMINATION_UNMATCHED_EVENT_SHORT = "contract-termination-unmatched";
|
|
143
155
|
export const CONTRACT_TERMINATION_UNMATCHED_EVENT_QN = `${BILLING_FOUNDATION_FEATURE}:event:${CONTRACT_TERMINATION_UNMATCHED_EVENT_SHORT}`;
|
|
@@ -44,7 +44,7 @@ const createCheckoutSessionSchema = z.object({
|
|
|
44
44
|
export function createCheckoutSessionHandler(options) {
|
|
45
45
|
const consumerProtection = options.consumerProtection;
|
|
46
46
|
const schema = consumerProtection
|
|
47
|
-
? createCheckoutSessionSchema.extend({ consent: consentPayloadSchema })
|
|
47
|
+
? createCheckoutSessionSchema.extend({ consent: consentPayloadSchema.optional() })
|
|
48
48
|
: createCheckoutSessionSchema;
|
|
49
49
|
return {
|
|
50
50
|
name: "create-checkout-session",
|
|
@@ -57,7 +57,7 @@ export function createCheckoutSessionHandler(options) {
|
|
|
57
57
|
// @cast-boundary engine-payload — dispatcher-zod-validated payload
|
|
58
58
|
const payload = event.payload;
|
|
59
59
|
const mode = payload.mode ?? "subscription";
|
|
60
|
-
const consent = consumerProtection
|
|
60
|
+
const consent = consumerProtection
|
|
61
61
|
? await prepareConsent(ctx, consumerProtection, payload.consent, mode)
|
|
62
62
|
: undefined;
|
|
63
63
|
const result = await openCheckout(ctx, { baseUrl: options.baseUrl, catalog: options.catalog, now: options.now }, {
|
|
@@ -14,7 +14,9 @@ export function createStartPlanCheckoutHandler(options, catalog) {
|
|
|
14
14
|
}
|
|
15
15
|
const consumerProtection = options.consumerProtection;
|
|
16
16
|
const tierSchema = z.object({ tier: z.enum(plans) });
|
|
17
|
-
const schema = (consumerProtection
|
|
17
|
+
const schema = (consumerProtection
|
|
18
|
+
? tierSchema.extend({ consent: consentPayloadSchema.optional() })
|
|
19
|
+
: tierSchema).strict();
|
|
18
20
|
return {
|
|
19
21
|
name: "start-plan-checkout",
|
|
20
22
|
description: "Starts a hosted checkout for a plan tier when the tenant has no active subscription; the server picks price and redirect URLs.",
|
|
@@ -34,7 +36,7 @@ export function createStartPlanCheckoutHandler(options, catalog) {
|
|
|
34
36
|
message: `subscription-foundation: no live price resolved for tier "${payload.tier}"`,
|
|
35
37
|
});
|
|
36
38
|
}
|
|
37
|
-
const consent = consumerProtection
|
|
39
|
+
const consent = consumerProtection
|
|
38
40
|
? await prepareConsent(ctx, consumerProtection, payload.consent, "subscription")
|
|
39
41
|
: undefined;
|
|
40
42
|
const baseUrl = options.baseUrl ?? "";
|
|
@@ -119,6 +119,9 @@ export const BILLING_FOUNDATION_I18N = {
|
|
|
119
119
|
"billing-foundation.errors.priceUnavailable": {
|
|
120
120
|
en: "This plan's price is temporarily unavailable.",
|
|
121
121
|
},
|
|
122
|
+
"billing-foundation.errors.consentRequired": {
|
|
123
|
+
en: "Confirm both consent boxes before you continue.",
|
|
124
|
+
},
|
|
122
125
|
"billing-foundation.errors.consentTextOutdated": {
|
|
123
126
|
en: "The consent text has changed. Reload the page and confirm again.",
|
|
124
127
|
},
|