feat(legal): electronic withdrawal function and remaining compliance fixes
Withdrawal function (§ 356a BGB, mandatory since 19 June 2026): - /vertrag-widerrufen: two-step flow "Vertrag widerrufen" -> "Widerruf bestätigen", no login, only name, order number and e-mail required. Prominent footer button on every page, prefilled link per order in the customer account. - /api/widerruf stores every declaration (matched to the order only if order number and e-mail fit), mails a receipt with the declaration and the exact time of receipt, notifies ADMIN_EMAIL. Same response for matching and non-matching orders (no enumeration), origin check and rate limit instead of a CAPTCHA that could block a statutory right. User input is HTML-escaped and stripped of line breaks in headers. - /admin/widerrufe lists declarations, open ones first, with an open-count badge and "mark as done". - Withdrawal notice gains the statutory sentence on the online function. Other fixes: - Newsletter stores confirmedAt as proof of consent (Art. 7(1) GDPR). - "Recently viewed" is only persisted with consent (§ 25(1) TDDDG); choosing "essential only" or withdrawing consent deletes it. The banner no longer promises audience-measurement cookies that do not exist. - Admin rejects an invalid company e-mail; an obfuscated stored value falls back to the configured address so the legal notice stays valid. - Privacy policy: Microsoft 365 as mail processor, log retention of at most 14 days, withdrawal-function processing, consent timestamps. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
4ab1a7edae
commit
dcfad4e011
27 files changed
+1049
-34
No files matched your search
@@ -439,6 +439,7 @@ model Order {
|
||||
pickup Pickup?
|
||||
invoice Invoice?
|
||||
returnRequest ReturnRequest?
|
||||
withdrawalDeclarations WithdrawalDeclaration[]
|
||||
|
||||
/// Bedient sowohl "Meine Bestellungen" (Filter nach customerId, sortiert nach
|
||||
/// createdAt) als auch den reinen customerId-Filter beim Datenexport.
|
||||
@@ -649,6 +650,9 @@ model NewsletterSubscriber {
|
||||
/// Abmelde-Link fuer Abonnenten ohne Kundenkonto.
|
||||
confirmationToken String? @unique
|
||||
subscribedAt DateTime @default(now())
|
||||
/// Zeitpunkt des Klicks auf den Double-Opt-in-Link — Nachweis der
|
||||
/// Einwilligung (Art. 7 Abs. 1 DSGVO). Null bei Altbestand vor Einfuehrung.
|
||||
confirmedAt DateTime?
|
||||
unsubscribedAt DateTime?
|
||||
|
||||
customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
|
||||
@@ -805,3 +809,31 @@ model BusinessInquiry {
|
||||
|
||||
@@map("business_inquiries")
|
||||
}
|
||||
|
||||
/// Widerrufserklaerung ueber die elektronische Widerrufsfunktion
|
||||
/// (§ 356a BGB, Seite /vertrag-widerrufen). Wird IMMER gespeichert, auch wenn
|
||||
/// Bestellnummer und E-Mail zu keiner Bestellung passen: der Widerruf ist mit
|
||||
/// dem Zugang erklaert, die Zuordnung ist unsere Sache. orderId ist nur die
|
||||
/// automatische Zuordnung, wenn beides passt.
|
||||
model WithdrawalDeclaration {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
/// Wortlaut wie eingegeben, nicht normalisiert — Beleg dessen, was erklaert wurde.
|
||||
orderNumber String
|
||||
email String
|
||||
message String?
|
||||
locale String @default("de")
|
||||
orderId String?
|
||||
/// Zugangszeitpunkt, steht so in der Eingangsbestaetigung (§ 356a Abs. 4 BGB).
|
||||
receivedAt DateTime @default(now())
|
||||
/// Gesetzt, sobald der Mailserver die Eingangsbestaetigung angenommen hat.
|
||||
confirmationSentAt DateTime?
|
||||
/// Im Admin als erledigt markiert (Ware zurueck, Erstattung veranlasst).
|
||||
processedAt DateTime?
|
||||
|
||||
order Order? @relation(fields: [orderId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([receivedAt])
|
||||
@@index([processedAt])
|
||||
@@map("withdrawal_declarations")
|
||||
}
|
||||
Reference in new issue
Block a user