From 20adfaa97e2cd25ef8c621c3d4a06c600ed44728 Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Tue, 6 Oct 2026 23:26:14 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Gutschein-Detailseite=20zeigt,=20ob=20d?= =?UTF-8?q?er=20Code=20gerade=20einl=C3=B6sbar=20ist=20und=20warum=20nicht?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) --- .../admin/(bereich)/gutscheine/[id]/page.tsx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/app/admin/(bereich)/gutscheine/[id]/page.tsx b/src/app/admin/(bereich)/gutscheine/[id]/page.tsx index 22076ae..2385a32 100644 --- a/src/app/admin/(bereich)/gutscheine/[id]/page.tsx +++ b/src/app/admin/(bereich)/gutscheine/[id]/page.tsx @@ -3,11 +3,33 @@ import { notFound } from "next/navigation"; import { connection } from "next/server"; import { Flash, PageTitle } from "@/components/admin/AdminUi"; import { VoucherForm } from "@/components/admin/VoucherForm"; +import { getCamp, salesStateFor } from "@/lib/camp"; import { getDb } from "@/lib/db"; +import { stripeModeOrNull } from "@/lib/env"; import { BOOKING_STATUS_LABEL, REDEMPTION_STATUS_LABEL } from "@/lib/labels"; +import { evaluateVoucher, VOUCHER_REJECTION_ADMIN_TEXT } from "@/lib/vouchers"; export const metadata = { title: "Gutschein" }; +/** + * Prueft wie der Buchungsdialog, ob der Code gerade einloesbar ist, und nennt + * den Grund, den Kundinnen bewusst nicht sehen (neutrale Ablehnung). + * Eine E-Mail-Bindung prueft erst der Checkout. + */ +async function redeemability(code: string): Promise<{ ok: boolean; text: string }> { + const mode = stripeModeOrNull(); + if (!mode) return { ok: false, text: "Nicht prüfbar: STRIPE_SECRET_KEY fehlt oder ist ungültig." }; + const db = getDb(); + const now = new Date(); + const camp = await getCamp(db); + const evaluation = await evaluateVoucher(db, { code, email: null, now, livemode: mode === "live", quota: camp.stammmodelQuota, lock: false }); + if (!evaluation.ok) return { ok: false, text: `Derzeit nicht einlösbar: ${VOUCHER_REJECTION_ADMIN_TEXT[evaluation.reason]}.` }; + if (!salesStateFor(camp, mode, now).checkoutOpen) { + return { ok: false, text: "Code ist gültig, aber die Buchung ist noch nicht geöffnet. Der Dialog zeigt deshalb keinen Preis und keinen Rabatt." }; + } + return { ok: true, text: "Derzeit einlösbar (10 % auf den gültigen Preis)." }; +} + export default async function VoucherPage({ params, searchParams }: { params: Promise<{ id: string }>; searchParams: Promise<{ ok?: string; fehler?: string }> }) { await connection(); const { id } = await params; @@ -17,11 +39,13 @@ export default async function VoucherPage({ params, searchParams }: { params: Pr include: { redemptions: { orderBy: { createdAt: "desc" }, include: { booking: { select: { id: true, reference: true, status: true, livemode: true } } } } }, }); if (!voucher) notFound(); + const check = await redeemability(voucher.code); return ( <> Alle Gutscheine {voucher.code} +

{check.text}