A cancelled order with no invoiceNumber is a Stripe payment that never
succeeded (failed or timed out before ever reaching received/invoiced),
not a real cancellation of something that actually happened — from the
customer's point of view it was never really an order. Filtered out of
getCustomerOrders/getCustomerOrderDetail by default; the row stays in
Payload for admin/audit purposes (shown there as "Zahlung
fehlgeschlagen", see backend).
getCustomerOrderDetail's filter is opt-in via a new optional parameter,
not the default — /api/checkout/status/route.ts's post-payment polling
needs to keep seeing exactly this order to show the "Zahlung
fehlgeschlagen, bitte erneut versuchen" retry state. The GDPR export
route also opts out for the same reason a legal completeness export
can't silently drop rows.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The width="56" height="56" HTML attributes on the icon badge <td> weren't
mirrored in its inline style — renderers that ignore/strip HTML
width/height (Brevo's own editor among them, per a real report) shrink
the cell to fit the glyph instead of staying a 56px circle. Add explicit
width:56px;height:56px to the style too.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Brevo's redirectionUrl now points here instead of the homepage — a
static page matching /bestellbestaetigung's visual language (brand-
tinted checkmark circle, serif display heading, thin brand divider).
No query params to read; Brevo's confirmation redirect carries nothing
this page needs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Was a plain POST /v3/contacts upsert (single opt-in — straight onto the
list, no confirmation required). Now calls doubleOptinConfirmation
instead, so a signup only requests subscription; Brevo sends its own
confirmation email and adds the contact to the real list only once they
click through. Needs BREVO_DOUBLE_OPTIN_TEMPLATE_ID set in Coolify
before this works — not yet configured, signups will fail closed with a
logged reason until it is.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both already route through the same Stripe PaymentIntent
(automatic_payment_methods: enabled — Stripe's own recommended Payment
Element pattern, letting Stripe itself decide which eligible method to
show). Pre-selecting one of two identical-behind-the-scenes rows before
the payment step was redundant friction, not a real choice. Collapses
them into one option with a hint text explaining the actual instrument
is picked on the next screen; Überweisung is unaffected.
Also refines paymentMethodTitle from a neutral "Online-Zahlung"
placeholder (snapshotted at order-creation time, before the customer has
picked an instrument) to the real one Stripe reports, once payment
confirms — carried through to both the stored order and the
sessionStorage snapshot shown on /bestellbestaetigung.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The webhook route previously assumed the backend's confirm-payment
endpoint sent the customer confirmation email; the backend assumed the
opposite. Net effect: a successful Stripe payment never triggered any
confirmation email. Consume the order snapshot confirm-payment now
returns and send it from here, matching what the checkout route already
does for a manual/Überweisung order.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Checkout now branches on payment-methods.provider: Überweisung stays
immediate/unchanged, Kreditkarte/PayPal creates a pending_payment order,
mounts Stripe's Payment Element, and defers invoice/email to a webhook-
verified confirm-payment call once the backend actually confirms payment.
Includes a PAYMENT_TEST_MODE mock provider so the whole gated pipeline is
exercisable locally without a real Stripe account.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Checkout: the 4 step cards' border-border and CheckoutSteps' connector
lines/upcoming-circle borders were nearly the same luminance as the
page's own bg-bg-base background, barely visible. Darker (#c4b8a0),
scoped to just these spots rather than the shared border-border token.
- Hero.tsx: fixes a mistake from the last Tablet pass — moving the CTA/
subtitle/icon/social-proof size overrides from lg: to md: (to match the
grid breakpoint move) actually removed their smaller sizing from the
whole Tablet range, recreating the exact 3-line-wrap problem the lg:
exception used to prevent. Reverted those specific overrides back to
lg: (grid structure stays at md:), and added a smaller heading size
(text-h1) below lg: too — text-display's 44px floor doesn't fit the
~283-320px Tablet column any better than the CTA did.
- About.tsx: the quote/divider/bio row and its divider orientation also
pushed from md: to lg: — still too tight for the Tablet column even
after the text/photo ratio swap from the previous pass.
- Hero.tsx: structural breakpoint reverted from lg: back to md: — the
smaller CTA/subtitle/icon sizes added during the Mobile pass fit
comfortably in the ~283px Tablet column, so the original 3-line-wrap
problem that justified lg: doesn't recur. Tablet gets the real 5/7
grid (image beside text) again instead of a stacked mobile layout.
- Tools.tsx: full-size (56px) card icon pushed from md: to lg: — at
Tablet it dwarfed the still-close-to-floor title/description text.
- About.tsx: text/photo flex ratio swapped at Tablet (text gets the
bigger share, no overlap) vs. the original ratio + overlap trick from
lg: up, where it was designed for — Tablet's text column was too
narrow for its fixed-width statement + quote/bio row otherwise.
- Newsletter.tsx/Footer.tsx: the input+button row and the logo/handle/
legal-links row both went side-by-side at md:, but their surrounding
columns didn't leave enough width at 768px — pushed to lg:flex-row.
RichText.tsx switched to Payload's official React renderer + custom
JSXConverters (same call signature, LiveRichText/LivePostContent
untouched) — needed to render the new Lexical Blocks the Payload repo's
Posts.content just gained. Converters follow the existing CMS-image
convention (relative + aspect-[...] + fill + object-cover); the video
block resolves YouTube/Vimeo links to an iframe embed.
New getSeoSettings() fetcher (same pattern as getKleinunternehmer()),
app/layout.tsx now generateMetadata() reading it with the same fallback
values it used to hardcode. Per-post SEO overrides (seoTitle/
seoDescription/seoImage) wired into the blog detail page's metadata,
falling back to title/excerpt/thumbnail when empty.
Also fixed while auditing every page's metadata: missing descriptions on
3 konto pages, a static title on the dynamic order-detail route, and
missing OG images on /shop and /blog.
Input and button were a fixed row at every width, squeezing together on a
narrow phone — now flex-col sm:flex-row, full width when stacked, same
pattern as Challenge's EmailCapture and the other mail CTAs already fixed.
- Checklist checkmarks (Challenge, todo-cards, newsletter): mt-0.5 -> mt-1,
centering against the first line's glyphs instead of sitting flush at
the very top edge.
- Navbar login icon: -translate-y-0.5 — its round head vs. wide shoulders
read as optically bottom-heavy next to the cart icon despite both
having a mathematically centered bounding box.
- /todo-cards's detail page (the only page resolving a product regardless
of active status) now renders both CTAs as "Ausverkauft" and disabled
when the product is deactivated, instead of fully buyable — variants are
dropped too, since AddToCartButton's outOfStock prop is otherwise ignored
whenever variants is non-empty.
- StepArrow: brand orange didn't fit here after all, switched to
near-black (#222221) per feedback.
- Newsletter/"Impulse & Tipps" checklist checkmarks (WeeklyImpulsesHero.tsx,
WeeklyBenefits.tsx) were still the old un-recolorable icon-check.svg —
same inline-SVG orange-checkmark fix as todo-cards/Challenge, plus
items-start instead of items-center on WeeklyImpulsesHero's list for
top-alignment consistency.
- PopIn (Home Hero's brand dot) switched from whileInView to animate — its
translate-based entrance could push the element off-screen on a narrow
phone before the IntersectionObserver ever saw it as visible, leaving it
stuck invisible permanently.
- Hero image: no longer wrapped in Reveal below lg: — whileInView's margin
meant it stayed at opacity:0 (a white gap above the fold) on short mobile
viewports until scrolled. Reveal's fade-in kept from lg: up.
- "→ Label" CTA links (Tools.tsx, Blog.tsx) now use a flex row with the
arrow as its own span instead of a literal inline "→" character, which
doesn't reliably align to the surrounding text's cap-height.
- Replaced icon-arrow-connector.svg with a new shared StepArrow component
(inline SVG) across all three step sections — the old asset's color
couldn't be overridden from outside the SVG file, so it could never
actually become brand-orange. Bigger and better-shaped below the
structural breakpoint per feedback.
- Added MobileSectionTOC (SectionTOC.tsx) — a <details> accordion shown
below lg: on Impressum/Datenschutz/AGB/Widerruf/Versand, which previously
had no on-page navigation aid at all below lg: (the sidebar TOC is
`hidden` entirely there).
- Updated the figma-to-nextjs skill with 8 new dated Gotchas from this
mobile-responsive pass, and expanded Step 6's verification checklist.
- README: new "Mobile responsive pass" section summarizing the above.
- Newsletter card icon: fixed an aspect-ratio distortion bug (w-16 maps
to this project's fluid --spacing-16, which floors to 40px below
768px, paired with a fixed 55px height — squished the icon on
mobile). Same root cause existed in NewsletterModal's envelope icon;
that one's now just hidden below md: instead per feedback.
- Hero: subtitle sized down and CTA checkmark icon scaled to match
below lg:, social proof text always wraps below the avatars there
instead of only when it doesn't fit.
- Werkzeuge connector arrows (todo-cards/newsletter/challenge
HowItWorks): object-contain added — the SVG has
preserveAspectRatio="none" and was stretching to fill the square
mobile box instead of keeping its thin-arrow shape.
- Challenge bottom CTA: icon now stacks above the copy, centered, below
lg: to match the Home Newsletter card's pattern.
- Homepage Werkzeuge icons: smaller below md: to match the (fluid-floor)
text size next to them.
- Blog detail "Passend dazu" card: the fixed w-[19rem] title column plus
"Entdecken" sharing its row overflowed on mobile; stacked below sm:
instead, with a little top margin on "Entdecken".
- Todo-Karten checklist checkmarks: recolored brand-orange (icon-check
.svg's fill lives in an internal CSS var that can't be overridden from
outside an <img>-loaded SVG, so switched to an inline SVG) and
top-aligned instead of vertically centered.
- AddToCartButton: added whitespace-nowrap to the stacked-label grid
(used to reserve button width across all possible label texts) — one
of those labels wrapping to two lines on a narrow w-full button was
inflating the row height for whichever label is actually showing,
leaving a tall empty gap under "Ausverkauft".
- Hero CTA: back to a single line below lg: (wrapping put the icon
beside two lines and looked broken), sized down instead so the full
phrase fits.
- Hero heading: dropped the forced <br> after "darf" at true mobile
widths, kept it only for the sm-lg tablet range it was added for.
- Hero social proof: avatars+text now wrap and center instead of
cramming onto one row (flex-wrap + justify-center, no fixed
breakpoint needed).
- Newsletter card icon: stacked above the copy and centered below md:
instead of squeezed into a row beside it.
- TrustRow: left-aligned below md: instead of centered (shop, cart, and
every other page using this component).
- Cart line item image: full width below sm: instead of a fixed 150px
square.
- Challenge EmailCapture (top + bottom CTA, same shared component):
input and button stack full-width below sm: instead of squeezing
into one row.
- Challenge steps: icon above text and centered at every breakpoint
(previously only from lg: up), connector arrows centered to match.
- Hero CTA: whitespace-nowrap forced the button as wide as the full
32-char phrase, overflowing the stacked mobile column. Now wraps to
two lines below lg:, single line from lg: up where there's room.
- Navbar: the account/cart icons are 44px touch targets with the glyph
centered inside, so the outer gap-2 read as too much space on top of
that padding. Grouped them with no gap between just the two.
- Divider: "Klarheit -> Fokus -> Entlastung" plus its connector icons
needs ~550px to fit one row even at the site's 768px fluid floor, far
more than a phone's ~310px content width. Added a component-scoped
--divider-word-size (independent from --text-h2) plus a <640px
override that shrinks the words/icons/gaps together.
FormField is now forwardRef so the email input can be targeted
imperatively — clicking the button clears the field (previous commit)
and now also focuses it, so the shopper can start typing immediately
instead of having to click in first.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
"Andere E-Mail-Adresse verwenden" only hid the inline login prompt
before, leaving the already-registered address still sitting in the
field — the shopper had to manually select/delete it before typing a
new one. Now clears email + loginPassword + the email field's error
alongside dismissing the prompt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AnbieterAngaben.tsx's address/VAT/register blocks each got the same
gap-4 as the section headings, so related lines (e.g. name/street/zip+
city) read as visually disconnected paragraphs instead of one block —
wrapped each in its own gap-1 container, keeping gap-4 only between
sections.
Reveal.tsx's shared fadeUp variant animated opacity and a 28px y-
translate together, which read as the whole section hopping into place
on top of the fade. Renamed to fadeIn, opacity only — applies site-wide
via Reveal/RevealGroup/RevealItem.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Login prompt (email already has an account) now renders inline under
Card 1's own email field instead of a separate block above the whole
form — no scrolling needed in the common case, and no more re-typing
the email into a second field. The submit-time fallback still scrolls
it into view via a useEffect, now that the target is conditionally
rendered.
/konto/profil's "Land" select was still hardcoded to Deutschland/
Österreich/Schweiz independently of /checkout's own Payload-configurable
shipping-countries list.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The new-account password field's onBlur re-focused itself whenever the
value was still invalid (<8 chars), the same generic pattern every other
blur-validated checkout field uses. For most fields that's a helpful
"fix it now" nudge, but it's a genuine trap on a password field near the
end of a card — a shopper could never Tab or click the submit button
until the password was already valid. Dropped just the refocus here
(inline error text still appears immediately); every other field keeps
the existing behavior.
TodoKartenHero.tsx's price line never showed "zzgl. Versand" at all
(pre-existing gap, unrelated to Kleinunternehmer), unlike Pricing.tsx/
ProductSpotlight.tsx — now consistent across all three.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Checkout forces 0% VAT without de-grossing prices when the tenant is a
Kleinunternehmer (a business decision, not just an engineering default —
unlike the existing intra-community VAT exemption, which does de-gross).
Snapshotted onto the order at checkout time so a later toggle of the
company-settings checkbox never rewrites an already-issued invoice's tax
treatment — same reasoning as the existing vatExempt field.
Threaded through: checkout route, order creation/confirmation email,
on-demand invoice/Storno/Gutschrift downloads, the Bestellbestätigung
page, and the account order-detail page. The four storefront "inkl. X%
MwSt." price hints (shop grid, cart upsell, ToDo-Karten landing page,
homepage spotlight) drop that clause live when the setting is on. The
company-settings Live Preview reflects the checkbox in real time too.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
VIES answers HTTP 200 even when it couldn't actually perform the check
(actionSucceed: false, e.g. MS_UNAVAILABLE — Germany's own national
gateway does this fairly regularly). checkVatIdViaVies() only ever read
data.valid, which is absent on that response shape, so it silently
read as valid: false — a real, currently-registered German VAT ID
(reported: DE351362947) looked rejected. Worse, /api/checkout/validate-
vat then wrapped even a correctly-returned ok:false as { ok: true,
valid: false }, which the client reads as "invalid" rather than
"unavailable" — the actual bug the user hit, compounding the vies.ts
gap. Both are fixed now: an unconfirmable check surfaces to the client
as ok:false, which CheckoutContent.tsx's handleVatIdBlur already
correctly renders as "USt-IdNr.-Prüfung derzeit nicht möglich"
instead of a rejection. Same fix applied to the payload backend's own
copy of vies.ts (company-settings' VAT check).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same short "check your inbox" message everywhere instead of each form
having its own success wording (they'd drifted since each is a
separately-coded component, not a shared one).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Order detail (/konto/bestellungen/[orderNumber]) already carried
companyName/vatId/vatExempt on CustomerOrderDetail but never rendered
them — a B2B customer looking at their own order couldn't see the
company/VAT info that's already on their invoice. Now shown in the
billing-address block, with a note when the order was VAT-exempt.
Also bumps @einfach-produktiv/invoicing to pick up the Netto-row fix
(now shown on every invoice, not just non-exempt ones).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extends the checkout pattern (inline red error text, refocus on
submit if invalid) to all four newsletter-signup entry points. Two of
them (WeeklyImpulsesHero's inline hero form on /newsletter, and
/challenge's EmailCapture) turned out to be completely non-functional
before this too — same static-markup-with-no-onSubmit issue as
Newsletter.tsx/NewsletterModal.tsx had, just missed in the previous
pass since they're separate components sharing only the visual
pattern, not the code.
Consolidated the shared email+consent+submit state (previously
duplicated per-component) into useNewsletterSignup.ts, and pulled the
plain email-format regex (previously duplicated in CheckoutContent.tsx
and the subscribe route) into lib/email.ts as a single source of
truth. /challenge's EmailCapture is now its own client component
(app/challenge/components/EmailCapture.tsx) since its parent page is
an async Server Component and can't hold form state itself.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both standalone signup forms (Newsletter.tsx on Home/newsletter page,
NewsletterModal.tsx from the Navbar CTA) were previously non-functional
— static markup with no onSubmit/state at all, nothing was ever
captured. They're now real client forms posting to the new
/api/newsletter/subscribe route, which upserts the contact into
Brevo's Contacts API (list id from BREVO_LIST_ID). Checkout's existing
newsletterOptIn checkbox gets the same sync, fire-and-forget alongside
the order-confirmation email — a failed marketing sync must never
fail checkout.
lib/brevo.ts is the only thing that talks to Brevo; this app still
never sends marketing mail itself. Whatever automation Brevo has
configured on the list (Welcome Flow etc.) runs entirely on their
side — Brevo's Automation workflows aren't manageable via their
public API at all, so that part can't be wired up from here.
Needs BREVO_API_KEY and BREVO_LIST_ID set in the frontend's Coolify
environment — not yet added there.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Re-focus still returns attention to the field, but no longer wipes
the customer's input via select() — a stray keystroke while glancing
at the error message shouldn't erase what they typed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both the billing and shipping-override country selects, plus PLZ
maxLength/pattern validation, now read from the new shipping-countries
collection (getShippingCountries()) rather than a hardcoded
Deutschland/Österreich(/Schweiz) array. Lets an admin add or reorder
destination countries without a frontend deploy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Generalizes what the USt-IdNr. field already did for an unconfirmed
VIES result — every blur-validated field now gets focus put right
back on it the moment its own validation fails, instead of letting
focus move on to wherever the customer tabbed/clicked next. The
correction happens immediately rather than being left for a submit
attempt to catch later.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Makes the "konnte nicht bestätigt werden" message read as an actual
error instead of neutral status text, and re-focuses + selects the
whole VAT ID on an unconfirmed result — the customer almost certainly
needs to retype it, so the next keystroke should just replace it
outright.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
VAT-ID validity and the exemption decision are separate questions.
Previously VIES was only ever called when the destination already
qualified for the cross-border exemption (Österreich), so a garbage
VAT ID on a domestic order (e.g. "ED123456789" — not even a real
country code) sailed through with no feedback at all, and a
Deutschland/Schweiz customer got no confirmation their real VAT ID
was valid either. Now VIES checks any format-valid VAT ID regardless
of destination (data quality, same reasoning as company-settings'
own check) — the exemption itself still only applies when the
destination is also Österreich, a validated German VAT ID never
zero-rates a domestic sale. The status message now always shows
("✓ USt-IdNr. bestätigt", plus the exemption note only when it
actually applies) instead of staying hidden for non-Österreich
orders.
Also added maxLength to PLZ (per-country digit count) and USt-IdNr.
(14) checkout fields — they had pattern validation but nothing
stopping the browser from accepting more characters than could ever
be valid.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A validated EU business buyer (Österreich, the one cross-border option
this checkout offers) gets the sale zero-rated per §4 Nr. 1b UStG —
but only after a live VIES lookup confirms the VAT ID is actually
registered right now, never from format-validity alone (real
compliance risk otherwise). VIES unreachable fails closed: normal VAT
applies, no guessed exemption.
- lib/vies.ts: calls the EU's public VIES REST API.
- lib/vatExemption.ts: de-grosses item/shipping prices and computes
the exempt totals; also picks the actual destination country
(shipping override when set, billing otherwise).
- api/checkout/validate-vat: on-blur live check for instant feedback;
api/checkout/route.ts re-runs the same check server-side at submit
as the actual source of truth, and re-prices every line net-of-VAT
when exempt.
- CheckoutContent.tsx: VIES status + a live exempt-totals preview;
BestellbestaetigungContent.tsx mirrors it from the persisted
snapshot. Both blur-validate every other checkout field now too
(immediate inline errors, not just on submit).
- vatExempt/vatIdValidatedAt threaded through orderServer.ts,
customerAuth.ts, orderEmail.ts, and both invoice-download routes so
the invoice PDF and its e-invoice XML (companion payload-repo
commit) reflect the exemption correctly wherever it's rendered.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
B2B checkout fields, split out from the e-invoicing migration and
picked back up now that it's shipped. Both fields are independently
optional, format-validated (shared regex in lib/vatId.ts, mirrored
server-side in api/checkout and api/account/profile), persisted in
the checkout draft, and saved as a customer profile default that
pre-fills future checkouts. Order/customer snapshot fields land in a
companion Payload backend commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Stock was only checked at checkout; a shopper could add more of a
product to the cart than was actually in stock and only find out at
the last step. Product/variant now carry a real maxQty, and
AddToCartButton/AddToCartInlineButton/the cart's quantity stepper all
disable or cap once the cart already holds that many.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bestellübersicht rows now show only text (Bestellnummer, Datum, Artikel-Anzahl, Status, Gesamtbetrag) — no product images. The order detail page (app/konto/bestellungen/[orderNumber]/page.tsx) is untouched and still shows a thumbnail per item, which is the only place they should appear.
Native HTML5 pattern validation, mirroring the same rules the backend now enforces (Orders.ts/Customers.ts, same commit on that repo): PLZ digit count by country (5/DE, 4/AT+CH), Packstationnummer 1-3 digits, Postnummer 6-10 digits — catches the exact mix-up (long number in Packstationnummer, short number in Postnummer) found in a real order's data while investigating this. Client-side only for instant feedback; the backend re-validates regardless.
Also bumps @einfach-produktiv/invoicing to pick up the invoice PDF layout fixes (centered footer, clustered summary rows, relocated paid badge, stacked Packstation/Postnummer address lines).
Corrected after user feedback: Stammkapital/Grundkapital is only required on business correspondence if voluntarily disclosed in the first place (§35a Abs. 1 S. 2 GmbHG) — not an unconditional Pflichtangabe. shareCapital's Impressum rendering stays (shown only if an admin voluntarily filled it in), but it's dropped from the email/invoice footer.
generalPartners is removed entirely — the legal basis was genuinely unclear on research (§125a HGB's Geschäftsbriefe-naming duty only applies to the narrow case where no partner is a natural person; whether §5 DDG's Impressum-specific "vertretungsberechtigte Person" requirement independently mandates it for the general OHG/KG case wasn't resolved with confidence) — reverted rather than shipped on an uncertain legal basis.
Also fixes stale "§5 TMG" citations to "§5 DDG" (TMG was replaced 14 May 2024).
Mirrors the backend's new CompanySettings.shareCapital/generalPartners fields: rendered in the Impressum (AnbieterAngaben.tsx — new "Gesellschafter" section, Stammkapital line under Handelsregister, and the "Verantwortlich für den Inhalt" fallback now considers a general partner before falling back to sellerName) and wired into buildLegalFooterLines() for the invoice/email footer, same as registerCourt/registerNumber/managingDirector already were.
No visible change today (current legalForm is sole-proprietorship, neither field is set) — this is prep so a future legalForm change in company-settings updates the Impressum automatically instead of needing a manual Impressum edit at that point.
invoiceData.ts's generateInvoicePdf()/generateCorrectionInvoicePdf() now
call renderInvoiceEInvoice()/renderCorrectionInvoiceEInvoice() instead of
the plain PDF renderers — both are the single wrapper every caller
already goes through (orderEmail.ts's checkout attachment, and the two
on-demand /invoice and /correction-invoice download routes), so this one
change switches all three. Buffer.from() wraps the library's Uint8Array
return value — every downstream consumer already expects a Buffer,
unchanged.
Imports from "@einfach-produktiv/invoicing/einvoice" (a new subpath, not
the package's main entry) — @e-invoice-eu/core pulls in Node-only
dependencies that broke the client bundle when reachable from the main
entry, which a Client Component also imports transitively (Live
Preview). See that package's own commit for the fix.
Existing failure-handling is unchanged and covers this: a PDF-generation
error still doesn't block the confirmation email, it just sends without
the attachment and alerts admin (see orderEmail.ts) — same safety net
that already existed for the plain-PDF path.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
"Angaben zum Anbieter"/"Umsatzsteuer"/"Handelsregister"/"Geschäftsführung"/
"Verantwortlich für den Inhalt" used to be hand-typed into the Impressum's
richText content (seed-legal-pages.ts) with no connection to the same
seller data the invoice PDFs and every email footer already pull from
company-settings — an admin updating one had no reason to remember the
other existed, and the old text was already stale in one concrete way:
it never showed Handelsregister/Geschäftsführung at all even though
company-settings has modeled both since the legal-form work shipped.
Now rendered by a new AnbieterAngaben component, straight from
getCompanySettings(), positioned above the CMS richText (which keeps
only genuinely editorial content: Kontakt, Haftung für Inhalte, Haftung
für Links, Urheberrecht). Same "structural/brand elements in code, only
pull the actual numbers/copy that need single-sourcing from data"
pattern this page's own Nachhaltigkeit sidebar card already used.
RichText.tsx's headingId() is now exported so the new block's headings
get the exact same id-assignment logic as CMS-driven ones, keeping the
SectionTOC sidebar's ids in sync with both sources.
/impressum moves from static to dynamic rendering (it now fetches live
company-settings data, cache: "no-store") — an acceptable tradeoff for
a legally-required page to never show stale seller info.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CompanySettings type now mirrors the payload repo's split bankDetails ->
iban/bic (see that repo's own commit for the reasoning). No behavior
change here beyond the type/fallback update — the actual footer
rendering lives in @einfach-produktiv/invoicing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phase 0 of the e-invoicing migration plan (see the E-Rechnung planning
session) — moves invoicePdf.tsx, correctionInvoicePdf.tsx, and
taxBreakdown.ts into a new shared package, consumed as a git dependency
by both this repo and the payload backend, instead of hand-duplicating
the correction-invoice logic between them (see that package's own README
for the three real drifts the duplication had already caused).
Consumed as raw TS/TSX source via next.config.ts's transpilePackages, not
a pre-built package. Needs `git` in the Docker deps stage and a
project .npmrc (allow-git=root) to let npm ci fetch a git-URL dependency
at all — npm 12+ disables that by default.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Missed in the earlier pass — /todo-cards has two independent purchase
CTAs (the hero at the top and the Pricing panel further down), and only
Pricing.tsx got the low-stock text line. The hero had no low-stock
logic at all before this.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The image-overlaid pill made the low-stock message read as clutter on
product photos and had no equivalent in the cart at all. It's now a text
line next to the price (ProductGrid/ProductSpotlight/RelatedProducts/
Pricing) and under the product name in cart line items (variant-specific,
not "any variant low"). The line's height is always reserved, not
conditionally rendered, so cards in the same row stay equal-height
regardless of low-stock state — the exact regression an earlier text-based
version of this hint caused before it was replaced by the image badge.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same row shape as the Gesamtsumme total line itself (label left, flex-1
spacer, value right) instead of the grid — label stays flush with
"Gesamtsumme", rate+amount land flush right under the total's own €
figure. Fixed-width rate column keeps multiple rates aligned to each
other regardless of digit count.