feat: Wabenhain Honig-Shop Anwendung

Vollstaendige Next.js/Prisma-E-Commerce-Anwendung fuer eine Imkerei:
Shop, Warenkorb, Stripe-Checkout, Kundenkonto mit 2FA, Admin-Bereich
(Produkte, Ernten, Chargen, Bestellungen, Gutscheine, admin-editierbare
Website-Inhalte), i18n (DE/EN), Rate-Limiting/CSP/Audit-Log-Haertung,
PostgreSQL- und MySQL-Unterstuetzung ueber austauschbare Prisma-
Treiber-Adapter, Unit-/Komponenten-/E2E-Tests.
This commit is contained in:
Nick Szaunig committed 2026-08-17 14:30:28 +02:00
1 parent 5c76fcc266
commit c7927f2883
240 files changed
+26096 -137

No files matched your search

+106
View File
@@ -0,0 +1,106 @@
"use server";
import { redirect } from "next/navigation";
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 { isLocale, defaultLocale } from "@/i18n/config";
function str(formData: FormData, key: string): string {
return String(formData.get(key) ?? "").trim();
}
function fail(path: string, message: string): never {
redirect(`${path}?error=${encodeURIComponent(message)}`);
}
export async function updateReturnRequestStatusAction(formData: FormData): Promise<void> {
const admin = await requireAdmin();
const id = str(formData, "id");
const status = str(formData, "status");
if (!["ANGENOMMEN", "ABGELEHNT"].includes(status)) return;
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;
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 });
await sendMail({ to: updated.order.email, subject, html, text });
revalidatePath("/admin/retouren");
}
export async function refundReturnRequestAction(formData: FormData): Promise<void> {
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 } } },
},
},
});
// Nur eine angenommene Retoure kann erstattet werden — verhindert
// Doppel-Erstattung, falls dieselbe Aktion mehrfach ausgeloest wird.
if (!returnRequest || returnRequest.status !== "ANGENOMMEN") return;
const paymentIntentId = returnRequest.order.payment?.stripePaymentIntentId;
if (!paymentIntentId) {
fail("/admin/retouren", "Keine Stripe-Zahlung zu dieser Bestellung hinterlegt — bitte manuell erstatten.");
}
const stripe = getStripeClient();
await stripe.refunds.create({ payment_intent: paymentIntentId });
await prisma.$transaction(async (tx) => {
await tx.returnRequest.update({ where: { id }, data: { status: "ERSTATTET" } });
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);
});
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,
});
await sendMail({ to: returnRequest.order.email, subject, html, text });
revalidatePath("/admin/retouren");
}