Completes the first milestone of The Pet Loft ecommerce platform: - apps/storefront: full customer-facing Next.js app with HeroUI (cart, checkout, orders, wishlist, product detail, shop, search, auth) - convex/: serverless backend with schema, queries, mutations, actions, HTTP routes, Stripe/Shippo integrations, and co-located tests - packages/types, packages/utils, packages/convex: shared workspace packages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
81 lines
2.2 KiB
TypeScript
81 lines
2.2 KiB
TypeScript
import { QueryCtx, MutationCtx } from "../_generated/server";
|
|
import { Id } from "../_generated/dataModel";
|
|
|
|
/**
|
|
* Recalculate product averageRating and reviewCount from approved reviews.
|
|
* Call after review approve/delete.
|
|
*/
|
|
export async function recalculateProductRating(
|
|
ctx: MutationCtx,
|
|
productId: Id<"products">,
|
|
) {
|
|
const approved = await ctx.db
|
|
.query("reviews")
|
|
.withIndex("by_product_approved", (q) =>
|
|
q.eq("productId", productId).eq("isApproved", true),
|
|
)
|
|
.collect();
|
|
|
|
const count = approved.length;
|
|
const averageRating =
|
|
count > 0
|
|
? approved.reduce((sum, r) => sum + r.rating, 0) / count
|
|
: undefined;
|
|
const reviewCount = count > 0 ? count : undefined;
|
|
|
|
await ctx.db.patch(productId, {
|
|
averageRating,
|
|
reviewCount,
|
|
updatedAt: Date.now(),
|
|
});
|
|
}
|
|
|
|
export async function getProductWithRelations(
|
|
ctx: QueryCtx,
|
|
productId: Id<"products">,
|
|
) {
|
|
const product = await ctx.db.get(productId);
|
|
if (!product) return null;
|
|
|
|
const [imagesRaw, variants, category] = await Promise.all([
|
|
ctx.db
|
|
.query("productImages")
|
|
.withIndex("by_product", (q) => q.eq("productId", productId))
|
|
.collect(),
|
|
ctx.db
|
|
.query("productVariants")
|
|
.withIndex("by_product_and_active", (q) =>
|
|
q.eq("productId", productId).eq("isActive", true),
|
|
)
|
|
.collect(),
|
|
ctx.db.get(product.categoryId),
|
|
]);
|
|
const images = imagesRaw.sort((a, b) => a.position - b.position);
|
|
|
|
return { ...product, images, variants, category };
|
|
}
|
|
|
|
export async function enrichProducts(
|
|
ctx: QueryCtx,
|
|
products: Awaited<ReturnType<typeof ctx.db.query>>[],
|
|
) {
|
|
return Promise.all(
|
|
products.map(async (product: any) => {
|
|
const [imagesRaw, variants] = await Promise.all([
|
|
ctx.db
|
|
.query("productImages")
|
|
.withIndex("by_product", (q) => q.eq("productId", product._id))
|
|
.collect(),
|
|
ctx.db
|
|
.query("productVariants")
|
|
.withIndex("by_product_and_active", (q) =>
|
|
q.eq("productId", product._id).eq("isActive", true),
|
|
)
|
|
.collect(),
|
|
]);
|
|
const images = imagesRaw.sort((a, b) => a.position - b.position);
|
|
return { ...product, images, variants };
|
|
}),
|
|
);
|
|
}
|