/** * Aktionen der Kundin auf ihrer Statusseite. Zugang nur ueber das * Statustoken aus Bestaetigungsmail bzw. Stripe-Ruecksprung. */ import { z } from "zod"; import { checkSameOrigin, fail, ok, rateLimited, readJson } from "@/lib/api"; import { abandonCheckout, installmentPaymentUrl } from "@/lib/checkout"; import { getDb } from "@/lib/db"; import { getGateway } from "@/lib/payment-gateway"; import { RATE_LIMITS } from "@/lib/rate-limit"; import { isWellFormedStatusToken, sha256Hex } from "@/lib/tokens"; const schema = z.discriminatedUnion("action", [ z.object({ action: z.literal("pay_installment"), token: z.string(), installmentId: z.string().min(1).max(64) }), z.object({ action: z.literal("resume_checkout"), token: z.string() }), z.object({ action: z.literal("abandon"), token: z.string() }), ]); export async function POST(request: Request) { const blocked = checkSameOrigin(request); if (blocked) return blocked; const limited = await rateLimited(request, RATE_LIMITS.statusAction); if (limited) return limited; const body = await readJson(request, schema); if (!body.ok) return body.response; const notFound = fail(404, { code: "NOT_FOUND", message: "Diese Buchung wurde nicht gefunden." }); if (!isWellFormedStatusToken(body.data.token)) return notFound; const db = getDb(); const booking = await db.booking.findUnique({ where: { statusTokenHash: sha256Hex(body.data.token) } }); if (!booking) return notFound; const gateway = getGateway(); if (booking.livemode !== (gateway.mode === "live")) return notFound; const now = new Date(); switch (body.data.action) { case "pay_installment": { const result = await installmentPaymentUrl(db, gateway, { bookingId: booking.id, installmentId: body.data.installmentId }); if (result.ok) return ok({ url: result.url }); return result.error === "NOT_PAYABLE" ? fail(409, { code: "NOT_PAYABLE", message: "Diese Rate kann gerade nicht bezahlt werden. Bitte lade die Seite neu." }) : fail(502, { code: "PAYMENT_PROVIDER", message: "Die Zahlungsseite ist gerade nicht erreichbar. Bitte versuche es später erneut." }); } case "resume_checkout": { if (booking.status === "PENDING" && booking.paymentStatus === "UNPAID" && booking.stripeCheckoutUrl && booking.priceLockedUntil > now) { return ok({ url: booking.stripeCheckoutUrl }); } return fail(409, { code: "EXPIRED", message: "Die Reservierung ist abgelaufen. Bitte starte eine neue Buchung." }); } case "abandon": { await abandonCheckout(db, gateway, booking, { now }); return ok({ abandoned: true }); } } }