"use server"; import { fail, succeed, MAIL_FAILED_HINT } from "@/lib/flash"; import { revalidatePath } from "next/cache"; import { prisma } from "@/lib/prisma"; import { requireAdmin } from "@/app/admin/actions"; import { getStripeClient } from "@/lib/stripe"; import { logAdminAction } from "@/lib/audit-log"; import { sendMail } from "@/lib/mail"; import { returnAcceptedEmail, returnRejectedEmail, returnRefundedEmail } from "@/lib/email-templates"; import { restockOrderItems } from "@/lib/inventory"; import { recordOrderReturn } from "@/lib/apicore-stock/stock-events"; import { scheduleApiCoreStockSync } from "@/lib/apicore-stock/schedule"; import { isLocale, defaultLocale } from "@/i18n/config"; function str(formData: FormData, key: string): string { return String(formData.get(key) ?? "").trim(); } export async function updateReturnRequestStatusAction(formData: FormData): Promise { const admin = await requireAdmin(); const id = str(formData, "id"); const status = str(formData, "status"); if (!["ANGENOMMEN", "ABGELEHNT"].includes(status)) return fail("/admin/retouren", "Ungültiger Status."); const returnRequest = await prisma.returnRequest.findUnique({ where: { id }, select: { status: true }, }); // Nur von "ANGEFRAGT" aus entscheiden — verhindert, dass eine bereits // erstattete oder abgelehnte Retoure nachtraeglich ueberschrieben wird. if (!returnRequest || returnRequest.status !== "ANGEFRAGT") { return fail("/admin/retouren", "Diese Retoure wurde bereits entschieden oder existiert nicht mehr."); } const updated = await prisma.returnRequest.update({ where: { id }, data: { status: status as "ANGENOMMEN" | "ABGELEHNT" }, include: { order: { select: { orderNumber: true, email: true, firstName: true, locale: true } } }, }); await logAdminAction(admin, { action: "return_request.status_change", entityType: "ReturnRequest", entityId: id, detail: `${updated.order.orderNumber}: ${status}`, }); const locale = isLocale(updated.order.locale) ? updated.order.locale : defaultLocale; const { subject, html, text } = status === "ANGENOMMEN" ? await returnAcceptedEmail({ locale, orderNumber: updated.order.orderNumber, firstName: updated.order.firstName }) : await returnRejectedEmail({ locale, orderNumber: updated.order.orderNumber, firstName: updated.order.firstName }); const mailSent = await sendMail({ to: updated.order.email, subject, html, text }); revalidatePath("/admin/retouren"); const done = `Retoure ${updated.order.orderNumber} ${status === "ANGENOMMEN" ? "angenommen" : "abgelehnt"}.`; if (!mailSent) return fail("/admin/retouren", `${done} Aber: ${MAIL_FAILED_HINT}`); return succeed("/admin/retouren", `${done} Die Kundschaft wurde per E-Mail informiert.`); } export async function refundReturnRequestAction(formData: FormData): Promise { const admin = await requireAdmin(); const id = str(formData, "id"); const returnRequest = await prisma.returnRequest.findUnique({ where: { id }, include: { order: { include: { items: true, payment: { select: { stripePaymentIntentId: true } } }, }, }, }); if (!returnRequest || returnRequest.status !== "ANGENOMMEN") { return fail("/admin/retouren", "Diese Retoure kann nicht (mehr) erstattet werden."); } const paymentIntentId = returnRequest.order.payment?.stripePaymentIntentId; if (!paymentIntentId) { return fail("/admin/retouren", "Keine Stripe-Zahlung zu dieser Bestellung hinterlegt — bitte manuell erstatten."); } // Status ATOMAR und VOR dem Stripe-Aufruf auf ERSTATTET setzen (bedingt auf // den noch erwarteten "ANGENOMMEN"-Stand) statt erst danach: der // findUnique-Check allein ist nicht atomar — bei zwei schnell // aufeinanderfolgenden Klicks (Doppelklick, zwei offene Tabs) koennten // sonst beide Aufrufe die Pruefung bestehen und zwei echte // Stripe-Erstattungen ausloesen. Schlaegt der Stripe-Aufruf unten fehl, // wird der Status wieder zurueckgesetzt, damit die Erstattung erneut // versucht werden kann. // // Eine stornierte Bestellung ist ausgeschlossen: updateOrderStatusAction // hat beim Stornieren die Ware schon zurueckgebucht und die Zahlung // erstattet — hier noch einmal gutzuschreiben erhoehte den Bestand um // Glaeser, die es nicht gibt (Gegenstueck zu needsRestockOnCancel). if (returnRequest.order.status === "STORNIERT") { return fail("/admin/retouren", "Die Bestellung wurde bereits storniert — Ware und Zahlung sind schon zurückgebucht."); } const claimed = await prisma.returnRequest.updateMany({ where: { id, status: "ANGENOMMEN", order: { status: { not: "STORNIERT" } } }, data: { status: "ERSTATTET" }, }); if (claimed.count === 0) return fail("/admin/retouren", "Diese Retoure wird bereits erstattet."); const stripe = getStripeClient(); try { await stripe.refunds.create({ payment_intent: paymentIntentId }); } catch (error: unknown) { await prisma.returnRequest.update({ where: { id }, data: { status: "ANGENOMMEN" } }); throw error; } await prisma.$transaction(async (tx) => { await tx.payment.update({ where: { orderId: returnRequest.orderId }, data: { status: "REFUNDED" } }); // Zurueckgesendete Ware wieder als verfuegbaren Bestand fuehren, analog // zur Stornierung im Checkout-Webhook. await restockOrderItems(tx, returnRequest.order.items); // Die zurueckgesendete Ware geht auch in ApiCore zurueck auf die // Chargen, aus denen sie verkauft wurde. await recordOrderReturn(tx, returnRequest.orderId); }); scheduleApiCoreStockSync(returnRequest.order.items.map((item) => item.productId)); await logAdminAction(admin, { action: "return_request.refund", entityType: "ReturnRequest", entityId: id, detail: returnRequest.order.orderNumber, }); const { subject, html, text } = await returnRefundedEmail({ locale: isLocale(returnRequest.order.locale) ? returnRequest.order.locale : defaultLocale, orderNumber: returnRequest.order.orderNumber, firstName: returnRequest.order.firstName, }); const mailSent = await sendMail({ to: returnRequest.order.email, subject, html, text }); revalidatePath("/admin/retouren"); const done = `Retoure ${returnRequest.order.orderNumber} erstattet — der Betrag geht über Stripe zurück.`; if (!mailSent) return fail("/admin/retouren", `${done} Aber: ${MAIL_FAILED_HINT}`); return succeed("/admin/retouren", `${done} Die Kundschaft wurde per E-Mail informiert.`); }