Profile: Rechnungsadresse is always a street address, no Packstation toggle

Matches /checkout's own billing card exactly (an invoice needs a real
postal address) — the profile form previously offered a Lieferart/
Packstation choice for what's actually always used as the billing
address, inconsistent now that a separate "Lieferadresse" section exists.
This commit is contained in:
Marco
2026-07-30 08:48:04 +00:00
parent e3352d7e32
commit 8c843c0ac1
3 changed files with 57 additions and 54 deletions
+37
View File
@@ -1999,6 +1999,43 @@ field-lock security logic is still tested in the **Payload backend's**
own `test:unit` — see that repo's README's own "Tests" section, since
that's where that logic actually lives.
## 2026-07-30 changes
**Blog posts can have multiple categories.** `BlogPost.categories` is now
`string[]` (was a single `category: string`), joined with `", "` wherever
a single category used to render (`/blog`, `/blog/[slug]`,
`LivePostContent.tsx`, `components/Blog.tsx`). Backend field is
`Posts.categories`, a `hasMany` relationship — see the Payload backend's
own README.
**Checkout's "Abweichende Lieferadresse" gained Firma + Kontakt-E-Mail/
Telefon** (all optional) — handed to the shipping carrier, not used for
any customer communication (that stays the account email above). Backed
by new `Orders.shipping*` fields with no shipping-side `vatId` (billing-
only concept, deliberately not mirrored).
**The customer profile (`/konto/profil`) can now store its own shipping
address**, mirroring the checkout's own "Abweichende Lieferadresse"
section field-for-field (same checkbox pattern, same fields incl. the new
Firma/Kontakt fields above) — prefills the checkout section for a
returning customer instead of always starting blank. Backed by
`Customers`' new "Lieferadresse" tab in the Payload backend.
**Products can have an optional SKU** even without variants
(`Products.sku` — variants already had their own). Snapshotted onto each
order item at checkout (`app/api/checkout/route.ts` — variant SKU takes
precedence over the product's own) and shown as "Art.-Nr." on the invoice
PDF, the order-confirmation email, and `/konto/bestellungen/[orderNumber]`.
**`@einfach-produktiv/invoicing` bumped to 0.2.7** for the SKU +
shipping-contact rendering above — re-run `npm install
@einfach-produktiv/invoicing` after pulling if your local `node_modules`
predates this.
Full plan for an n8n-driven blog-post content-automation pipeline (not yet
built) lives in the Payload backend repo's `docs/blog-automation-plan.md`,
not duplicated here since it changes independently of this frontend.
## Deployment
- **Dockerfile**: 3-stage build (`deps``builder``runner`) with
+3 -13
View File
@@ -16,10 +16,7 @@ export async function PATCH(request: Request) {
const {
firstName,
lastName,
deliveryMethod,
street,
packstationNumber,
postNumber,
zip,
city,
country,
@@ -44,7 +41,8 @@ export async function PATCH(request: Request) {
!firstName ||
typeof lastName !== "string" ||
!lastName ||
(deliveryMethod !== "address" && deliveryMethod !== "packstation") ||
typeof street !== "string" ||
!street ||
typeof zip !== "string" ||
!zip ||
typeof city !== "string" ||
@@ -54,12 +52,6 @@ export async function PATCH(request: Request) {
) {
return NextResponse.json({ ok: false, reason: "Bitte alle Pflichtfelder ausfüllen." }, { status: 400 });
}
if (deliveryMethod === "address" && !street) {
return NextResponse.json({ ok: false, reason: "Bitte Straße und Hausnummer angeben." }, { status: 400 });
}
if (deliveryMethod === "packstation" && (!packstationNumber || !postNumber)) {
return NextResponse.json({ ok: false, reason: "Bitte Packstation- und Postnummer angeben." }, { status: 400 });
}
// Both independently optional (see Customers.ts's own comment) — only
// format-checked when actually provided, same as the backend field itself.
const normalizedVatId = typeof vatId === "string" && vatId ? normalizeVatId(vatId) : undefined;
@@ -97,10 +89,8 @@ export async function PATCH(request: Request) {
const result = await updateCustomerProfile(session.token, session.customer.id, {
firstName,
lastName,
deliveryMethod,
deliveryMethod: "address",
street,
packstationNumber,
postNumber,
zip,
city,
country,
+17 -41
View File
@@ -34,7 +34,6 @@ export function ProfileForm({
shippingCountries: ShippingCountry[];
}) {
const router = useRouter();
const [deliveryMethod, setDeliveryMethod] = useState<"address" | "packstation">(profile.deliveryMethod ?? "address");
const [hasDifferentShippingAddress, setHasDifferentShippingAddress] = useState(profile.hasDifferentShippingAddress);
const [shippingDeliveryMethod, setShippingDeliveryMethod] = useState<"address" | "packstation">(
profile.shippingDeliveryMethod ?? "address",
@@ -53,10 +52,12 @@ export function ProfileForm({
const body = {
firstName: String(form.get("firstName") ?? ""),
lastName: String(form.get("lastName") ?? ""),
deliveryMethod,
// Always a plain street address — same "Rechnungsadresse is never a
// Packstation" rule as /checkout's own billing card (an invoice
// needs a real postal address). Packstation is only ever offered
// below, in the optional "Lieferadresse" section.
deliveryMethod: "address" as const,
street: String(form.get("street") ?? "") || undefined,
packstationNumber: String(form.get("packstationNumber") ?? "") || undefined,
postNumber: String(form.get("postNumber") ?? "") || undefined,
zip: String(form.get("zip") ?? ""),
city: String(form.get("city") ?? ""),
country: String(form.get("country") ?? ""),
@@ -136,43 +137,18 @@ export function ProfileForm({
/>
</div>
<div className="w-full flex flex-col gap-2 items-start">
<span className="text-label text-text-muted">Lieferart</span>
<div className="flex w-full max-w-sm rounded-sm border border-border overflow-hidden">
<button
type="button"
onClick={() => setDeliveryMethod("address")}
aria-pressed={deliveryMethod === "address"}
className={`flex-1 py-3 text-body-sm font-bold transition-colors ${deliveryMethod === "address" ? "bg-brand text-text-primary" : "text-text-muted hover:text-text-primary"}`}
>
Lieferadresse
</button>
<button
type="button"
onClick={() => setDeliveryMethod("packstation")}
aria-pressed={deliveryMethod === "packstation"}
className={`flex-1 py-3 text-body-sm font-bold border-l border-border transition-colors ${deliveryMethod === "packstation" ? "bg-brand text-text-primary" : "text-text-muted hover:text-text-primary"}`}
>
Packstation
</button>
</div>
</div>
{deliveryMethod === "address" ? (
<Field
label="Straße und Hausnummer"
name="street"
type="text"
defaultValue={profile.street ?? ""}
required
wrapperClassName="w-full"
/>
) : (
<div className="flex flex-col sm:flex-row gap-4 w-full">
<Field label="Packstationnummer" name="packstationNumber" type="text" defaultValue={profile.packstationNumber ?? ""} required />
<Field label="Postnummer" name="postNumber" type="text" defaultValue={profile.postNumber ?? ""} required />
</div>
)}
{/* Always a plain street address, no Lieferart/Packstation toggle
matches /checkout's own Rechnungsadresse card exactly (an
invoice needs a real postal address). Packstation is only ever
offered below, in the optional "Lieferadresse" section. */}
<Field
label="Straße und Hausnummer"
name="street"
type="text"
defaultValue={profile.street ?? ""}
required
wrapperClassName="w-full"
/>
<div className="flex flex-col sm:flex-row gap-4 w-full">
<Field label="PLZ" name="zip" type="text" defaultValue={profile.zip ?? ""} required />