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:
1 parent
5c76fcc266
commit
c7927f2883
240 files changed
+26096
-137
No files matched your search
@@ -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");
|
||||
}
|
||||
Reference in new issue
Block a user