@cosmicdrift/kumiko-bundled-features 0.337.1 → 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/dist/agent-tools/tool-catalog.js +37 -17
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +3 -2
- package/dist/auth-email-password/handlers/invite-accept.write.js +3 -4
- package/dist/auth-email-password/handlers/invite-info.query.js +4 -1
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +3 -4
- package/dist/auth-email-password/handlers/signup-confirm.write.js +2 -1
- package/dist/auth-email-password/handlers/signup-request.write.js +2 -2
- 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 +2 -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/aggregate-id.d.ts +2 -0
- package/dist/billing-foundation/aggregate-id.js +8 -0
- package/dist/billing-foundation/checkout-core.d.ts +3 -0
- package/dist/billing-foundation/checkout-core.js +3 -0
- package/dist/billing-foundation/constants.d.ts +38 -0
- package/dist/billing-foundation/constants.js +43 -0
- package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +21 -0
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +125 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +32 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.js +96 -0
- package/dist/billing-foundation/consumer-protection/consent-locale.d.ts +5 -0
- package/dist/billing-foundation/consumer-protection/consent-locale.js +17 -0
- package/dist/billing-foundation/consumer-protection/consent-text.d.ts +11 -0
- package/dist/billing-foundation/consumer-protection/consent-text.js +28 -0
- package/dist/billing-foundation/consumer-protection/issue-confirmation.d.ts +15 -0
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +149 -0
- package/dist/billing-foundation/consumer-protection/register-consent.d.ts +3 -0
- package/dist/billing-foundation/consumer-protection/register-consent.js +63 -0
- package/dist/billing-foundation/consumer-protection/register-termination.d.ts +3 -0
- package/dist/billing-foundation/consumer-protection/register-termination.js +40 -0
- package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +45 -0
- package/dist/billing-foundation/consumer-protection/termination-mail.js +76 -0
- 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.d.ts +8 -0
- package/dist/billing-foundation/consumer-protection/termination-pages.js +268 -0
- package/dist/billing-foundation/consumer-protection/termination-record.d.ts +73 -0
- package/dist/billing-foundation/consumer-protection/termination-record.js +230 -0
- package/dist/billing-foundation/consumer-protection/termination-request.d.ts +45 -0
- package/dist/billing-foundation/consumer-protection/termination-request.js +231 -0
- package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +62 -0
- package/dist/billing-foundation/consumer-protection/termination-texts.js +168 -0
- package/dist/billing-foundation/events.d.ts +105 -2
- package/dist/billing-foundation/events.js +88 -2
- package/dist/billing-foundation/feature.js +6 -2
- package/dist/billing-foundation/handlers/billing-plans.query.js +19 -1
- package/dist/billing-foundation/handlers/create-checkout-session.write.js +33 -2
- package/dist/billing-foundation/handlers/process-event.write.d.ts +2 -2
- package/dist/billing-foundation/handlers/process-event.write.js +43 -15
- package/dist/billing-foundation/handlers/process-payment-event.write.d.ts +2 -1
- package/dist/billing-foundation/handlers/process-payment-event.write.js +6 -2
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +17 -1
- package/dist/billing-foundation/handlers/sync-subscription.write.js +0 -1
- package/dist/billing-foundation/i18n.js +61 -0
- package/dist/billing-foundation/index.d.ts +3 -2
- package/dist/billing-foundation/index.js +2 -1
- package/dist/billing-foundation/tenant-destroy-hook.js +4 -4
- package/dist/billing-foundation/types.d.ts +54 -9
- package/dist/billing-foundation/validate-options.js +51 -10
- package/dist/billing-foundation/web/billing-plans-panel.js +40 -3
- package/dist/billing-foundation/web/cancel-contract-dialog.d.ts +7 -0
- package/dist/billing-foundation/web/cancel-contract-dialog.js +57 -0
- package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +28 -0
- package/dist/billing-foundation/web/checkout-consent-dialog.js +38 -0
- package/dist/billing-foundation/webhook-handler.js +5 -2
- package/dist/cap-counter/with-cap-enforcement.js +7 -3
- package/dist/cap-overview/access.d.ts +2 -1
- package/dist/cap-overview/access.js +26 -5
- package/dist/cap-overview/feature.d.ts +4 -0
- package/dist/cap-overview/feature.js +8 -6
- package/dist/cap-overview/handlers/caps-usage.query.d.ts +1 -1
- package/dist/cap-overview/handlers/caps-usage.query.js +6 -4
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +12 -7
- package/dist/cap-overview/index.d.ts +2 -2
- package/dist/cap-overview/index.js +2 -2
- package/dist/cap-overview/screens.d.ts +1 -1
- package/dist/cap-overview/screens.js +17 -16
- 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/config/handlers/set.write.js +18 -2
- package/dist/crypto-shredding/feature.js +3 -2
- package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +1 -1
- package/dist/crypto-shredding/handlers/forget-subject.write.js +30 -19
- package/dist/custom-fields/feature.js +9 -6
- package/dist/custom-fields/handlers/clear-custom-field.write.js +2 -1
- package/dist/custom-fields/handlers/define-tenant-field.write.js +2 -1
- package/dist/custom-fields/handlers/set-custom-field.write.js +2 -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 +12 -7
- 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/document-ingest-foundation/feature.js +3 -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 +2 -1
- package/dist/form-draft/handlers/save.write.js +2 -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 +2 -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/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 +2 -1
- package/dist/notes-history/executor.d.ts +2 -0
- package/dist/notes-history/executor.js +9 -1
- package/dist/notes-history/feature.d.ts +2 -3
- package/dist/notes-history/feature.js +2 -1
- package/dist/notes-history/handlers/add-note.write.d.ts +2 -1
- package/dist/notes-history/handlers/add-note.write.js +21 -4
- package/dist/notes-history-user-data/hooks.js +26 -8
- 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 +2 -1
- package/dist/subscription-mollie/plugin-methods.js +1 -0
- package/dist/subscription-mollie/verify-webhook.js +5 -3
- package/dist/subscription-stripe/plugin-methods.d.ts +3 -1
- package/dist/subscription-stripe/plugin-methods.js +78 -13
- package/dist/subscription-stripe/verify-webhook.js +7 -3
- package/dist/tags/executor.d.ts +2 -0
- package/dist/tags/executor.js +9 -1
- package/dist/tags/feature.d.ts +4 -3
- package/dist/tags/feature.js +4 -2
- package/dist/tags/handlers/assign-tag.write.d.ts +2 -1
- package/dist/tags/handlers/assign-tag.write.js +4 -4
- package/dist/tags/handlers/remove-tag.write.d.ts +2 -1
- package/dist/tags/handlers/remove-tag.write.js +3 -3
- package/dist/tenant-handover/handlers/claim.write.js +2 -2
- package/dist/tenant-lifecycle/feature.js +1 -1
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +4 -1
- package/dist/tenant-lifecycle/handlers/request-destruction.write.js +2 -1
- package/dist/tier-engine/feature.js +1 -1
- package/dist/tier-engine/handlers/get-tenant-tier.query.js +2 -1
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +2 -1
- package/dist/user/handlers/detail.query.js +1 -0
- package/dist/user/handlers/list.query.js +1 -0
- package/dist/user-data-rights/feature.js +2 -2
- package/dist/user-data-rights/handlers/cancel-deletion.write.js +2 -1
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +1 -1
- package/dist/user-data-rights/handlers/download-attempt-list.query.js +1 -0
- package/dist/user-data-rights/handlers/download-by-job.query.js +2 -1
- package/dist/user-data-rights/handlers/download-by-token.query.js +2 -1
- package/dist/user-data-rights/handlers/export-status.query.js +2 -1
- package/dist/user-data-rights/handlers/lift-restriction.write.js +3 -4
- package/dist/user-data-rights/handlers/my-audit-log.query.js +2 -1
- package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +2 -1
- package/dist/user-data-rights/handlers/request-deletion.write.js +1 -1
- package/dist/user-data-rights/handlers/request-export.write.js +2 -1
- package/dist/user-data-rights/handlers/restrict-account.write.js +2 -2
- package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +2 -1
- package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
- package/dist/user-data-rights/run-forget-cleanup.js +30 -15
- package/dist/workflow-runner/event-subscriber.js +6 -2
- package/dist/workflow-runner/event-trigger.js +5 -0
- 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/agent-tools/changes.json +14 -0
- package/src/auth-email-password/changes.json +7 -0
- package/src/billing-foundation/changes.json +83 -0
- package/src/cap-counter/changes.json +7 -0
- package/src/cap-overview/changes.json +14 -0
- package/src/config/changes.json +7 -0
- package/src/crypto-shredding/changes.json +13 -0
- package/src/custom-fields/changes.json +7 -0
- package/src/delivery/changes.json +14 -0
- package/src/inbound-mail-foundation/changes.json +7 -0
- package/src/notes-history/changes.json +7 -0
- package/src/renderer-simple/changes.json +9 -1
- package/src/subscription-mollie/changes.json +13 -0
- package/src/subscription-stripe/changes.json +13 -0
- package/src/tags/changes.json +7 -0
- package/src/user-data-rights/changes.json +6 -0
- package/src/workflow-runner/changes.json +7 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// Public § 312k pages (/legal/kuendigen, /legal/cancel): form → review → confirm,
|
|
2
|
+
// no JavaScript. Every echoed value goes through escapeHtml/escapeHtmlAttr.
|
|
3
|
+
import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
4
|
+
import { getCookie } from "hono/cookie";
|
|
5
|
+
import { securePageHeaders, wrapInLayout } from "../../page-render/index.js";
|
|
6
|
+
import { SubscriptionFoundationHandlers } from "../constants.js";
|
|
7
|
+
import { CONTRACT_TERMINATION_DECLARATION_TYPES, CONTRACT_TERMINATION_KINDS } from "../events.js";
|
|
8
|
+
import { formatReceivedAt } from "./termination-mail.js";
|
|
9
|
+
import { requestContractTerminationSchema } from "./termination-request.js";
|
|
10
|
+
import { TERMINATION_TEXTS } from "./termination-texts.js";
|
|
11
|
+
const DEFAULT_PATHS = {
|
|
12
|
+
de: "/legal/kuendigen",
|
|
13
|
+
en: "/legal/cancel",
|
|
14
|
+
};
|
|
15
|
+
const FORM_FIELDS = [
|
|
16
|
+
"declarationType",
|
|
17
|
+
"terminationKind",
|
|
18
|
+
"name",
|
|
19
|
+
"email",
|
|
20
|
+
"customerReference",
|
|
21
|
+
"reason",
|
|
22
|
+
];
|
|
23
|
+
const EMPTY_TEXT = "";
|
|
24
|
+
const EMPTY_FORM = {
|
|
25
|
+
declarationType: "termination",
|
|
26
|
+
terminationKind: "ordinary",
|
|
27
|
+
name: "",
|
|
28
|
+
email: "",
|
|
29
|
+
customerReference: "",
|
|
30
|
+
reason: EMPTY_TEXT,
|
|
31
|
+
};
|
|
32
|
+
// The shared security headers leave framing to SAMEORIGIN; this form must
|
|
33
|
+
// never be framed (clickjacking a "cancel now" button).
|
|
34
|
+
function pageHeaders() {
|
|
35
|
+
const headers = securePageHeaders({
|
|
36
|
+
"content-type": "text/html; charset=utf-8",
|
|
37
|
+
"cache-control": "no-store",
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
...headers,
|
|
41
|
+
"content-security-policy": `${headers["content-security-policy"] ?? ""}; frame-ancestors 'none'; form-action 'self'`,
|
|
42
|
+
"x-frame-options": "DENY",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function htmlResponse(locale, title, bodyHtml, status) {
|
|
46
|
+
return new Response(wrapInLayout({ title, bodyHtml, lang: locale }), {
|
|
47
|
+
status,
|
|
48
|
+
headers: pageHeaders(),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function isOneOf(options, value) {
|
|
52
|
+
return options.includes(value);
|
|
53
|
+
}
|
|
54
|
+
async function readForm(c) {
|
|
55
|
+
const body = await c.req.parseBody();
|
|
56
|
+
const text = (key) => {
|
|
57
|
+
const value = body[key];
|
|
58
|
+
return typeof value === "string" ? value : "";
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
step: text("step"),
|
|
62
|
+
values: {
|
|
63
|
+
declarationType: text("declarationType"),
|
|
64
|
+
terminationKind: text("terminationKind"),
|
|
65
|
+
name: text("name").trim(),
|
|
66
|
+
email: text("email").trim(),
|
|
67
|
+
customerReference: text("customerReference").trim(),
|
|
68
|
+
reason: text("reason").trim(),
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function payloadOf(values, locale) {
|
|
73
|
+
return {
|
|
74
|
+
declarationType: values.declarationType,
|
|
75
|
+
terminationKind: values.terminationKind,
|
|
76
|
+
name: values.name,
|
|
77
|
+
email: values.email,
|
|
78
|
+
locale,
|
|
79
|
+
...(values.customerReference !== "" && { customerReference: values.customerReference }),
|
|
80
|
+
...(values.reason !== "" && { reason: values.reason }),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function validationMessages(values, locale) {
|
|
84
|
+
const parsed = requestContractTerminationSchema.safeParse(payloadOf(values, locale));
|
|
85
|
+
if (parsed.success)
|
|
86
|
+
return [];
|
|
87
|
+
const page = TERMINATION_TEXTS[locale].page;
|
|
88
|
+
const byField = {
|
|
89
|
+
name: page.errorName,
|
|
90
|
+
email: page.errorEmail,
|
|
91
|
+
reason: page.errorReason,
|
|
92
|
+
declarationType: page.errorChoice,
|
|
93
|
+
terminationKind: page.errorChoice,
|
|
94
|
+
};
|
|
95
|
+
return [
|
|
96
|
+
...new Set(parsed.error.issues.map((issue) => byField[String(issue.path[0])] ?? page.errorGeneric)),
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
function errorsHtml(messages, page) {
|
|
100
|
+
if (messages.length === 0)
|
|
101
|
+
return "";
|
|
102
|
+
return `<div role="alert"><h2>${escapeHtml(page.errorsHeading)}</h2><ul>${messages
|
|
103
|
+
.map((message) => `<li>${escapeHtml(message)}</li>`)
|
|
104
|
+
.join("")}</ul></div>`;
|
|
105
|
+
}
|
|
106
|
+
function radioGroup(name, options, labels, selected) {
|
|
107
|
+
return options
|
|
108
|
+
.map((option) => `<label><input type="radio" name="${name}" value="${escapeHtmlAttr(option)}"${option === selected ? " checked" : ""}> ${escapeHtml(labels[option])}</label><br>`)
|
|
109
|
+
.join("");
|
|
110
|
+
}
|
|
111
|
+
function textField(name, label, value, attributes) {
|
|
112
|
+
return `<p><label>${escapeHtml(label)}<br><input type="${escapeHtmlAttr(attributes.type ?? "text")}" name="${name}" value="${escapeHtmlAttr(value)}" maxlength="${attributes.maxlength}"${attributes.required ? " required" : ""}></label></p>`;
|
|
113
|
+
}
|
|
114
|
+
function formPage(locale, path, values, messages, status) {
|
|
115
|
+
const texts = TERMINATION_TEXTS[locale];
|
|
116
|
+
const { page } = texts;
|
|
117
|
+
const body = `<h1>${escapeHtml(page.title)}</h1>
|
|
118
|
+
<p>${escapeHtml(page.lead)}</p>
|
|
119
|
+
${errorsHtml(messages, page)}
|
|
120
|
+
<form method="post" action="${escapeHtmlAttr(path)}">
|
|
121
|
+
<input type="hidden" name="step" value="review">
|
|
122
|
+
<fieldset><legend>${escapeHtml(page.declarationTypeHeading)}</legend>${radioGroup("declarationType", CONTRACT_TERMINATION_DECLARATION_TYPES, texts.declarationTypeLabel, values.declarationType)}</fieldset>
|
|
123
|
+
<fieldset><legend>${escapeHtml(page.kindHeading)}</legend>${radioGroup("terminationKind", CONTRACT_TERMINATION_KINDS, texts.terminationKindLabel, values.terminationKind)}<p>${escapeHtml(page.earliestDate)}</p></fieldset>
|
|
124
|
+
${textField("name", page.nameLabel, values.name, { required: true, maxlength: 200 })}
|
|
125
|
+
${textField("email", page.emailLabel, values.email, { type: "email", required: true, maxlength: 254 })}
|
|
126
|
+
${textField("customerReference", page.customerReferenceLabel, values.customerReference, { maxlength: 200 })}
|
|
127
|
+
<p><label>${escapeHtml(page.reasonLabel)} (${escapeHtml(page.reasonHint)})<br><textarea name="reason" rows="5" maxlength="2000">${escapeHtml(values.reason)}</textarea></label></p>
|
|
128
|
+
<p><button type="submit">${escapeHtml(page.reviewButton)}</button></p>
|
|
129
|
+
</form>`;
|
|
130
|
+
return htmlResponse(locale, page.title, body, status);
|
|
131
|
+
}
|
|
132
|
+
function hiddenInputs(values) {
|
|
133
|
+
return FORM_FIELDS.map((field) => `<input type="hidden" name="${field}" value="${escapeHtmlAttr(values[field])}">`).join("\n");
|
|
134
|
+
}
|
|
135
|
+
function reviewPage(locale, path, values) {
|
|
136
|
+
const texts = TERMINATION_TEXTS[locale];
|
|
137
|
+
const { page } = texts;
|
|
138
|
+
// validationMessages already proved the enum values.
|
|
139
|
+
const declarationType = isOneOf(CONTRACT_TERMINATION_DECLARATION_TYPES, values.declarationType)
|
|
140
|
+
? values.declarationType
|
|
141
|
+
: "termination";
|
|
142
|
+
const terminationKind = isOneOf(CONTRACT_TERMINATION_KINDS, values.terminationKind)
|
|
143
|
+
? values.terminationKind
|
|
144
|
+
: "ordinary";
|
|
145
|
+
const row = (label, value) => value === ""
|
|
146
|
+
? ""
|
|
147
|
+
: `<tr><th scope="row">${escapeHtml(label)}</th><td>${escapeHtml(value)}</td></tr>`;
|
|
148
|
+
const body = `<h1>${escapeHtml(page.reviewTitle)}</h1>
|
|
149
|
+
<p>${escapeHtml(page.reviewLead)}</p>
|
|
150
|
+
<table>
|
|
151
|
+
${row(texts.declarationType, texts.declarationTypeLabel[declarationType])}
|
|
152
|
+
${row(texts.terminationKind, texts.terminationKindLabel[terminationKind])}
|
|
153
|
+
${row(texts.name, values.name)}
|
|
154
|
+
${row(texts.email, values.email)}
|
|
155
|
+
${row(texts.customerReference, values.customerReference)}
|
|
156
|
+
${row(texts.reasonFieldLabel, values.reason)}
|
|
157
|
+
</table>
|
|
158
|
+
<form method="post" action="${escapeHtmlAttr(path)}">
|
|
159
|
+
${hiddenInputs(values)}
|
|
160
|
+
<button type="submit" name="step" value="confirm">${escapeHtml(page.confirmButton[declarationType])}</button>
|
|
161
|
+
<button type="submit" name="step" value="edit">${escapeHtml(page.backButton)}</button>
|
|
162
|
+
</form>`;
|
|
163
|
+
return htmlResponse(locale, page.reviewTitle, body, 200);
|
|
164
|
+
}
|
|
165
|
+
function resultPage(locale, receivedAtIso, requestId) {
|
|
166
|
+
const { page } = TERMINATION_TEXTS[locale];
|
|
167
|
+
const body = `<h1>${escapeHtml(page.resultTitle)}</h1>
|
|
168
|
+
<p>${escapeHtml(page.resultLead)}</p>
|
|
169
|
+
<p>${escapeHtml(page.resultReceivedAt)}: <strong>${escapeHtml(formatReceivedAt(receivedAtIso, locale))}</strong></p>
|
|
170
|
+
<p>${escapeHtml(page.resultRequestId)}: <code>${escapeHtml(requestId)}</code></p>
|
|
171
|
+
<p>${escapeHtml(page.resultMailNote)}</p>`;
|
|
172
|
+
return htmlResponse(locale, page.resultTitle, body, 200);
|
|
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
|
+
}
|
|
201
|
+
async function submitDeclaration(c, app, clientIp, values, locale) {
|
|
202
|
+
// deps.write only resolves a session user under /api/*; this page lives
|
|
203
|
+
// outside it, so it re-enters the dispatcher through the app like the
|
|
204
|
+
// user-export by-token route. The resolved clientIp as env keeps the
|
|
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.
|
|
208
|
+
const response = await app.fetch(new Request(`${new URL(c.req.url).origin}/api/write`, {
|
|
209
|
+
method: "POST",
|
|
210
|
+
headers: reentryHeaders(c),
|
|
211
|
+
body: JSON.stringify({
|
|
212
|
+
type: SubscriptionFoundationHandlers.requestContractTermination,
|
|
213
|
+
payload: payloadOf(values, locale),
|
|
214
|
+
}),
|
|
215
|
+
}), clientIp);
|
|
216
|
+
const parsed = await response.json().catch(() => null);
|
|
217
|
+
if (response.status === 429 || parsed?.error?.code === RATE_LIMITED_CODE) {
|
|
218
|
+
return { kind: "rate-limited" };
|
|
219
|
+
}
|
|
220
|
+
const requestId = parsed?.data?.requestId;
|
|
221
|
+
const receivedAtIso = parsed?.data?.receivedAtIso;
|
|
222
|
+
if (response.ok &&
|
|
223
|
+
parsed?.isSuccess === true &&
|
|
224
|
+
typeof requestId === "string" &&
|
|
225
|
+
typeof receivedAtIso === "string") {
|
|
226
|
+
return { kind: "received", requestId, receivedAtIso };
|
|
227
|
+
}
|
|
228
|
+
return { kind: "failed" };
|
|
229
|
+
}
|
|
230
|
+
function routesFor(locale, path) {
|
|
231
|
+
return [
|
|
232
|
+
{
|
|
233
|
+
method: "GET",
|
|
234
|
+
path,
|
|
235
|
+
entry: "anonymous",
|
|
236
|
+
handler: () => formPage(locale, path, EMPTY_FORM, [], 200),
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
method: "POST",
|
|
240
|
+
path,
|
|
241
|
+
entry: "anonymous",
|
|
242
|
+
handler: async (c, deps) => {
|
|
243
|
+
const { step, values } = await readForm(c);
|
|
244
|
+
if (step === "edit")
|
|
245
|
+
return formPage(locale, path, values, [], 200);
|
|
246
|
+
const messages = validationMessages(values, locale);
|
|
247
|
+
if (messages.length > 0)
|
|
248
|
+
return formPage(locale, path, values, messages, 400);
|
|
249
|
+
if (step !== "confirm")
|
|
250
|
+
return reviewPage(locale, path, values);
|
|
251
|
+
const outcome = await submitDeclaration(c, deps.app, deps.clientIp, values, locale);
|
|
252
|
+
const { page } = TERMINATION_TEXTS[locale];
|
|
253
|
+
if (outcome.kind === "received") {
|
|
254
|
+
return resultPage(locale, outcome.receivedAtIso, outcome.requestId);
|
|
255
|
+
}
|
|
256
|
+
if (outcome.kind === "rate-limited") {
|
|
257
|
+
return htmlResponse(locale, page.rateLimitedTitle, `<h1>${escapeHtml(page.rateLimitedTitle)}</h1><p>${escapeHtml(page.rateLimitedBody)}</p>`, 429);
|
|
258
|
+
}
|
|
259
|
+
return formPage(locale, path, values, [page.errorGeneric], 400);
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
];
|
|
263
|
+
}
|
|
264
|
+
/** GET + POST pages for de (`/legal/kuendigen`) and en (`/legal/cancel`);
|
|
265
|
+
* mount via the app's `extraRoutes`. */
|
|
266
|
+
export function createContractTerminationRoutes(options = {}) {
|
|
267
|
+
return ["de", "en"].flatMap((locale) => routesFor(locale, options.paths?.[locale] ?? DEFAULT_PATHS[locale]));
|
|
268
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type QueryHandlerDef, type WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { type ProviderCancelOutcome } from "../events.js";
|
|
4
|
+
import type { ConsumerProtectionOptions, ResolvedBillingFoundationOptions } from "../types.js";
|
|
5
|
+
import type { OperatorNoticeReason } from "./termination-texts.js";
|
|
6
|
+
export declare const recordContractTerminationSchema: z.ZodObject<{
|
|
7
|
+
requestId: z.ZodString;
|
|
8
|
+
declarationType: z.ZodEnum<{
|
|
9
|
+
termination: "termination";
|
|
10
|
+
withdrawal: "withdrawal";
|
|
11
|
+
}>;
|
|
12
|
+
terminationKind: z.ZodEnum<{
|
|
13
|
+
extraordinary: "extraordinary";
|
|
14
|
+
ordinary: "ordinary";
|
|
15
|
+
}>;
|
|
16
|
+
channel: z.ZodEnum<{
|
|
17
|
+
account: "account";
|
|
18
|
+
public: "public";
|
|
19
|
+
}>;
|
|
20
|
+
when: z.ZodEnum<{
|
|
21
|
+
immediately: "immediately";
|
|
22
|
+
none: "none";
|
|
23
|
+
"period-end": "period-end";
|
|
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
|
+
}>;
|
|
46
|
+
}, z.core.$strict>;
|
|
47
|
+
export type RecordContractTerminationResult = {
|
|
48
|
+
readonly receivedAtIso: string;
|
|
49
|
+
readonly effectiveAtIso: string | null;
|
|
50
|
+
readonly providerCancel: ProviderCancelOutcome;
|
|
51
|
+
readonly noticeReason: Extract<OperatorNoticeReason, "provider_cannot_cancel" | "provider_error" | "no_active_subscription"> | null;
|
|
52
|
+
};
|
|
53
|
+
export declare const recordUnmatchedContractTerminationSchema: z.ZodObject<{
|
|
54
|
+
requestId: z.ZodString;
|
|
55
|
+
declarationType: z.ZodEnum<{
|
|
56
|
+
termination: "termination";
|
|
57
|
+
withdrawal: "withdrawal";
|
|
58
|
+
}>;
|
|
59
|
+
terminationKind: z.ZodEnum<{
|
|
60
|
+
extraordinary: "extraordinary";
|
|
61
|
+
ordinary: "ordinary";
|
|
62
|
+
}>;
|
|
63
|
+
receivedAtIso: z.ZodString;
|
|
64
|
+
matchResult: z.ZodEnum<{
|
|
65
|
+
ambiguous: "ambiguous";
|
|
66
|
+
none: "none";
|
|
67
|
+
}>;
|
|
68
|
+
}, z.core.$strict>;
|
|
69
|
+
export declare const terminableSubscriptionSchema: z.ZodObject<{}, z.core.$strict>;
|
|
70
|
+
export declare function createTerminableSubscriptionQuery(): QueryHandlerDef;
|
|
71
|
+
export declare function createDeclareContractTerminationHandler(): WriteHandlerDef;
|
|
72
|
+
export declare function createRecordContractTerminationHandler(options: ResolvedBillingFoundationOptions, consumerProtection: ConsumerProtectionOptions): WriteHandlerDef;
|
|
73
|
+
export declare function createRecordUnmatchedContractTerminationHandler(): WriteHandlerDef;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// System-only building blocks of the § 312k flow: the per-tenant record
|
|
2
|
+
// handler (provider cancel + event), the system-stream handler for
|
|
3
|
+
// declarations no tenant matched, and the non-terminal probe the anonymous
|
|
4
|
+
// request handler uses to find candidate tenants.
|
|
5
|
+
import { access, SYSTEM_ROLE, } from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
+
import { UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
|
|
7
|
+
import * as z from "zod";
|
|
8
|
+
import { subscriptionAggregateId, terminationUnmatchedAggregateId } from "../aggregate-id.js";
|
|
9
|
+
import { resolveProviderPlugin } from "../checkout-core.js";
|
|
10
|
+
import { isTerminalSubscriptionStatus, SubscriptionCancelTimings } from "../constants.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
|
+
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";
|
|
15
|
+
const REQUESTED_CANCEL_TIMINGS = [
|
|
16
|
+
SubscriptionCancelTimings.periodEnd,
|
|
17
|
+
SubscriptionCancelTimings.immediately,
|
|
18
|
+
"none",
|
|
19
|
+
];
|
|
20
|
+
export const recordContractTerminationSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
requestId: z.string().min(1).max(100),
|
|
23
|
+
declarationType: z.enum(CONTRACT_TERMINATION_DECLARATION_TYPES),
|
|
24
|
+
terminationKind: z.enum(CONTRACT_TERMINATION_KINDS),
|
|
25
|
+
channel: z.enum(CONTRACT_TERMINATION_CHANNELS),
|
|
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),
|
|
40
|
+
})
|
|
41
|
+
.strict();
|
|
42
|
+
export const recordUnmatchedContractTerminationSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
requestId: z.string().min(1).max(100),
|
|
45
|
+
declarationType: z.enum(CONTRACT_TERMINATION_DECLARATION_TYPES),
|
|
46
|
+
terminationKind: z.enum(CONTRACT_TERMINATION_KINDS),
|
|
47
|
+
receivedAtIso: z.string().min(1),
|
|
48
|
+
matchResult: z.enum(["none", "ambiguous"]),
|
|
49
|
+
})
|
|
50
|
+
.strict();
|
|
51
|
+
export const terminableSubscriptionSchema = z.object({}).strict();
|
|
52
|
+
export function createTerminableSubscriptionQuery() {
|
|
53
|
+
return {
|
|
54
|
+
name: "terminable-subscription",
|
|
55
|
+
description: "Tells whether the caller tenant holds a subscription that is not terminal; the anonymous termination request uses it to pick the tenant a declaration belongs to.",
|
|
56
|
+
agent: { expose: false },
|
|
57
|
+
schema: terminableSubscriptionSchema,
|
|
58
|
+
access: { roles: [SYSTEM_ROLE] },
|
|
59
|
+
handler: async (_query, ctx) => {
|
|
60
|
+
const subscription = await getSubscriptionForTenant(ctx, ctx.user.tenantId);
|
|
61
|
+
return {
|
|
62
|
+
terminable: subscription !== null && !isTerminalSubscriptionStatus(subscription.status),
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
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) {
|
|
117
|
+
return {
|
|
118
|
+
name: "record-contract-termination",
|
|
119
|
+
agent: { expose: false },
|
|
120
|
+
schema: recordContractTerminationSchema,
|
|
121
|
+
access: { roles: access.system },
|
|
122
|
+
handler: async (event, ctx) => {
|
|
123
|
+
// @cast-boundary engine-payload — dispatcher-zod-validated payload
|
|
124
|
+
const payload = event.payload;
|
|
125
|
+
const tenantId = event.user.tenantId;
|
|
126
|
+
if (payload.channel === "public" && payload.when === SubscriptionCancelTimings.immediately) {
|
|
127
|
+
throw new UnprocessableError("immediate_cancel_not_allowed", {
|
|
128
|
+
message: "billing-foundation: the public channel never cancels at the provider immediately",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const receivedAtIso = payload.receivedAtIso ?? options.now().toString();
|
|
132
|
+
const stream = await ctx.fetchForWriting({
|
|
133
|
+
aggregateId: subscriptionAggregateId(tenantId),
|
|
134
|
+
aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
|
|
135
|
+
});
|
|
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
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const { providerCancel, effectiveAtIso, noticeReason } = await cancelAtProvider(ctx, tenantId, payload.when, receivedAtIso);
|
|
155
|
+
const recorded = {
|
|
156
|
+
requestId: payload.requestId,
|
|
157
|
+
declarationType: payload.declarationType,
|
|
158
|
+
terminationKind: payload.terminationKind,
|
|
159
|
+
channel: payload.channel,
|
|
160
|
+
receivedAtIso,
|
|
161
|
+
effectiveAtIso,
|
|
162
|
+
providerCancel,
|
|
163
|
+
};
|
|
164
|
+
await stream.appendOne({
|
|
165
|
+
type: CONTRACT_TERMINATION_REQUESTED_EVENT_QN,
|
|
166
|
+
payload: recorded,
|
|
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
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
isSuccess: true,
|
|
187
|
+
data: {
|
|
188
|
+
receivedAtIso,
|
|
189
|
+
effectiveAtIso,
|
|
190
|
+
providerCancel,
|
|
191
|
+
noticeReason,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function findPlugin(ctx, providerName) {
|
|
198
|
+
try {
|
|
199
|
+
return resolveProviderPlugin(ctx, providerName).plugin;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
export function createRecordUnmatchedContractTerminationHandler() {
|
|
206
|
+
return {
|
|
207
|
+
name: "record-unmatched-contract-termination",
|
|
208
|
+
agent: { expose: false },
|
|
209
|
+
schema: recordUnmatchedContractTerminationSchema,
|
|
210
|
+
access: { roles: access.system },
|
|
211
|
+
handler: async (event, ctx) => {
|
|
212
|
+
// @cast-boundary engine-payload — dispatcher-zod-validated payload
|
|
213
|
+
const payload = event.payload;
|
|
214
|
+
const stream = await ctx.fetchForWriting({
|
|
215
|
+
aggregateId: terminationUnmatchedAggregateId(payload.requestId),
|
|
216
|
+
aggregateType: CONTRACT_TERMINATION_UNMATCHED_AGGREGATE_TYPE,
|
|
217
|
+
});
|
|
218
|
+
const recorded = {
|
|
219
|
+
requestId: payload.requestId,
|
|
220
|
+
declarationType: payload.declarationType,
|
|
221
|
+
terminationKind: payload.terminationKind,
|
|
222
|
+
channel: "public",
|
|
223
|
+
receivedAtIso: payload.receivedAtIso,
|
|
224
|
+
matchResult: payload.matchResult,
|
|
225
|
+
};
|
|
226
|
+
await stream.appendOne({ type: CONTRACT_TERMINATION_UNMATCHED_EVENT_QN, payload: recorded });
|
|
227
|
+
return { isSuccess: true, data: { recorded: true } };
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { type ProviderCancelOutcome } from "../events.js";
|
|
4
|
+
import type { ConsumerProtectionOptions, ResolvedBillingFoundationOptions } from "../types.js";
|
|
5
|
+
export declare const requestContractTerminationSchema: z.ZodObject<{
|
|
6
|
+
declarationType: z.ZodEnum<{
|
|
7
|
+
termination: "termination";
|
|
8
|
+
withdrawal: "withdrawal";
|
|
9
|
+
}>;
|
|
10
|
+
terminationKind: z.ZodEnum<{
|
|
11
|
+
extraordinary: "extraordinary";
|
|
12
|
+
ordinary: "ordinary";
|
|
13
|
+
}>;
|
|
14
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
15
|
+
name: z.ZodString;
|
|
16
|
+
email: z.ZodEmail;
|
|
17
|
+
customerReference: z.ZodOptional<z.ZodString>;
|
|
18
|
+
locale: z.ZodOptional<z.ZodEnum<{
|
|
19
|
+
de: "de";
|
|
20
|
+
en: "en";
|
|
21
|
+
}>>;
|
|
22
|
+
}, z.core.$strict>;
|
|
23
|
+
export declare const terminateContractSchema: z.ZodObject<{
|
|
24
|
+
declarationType: z.ZodEnum<{
|
|
25
|
+
termination: "termination";
|
|
26
|
+
withdrawal: "withdrawal";
|
|
27
|
+
}>;
|
|
28
|
+
terminationKind: z.ZodEnum<{
|
|
29
|
+
extraordinary: "extraordinary";
|
|
30
|
+
ordinary: "ordinary";
|
|
31
|
+
}>;
|
|
32
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
33
|
+
}, z.core.$strict>;
|
|
34
|
+
export type RequestContractTerminationResult = {
|
|
35
|
+
readonly requestId: string;
|
|
36
|
+
readonly receivedAtIso: string;
|
|
37
|
+
};
|
|
38
|
+
export type TerminateContractResult = {
|
|
39
|
+
readonly requestId: string;
|
|
40
|
+
readonly receivedAtIso: string;
|
|
41
|
+
readonly effectiveAtIso: string | null;
|
|
42
|
+
readonly providerCancel: ProviderCancelOutcome;
|
|
43
|
+
};
|
|
44
|
+
export declare function createRequestContractTerminationHandler(options: ResolvedBillingFoundationOptions, consumerProtection: ConsumerProtectionOptions): WriteHandlerDef;
|
|
45
|
+
export declare function createTerminateContractHandler(options: ResolvedBillingFoundationOptions, consumerProtection: ConsumerProtectionOptions): WriteHandlerDef;
|