@cosmicdrift/kumiko-samples 0.338.0 → 0.339.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 +21 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +7 -0
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +2 -6
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +2 -6
- package/packages/bundled-features/src/auth-email-password/handlers/invite-info.query.ts +3 -5
- package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts +2 -8
- package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +35 -3
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +60 -0
- package/packages/bundled-features/src/auth-email-password/web/client-plugin.ts +10 -0
- package/packages/bundled-features/src/auth-email-password/web/index.ts +8 -0
- package/packages/bundled-features/src/auth-email-password/web/session.tsx +15 -2
- package/packages/bundled-features/src/auth-mfa/feature.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +1 -7
- package/packages/bundled-features/src/auth-mfa/handlers/enable-start-preauth.write.ts +1 -5
- package/packages/bundled-features/src/auth-mfa/handlers/verify.write.ts +1 -7
- package/packages/bundled-features/src/auth-mfa/mfa-code-verifier.ts +1 -5
- package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +1 -7
- package/packages/bundled-features/src/billing-foundation/changes.json +34 -0
- package/packages/bundled-features/src/billing-foundation/constants.ts +11 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +3 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +11 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-text.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +48 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +10 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +44 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +32 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +129 -33
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +13 -59
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +20 -21
- package/packages/bundled-features/src/billing-foundation/events.ts +18 -0
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +4 -5
- package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +6 -5
- package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
- package/packages/bundled-features/src/billing-foundation/tenant-destroy-hook.ts +4 -4
- package/packages/bundled-features/src/billing-foundation/types.ts +6 -0
- package/packages/bundled-features/src/billing-foundation/validate-options.ts +6 -0
- package/packages/bundled-features/src/channel-discord/discord-channel.ts +32 -0
- package/packages/bundled-features/src/channel-discord/feature.ts +40 -0
- package/packages/bundled-features/src/channel-discord/index.ts +2 -0
- package/packages/bundled-features/src/channel-slack/feature.ts +38 -0
- package/packages/bundled-features/src/channel-slack/index.ts +2 -0
- package/packages/bundled-features/src/channel-slack/slack-channel.ts +29 -0
- package/packages/bundled-features/src/channel-teams/feature.ts +42 -0
- package/packages/bundled-features/src/channel-teams/index.ts +2 -0
- package/packages/bundled-features/src/channel-teams/teams-channel.ts +50 -0
- package/packages/bundled-features/src/channel-telegram/feature.ts +35 -0
- package/packages/bundled-features/src/channel-telegram/index.ts +2 -0
- package/packages/bundled-features/src/channel-telegram/telegram-channel.ts +74 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +6 -18
- package/packages/bundled-features/src/custom-fields/handlers/clear-custom-field.write.ts +1 -1
- package/packages/bundled-features/src/custom-fields/handlers/define-tenant-field.write.ts +1 -4
- package/packages/bundled-features/src/custom-fields/handlers/set-custom-field.write.ts +1 -1
- package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +1 -1
- package/packages/bundled-features/src/data-retention/feature.ts +2 -3
- package/packages/bundled-features/src/delivery/changes.json +7 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +3 -1
- package/packages/bundled-features/src/delivery/chat-webhook-channel.ts +76 -0
- package/packages/bundled-features/src/delivery/chat-webhook-sender.ts +135 -0
- package/packages/bundled-features/src/delivery/delivery-service.ts +3 -0
- package/packages/bundled-features/src/delivery/index.ts +15 -0
- package/packages/bundled-features/src/delivery/jobs.ts +1 -1
- package/packages/bundled-features/src/delivery/testing.ts +2 -0
- package/packages/bundled-features/src/delivery/types.ts +8 -3
- package/packages/bundled-features/src/file-derivatives/handlers/public-variant-by-file-ref.query.ts +1 -1
- package/packages/bundled-features/src/files-tenant-data/index.ts +1 -1
- package/packages/bundled-features/src/form-draft/feature.ts +1 -1
- package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +1 -1
- package/packages/bundled-features/src/form-draft/handlers/save.write.ts +1 -5
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +7 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +29 -13
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/index.ts +7 -0
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +201 -0
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +17 -1
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +74 -36
- package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +1 -1
- package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +7 -10
- package/packages/bundled-features/src/renderer-simple/changes.json +9 -1
- package/packages/bundled-features/src/renderer-simple/feature.ts +14 -4
- package/packages/bundled-features/src/renderer-simple/index.ts +2 -2
- package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +94 -31
- package/packages/bundled-features/src/secrets/feature.ts +1 -1
- package/packages/bundled-features/src/sessions/feature.ts +1 -1
- package/packages/bundled-features/src/sessions/handlers/revoke-all-for-user.write.ts +1 -1
- package/packages/bundled-features/src/tenant-handover/handlers/claim.write.ts +1 -4
- package/packages/bundled-features/src/tenant-lifecycle/feature.ts +1 -3
- package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +3 -5
- package/packages/bundled-features/src/tenant-lifecycle/handlers/request-destruction.write.ts +1 -1
- package/packages/bundled-features/src/tier-engine/feature.ts +1 -1
- package/packages/bundled-features/src/tier-engine/handlers/get-tenant-tier.query.ts +1 -1
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/feature.ts +4 -6
- package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/confirm-deletion-by-token.write.ts +1 -3
- package/packages/bundled-features/src/user-data-rights/handlers/download-by-job.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/download-by-token.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/export-status.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/lift-restriction.write.ts +2 -6
- package/packages/bundled-features/src/user-data-rights/handlers/my-audit-log.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-deletion-by-email.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-export.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/restrict-account.write.ts +2 -8
- package/packages/bundled-features/src/user-data-rights/handlers/run-forget-cleanup.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/run-user-export.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/changes.json +7 -0
- package/packages/bundled-features/src/workflow-runner/event-subscriber.ts +7 -2
- package/packages/bundled-features/src/workflow-runner/event-trigger.ts +5 -0
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +231 -77
- package/packages/bundled-features/src/workflow-runner/pending-projection.ts +1 -0
- package/packages/bundled-features/src/workflow-runner/runner.ts +12 -2
- package/packages/bundled-features/src/workflow-runner/tables.ts +8 -1
- package/samples/apps/styleguide/src/features/widgets/i18n.ts +5 -0
- package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +11 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +16 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +152 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +82 -0
- package/samples/apps/use-all-bundled/feature-manifest.json +146 -1
- package/samples/apps/use-all-bundled/package.json +1 -0
- package/samples/apps/use-all-bundled/playwright.billing-gallery.config.ts +14 -0
- package/samples/apps/use-all-bundled/schema/generate.ts +20 -0
- package/samples/apps/use-all-bundled/scripts/gen-feature-manifest.ts +1 -0
- package/samples/apps/use-all-bundled/src/run-config.ts +10 -0
- package/samples/apps/use-all-bundled/tsconfig.json +1 -0
- package/samples/recipes/chat-channels/README.md +19 -0
- package/samples/recipes/chat-channels/package.json +9 -0
- package/samples/recipes/chat-channels/src/feature.ts +40 -0
- package/samples/recipes/public-share-token/src/feature.ts +1 -1
- package/samples/recipes/workflow-engine/src/event-trigger.ts +5 -0
- package/samples/recipes/workflow-engine/src/postgres-resume-loop.ts +50 -9
- package/samples/recipes/workflow-engine/src/workflow-runner.ts +12 -2
package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { createSystemUser, type FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { SubscriptionCancelTimings, SubscriptionFoundationHandlers } from "../constants.js";
|
|
2
3
|
import {
|
|
4
|
+
CONTRACT_TERMINATION_DECLARED_EVENT_QN,
|
|
5
|
+
CONTRACT_TERMINATION_DECLARED_EVENT_SHORT,
|
|
3
6
|
CONTRACT_TERMINATION_REQUESTED_EVENT_SHORT,
|
|
4
7
|
CONTRACT_TERMINATION_UNMATCHED_EVENT_SHORT,
|
|
8
|
+
contractTerminationDeclaredPayloadSchema,
|
|
5
9
|
contractTerminationRequestedPayloadSchema,
|
|
6
10
|
contractTerminationUnmatchedPayloadSchema,
|
|
7
11
|
} from "../events.js";
|
|
8
12
|
import type { ConsumerProtectionOptions, ResolvedBillingFoundationOptions } from "../types.js";
|
|
9
13
|
import {
|
|
14
|
+
createDeclareContractTerminationHandler,
|
|
10
15
|
createRecordContractTerminationHandler,
|
|
11
16
|
createRecordUnmatchedContractTerminationHandler,
|
|
12
17
|
createTerminableSubscriptionQuery,
|
|
@@ -26,14 +31,55 @@ export function registerContractTermination(
|
|
|
26
31
|
contractTerminationRequestedPayloadSchema,
|
|
27
32
|
{ piiFields: "none" },
|
|
28
33
|
);
|
|
34
|
+
r.defineEvent(
|
|
35
|
+
CONTRACT_TERMINATION_DECLARED_EVENT_SHORT,
|
|
36
|
+
contractTerminationDeclaredPayloadSchema,
|
|
37
|
+
{ piiFields: "none" },
|
|
38
|
+
);
|
|
29
39
|
r.defineEvent(
|
|
30
40
|
CONTRACT_TERMINATION_UNMATCHED_EVENT_SHORT,
|
|
31
41
|
contractTerminationUnmatchedPayloadSchema,
|
|
32
42
|
{ piiFields: "none" },
|
|
33
43
|
);
|
|
34
44
|
r.queryHandler(createTerminableSubscriptionQuery());
|
|
35
|
-
r.writeHandler(
|
|
45
|
+
r.writeHandler(createDeclareContractTerminationHandler());
|
|
46
|
+
r.writeHandler(createRecordContractTerminationHandler(options, consumerProtection));
|
|
36
47
|
r.writeHandler(createRecordUnmatchedContractTerminationHandler());
|
|
37
48
|
r.writeHandler(createRequestContractTerminationHandler(options, consumerProtection));
|
|
38
49
|
r.writeHandler(createTerminateContractHandler(options, consumerProtection));
|
|
50
|
+
|
|
51
|
+
// The provider cancel of a public declaration runs here, not in the
|
|
52
|
+
// request: matched and unmatched requests then do the same work and the
|
|
53
|
+
// response time does not tell whether the email belongs to a customer.
|
|
54
|
+
// The tenant comes from the stored trigger event (the job's system user).
|
|
55
|
+
r.job(
|
|
56
|
+
"cancel-on-public-termination-declared",
|
|
57
|
+
{ trigger: { on: CONTRACT_TERMINATION_DECLARED_EVENT_QN }, runIn: "worker" },
|
|
58
|
+
async (payload, ctx) => {
|
|
59
|
+
const declared = contractTerminationDeclaredPayloadSchema.safeParse(payload);
|
|
60
|
+
// skip: trigger event carries no usable declaration
|
|
61
|
+
if (!declared.success) return;
|
|
62
|
+
const result = await ctx.writeAs(
|
|
63
|
+
createSystemUser(ctx.systemUser.tenantId),
|
|
64
|
+
SubscriptionFoundationHandlers.recordContractTermination,
|
|
65
|
+
{
|
|
66
|
+
requestId: declared.data.requestId,
|
|
67
|
+
declarationType: declared.data.declarationType,
|
|
68
|
+
terminationKind: declared.data.terminationKind,
|
|
69
|
+
channel: "public",
|
|
70
|
+
when:
|
|
71
|
+
declared.data.declarationType === "withdrawal"
|
|
72
|
+
? "none"
|
|
73
|
+
: SubscriptionCancelTimings.periodEnd,
|
|
74
|
+
receivedAtIso: declared.data.receivedAtIso,
|
|
75
|
+
locale: declared.data.locale,
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
if (!result.isSuccess) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`billing-foundation:cancel-on-public-termination-declared: record-contract-termination failed: ${JSON.stringify(result.error)}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
);
|
|
39
85
|
}
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts
CHANGED
|
@@ -24,6 +24,13 @@ export type TerminationDeclaration = {
|
|
|
24
24
|
readonly reason?: string;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
/** The operator notice for a provider problem has no declarant data at all. */
|
|
28
|
+
export type OperatorNoticeDeclaration = Pick<
|
|
29
|
+
TerminationDeclaration,
|
|
30
|
+
"declarationType" | "terminationKind"
|
|
31
|
+
> &
|
|
32
|
+
Partial<Omit<TerminationDeclaration, "declarationType" | "terminationKind">>;
|
|
33
|
+
|
|
27
34
|
const TIME_ZONE_BY_LOCALE: Readonly<Record<ConsentLocale, string>> = {
|
|
28
35
|
de: "Europe/Berlin",
|
|
29
36
|
en: "UTC",
|
|
@@ -44,7 +51,7 @@ export function formatReceivedAt(iso: string, locale: ConsentLocale): string {
|
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
function declarationLines(
|
|
47
|
-
declaration:
|
|
54
|
+
declaration: OperatorNoticeDeclaration,
|
|
48
55
|
locale: ConsentLocale,
|
|
49
56
|
): readonly { readonly text: string }[] {
|
|
50
57
|
const texts = TERMINATION_TEXTS[locale];
|
|
@@ -52,7 +59,7 @@ function declarationLines(
|
|
|
52
59
|
`${texts.declarationType}: ${texts.declarationTypeLabel[declaration.declarationType]}`,
|
|
53
60
|
`${texts.terminationKind}: ${texts.terminationKindLabel[declaration.terminationKind]}`,
|
|
54
61
|
...(declaration.name !== undefined ? [`${texts.name}: ${declaration.name}`] : []),
|
|
55
|
-
`${texts.email}: ${declaration.email}
|
|
62
|
+
...(declaration.email !== undefined ? [`${texts.email}: ${declaration.email}`] : []),
|
|
56
63
|
...(declaration.customerReference
|
|
57
64
|
? [`${texts.customerReference}: ${declaration.customerReference}`]
|
|
58
65
|
: []),
|
|
@@ -96,7 +103,7 @@ export type RenderOperatorNoticeArgs = {
|
|
|
96
103
|
readonly requestId: string;
|
|
97
104
|
readonly receivedAtIso: string;
|
|
98
105
|
readonly channel: "public" | "account";
|
|
99
|
-
readonly declaration:
|
|
106
|
+
readonly declaration: OperatorNoticeDeclaration;
|
|
100
107
|
readonly reasons: readonly OperatorNoticeReason[];
|
|
101
108
|
readonly tenantId?: string;
|
|
102
109
|
readonly providerCancel?: ProviderCancelOutcome;
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
+
import { CONTRACT_TERMINATION_OPERATOR_NOTIFICATION_TYPE } from "../constants.js";
|
|
4
|
+
import type { ProviderCancelOutcome } from "../events.js";
|
|
5
|
+
import type { ConsumerProtectionOptions } from "../types.js";
|
|
6
|
+
import type { ConsentLocale } from "./consent-text.js";
|
|
7
|
+
import { type OperatorNoticeDeclaration, renderOperatorNotice } from "./termination-mail.js";
|
|
8
|
+
import type { OperatorNoticeReason } from "./termination-texts.js";
|
|
9
|
+
|
|
10
|
+
export type OperatorNoticePlan = {
|
|
11
|
+
readonly locale: ConsentLocale;
|
|
12
|
+
readonly requestId: string;
|
|
13
|
+
readonly receivedAtIso: string;
|
|
14
|
+
readonly channel: "public" | "account";
|
|
15
|
+
readonly declaration: OperatorNoticeDeclaration;
|
|
16
|
+
readonly reasons: readonly OperatorNoticeReason[];
|
|
17
|
+
readonly tenantId?: string;
|
|
18
|
+
readonly providerCancel?: ProviderCancelOutcome;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function requireNotify(ctx: HandlerContext): NonNullable<HandlerContext["notify"]> {
|
|
22
|
+
if (!ctx.notify) {
|
|
23
|
+
throw new InternalError({
|
|
24
|
+
message:
|
|
25
|
+
"billing-foundation:termination: ctx.notify unavailable — the delivery feature must be mounted",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return ctx.notify;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function notifyOperator(
|
|
32
|
+
ctx: HandlerContext,
|
|
33
|
+
consumerProtection: ConsumerProtectionOptions,
|
|
34
|
+
plan: OperatorNoticePlan,
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
await requireNotify(ctx)(CONTRACT_TERMINATION_OPERATOR_NOTIFICATION_TYPE, {
|
|
37
|
+
route: { email: consumerProtection.operatorEmail },
|
|
38
|
+
data: renderOperatorNotice({
|
|
39
|
+
...plan,
|
|
40
|
+
operatorEmail: consumerProtection.operatorEmail,
|
|
41
|
+
}),
|
|
42
|
+
priority: "critical",
|
|
43
|
+
});
|
|
44
|
+
}
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { AnonymousExtraRoute } from "@cosmicdrift/kumiko-framework/api";
|
|
5
5
|
import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
6
6
|
import type { Context } from "hono";
|
|
7
|
+
import { getCookie } from "hono/cookie";
|
|
7
8
|
import { securePageHeaders, wrapInLayout } from "../../page-render/index.js";
|
|
8
9
|
import { SubscriptionFoundationHandlers } from "../constants.js";
|
|
9
10
|
import { CONTRACT_TERMINATION_DECLARATION_TYPES, CONTRACT_TERMINATION_KINDS } from "../events.js";
|
|
@@ -247,6 +248,33 @@ type WriteResponseBody = {
|
|
|
247
248
|
|
|
248
249
|
const RATE_LIMITED_CODE = "rate_limited";
|
|
249
250
|
|
|
251
|
+
const TENANT_HEADER = "x-tenant";
|
|
252
|
+
const TENANT_COOKIE = "kumiko_tenant";
|
|
253
|
+
// Exactly what the anonymous tenant resolution reads. The resolver and its
|
|
254
|
+
// trust rules still decide; the re-entry only sees what a direct /api/write
|
|
255
|
+
// from this client would see. Never the session cookie or Authorization (the
|
|
256
|
+
// handler is anonymous) and never X-Forwarded-For (the resolved clientIp
|
|
257
|
+
// travels as env).
|
|
258
|
+
const FORWARDED_TENANT_HEADERS = [
|
|
259
|
+
"host",
|
|
260
|
+
"x-forwarded-host",
|
|
261
|
+
"x-forwarded-proto",
|
|
262
|
+
TENANT_HEADER,
|
|
263
|
+
] as const;
|
|
264
|
+
|
|
265
|
+
function reentryHeaders(c: Context): Headers {
|
|
266
|
+
const headers = new Headers({ "content-type": "application/json" });
|
|
267
|
+
for (const name of FORWARDED_TENANT_HEADERS) {
|
|
268
|
+
const value = c.req.header(name);
|
|
269
|
+
if (value !== undefined) headers.set(name, value);
|
|
270
|
+
}
|
|
271
|
+
const tenantCookie = getCookie(c, TENANT_COOKIE);
|
|
272
|
+
if (tenantCookie !== undefined) {
|
|
273
|
+
headers.set("cookie", `${TENANT_COOKIE}=${encodeURIComponent(tenantCookie)}`);
|
|
274
|
+
}
|
|
275
|
+
return headers;
|
|
276
|
+
}
|
|
277
|
+
|
|
250
278
|
async function submitDeclaration(
|
|
251
279
|
c: Context,
|
|
252
280
|
app: { fetch: (request: Request, env?: unknown) => Response | Promise<Response> },
|
|
@@ -261,11 +289,13 @@ async function submitDeclaration(
|
|
|
261
289
|
// deps.write only resolves a session user under /api/*; this page lives
|
|
262
290
|
// outside it, so it re-enters the dispatcher through the app like the
|
|
263
291
|
// user-export by-token route. The resolved clientIp as env keeps the
|
|
264
|
-
// handler's per-IP rate limit per visitor instead of one shared bucket
|
|
292
|
+
// handler's per-IP rate limit per visitor instead of one shared bucket;
|
|
293
|
+
// the forwarded host headers let a host-based tenant resolver see this
|
|
294
|
+
// visitor's host.
|
|
265
295
|
const response = await app.fetch(
|
|
266
296
|
new Request(`${new URL(c.req.url).origin}/api/write`, {
|
|
267
297
|
method: "POST",
|
|
268
|
-
headers:
|
|
298
|
+
headers: reentryHeaders(c),
|
|
269
299
|
body: JSON.stringify({
|
|
270
300
|
type: SubscriptionFoundationHandlers.requestContractTermination,
|
|
271
301
|
payload: payloadOf(values, locale),
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
access,
|
|
8
|
+
type HandlerContext,
|
|
8
9
|
type QueryHandlerDef,
|
|
9
10
|
SYSTEM_ROLE,
|
|
10
11
|
type WriteHandlerDef,
|
|
@@ -17,17 +18,22 @@ import { isTerminalSubscriptionStatus, SubscriptionCancelTimings } from "../cons
|
|
|
17
18
|
import {
|
|
18
19
|
CONTRACT_TERMINATION_CHANNELS,
|
|
19
20
|
CONTRACT_TERMINATION_DECLARATION_TYPES,
|
|
21
|
+
CONTRACT_TERMINATION_DECLARED_EVENT_QN,
|
|
20
22
|
CONTRACT_TERMINATION_KINDS,
|
|
21
23
|
CONTRACT_TERMINATION_REQUESTED_EVENT_QN,
|
|
22
24
|
CONTRACT_TERMINATION_UNMATCHED_AGGREGATE_TYPE,
|
|
23
25
|
CONTRACT_TERMINATION_UNMATCHED_EVENT_QN,
|
|
26
|
+
type ContractTerminationDeclaredPayload,
|
|
24
27
|
type ContractTerminationRequestedPayload,
|
|
25
28
|
type ContractTerminationUnmatchedPayload,
|
|
29
|
+
contractTerminationRequestedPayloadSchema,
|
|
26
30
|
type ProviderCancelOutcome,
|
|
27
31
|
SUBSCRIPTION_AGGREGATE_TYPE,
|
|
28
32
|
} from "../events.js";
|
|
29
33
|
import { getSubscriptionForTenant } from "../get-subscription-for-tenant.js";
|
|
30
|
-
import type { ResolvedBillingFoundationOptions } from "../types.js";
|
|
34
|
+
import type { ConsumerProtectionOptions, ResolvedBillingFoundationOptions } from "../types.js";
|
|
35
|
+
import { CONSENT_LOCALES, FALLBACK_CONSENT_LOCALE } from "./consent-locale.js";
|
|
36
|
+
import { notifyOperator } from "./termination-notify.js";
|
|
31
37
|
import type { OperatorNoticeReason } from "./termination-texts.js";
|
|
32
38
|
|
|
33
39
|
const REQUESTED_CANCEL_TIMINGS = [
|
|
@@ -43,6 +49,20 @@ export const recordContractTerminationSchema = z
|
|
|
43
49
|
terminationKind: z.enum(CONTRACT_TERMINATION_KINDS),
|
|
44
50
|
channel: z.enum(CONTRACT_TERMINATION_CHANNELS),
|
|
45
51
|
when: z.enum(REQUESTED_CANCEL_TIMINGS),
|
|
52
|
+
// The public job passes the time the declaration arrived and the declarant's
|
|
53
|
+
// locale, so the record and a provider-problem notice do not take the job's.
|
|
54
|
+
receivedAtIso: z.string().min(1).optional(),
|
|
55
|
+
locale: z.enum(CONSENT_LOCALES).optional(),
|
|
56
|
+
})
|
|
57
|
+
.strict();
|
|
58
|
+
|
|
59
|
+
export const declareContractTerminationSchema = z
|
|
60
|
+
.object({
|
|
61
|
+
requestId: z.string().min(1).max(100),
|
|
62
|
+
declarationType: z.enum(CONTRACT_TERMINATION_DECLARATION_TYPES),
|
|
63
|
+
terminationKind: z.enum(CONTRACT_TERMINATION_KINDS),
|
|
64
|
+
receivedAtIso: z.string().min(1),
|
|
65
|
+
locale: z.enum(CONSENT_LOCALES),
|
|
46
66
|
})
|
|
47
67
|
.strict();
|
|
48
68
|
|
|
@@ -85,8 +105,71 @@ export function createTerminableSubscriptionQuery(): QueryHandlerDef {
|
|
|
85
105
|
};
|
|
86
106
|
}
|
|
87
107
|
|
|
108
|
+
type ProviderCancelResult = Pick<
|
|
109
|
+
RecordContractTerminationResult,
|
|
110
|
+
"providerCancel" | "effectiveAtIso" | "noticeReason"
|
|
111
|
+
>;
|
|
112
|
+
|
|
113
|
+
async function cancelAtProvider(
|
|
114
|
+
ctx: HandlerContext,
|
|
115
|
+
tenantId: string,
|
|
116
|
+
when: (typeof REQUESTED_CANCEL_TIMINGS)[number],
|
|
117
|
+
receivedAtIso: string,
|
|
118
|
+
): Promise<ProviderCancelResult> {
|
|
119
|
+
// skip: a withdrawal over the public channel makes no provider call
|
|
120
|
+
if (when === "none") return { providerCancel: "none", effectiveAtIso: null, noticeReason: null };
|
|
121
|
+
const subscription = await getSubscriptionForTenant(ctx, tenantId);
|
|
122
|
+
if (subscription === null || isTerminalSubscriptionStatus(subscription.status)) {
|
|
123
|
+
return { providerCancel: "none", effectiveAtIso: null, noticeReason: "no_active_subscription" };
|
|
124
|
+
}
|
|
125
|
+
const plugin = findPlugin(ctx, subscription.providerName);
|
|
126
|
+
if (!plugin?.cancelSubscription) {
|
|
127
|
+
return { providerCancel: "none", effectiveAtIso: null, noticeReason: "provider_cannot_cancel" };
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
await plugin.cancelSubscription(ctx, {
|
|
131
|
+
providerSubscriptionId: subscription.providerSubscriptionId,
|
|
132
|
+
when,
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
ctx.log?.warn(
|
|
136
|
+
`[billing-foundation:record-contract-termination] provider cancel failed for tenant ${tenantId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
137
|
+
);
|
|
138
|
+
return { providerCancel: "none", effectiveAtIso: null, noticeReason: "provider_error" };
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
providerCancel: when,
|
|
142
|
+
effectiveAtIso:
|
|
143
|
+
when === SubscriptionCancelTimings.immediately
|
|
144
|
+
? receivedAtIso
|
|
145
|
+
: subscription.currentPeriodEnd.toString(),
|
|
146
|
+
noticeReason: null,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function createDeclareContractTerminationHandler(): WriteHandlerDef {
|
|
151
|
+
return {
|
|
152
|
+
name: "declare-contract-termination",
|
|
153
|
+
agent: { expose: false },
|
|
154
|
+
schema: declareContractTerminationSchema,
|
|
155
|
+
access: { roles: access.system },
|
|
156
|
+
handler: async (event, ctx) => {
|
|
157
|
+
// @cast-boundary engine-payload — dispatcher-zod-validated payload
|
|
158
|
+
const payload = event.payload as z.infer<typeof declareContractTerminationSchema>;
|
|
159
|
+
const stream = await ctx.fetchForWriting({
|
|
160
|
+
aggregateId: subscriptionAggregateId(event.user.tenantId),
|
|
161
|
+
aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
|
|
162
|
+
});
|
|
163
|
+
const declared: ContractTerminationDeclaredPayload = payload;
|
|
164
|
+
await stream.appendOne({ type: CONTRACT_TERMINATION_DECLARED_EVENT_QN, payload: declared });
|
|
165
|
+
return { isSuccess: true as const, data: { declared: true } };
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
88
170
|
export function createRecordContractTerminationHandler(
|
|
89
171
|
options: ResolvedBillingFoundationOptions,
|
|
172
|
+
consumerProtection: ConsumerProtectionOptions,
|
|
90
173
|
): WriteHandlerDef {
|
|
91
174
|
return {
|
|
92
175
|
name: "record-contract-termination",
|
|
@@ -104,45 +187,40 @@ export function createRecordContractTerminationHandler(
|
|
|
104
187
|
});
|
|
105
188
|
}
|
|
106
189
|
|
|
107
|
-
const receivedAtIso = options.now().toString();
|
|
190
|
+
const receivedAtIso = payload.receivedAtIso ?? options.now().toString();
|
|
108
191
|
const stream = await ctx.fetchForWriting({
|
|
109
192
|
aggregateId: subscriptionAggregateId(tenantId),
|
|
110
193
|
aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
|
|
111
194
|
});
|
|
112
195
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
effectiveAtIso =
|
|
133
|
-
payload.when === SubscriptionCancelTimings.immediately
|
|
134
|
-
? receivedAtIso
|
|
135
|
-
: subscription.currentPeriodEnd.toString();
|
|
136
|
-
} catch (error) {
|
|
137
|
-
ctx.log?.warn(
|
|
138
|
-
`[billing-foundation:record-contract-termination] provider cancel failed for tenant ${tenantId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
139
|
-
);
|
|
140
|
-
noticeReason = "provider_error";
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
196
|
+
// The public job runs at least once: a second run must not call the
|
|
197
|
+
// provider or append again.
|
|
198
|
+
const alreadyRecorded = stream.events
|
|
199
|
+
.map((e) =>
|
|
200
|
+
e.type === CONTRACT_TERMINATION_REQUESTED_EVENT_QN
|
|
201
|
+
? contractTerminationRequestedPayloadSchema.safeParse(e.payload).data
|
|
202
|
+
: undefined,
|
|
203
|
+
)
|
|
204
|
+
.find((recordedPayload) => recordedPayload?.requestId === payload.requestId);
|
|
205
|
+
if (alreadyRecorded) {
|
|
206
|
+
return {
|
|
207
|
+
isSuccess: true as const,
|
|
208
|
+
data: {
|
|
209
|
+
receivedAtIso: alreadyRecorded.receivedAtIso,
|
|
210
|
+
effectiveAtIso: alreadyRecorded.effectiveAtIso,
|
|
211
|
+
providerCancel: alreadyRecorded.providerCancel,
|
|
212
|
+
noticeReason: null,
|
|
213
|
+
} satisfies RecordContractTerminationResult,
|
|
214
|
+
};
|
|
144
215
|
}
|
|
145
216
|
|
|
217
|
+
const { providerCancel, effectiveAtIso, noticeReason } = await cancelAtProvider(
|
|
218
|
+
ctx,
|
|
219
|
+
tenantId,
|
|
220
|
+
payload.when,
|
|
221
|
+
receivedAtIso,
|
|
222
|
+
);
|
|
223
|
+
|
|
146
224
|
const recorded: ContractTerminationRequestedPayload = {
|
|
147
225
|
requestId: payload.requestId,
|
|
148
226
|
declarationType: payload.declarationType,
|
|
@@ -157,6 +235,24 @@ export function createRecordContractTerminationHandler(
|
|
|
157
235
|
payload: recorded,
|
|
158
236
|
});
|
|
159
237
|
|
|
238
|
+
// The account path reports the provider outcome to its caller and the
|
|
239
|
+
// operator in its own mail; the public path has nobody else to tell.
|
|
240
|
+
if (payload.channel === "public" && noticeReason !== null) {
|
|
241
|
+
await notifyOperator(ctx, consumerProtection, {
|
|
242
|
+
locale: payload.locale ?? FALLBACK_CONSENT_LOCALE,
|
|
243
|
+
requestId: payload.requestId,
|
|
244
|
+
receivedAtIso,
|
|
245
|
+
channel: "public",
|
|
246
|
+
declaration: {
|
|
247
|
+
declarationType: payload.declarationType,
|
|
248
|
+
terminationKind: payload.terminationKind,
|
|
249
|
+
},
|
|
250
|
+
reasons: [noticeReason],
|
|
251
|
+
tenantId,
|
|
252
|
+
providerCancel,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
160
256
|
return {
|
|
161
257
|
isSuccess: true as const,
|
|
162
258
|
data: {
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts
CHANGED
|
@@ -16,7 +16,6 @@ import * as z from "zod";
|
|
|
16
16
|
import { TenantQueries } from "../../tenant/index.js";
|
|
17
17
|
import { UserQueries } from "../../user/index.js";
|
|
18
18
|
import {
|
|
19
|
-
CONTRACT_TERMINATION_OPERATOR_NOTIFICATION_TYPE,
|
|
20
19
|
CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE,
|
|
21
20
|
SubscriptionCancelTimings,
|
|
22
21
|
SubscriptionFoundationHandlers,
|
|
@@ -31,12 +30,9 @@ import {
|
|
|
31
30
|
} from "../events.js";
|
|
32
31
|
import { purchaseRolesOf } from "../plan-catalog.js";
|
|
33
32
|
import type { ConsumerProtectionOptions, ResolvedBillingFoundationOptions } from "../types.js";
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
|
|
37
|
-
renderTerminationReceipt,
|
|
38
|
-
type TerminationDeclaration,
|
|
39
|
-
} from "./termination-mail.js";
|
|
33
|
+
import { resolveConsentLocale } from "./consent-text.js";
|
|
34
|
+
import { renderTerminationReceipt, type TerminationDeclaration } from "./termination-mail.js";
|
|
35
|
+
import { notifyOperator, type OperatorNoticePlan, requireNotify } from "./termination-notify.js";
|
|
40
36
|
import type { RecordContractTerminationResult } from "./termination-record.js";
|
|
41
37
|
import type { OperatorNoticeReason } from "./termination-texts.js";
|
|
42
38
|
|
|
@@ -106,29 +102,14 @@ function needsOperatorNotice(declaration: {
|
|
|
106
102
|
];
|
|
107
103
|
}
|
|
108
104
|
|
|
109
|
-
type TerminationMailPlan = {
|
|
110
|
-
readonly locale: ConsentLocale;
|
|
111
|
-
readonly requestId: string;
|
|
112
|
-
readonly receivedAtIso: string;
|
|
113
|
-
readonly channel: "public" | "account";
|
|
114
|
-
readonly declaration: TerminationDeclaration;
|
|
115
|
-
readonly reasons: readonly OperatorNoticeReason[];
|
|
116
|
-
readonly tenantId?: string;
|
|
117
|
-
readonly providerCancel?: ProviderCancelOutcome;
|
|
118
|
-
};
|
|
105
|
+
type TerminationMailPlan = OperatorNoticePlan & { readonly declaration: TerminationDeclaration };
|
|
119
106
|
|
|
120
107
|
async function sendTerminationMails(
|
|
121
108
|
ctx: HandlerContext,
|
|
122
109
|
consumerProtection: ConsumerProtectionOptions,
|
|
123
110
|
plan: TerminationMailPlan,
|
|
124
111
|
): Promise<void> {
|
|
125
|
-
|
|
126
|
-
throw new InternalError({
|
|
127
|
-
message:
|
|
128
|
-
"billing-foundation:termination: ctx.notify unavailable — the delivery feature must be mounted",
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
await ctx.notify(CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE, {
|
|
112
|
+
await requireNotify(ctx)(CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE, {
|
|
132
113
|
route: { email: plan.declaration.email },
|
|
133
114
|
data: renderTerminationReceipt({
|
|
134
115
|
locale: plan.locale,
|
|
@@ -141,21 +122,7 @@ async function sendTerminationMails(
|
|
|
141
122
|
});
|
|
142
123
|
// skip: no operator notice needed for this declaration
|
|
143
124
|
if (plan.reasons.length === 0) return;
|
|
144
|
-
await ctx
|
|
145
|
-
route: { email: consumerProtection.operatorEmail },
|
|
146
|
-
data: renderOperatorNotice({
|
|
147
|
-
locale: plan.locale,
|
|
148
|
-
requestId: plan.requestId,
|
|
149
|
-
receivedAtIso: plan.receivedAtIso,
|
|
150
|
-
channel: plan.channel,
|
|
151
|
-
declaration: plan.declaration,
|
|
152
|
-
reasons: plan.reasons,
|
|
153
|
-
operatorEmail: consumerProtection.operatorEmail,
|
|
154
|
-
...(plan.tenantId !== undefined && { tenantId: plan.tenantId }),
|
|
155
|
-
...(plan.providerCancel !== undefined && { providerCancel: plan.providerCancel }),
|
|
156
|
-
}),
|
|
157
|
-
priority: "critical",
|
|
158
|
-
});
|
|
125
|
+
await notifyOperator(ctx, consumerProtection, plan);
|
|
159
126
|
}
|
|
160
127
|
|
|
161
128
|
// Tenants the entered email can terminate a contract for: tenants where the
|
|
@@ -212,6 +179,7 @@ export function createRequestContractTerminationHandler(
|
|
|
212
179
|
schema: requestContractTerminationSchema,
|
|
213
180
|
access: { roles: ["anonymous"] },
|
|
214
181
|
rateLimit: { per: "ip+handler", limit: 5, windowSeconds: 600 },
|
|
182
|
+
...(consumerProtection.terminationScope === "platform" && { tenantlessAnonymous: true }),
|
|
215
183
|
escapeHatch: {
|
|
216
184
|
reason:
|
|
217
185
|
"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.",
|
|
@@ -223,32 +191,19 @@ export function createRequestContractTerminationHandler(
|
|
|
223
191
|
const receivedAtIso = options.now().toString();
|
|
224
192
|
const { declarationType, terminationKind } = payload;
|
|
225
193
|
const reasons: OperatorNoticeReason[] = [];
|
|
194
|
+
const locale = resolveConsentLocale(payload.locale);
|
|
226
195
|
let matchedTenantId: string | undefined;
|
|
227
|
-
let providerCancel: ProviderCancelOutcome | undefined;
|
|
228
196
|
|
|
229
197
|
const candidates = await findTerminableTenantIds(ctx, event.user.tenantId, payload.email);
|
|
230
198
|
const [onlyCandidate] = candidates;
|
|
231
199
|
if (candidates.length === 1 && onlyCandidate !== undefined) {
|
|
232
200
|
matchedTenantId = onlyCandidate;
|
|
233
|
-
const
|
|
201
|
+
const declared = await ctx.writeAs(
|
|
234
202
|
createSystemUser(onlyCandidate),
|
|
235
|
-
SubscriptionFoundationHandlers.
|
|
236
|
-
{
|
|
237
|
-
requestId,
|
|
238
|
-
declarationType,
|
|
239
|
-
terminationKind,
|
|
240
|
-
channel: "public",
|
|
241
|
-
when: declarationType === "withdrawal" ? "none" : SubscriptionCancelTimings.periodEnd,
|
|
242
|
-
},
|
|
203
|
+
SubscriptionFoundationHandlers.declareContractTermination,
|
|
204
|
+
{ requestId, declarationType, terminationKind, receivedAtIso, locale },
|
|
243
205
|
);
|
|
244
|
-
if (
|
|
245
|
-
// @cast-boundary engine-payload — our own record handler's return shape
|
|
246
|
-
const data = recorded.data as RecordContractTerminationResult;
|
|
247
|
-
providerCancel = data.providerCancel;
|
|
248
|
-
if (data.noticeReason !== null) reasons.push(data.noticeReason);
|
|
249
|
-
} else {
|
|
250
|
-
reasons.push("recording_failed");
|
|
251
|
-
}
|
|
206
|
+
if (!declared.isSuccess) reasons.push("recording_failed");
|
|
252
207
|
} else {
|
|
253
208
|
reasons.push(candidates.length === 0 ? "unmatched" : "ambiguous");
|
|
254
209
|
const recorded = await ctx.writeAs(
|
|
@@ -267,7 +222,7 @@ export function createRequestContractTerminationHandler(
|
|
|
267
222
|
reasons.push(...needsOperatorNotice(payload));
|
|
268
223
|
|
|
269
224
|
await sendTerminationMails(ctx, consumerProtection, {
|
|
270
|
-
locale
|
|
225
|
+
locale,
|
|
271
226
|
requestId,
|
|
272
227
|
receivedAtIso,
|
|
273
228
|
channel: "public",
|
|
@@ -283,7 +238,6 @@ export function createRequestContractTerminationHandler(
|
|
|
283
238
|
},
|
|
284
239
|
reasons,
|
|
285
240
|
...(matchedTenantId !== undefined && { tenantId: matchedTenantId }),
|
|
286
|
-
...(providerCancel !== undefined && { providerCancel }),
|
|
287
241
|
});
|
|
288
242
|
|
|
289
243
|
return {
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts
CHANGED
|
@@ -90,21 +90,21 @@ export const TERMINATION_TEXTS: Readonly<Record<ConsentLocale, TerminationTexts>
|
|
|
90
90
|
customerReference: "Kundennummer / Referenz",
|
|
91
91
|
reasonFieldLabel: "Begründung",
|
|
92
92
|
receiptSubject: {
|
|
93
|
-
termination: "Eingangsbestätigung
|
|
94
|
-
withdrawal: "Eingangsbestätigung
|
|
93
|
+
termination: "Eingangsbestätigung deiner Kündigung",
|
|
94
|
+
withdrawal: "Eingangsbestätigung deines Widerrufs",
|
|
95
95
|
},
|
|
96
96
|
receiptIntro:
|
|
97
|
-
"wir bestätigen den Eingang
|
|
97
|
+
"wir bestätigen den Eingang deiner Erklärung mit folgendem Inhalt. Diese E-Mail ist eine Eingangsbestätigung, keine Bestätigung der Wirksamkeit.",
|
|
98
98
|
receiptNextSteps:
|
|
99
|
-
"Wir prüfen
|
|
99
|
+
"Wir prüfen deine Erklärung und melden uns, sobald sie bearbeitet ist oder wir Rückfragen haben.",
|
|
100
100
|
receiptRevoke: (operatorEmail) =>
|
|
101
|
-
`
|
|
102
|
-
contact: "Fragen
|
|
101
|
+
`Hast du diese Erklärung nicht abgegeben oder möchtest du sie zurücknehmen? Schreib uns bitte umgehend an ${operatorEmail}.`,
|
|
102
|
+
contact: "Fragen richtest du bitte an",
|
|
103
103
|
operatorSubject: {
|
|
104
104
|
termination: "Kündigung: Bearbeitung nötig",
|
|
105
105
|
withdrawal: "Widerruf: Bearbeitung nötig",
|
|
106
106
|
},
|
|
107
|
-
operatorIntro: "Es ist eine Erklärung eingegangen, die
|
|
107
|
+
operatorIntro: "Es ist eine Erklärung eingegangen, die bearbeitet werden muss.",
|
|
108
108
|
operatorReasons: {
|
|
109
109
|
unmatched: "Kein passender Vertrag zur angegebenen E-Mail gefunden",
|
|
110
110
|
ambiguous: "Die angegebene E-Mail passt zu mehreren Verträgen",
|
|
@@ -127,8 +127,8 @@ export const TERMINATION_TEXTS: Readonly<Record<ConsentLocale, TerminationTexts>
|
|
|
127
127
|
},
|
|
128
128
|
page: {
|
|
129
129
|
title: "Vertrag kündigen oder widerrufen",
|
|
130
|
-
lead: "Mit diesem Formular
|
|
131
|
-
declarationTypeHeading: "Was
|
|
130
|
+
lead: "Mit diesem Formular erklärst du die Kündigung oder den Widerruf deines Vertrags. Du brauchst dafür kein Kundenkonto.",
|
|
131
|
+
declarationTypeHeading: "Was möchtest du erklären?",
|
|
132
132
|
kindHeading: "Art der Kündigung",
|
|
133
133
|
reasonHint: "Pflicht bei außerordentlicher Kündigung.",
|
|
134
134
|
earliestDate: "Die Kündigung gilt zum nächstmöglichen Zeitpunkt.",
|
|
@@ -137,25 +137,24 @@ export const TERMINATION_TEXTS: Readonly<Record<ConsentLocale, TerminationTexts>
|
|
|
137
137
|
customerReferenceLabel: "Kundennummer (optional)",
|
|
138
138
|
reasonLabel: "Begründung",
|
|
139
139
|
reviewButton: "Weiter zur Prüfung",
|
|
140
|
-
reviewTitle: "Bitte
|
|
141
|
-
reviewLead: "Erst mit dem Klick auf den Button unten wird
|
|
140
|
+
reviewTitle: "Bitte prüfe deine Angaben",
|
|
141
|
+
reviewLead: "Erst mit dem Klick auf den Button unten wird deine Erklärung abgegeben.",
|
|
142
142
|
confirmButton: { termination: "Jetzt kündigen", withdrawal: "Jetzt widerrufen" },
|
|
143
143
|
backButton: "Angaben ändern",
|
|
144
|
-
resultTitle: "
|
|
145
|
-
resultLead: "Wir haben
|
|
144
|
+
resultTitle: "Deine Erklärung ist eingegangen",
|
|
145
|
+
resultLead: "Wir haben deine Erklärung erhalten und senden dir eine Eingangsbestätigung.",
|
|
146
146
|
resultReceivedAt: "Eingegangen am",
|
|
147
147
|
resultRequestId: "Vorgangsnummer",
|
|
148
|
-
resultMailNote: "Die Eingangsbestätigung geht an die von
|
|
149
|
-
errorsHeading: "Bitte
|
|
150
|
-
errorName: "Bitte
|
|
151
|
-
errorEmail: "Bitte
|
|
148
|
+
resultMailNote: "Die Eingangsbestätigung geht an die von dir angegebene E-Mail-Adresse.",
|
|
149
|
+
errorsHeading: "Bitte korrigiere deine Angaben",
|
|
150
|
+
errorName: "Bitte gib deinen Namen an.",
|
|
151
|
+
errorEmail: "Bitte gib eine gültige E-Mail-Adresse an.",
|
|
152
152
|
errorReason: "Bei einer außerordentlichen Kündigung ist eine Begründung nötig.",
|
|
153
|
-
errorChoice: "Bitte
|
|
154
|
-
errorGeneric:
|
|
155
|
-
"Ihre Erklärung konnte nicht verarbeitet werden. Bitte versuchen Sie es erneut.",
|
|
153
|
+
errorChoice: "Bitte wähle eine Erklärung und die Art der Kündigung.",
|
|
154
|
+
errorGeneric: "Deine Erklärung konnte nicht verarbeitet werden. Bitte versuch es erneut.",
|
|
156
155
|
rateLimitedTitle: "Zu viele Anfragen",
|
|
157
156
|
rateLimitedBody:
|
|
158
|
-
"Von
|
|
157
|
+
"Von deiner Adresse kamen zu viele Anfragen. Bitte versuch es später erneut.",
|
|
159
158
|
},
|
|
160
159
|
},
|
|
161
160
|
en: {
|