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:
Nick SzaunigandClaude Opus 5.5 committed 2026-09-26 11:14:02 +02:00
1 parent 4ab1a7edae
commit dcfad4e011
27 files changed
+1049 -34

No files matched your search

+53 -8
View File
@@ -173,6 +173,30 @@ export const en: Dictionary = {
tocLabel: "Contents",
revisionLabel: "Last updated",
},
withdrawalPage: {
title: "Withdraw from contract",
intro: "Here you can withdraw from your purchase contract online within the 14-day withdrawal period. You do not need a customer account and do not have to give a reason.",
nameLabel: "Your name",
orderNumberLabel: "Order number",
orderNumberHint: "Shown in your order confirmation, e.g. WH-2026-0042.",
emailLabel: "Email address",
emailHint: "We will send the confirmation of receipt to this address.",
messageLabel: "Message (optional)",
requiredNote: "Only name, order number and email address are required.",
startButton: "Withdraw from contract",
reviewHeading: "Please check your withdrawal",
reviewIntro: "By clicking “Confirm withdrawal” we will submit this notice:",
confirmButton: "Confirm withdrawal",
editButton: "Edit details",
submitting: "Sending …",
successHeading: "Your withdrawal has been received",
successMailSent: "We have just emailed you a confirmation of receipt containing your notice and the date and time it was received.",
successMailFailed: "Your withdrawal has been validly received. The confirmation email could not be sent just now; we will send it to you as soon as possible. Please save this page as proof.",
receivedAtLabel: "Received on",
errorMessage: "Your withdrawal could not be sent. Please try again or email us — a withdrawal by email or letter is also valid.",
policyLink: "Right of withdrawal",
footerButton: "Withdraw from contract",
},
notFoundPage: {
title: "This page doesn't exist, at least not anymore.",
text: "The link might be outdated, or the page has moved. Take a look around the shop, or head back to the homepage.",
@@ -188,7 +212,7 @@ export const en: Dictionary = {
cookieConsent: {
dialogLabel: "Cookie settings",
bannerTextBeforeLink:
"We only use technically necessary cookies (e.g. for logging into your customer account). With your consent, we also use cookies for audience measurement. Details can be found in our ",
"We use technically necessary cookies (e.g. for logging into your customer account). With your consent, we also remember in your browser which products you viewed recently. We do not use tracking or advertising. Details can be found in our ",
policyLinkLabel: "Cookie Policy",
bannerTextAfterLink: ".",
essentialOnly: "Necessary only",
@@ -266,7 +290,7 @@ export const en: Dictionary = {
{
heading: "3. Server Log Files When Visiting the Website",
paragraphs: [
"When you access our website, we automatically collect on our server technical access data (e.g. IP address, date and time of access, page accessed, browser type, referrer URL) in what are known as server log files. This processing is technically necessary to provide the website (Art. 6(1)(f) GDPR) The log files are not combined with other data and are deleted automatically once they are no longer needed for security and error analysis.",
"When you access our website, we automatically collect on our server technical access data (e.g. IP address, date and time of access, page accessed, browser type, referrer URL) in what are known as server log files. This processing is technically necessary to provide the website (Art. 6(1)(f) GDPR) The log files are not combined with other data and are deleted automatically once they are no longer needed for security and error analysis, after 14 days at the latest.",
],
},
{
@@ -277,6 +301,8 @@ export const en: Dictionary = {
"Payments are processed by our payment provider, Stripe Payments Europe, Ltd. The data necessary for payment processing is transmitted to Stripe for this purpose. Stripe's privacy policy also applies (stripe.com/privacy). Where this involves transferring data to Stripe-affiliated companies outside the EU/EEA (in particular Stripe, Inc. in the USA), this is done on the basis of EU Standard Contractual Clauses pursuant to Art. 46 GDPR.",
"If you choose PayPal or Klarna in the payment step, you are redirected to the respective provider and the order data required for payment is transmitted to them: PayPal (Europe) S.à r.l. et Cie, S.C.A., 22–24 Boulevard Royal, L-2449 Luxembourg, or Klarna Bank AB (publ), Sveavägen 46, 111 34 Stockholm, Sweden. These providers may carry out their own checks, e.g. of identity or creditworthiness; they are responsible for these themselves and their privacy notices apply. Legal basis: Art. 6(1)(b) GDPR.",
"Order and invoice data is retained for the legally required period due to commercial and tax record-keeping obligations (§ 257 HGB, § 147 AO of German law).",
"We send order confirmations and other emails about your contract or customer account via Microsoft 365 (Microsoft Ireland Operations Limited, One Microsoft Place, South County Business Park, Leopardstown, Dublin 18, Ireland). Microsoft processes the data as our processor under a contract pursuant to Art. 28 GDPR. Data may be transferred to Microsoft Corporation in the USA; Microsoft is certified under the EU-US Data Privacy Framework. Legal basis: Art. 6(1)(b) GDPR.",
"If you withdraw from a contract via our online withdrawal function, we store your name, order number, email address, your optional message and the time of receipt, send you a confirmation of receipt and match the withdrawal to your order. Legal basis: Art. 6(1)(c) GDPR in conjunction with § 356a BGB. We keep the notice as proof until the limitation periods for claims under the contract have expired.",
],
},
{
@@ -291,7 +317,7 @@ export const en: Dictionary = {
heading: "6. Newsletter (Harvest Mail)",
paragraphs: [
"If you sign up for our harvest mail, we use your email address exclusively to send information about new harvests. The legal basis is your consent (Art. 6(1)(a) GDPR). You can unsubscribe at any time via the link in every email or through your customer account.",
"Sign-up uses a double opt-in: you are only added once you click the link in our confirmation email. Unsubscribing withdraws your consent with effect for the future (Art. 7(3) GDPR). We then keep your address together with the time of unsubscribing so that we can demonstrate your consent and its withdrawal and send you no further harvest mail (Art. 6(1)(f) GDPR).",
"Sign-up uses a double opt-in: you are only added once you click the link in our confirmation email; we store the time of this confirmation as proof of your consent. If you subscribe in your customer account, the click there constitutes your consent and we store its time as well. Unsubscribing withdraws your consent with effect for the future (Art. 7(3) GDPR). We then keep your address together with the time of unsubscribing so that we can demonstrate your consent and its withdrawal and send you no further harvest mail (Art. 6(1)(f) GDPR).",
],
},
{
@@ -425,6 +451,7 @@ export const en: Dictionary = {
"You have the right to withdraw from this contract within fourteen days without giving any reason.",
"The withdrawal period is fourteen days from the day on which you, or a third party other than the carrier named by you, took possession of the goods.",
"To exercise your right of withdrawal, you must inform us ({{anbieter}}) of your decision to withdraw from this contract by means of a clear statement (e.g. a letter sent by post or an email). You may use the model withdrawal form below, but it is not obligatory.",
"You can also exercise your right of withdrawal online at {{widerrufsfunktion}}. If you use this online function, we will send you a confirmation of receipt on a durable medium (e.g. by email) without undue delay.",
"To meet the withdrawal deadline, it is sufficient for you to send your communication concerning your exercise of the right of withdrawal before the withdrawal period has expired.",
],
},
@@ -492,13 +519,13 @@ export const en: Dictionary = {
heading: "Legal basis",
paragraphs: [
"Storing information on your device or reading it from there generally requires your consent (§ 25(1) TDDDG). No consent is needed where the storage is strictly necessary for us to provide a service you have explicitly requested (§ 25(2) No. 2 TDDDG), such as signing in to your customer account or completing an order.",
"Where personal data is processed in the process, we rely on Art. 6(1)(b) GDPR (contract and pre-contractual steps, e.g. customer account and order) or Art. 6(1)(f) GDPR (our legitimate interest in a secure, functioning website). Should we use technologies requiring consent in the future, the legal basis will be your consent under § 25(1) TDDDG in conjunction with Art. 6(1)(a) GDPR.",
"Where personal data is processed in the process, we rely on Art. 6(1)(b) GDPR (contract and pre-contractual steps, e.g. customer account and order) or Art. 6(1)(f) GDPR (our legitimate interest in a secure, functioning website). For storage requiring consent (see “Cookies requiring consent”), the legal basis is your consent under § 25(1) TDDDG in conjunction with Art. 6(1)(a) GDPR.",
],
},
{
heading: "Overview",
paragraphs: [
"We only use technically necessary cookies and storage entries. We do not use analytics, statistics, marketing or tracking cookies, we do not build usage profiles, and we do not pass on data to third parties for advertising purposes.",
"We use technically necessary cookies and storage entries and — only with your consent — one further storage entry for the list of recently viewed products. We do not use analytics, statistics, marketing or tracking cookies, we do not build usage profiles, and we do not pass on data to third parties for advertising purposes.",
"Most of the cookies listed below are only created when you use a specific feature (e.g. signing in, paying, switching language). Merely visiting the website sets no cookie.",
],
},
@@ -528,7 +555,6 @@ export const en: Dictionary = {
paragraphs: [
"The following entries are kept in your browser's local storage. Unlike cookies, they are not automatically sent to us with every page request and do not expire on their own: they remain until you delete them (via your browser's “clear site data” setting) or the website removes them. Legal basis: § 25(2) No. 2 TDDDG and Art. 6(1)(b) or (f) GDPR.",
"wabenhain-cart – Your cart (items, quantities, prices). It is only transmitted to us when you submit an order, and it is emptied after a successful order. Storage period: until your next order or until you empty it yourself.",
"wabenhain-recently-viewed – The identifiers of up to 6 recently viewed products, so they can be shown to you again on product pages. Only these product identifiers are requested for display; we store no browsing history on our servers. Storage period: until you clear the site data in your browser.",
"wabenhain-cookie-consent – Your choice in the cookie banner (“Essential only” or “Accept all”), so the banner does not reappear on every visit and we can demonstrate your decision. Storage period: until you change your choice or clear the site data. Legal basis: § 25(2) No. 2 TDDDG and Art. 6(1)(c) GDPR in conjunction with Art. 7(1) GDPR.",
],
},
@@ -544,8 +570,9 @@ export const en: Dictionary = {
{
heading: "Cookies requiring consent",
paragraphs: [
"We currently use no cookies or technologies that require your consent. Choosing “Accept all” in the cookie banner therefore enables nothing additional at present; with “Essential only” you can use the website without any restriction.",
"Should we introduce something like audience measurement in the future, we will name it here beforehand with provider, purpose and storage period, and only activate it after your explicit, voluntary consent via the cookie banner. Declining has no disadvantages for you.",
"We only store the following entry if you choose “Accept all” in the cookie banner. Without consent it is not created; the list then only lasts while the tab is open. If you choose “Essential only” or withdraw your consent, we delete the entry immediately. Legal basis: § 25(1) TDDDG in conjunction with Art. 6(1)(a) GDPR.",
"wabenhain-recently-viewed – The identifiers of up to 6 recently viewed products (localStorage), so they can be shown to you again on product pages. Only these product identifiers are requested for display; we store no browsing history on our servers. Storage period: until you withdraw your consent or clear the site data in your browser.",
"We do not use any other technologies requiring consent, such as audience measurement. Should that change, we will name them here beforehand with provider, purpose and storage period, and only activate it after your explicit, voluntary consent via the cookie banner. Declining has no disadvantages for you.",
],
},
{
@@ -1204,6 +1231,23 @@ export const en: Dictionary = {
sectionVisibility: "Visibility",
create: "Create",
},
adminWithdrawalsPage: {
title: "Withdrawals",
intro: "Notices submitted via the online withdrawal function (§ 356a BGB). The refund is due no later than 14 days after receipt; you may withhold it until the goods are back or proof of return has been provided.",
backToDashboard: "Back to dashboard",
empty: "No withdrawals received yet.",
statusOpen: "Open",
statusProcessed: "Done",
matched: "Order matched",
unmatched: "No matching order — please check",
mailSent: "Confirmation of receipt sent",
mailNotSent: "Confirmation of receipt NOT sent — please send manually",
receivedAt: "Received",
processedOn: (date) => `Done on ${date}`,
markProcessed: "Mark as done",
reopen: "Reopen",
viewOrder: "View order",
},
adminReturnsPage: {
refundConfirm: (orderNumber, amount) => `Refund ${amount} for order ${orderNumber} via Stripe now? This cannot be undone.`,
rejectConfirm: (orderNumber) => `Really reject the return for order ${orderNumber}?`,
@@ -1416,6 +1460,7 @@ export const en: Dictionary = {
navDashboard: "Dashboard",
navOrders: "Orders",
navReturns: "Returns",
navWithdrawals: "Withdrawals",
navProducts: "Products",
navHarvests: "Harvests",
navBatches: "Batches",