Add contract renewal reminder emails (keep/cancel decision links)

Sends a one-time email ~30 days before any contract's term_end
(whether renewal='auto' or 'none'), with "Behalten" and "Kündigen"
action links. "Behalten" extends the contract by one renewal term;
"Kündigen" sets the same cancellation effective-date logic as the
customer-facing cancel flow and opens a staff ticket so the external
deregistration (e.g. with a domain registrar) actually gets done.

Uses a dedicated single-use token table (contract_renewal_tokens)
since contracts, unlike user_tokens, are not scoped to a single user.
The public confirmation page lives at /vertrag-entscheidung.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kundencenter 2026-10-01 11:51:19 +02:00
parent 29c7273f4d
commit dad9528295
6 changed files with 233 additions and 8 deletions

View file

@ -53,6 +53,16 @@ const checkLicense = async () => {
};
setInterval(() => void checkLicense(), 12 * 3600_000); void checkLicense();
// Vertrags-Erinnerung: ~30 Tage vor Laufzeitende Mail mit "Behalten"/"Kündigen"-Link (einmal je Vertrag,
// markiert über renewal_reminder_sent_at). Unabhängig von processContractLifecycle oben, die nur zum
// tatsächlichen Laufzeitende selbst verlängert/beendet.
import { sendRenewalReminders } from '@kc/platform/contractRenewal';
const checkRenewals = async () => {
try { const r = await sendRenewalReminders(); if (r.sent || r.skipped) log(`Vertrags-Erinnerungen: ${r.sent} gesendet, ${r.skipped} übersprungen`); }
catch (e) { log(`Vertrags-Erinnerung fehlgeschlagen: ${(e as Error).message}`); }
};
setInterval(() => void checkRenewals(), 24 * 3600_000); void checkRenewals();
// Audit-Aufbewahrung: Einträge jenseits der konfigurierten Frist löschen (Einstellungen > Firma/Audit; Default
// spiegelt den DSGVO-Grundsatz der Speicherbegrenzung, keine feste Gesetzeszahl). Idempotent, daher unproblematisch,
// mehrfach am Tag zu laufen; kein eigenes Datums-Gating nötig.