templates2.Store.Next/lib/server/store.ts
w4c-templates bb75e7c742
Some checks are pending
CI / quality (push) Waiting to run
chore(template): publish Store.Next
2026-10-11 23:24:13 +03:00

126 lines
4.3 KiB
TypeScript

import 'server-only';
import { z } from 'zod';
import { db } from './db';
import { getPayments } from './payments';
import { validateLines, type CartLine } from '@/lib/cart';
export const cartLineSchema = z.object({
productId: z.string().min(1),
variant: z.string().max(60).optional(),
quantity: z.number().int().min(1).max(99),
});
export const checkoutSchema = z.object({
email: z.string().email(),
address: z.string().min(5).max(300),
lines: z.array(cartLineSchema).min(1).max(50),
});
export async function listCategories() {
return db.category.findMany({ orderBy: { name: 'asc' } });
}
export type ProductQuery = { q?: string; category?: string; sort?: string; page?: number; pageSize?: number };
export async function listProducts({ q, category, sort, page = 1, pageSize = 9 }: ProductQuery) {
const where = {
active: true,
...(category ? { category: { slug: category } } : {}),
...(q ? { OR: [{ name: { contains: q } }, { description: { contains: q } }] } : {}),
};
const orderBy = sort === 'price-asc' ? { price: 'asc' as const } : sort === 'price-desc' ? { price: 'desc' as const } : { createdAt: 'desc' as const };
const [items, total] = await Promise.all([
db.product.findMany({ where, orderBy, skip: (page - 1) * pageSize, take: pageSize, include: { category: true } }),
db.product.count({ where }),
]);
return { items, total, page, pageSize, pages: Math.max(1, Math.ceil(total / pageSize)) };
}
export function getProductBySlug(slug: string) {
return db.product.findUnique({ where: { slug }, include: { category: true } });
}
export async function createOrder(input: z.infer<typeof checkoutSchema>, userId?: string | null) {
const ids = [...new Set(input.lines.map((l) => l.productId))];
const products = await db.product.findMany({ where: { id: { in: ids } } });
const result = validateLines(input.lines as CartLine[], products);
if (!result.ok) return result;
const provider = getPayments();
const providerRef = `${provider.name}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
const order = await db.order.create({
data: {
userId: userId ?? null,
email: input.email.toLowerCase(),
address: input.address,
total: result.total,
currency: 'USD',
status: provider.name === 'mock' ? 'paid' : 'pending',
provider: provider.name,
providerRef,
items: {
create: result.items.map((i) => ({
productId: i.productId,
name: i.name,
variant: i.variant ?? null,
price: i.price,
quantity: i.quantity,
})),
},
},
include: { items: true },
});
// Decrement inventory (server-side, after a validated order).
for (const item of result.items) {
await db.product.update({ where: { id: item.productId }, data: { stock: { decrement: item.quantity } } });
}
return { ok: true as const, order };
}
export function listOrdersForUser(userId: string) {
return db.order.findMany({ where: { userId }, include: { items: true }, orderBy: { createdAt: 'desc' } });
}
export function listAllOrders() {
return db.order.findMany({ include: { items: true }, orderBy: { createdAt: 'desc' }, take: 100 });
}
export function updateOrderStatus(id: string, status: string) {
return db.order.update({ where: { id }, data: { status } });
}
export function adminListProducts() {
return db.product.findMany({ include: { category: true }, orderBy: { createdAt: 'desc' } });
}
export const productSchema = z.object({
name: z.string().min(1).max(160),
slug: z.string().min(1).max(160).regex(/^[a-z0-9-]+$/),
description: z.string().max(2000).optional().or(z.literal('')),
price: z.number().int().min(0),
stock: z.number().int().min(0),
categoryId: z.string().optional().nullable(),
active: z.boolean().optional(),
});
export async function upsertProduct(data: z.infer<typeof productSchema>, id?: string) {
const payload = {
name: data.name,
slug: data.slug,
description: data.description || '',
price: data.price,
stock: data.stock,
categoryId: data.categoryId || null,
active: data.active ?? true,
};
if (id) return db.product.update({ where: { id }, data: payload });
return db.product.create({ data: payload });
}
export function deleteProduct(id: string) {
return db.product.delete({ where: { id } });
}