import { prisma } from '../../lib/prisma.js' import { publicReviewAuthorDisplay } from '../../lib/review-display.js' import { persistMultipartImages } from '../../lib/upload-images.js' import { formatFileTooLargeMessage, getOtherUploadMaxFileBytes, isMultipartFileTooLargeError, } from '../../lib/upload-limits.js' export async function registerPublicReviewRoutes(fastify) { fastify.post('/api/reviews/upload-image', { preHandler: [fastify.authenticate] }, async (request, reply) => { try { const urls = await persistMultipartImages(request, { maxFiles: 1, maxFileBytes: getOtherUploadMaxFileBytes(), subdir: 'reviews', }) if (urls.length !== 1) return reply.code(400).send({ error: 'Нужно прикрепить 1 изображение' }) return { url: urls[0] } } catch (error) { let message = error instanceof Error ? error.message : 'Не удалось загрузить изображение' let statusCode = error && typeof error === 'object' && 'statusCode' in error && Number.isInteger(error.statusCode) ? Number(error.statusCode) : 400 if (isMultipartFileTooLargeError(error)) { message = formatFileTooLargeMessage(getOtherUploadMaxFileBytes()) statusCode = 413 } return reply.code(statusCode).send({ error: message }) } }) fastify.get('/api/reviews/latest', async (request, reply) => { const limitRaw = request.query?.limit const limitParsed = typeof limitRaw === 'string' ? Number(limitRaw) : Number(limitRaw) const parsed = Number.isFinite(limitParsed) && limitParsed > 0 ? Math.floor(limitParsed) : 5 const take = Math.min(parsed, 5) const rows = await prisma.review.findMany({ where: { status: 'approved', product: { published: true } }, include: { user: { select: { email: true, displayName: true } }, product: { select: { id: true, title: true } }, }, orderBy: { createdAt: 'desc' }, take, }) const items = rows.map((r) => ({ id: r.id, rating: r.rating, text: r.text, imageUrl: r.imageUrl, createdAt: r.createdAt, authorDisplay: publicReviewAuthorDisplay(r.user), productId: r.productId, productTitle: r.product?.title ?? '', })) return { items } }) fastify.get('/api/products/:id/reviews', async (request, reply) => { const { id } = request.params const pageRaw = request.query?.page const pageParsed = typeof pageRaw === 'string' ? Number(pageRaw) : Number(pageRaw) const page = Number.isFinite(pageParsed) && pageParsed > 0 ? Math.floor(pageParsed) : 1 const pageSizeRaw = request.query?.pageSize const pageSizeParsed = typeof pageSizeRaw === 'string' ? Number(pageSizeRaw) : Number(pageSizeRaw) const pageSize = Number.isFinite(pageSizeParsed) && pageSizeParsed > 0 ? Math.floor(pageSizeParsed) : 10 if (pageSize > 50) return reply.code(400).send({ error: 'pageSize должен быть ≤ 50' }) const product = await prisma.product.findFirst({ where: { id, published: true } }) if (!product) return reply.code(404).send({ error: 'Товар не найден' }) const where = { productId: id, status: 'approved' } const total = await prisma.review.count({ where }) const rawItems = await prisma.review.findMany({ where, include: { user: { select: { email: true, displayName: true } } }, orderBy: { createdAt: 'desc' }, skip: (page - 1) * pageSize, take: pageSize, }) const items = rawItems.map((r) => ({ id: r.id, rating: r.rating, text: r.text, imageUrl: r.imageUrl, createdAt: r.createdAt, authorDisplay: publicReviewAuthorDisplay(r.user), })) return { items, total, page, pageSize } }) fastify.post('/api/products/:id/reviews', { preHandler: [fastify.authenticate] }, async (request, reply) => { const userId = request.user.sub const { id: productId } = request.params const product = await prisma.product.findFirst({ where: { id: productId, published: true } }) if (!product) return reply.code(404).send({ error: 'Товар не найден' }) const rating = Number(request.body?.rating) if (!Number.isFinite(rating) || rating < 1 || rating > 5) { return reply.code(400).send({ error: 'rating должен быть от 1 до 5' }) } const textRaw = request.body?.text const text = textRaw === null || textRaw === undefined ? null : String(textRaw).trim() if (text !== null && text.length > 1000) return reply.code(400).send({ error: 'Отзыв слишком длинный' }) const imageUrlRaw = request.body?.imageUrl const imageUrl = imageUrlRaw === null || imageUrlRaw === undefined ? null : String(imageUrlRaw).trim() if (imageUrl !== null && imageUrl.length > 300) return reply.code(400).send({ error: 'Ссылка на фото слишком длинная' }) if (imageUrl !== null && imageUrl.length > 0 && !imageUrl.startsWith('/uploads/')) { return reply.code(400).send({ error: 'Некорректная ссылка на изображение' }) } try { const created = await prisma.review.create({ data: { productId, userId, rating: Math.floor(rating), text: text && text.length ? text : null, imageUrl: imageUrl && imageUrl.length ? imageUrl : null, status: 'pending', }, }) return reply.code(201).send({ item: created }) } catch { return reply.code(409).send({ error: 'Вы уже оставляли отзыв на этот товар' }) } }) }