feat: Bestellungen ohne Geschaeftsvorfall lassen sich im Admin loeschen
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m12s

Stornieren gab es bereits, Loeschen nicht — abgebrochene Checkouts und
Testbestellungen blieben dauerhaft in der Liste stehen.

Loeschbar ist bewusst nur, was nie ein Geschaeftsvorfall war: keine Zahlung
erfasst, keine Rechnung erzeugt, Status NEU oder STORNIERT. Eine bezahlte
Bestellung und ihre Rechnung sind Buchungsbelege (§ 147 AO, § 257 HGB) und
muessen jahrelang vorgelegt werden koennen; ein Loeschknopf darauf wuerde
nicht aufraeumen, sondern Unterlagen vernichten. Dafuer gibt es den Storno:
er erstattet das Geld, bucht die Ware zurueck und laesst den Beleg stehen.

Die Regel steht in lib/order-delete.ts als reine Logik ohne Datenbankzugriff
— dieselbe Trennung wie bei shipping-calc.ts. Server Action und Admin-Seite
teilen sie sich, statt die Bedingung zweimal zu formulieren und irgendwann
auseinanderzulaufen, und sie laesst sich ohne DATABASE_URL testen (13 neue
Faelle, u.a. dass bei Rechnung UND Zahlung die Rechnung als Grund gemeldet
wird).

Drei Dinge, die ein blosses delete() falsch gemacht haette:

- LAGERBESTAND: bei NEU liegt die Ware reserviert und wird zurueckgebucht,
  bei STORNIERT ist das beim Stornieren bereits geschehen. Dort erneut
  gutzuschreiben wuerde den Bestand erhoehen, ohne dass je ein Glas zurueckkam.
- ZAHLUNGSSITZUNG: eine Bestellung im Status NEU kann eine offene
  Stripe-Sitzung haben. Wird sie geloescht, ohne die Sitzung zu schliessen,
  kann Minuten spaeter noch bezahlt werden — der Webhook findet dann keine
  Bestellung mehr, und das Geld liegt bei Stripe, ohne dass im Shop etwas
  davon zu sehen waere. Die Sitzung wird deshalb VOR dem Loeschen geschlossen;
  gelingt das nicht sicher, wird gar nicht geloescht. Ist bei Stripe
  zwischenzeitlich bezahlt worden, verweigert die Aktion das Loeschen.
- WETTLAUF: die Bedingungen stehen zusaetzlich in der WHERE-Klausel des
  DELETE. Zwischen Pruefung und Loeschen koennte der Webhook eine Zahlung
  eingetragen haben; die Pruefung sieht nur einen Schnappschuss, die
  WHERE-Klausel wird atomar gegen den aktuellen Zeilenstand ausgewertet.

Der Audit-Log-Eintrag ist ausfuehrlicher als bei anderen Aktionen und traegt
Bestellnummer, Status, Betrag, E-Mail und Positionsanzahl — der Datensatz ist
danach weg, dieser Eintrag ist die einzige verbleibende Spur.

Die Oberflaeche zeigt den Knopf am Seitenende in einem abgesetzten Bereich,
damit er nicht neben dem Statusfeld liegt, mit Browser-Rueckfrage ueber
ConfirmForm. Ist die Bestellung nicht loeschbar, steht dort statt des Knopfes
der Grund und der Verweis auf den Storno. Die Bestellliste zeigt jetzt auch
Erfolgsmeldungen, nicht nur Fehler.

tsc: 0 Fehler. vitest: 372/372 gruen. eslint: 0 Warnungen. next build: gruen.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5 committed 2026-09-22 02:05:43 +02:00
1 parent 256e823c4f
commit c35d61bfd6
8 files changed
+318 -2

No files matched your search

+4 -1
View File
@@ -12,7 +12,7 @@ import { getDictionary } from "@/i18n/get-dictionary";
export default async function AdminOrdersPage(props: PageProps<"/admin/bestellungen">) {
const t = getDictionary(await getLocale());
const statusLabels = t.orderStatusBadge;
const { q, status, error } = await props.searchParams;
const { q, status, error, message } = await props.searchParams;
const query = typeof q === "string" ? q.trim() : "";
const statusFilter = typeof status === "string" && status in statusLabels ? (status as OrderStatus) : undefined;
@@ -41,6 +41,9 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
{/* Die DHL-Aktionen leiten bei einer nicht mehr auffindbaren Bestellung
hierher zurueck (admin/actions/dhl.ts). Ohne diese Ausgabe sah das
aus wie ein folgenloser Klick. */}
{message ? (
<p className="mb-6 text-sm text-forest bg-forest/5 border border-forest/20 px-4 py-3">{message}</p>
) : null}
{error ? (
<p className="mb-6 text-sm text-red-700 bg-red-50 border border-red-200 px-4 py-3">{error}</p>
) : null}