Files
Wabenhain/src/app/konto/patenschaft/page.tsx
T
Nick SzaunigandClaude Opus 5.5 515fca1c16
Docker Image bauen und veroeffentlichen / typecheck (push) Successful in 2m57s
Docker Image bauen und veroeffentlichen / lint (push) Successful in 3m7s
Docker Image bauen und veroeffentlichen / build-and-push (push) Canceled after 0s
Docker Image bauen und veroeffentlichen / test (push) Canceled after 1m22s
fix: Befunde aus Sicherheits- und Korrektheitsreview der Bienenpatenschaft
- Oeffentliche Kuendigung: exakte Identifikation, Limits je Vertrag und
  Adresse, Bestaetigung nur an die Kontoadresse mit Ruecknahme-Link (HMAC)
- Vorschau: Body-Grenze beim Lesen, Parallelitaets- und Minutenlimit, Cache
- Rabatte gelten nicht fuer Patenschaftspositionen
- Abo aktiv erst nach bestaetigter Zahlung, offene Abschluesse zaehlen
  zum Kontingent und werden nach 24 h aufgeraeumt, Sitzung 58 Minuten
- Konto-Loeschung nur ohne laufendes Abo, Urkunden-Dateien werden entfernt
- Abo-Lieferungen mit Zahlungszeile (DHL-Marke moeglich)
- Shop zeigt freien Bestand nach Abo-Reservierung
- Storno/volle Erstattung widerruft die Patenschaft (Status WIDERRUFEN)
- Umsatzsteuer der Patenschaft per VAT_RATE_PERCENT_PATENSCHAFT (19 %)
- Digitale Bestellungen archivieren sich, Kalendertage in Europe/Berlin,
  Monatsenden begrenzt, Datenauskunft um Patenschaften ergaenzt

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-16 15:09:32 +02:00

81 lines
3.5 KiB
TypeScript

import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { Container, SectionHeading, LinkButton } from "@/components/ui";
import { BackLink } from "@/components/back-link";
import { AccountEmptyState } from "@/components/account-ui";
import { getCurrentCustomer } from "@/lib/auth";
import { listOwnedSponsorships } from "@/lib/sponsorship/access";
import { hasActiveOffers } from "@/lib/sponsorship/offers";
import { getCustomerLoyalty } from "@/lib/loyalty";
import { getLocale } from "@/i18n/locale";
import { getAccountSponsorshipStrings } from "@/i18n/sponsorship/account";
import { MedalShelf } from "./_components/medal-shelf";
import { SponsorshipRow } from "./_components/sponsorship-row";
import { PendingRefresher } from "./_components/pending-refresher";
import { BeeSealIcon } from "./_components/icons";
export const dynamic = "force-dynamic";
export async function generateMetadata(): Promise<Metadata> {
return { title: getAccountSponsorshipStrings(await getLocale()).overview.metaTitle };
}
export default async function KontoPatenschaftPage() {
const locale = await getLocale();
const t = getAccountSponsorshipStrings(locale);
const customer = await getCurrentCustomer();
if (!customer) redirect("/konto");
const [sponsorships, loyalty] = await Promise.all([
listOwnedSponsorships(customer.id, locale),
getCustomerLoyalty(customer.id, locale),
]);
// Widerrufene ohne Urkunde werden nie mehr fertig — kein Dauer-Neuladen.
const hasPending = sponsorships.some((sponsorship) => sponsorship.revision === null && sponsorship.lifecycle !== "REVOKED");
return (
<div className="py-20">
<Container className="max-w-4xl">
<BackLink href="/konto" label={t.overview.back} />
<SectionHeading level="h1" title={t.overview.title} subtitle={t.overview.subtitle} />
{sponsorships.length === 0 ? (
<AccountEmptyState
icon={<BeeSealIcon />}
title={t.overview.emptyTitle}
description={t.overview.emptyDescription}
action={(await hasActiveOffers()) ? <LinkButton href="/patenschaft">{t.overview.emptyAction}</LinkButton> : undefined}
/>
) : null}
{sponsorships.length > 0 ? (
<section aria-labelledby="sponsorship-list-heading" className="mt-12">
<h2 id="sponsorship-list-heading" className="sr-only">
{t.overview.listTitle}
</h2>
{hasPending ? (
<p role="status" className="mb-5 flex items-start gap-3 border border-honey/30 bg-honey/5 px-4 py-3 text-sm text-ink/75">
<span aria-hidden="true" className="motion-pulse mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-honey animate-[wh-pulse_2s_ease-out_infinite]" />
<span>
<span className="text-heading">{t.overview.pendingTitle}.</span> {t.overview.pendingHint}
</span>
</p>
) : null}
<ul className="space-y-5">
{sponsorships.map((sponsorship, index) => (
<SponsorshipRow key={sponsorship.id} sponsorship={sponsorship} locale={locale} t={t} index={index} />
))}
</ul>
</section>
) : null}
{sponsorships.length > 0 || loyalty.regular.level || loyalty.subscription?.level ? (
<MedalShelf sponsorships={sponsorships} loyalty={loyalty} t={t} />
) : null}
{hasPending ? <PendingRefresher /> : null}
</Container>
</div>
);
}