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,73 @@
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import type { Review } from "@/generated/prisma/client";
|
||||
|
||||
export type ReviewWithCustomer = Review & {
|
||||
customer: { firstName: string; lastName: string };
|
||||
};
|
||||
|
||||
export interface ReviewSummary {
|
||||
average: number | null;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export async function getApprovedReviews(productId: string): Promise<ReviewWithCustomer[]> {
|
||||
return prisma.review.findMany({
|
||||
where: { productId, isApproved: true },
|
||||
include: { customer: { select: { firstName: true, lastName: true } } },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getReviewSummary(productId: string): Promise<ReviewSummary> {
|
||||
const result = await prisma.review.aggregate({
|
||||
where: { productId, isApproved: true },
|
||||
_avg: { rating: true },
|
||||
_count: true,
|
||||
});
|
||||
return { average: result._avg.rating, count: result._count };
|
||||
}
|
||||
|
||||
/**
|
||||
* Wie getReviewSummary, aber fuer mehrere Produkte in einer Abfrage — vermeidet
|
||||
* N+1-Abfragen beim Rendern von Produktrastern (Shop-Uebersicht u.ae.).
|
||||
*/
|
||||
export async function getReviewSummariesByProductIds(
|
||||
productIds: string[]
|
||||
): Promise<Map<string, ReviewSummary>> {
|
||||
if (productIds.length === 0) return new Map();
|
||||
|
||||
const results = await prisma.review.groupBy({
|
||||
by: ["productId"],
|
||||
where: { productId: { in: productIds }, isApproved: true },
|
||||
_avg: { rating: true },
|
||||
_count: true,
|
||||
});
|
||||
|
||||
return new Map(results.map((r) => [r.productId, { average: r._avg.rating, count: r._count }]));
|
||||
}
|
||||
|
||||
/** Eigene Bewertung des Kunden fuer dieses Produkt, unabhaengig vom Freischaltungsstatus. */
|
||||
export async function getCustomerReview(productId: string, customerId: string): Promise<Review | null> {
|
||||
return prisma.review.findUnique({
|
||||
where: { productId_customerId: { productId, customerId } },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* "Verifizierter Kauf": mindestens eine bezahlte, nicht stornierte Bestellung
|
||||
* des Kunden enthaelt dieses Produkt. Gastbestellungen (ohne customerId)
|
||||
* zaehlen bewusst nicht, da eine Bewertung an ein Kundenkonto gebunden ist.
|
||||
*/
|
||||
export async function hasCustomerPurchased(productId: string, customerId: string): Promise<boolean> {
|
||||
const orderItem = await prisma.orderItem.findFirst({
|
||||
where: {
|
||||
productId,
|
||||
order: {
|
||||
customerId,
|
||||
status: { notIn: ["NEU", "STORNIERT"] },
|
||||
},
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
return orderItem !== null;
|
||||
}
|
||||
Reference in new issue
Block a user