diff --git a/README.md b/README.md index 1bd50f6..2decacf 100644 --- a/README.md +++ b/README.md @@ -511,6 +511,24 @@ catalog, since an order only ever snapshots a numeric product id). touched spots and why some read a live setting and others a persisted per-order snapshot. +**Vorkasse instruction in the confirmation email (2026-07-25)** — the +invoice PDF already showed this (see `@einfach-produktiv/invoicing`'s own +`unpaidNoticeText`), but a customer often only glances at the email body +itself, not the attached PDF. `OrderConfirmationData` gained a required +`isManualPayment: boolean` field — **explicitly set by each caller** +(checkout route's manual branch: `true`; the Stripe webhook path: +always `false`, since only a *paid* Stripe order ever reaches that send at +all), deliberately **not** derived from `paymentMethodTitle` inside +`emailTemplates.ts` itself — that string ("Online-Zahlung", "Kreditkarte", +"Überweisung (Vorkasse)", ...) is exactly the kind of thing a +payment-methods rename already broke once this session (see +`isPaidImmediately()` in the invoicing package). When `isManualPayment` is +true, `vorkasseNotice()` renders a full-width block (own row, `margin-top: +20px` — not squeezed into the Gesamtsumme table) naming the bank details +(`CompanySettings.bankName`/`iban`/`bic`, now also exposed on the +frontend's own `CompanySettings` type — previously only `iban`/`bic` were) +and the "processed within 1–2 business days after payment received" note. + ### Checkout state persistence `app/lib/checkoutDraft.ts` — `localStorage` under `ep_checkout_draft`, @@ -1148,12 +1166,30 @@ unsynced. heading, thin brand divider). No query params to read — Brevo's redirect carries nothing this page needs, unlike `/checkout/verarbeitung` which polls actual payment status. +- **Already-subscribed detection (2026-07-25)** — `doubleOptinConfirmation` + itself gives no way to tell a brand-new signup apart from an + already-confirmed contact re-submitting the form (verified directly: + calling it twice for the same confirmed contact returns the identical + `201` both times, just silently resends the confirmation mail). So + `upsertNewsletterContact()` checks first via `GET /v3/contacts/{email}` + — a contact's `listIds` on Brevo is only populated once double opt-in + actually confirms, never for a merely-requested one, so its presence is + a reliable signal. If already subscribed: skips the resend entirely and + returns `{ ok: true, alreadySubscribed: true }` instead, which the UI + (`useNewsletterSignup.ts`) surfaces as a different message ("Diese + E-Mail-Adresse ist schon für unseren Newsletter angemeldet.") instead of + the normal "check your inbox" copy. The check fails open (any error → + proceed to the normal signup flow) — it's a UX nicety, never a reason to + block a real signup. - **`app/lib/useNewsletterSignup.ts`** — the shared email/consent/submit state + on-blur validation + refocus-on-invalid-submit behind all four forms (same "state of the art, simple" input-quality bar as checkout's own fields). Each form keeps its own markup/visual style (`Newsletter`'s panel layout, `/challenge`'s hardcoded-hex-color palette, etc.) — only - the logic is shared, not a one-size-fits-all component. + the logic is shared, not a one-size-fits-all component. The success + message text itself also now lives here (`successMessage`), not + hardcoded 4 times per form — one of two variants depending on + `alreadySubscribed`. - **`app/lib/email.ts`** — `isValidEmail()`/`validateEmailFormat()`, the single plain-email-format check shared by every newsletter form *and* checkout's own email field (previously duplicated between @@ -1724,6 +1760,44 @@ breakpoint no longer needed to be as conservative as originally set: 768px. Both pushed from `md:flex-row` to `lg:flex-row` — stacked through the whole Tablet range, side by side again once there's real room. +## Structured data (schema.org, 2026-07-25) + +JSON-LD (`