From 30202ee828375a62e658c263bd0baee3dd24787f Mon Sep 17 00:00:00 2001 From: Meirbek-dev Date: Mon, 16 Dec 2024 19:05:15 +0500 Subject: [PATCH 1/8] improved error handling for colors route --- app/api/[storeId]/colors/route.ts | 107 ++++++++++++++++++------------ 1 file changed, 65 insertions(+), 42 deletions(-) diff --git a/app/api/[storeId]/colors/route.ts b/app/api/[storeId]/colors/route.ts index 1460147..6bf0b31 100644 --- a/app/api/[storeId]/colors/route.ts +++ b/app/api/[storeId]/colors/route.ts @@ -1,81 +1,104 @@ import { auth } from '@clerk/nextjs/server'; import { NextResponse } from 'next/server'; +import { z } from 'zod'; import prismadb from '@/lib/prismadb'; +import { errorResponses } from '@/lib/error-responses'; + +// Схема валидации для создания цвета +const colorSchema = z.object({ + name: z.string().min(2, 'Название должно содержать минимум 2 символа'), + value: z.string() + .min(4, 'Неверный формат цвета') + .max(9, 'Неверный формат цвета') + .regex(/^#/, { message: 'Цвет должен начинаться с #' }), +}); export async function POST(request: Request, props: { params: Promise<{ storeId: string }> }) { - const params = await props.params; try { - const { userId }: { userId: string | null } = await auth(); - - const body = await request.json(); - - const { name, value } = body; + const params = await props.params; + const { userId } = await auth(); if (!userId) { - return new NextResponse('Пользователь не аутентифицирован', { - status: 403, - }); + return errorResponses.unauthorized(); } - if (!name) { - return new NextResponse('Укажите название.', { status: 400 }); + if (!params.storeId) { + return errorResponses.badRequest('Необходим идентификатор магазина'); } - if (!value) { - return new NextResponse('Укажите значение.', { status: 400 }); - } + const body = await request.json(); + const validationResult = colorSchema.safeParse(body); - if (!params.storeId) { - return new NextResponse('Необходим идентификатор магазина.', { - status: 400, - }); + if (!validationResult.success) { + return errorResponses.validationError(validationResult.error.message); } - const storeByUserId = await prismadb.store.findFirst({ - where: { - id: params.storeId, - userId, - }, - }); + // Используем транзакцию для проверки прав и создания цвета + const color = await prismadb.$transaction(async (tx) => { + const storeByUserId = await tx.store.findFirst({ + where: { + id: params.storeId, + userId, + }, + select: { id: true }, + }); - if (!storeByUserId) { - return new NextResponse('Не авторизованный доступ', { status: 405 }); - } + if (!storeByUserId) { + throw new Error('Не авторизованный доступ'); + } - const color = await prismadb.color.create({ - data: { - name, - value, - storeId: params.storeId, - }, + return tx.color.create({ + data: { + name: validationResult.data.name, + value: validationResult.data.value, + storeId: params.storeId, + }, + select: { + id: true, + name: true, + value: true, + createdAt: true, + }, + }); }); - return NextResponse.json(color); + return NextResponse.json(color, { status: 201 }); } catch (error) { - console.log('[COLORS_POST]', error); - return new NextResponse('Ошибка сервера', { status: 500 }); + console.error('[COLORS_POST]', error); + if (error instanceof Error && error.message === 'Не авторизованный доступ') { + return errorResponses.forbidden(); + } + return errorResponses.serverError(error); } } export async function GET(request: Request, props: { params: Promise<{ storeId: string }> }) { - const params = await props.params; try { + const params = await props.params; + if (!params.storeId) { - return new NextResponse('Необходим идентификатор магазина.', { - status: 400, - }); + return errorResponses.badRequest('Необходим идентификатор магазина'); } const colors = await prismadb.color.findMany({ where: { storeId: params.storeId, }, + select: { + id: true, + name: true, + value: true, + createdAt: true, + }, + orderBy: { + createdAt: 'desc', + }, }); return NextResponse.json(colors); } catch (error) { - console.log('[COLORS_GET]', error); - return new NextResponse('Ошибка сервера', { status: 500 }); + console.error('[COLORS_GET]', error); + return errorResponses.serverError(error); } } From 2b1d6735a40d272f7e0a322128d5aee8566b0946 Mon Sep 17 00:00:00 2001 From: Meirbek-dev Date: Mon, 16 Dec 2024 19:10:36 +0500 Subject: [PATCH 2/8] improved error handling for colorId route --- app/api/[storeId]/colors/[colorId]/route.ts | 170 ++++++++++++-------- 1 file changed, 101 insertions(+), 69 deletions(-) diff --git a/app/api/[storeId]/colors/[colorId]/route.ts b/app/api/[storeId]/colors/[colorId]/route.ts index d042a7a..08a1b90 100644 --- a/app/api/[storeId]/colors/[colorId]/route.ts +++ b/app/api/[storeId]/colors/[colorId]/route.ts @@ -1,27 +1,46 @@ import { auth } from '@clerk/nextjs/server'; import { NextResponse } from 'next/server'; +import { z } from 'zod'; import prismadb from '@/lib/prismadb'; +import { errorResponses } from '@/lib/error-responses'; + +// Схема валидации для обновления цвета +const updateColorSchema = z.object({ + name: z.string().min(2, 'Название должно содержать минимум 2 символа'), + value: z.string() + .min(4, 'Неверный формат цвета') + .max(9, 'Неверный формат цвета') + .regex(/^#/, { message: 'Цвет должен начинаться с #' }), +}); export async function GET(request: Request, props: { params: Promise<{ colorId: string }> }) { - const params = await props.params; try { + const params = await props.params; + if (!params.colorId) { - return new NextResponse('Необходим идентификатор цвета.', { - status: 400, - }); + return errorResponses.badRequest('Необходим идентификатор цвета'); } const color = await prismadb.color.findUnique({ - where: { - id: params.colorId, + where: { id: params.colorId }, + select: { + id: true, + name: true, + value: true, + createdAt: true, + updatedAt: true, }, }); + if (!color) { + return errorResponses.notFound('Цвет не найден'); + } + return NextResponse.json(color); } catch (error) { - console.log('[COLOR_GET]', error); - return new NextResponse('Ошибка сервера', { status: 500 }); + console.error('[COLOR_GET]', error); + return errorResponses.serverError(error); } } @@ -29,43 +48,61 @@ export async function DELETE( request: Request, props: { params: Promise<{ colorId: string; storeId: string }> }, ) { - const params = await props.params; try { - const { userId }: { userId: string | null } = await auth(); + const params = await props.params; + const { userId } = await auth(); if (!userId) { - return new NextResponse('Пользователь не аутентифицирован', { - status: 403, - }); + return errorResponses.unauthorized(); } if (!params.colorId) { - return new NextResponse('Необходим идентификатор цвета.', { - status: 400, - }); + return errorResponses.badRequest('Необходим идентификатор цвета'); } + const color = await prismadb.$transaction(async (tx) => { + const storeByUserId = await tx.store.findFirst({ + where: { + id: params.storeId, + userId, + }, + select: { id: true }, + }); - const storeByUserId = await prismadb.store.findFirst({ - where: { - id: params.storeId, - userId, - }, - }); + if (!storeByUserId) { + throw new Error('Не авторизованный доступ'); + } - if (!storeByUserId) { - return new NextResponse('Не авторизованный доступ', { status: 405 }); - } + // Проверяем использование цвета в товарах + const productsUsingColor = await tx.product.count({ + where: { colorId: params.colorId }, + }); - const color = await prismadb.color.delete({ - where: { - id: params.colorId, - }, + if (productsUsingColor > 0) { + throw new Error('Цвет используется в товарах'); + } + + return tx.color.delete({ + where: { id: params.colorId }, + select: { + id: true, + name: true, + value: true, + }, + }); }); return NextResponse.json(color); } catch (error) { - console.log('[COLOR_DELETE]', error); - return new NextResponse('Ошибка сервера', { status: 500 }); + console.error('[COLOR_DELETE]', error); + if (error instanceof Error) { + if (error.message === 'Не авторизованный доступ') { + return errorResponses.forbidden(); + } + if (error.message === 'Цвет используется в товарах') { + return errorResponses.conflict('Нельзя удалить цвет, который используется в товарах'); + } + } + return errorResponses.serverError(error); } } @@ -73,58 +110,53 @@ export async function PATCH( request: Request, props: { params: Promise<{ colorId: string; storeId: string }> }, ) { - const params = await props.params; try { - const { userId }: { userId: string | null } = await auth(); - - const body = await request.json(); - - const { name, value } = body; + const params = await props.params; + const { userId } = await auth(); if (!userId) { - return new NextResponse('Пользователь не аутентифицирован', { - status: 403, - }); - } - - if (!name) { - return new NextResponse('Укажите название.', { status: 400 }); - } - - if (!value) { - return new NextResponse('Укажите значение.', { status: 400 }); + return errorResponses.unauthorized(); } if (!params.colorId) { - return new NextResponse('Необходим идентификатор цвета.', { - status: 400, - }); + return errorResponses.badRequest('Необходим идентификатор цвета'); } - const storeByUserId = await prismadb.store.findFirst({ - where: { - id: params.storeId, - userId, - }, - }); + const body = await request.json(); + const validationResult = updateColorSchema.safeParse(body); - if (!storeByUserId) { - return new NextResponse('Не авторизованный доступ', { status: 405 }); + if (!validationResult.success) { + return errorResponses.validationError(validationResult.error.message); } - const color = await prismadb.color.update({ - where: { - id: params.colorId, - }, - data: { - name, - value, - }, + const color = await prismadb.$transaction(async (tx) => { + const storeByUserId = await tx.store.findFirst({ + where: { + id: params.storeId, + userId, + }, + select: { id: true }, + }); + + if (!storeByUserId) { + throw new Error('Не авторизованный доступ'); + } + + return tx.color.update({ + where: { id: params.colorId }, + data: validationResult.data, + select: { + id: true, + name: true, + value: true, + updatedAt: true, + }, + }); }); return NextResponse.json(color); } catch (error) { - console.log('[COLOR_PATCH]', error); - return new NextResponse('Ошибка сервера', { status: 500 }); + console.error('[COLOR_PATCH]', error); + return errorResponses.serverError(error); } } From fec03f315672033bd3a7d4c8cff11f13571c453c Mon Sep 17 00:00:00 2001 From: Meirbek-dev Date: Mon, 16 Dec 2024 19:15:43 +0500 Subject: [PATCH 3/8] improved error handling for checkout route --- app/api/[storeId]/checkout/route.ts | 137 +++++++++++------- app/api/[storeId]/colors/[colorId]/route.ts | 3 +- app/api/[storeId]/colors/route.ts | 3 +- .../[storeId]/products/[productId]/route.ts | 1 - app/api/[storeId]/sizes/[sizeId]/route.ts | 1 - next.config.mjs | 2 +- 6 files changed, 87 insertions(+), 60 deletions(-) diff --git a/app/api/[storeId]/checkout/route.ts b/app/api/[storeId]/checkout/route.ts index 0bb8e11..33d0b83 100644 --- a/app/api/[storeId]/checkout/route.ts +++ b/app/api/[storeId]/checkout/route.ts @@ -1,8 +1,15 @@ import { NextResponse } from 'next/server'; import type Stripe from 'stripe'; +import { z } from 'zod'; import prismadb from '@/lib/prismadb'; import { stripe } from '@/lib/stripe'; +import { errorResponses } from '@/lib/error-responses'; + +// Схема валидации для запроса +const checkoutSchema = z.object({ + productIds: z.array(z.string().uuid()).min(1, 'Необходимо выбрать хотя бы один товар'), +}); const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -15,72 +22,92 @@ export async function OPTIONS() { } export async function POST(request: Request, props: { params: Promise<{ storeId: string }> }) { - const params = await props.params; - const { productIds } = await request.json(); + try { + const params = await props.params; + const body = await request.json(); - if (!productIds || productIds.length === 0) { - return new NextResponse('Необходимы идентификаторы продуктов.', { - status: 400, - }); - } + const validationResult = checkoutSchema.safeParse(body); - const products = await prismadb.product.findMany({ - where: { - id: { - in: productIds, - }, - }, - }); + if (!validationResult.success) { + return errorResponses.validationError(validationResult.error.message); + } + + const { productIds } = validationResult.data; + + const { products, order } = await prismadb.$transaction(async (tx) => { + const products = await tx.product.findMany({ + where: { + id: { in: productIds }, + isArchived: false, // Проверяем, что товар не в архиве + }, + select: { + id: true, + name: true, + price: true, + storeId: true, + }, + }); + + if (products.length !== productIds.length) { + throw new Error('Некоторые товары не найдены или недоступны'); + } + + // Проверяем, что все товары из одного магазина + if (!products.every((product) => product.storeId === params.storeId)) { + throw new Error('Товары должны быть из одного магазина'); + } + + const order = await tx.order.create({ + data: { + storeId: params.storeId, + isPaid: false, + orderItems: { + create: productIds.map((productId) => ({ + product: { + connect: { id: productId }, + }, + })), + }, + }, + select: { + id: true, + }, + }); - const lineItems: Stripe.Checkout.SessionCreateParams.LineItem[] = []; + return { products, order }; + }); - for (const product of products) { - lineItems.push({ + const lineItems: Stripe.Checkout.SessionCreateParams.LineItem[] = products.map((product) => ({ quantity: 1, price_data: { currency: 'KZT', product_data: { name: product.name, }, - unit_amount: product.price.toNumber() * 100, + unit_amount: Math.round(product.price * 100), }, - }); - } + })); - const order = await prismadb.order.create({ - data: { - storeId: params.storeId, - isPaid: false, - orderItems: { - create: productIds.map((productId: string) => ({ - product: { - connect: { - id: productId, - }, - }, - })), + const session = await stripe.checkout.sessions.create({ + line_items: lineItems, + mode: 'payment', + billing_address_collection: 'required', + phone_number_collection: { + enabled: true, + }, + success_url: `${process.env.FRONTEND_STORE_URL}/cart?success=1`, + cancel_url: `${process.env.FRONTEND_STORE_URL}/cart?canceled=1`, + metadata: { + orderId: order.id, }, - }, - }); - - const session = await stripe.checkout.sessions.create({ - line_items: lineItems, - mode: 'payment', - billing_address_collection: 'required', - phone_number_collection: { - enabled: true, - }, - success_url: `${process.env.FRONTEND_STORE_URL}/cart?success=1`, - cancel_url: `${process.env.FRONTEND_STORE_URL}/cart?canceled=1`, - metadata: { - orderId: order.id, - }, - }); - - return NextResponse.json( - { url: session.url }, - { - headers: corsHeaders, - }, - ); + }); + + return NextResponse.json({ url: session.url }, { headers: corsHeaders }); + } catch (error) { + console.error('[CHECKOUT_POST]', error); + if (error instanceof Error) { + return errorResponses.badRequest(error.message); + } + return errorResponses.serverError(error); + } } diff --git a/app/api/[storeId]/colors/[colorId]/route.ts b/app/api/[storeId]/colors/[colorId]/route.ts index 08a1b90..64b9025 100644 --- a/app/api/[storeId]/colors/[colorId]/route.ts +++ b/app/api/[storeId]/colors/[colorId]/route.ts @@ -8,7 +8,8 @@ import { errorResponses } from '@/lib/error-responses'; // Схема валидации для обновления цвета const updateColorSchema = z.object({ name: z.string().min(2, 'Название должно содержать минимум 2 символа'), - value: z.string() + value: z + .string() .min(4, 'Неверный формат цвета') .max(9, 'Неверный формат цвета') .regex(/^#/, { message: 'Цвет должен начинаться с #' }), diff --git a/app/api/[storeId]/colors/route.ts b/app/api/[storeId]/colors/route.ts index 6bf0b31..f7e20d0 100644 --- a/app/api/[storeId]/colors/route.ts +++ b/app/api/[storeId]/colors/route.ts @@ -8,7 +8,8 @@ import { errorResponses } from '@/lib/error-responses'; // Схема валидации для создания цвета const colorSchema = z.object({ name: z.string().min(2, 'Название должно содержать минимум 2 символа'), - value: z.string() + value: z + .string() .min(4, 'Неверный формат цвета') .max(9, 'Неверный формат цвета') .regex(/^#/, { message: 'Цвет должен начинаться с #' }), diff --git a/app/api/[storeId]/products/[productId]/route.ts b/app/api/[storeId]/products/[productId]/route.ts index 59df3cb..bf7d525 100644 --- a/app/api/[storeId]/products/[productId]/route.ts +++ b/app/api/[storeId]/products/[productId]/route.ts @@ -63,7 +63,6 @@ export async function DELETE( return errorResponses.unauthorized(); } - // Используем транзакцию для обеспечения целостности данных const product = await prismadb.$transaction(async (tx) => { const storeByUserId = await tx.store.findFirst({ where: { id: params.storeId, userId }, diff --git a/app/api/[storeId]/sizes/[sizeId]/route.ts b/app/api/[storeId]/sizes/[sizeId]/route.ts index 7c0ba1b..86d6133 100644 --- a/app/api/[storeId]/sizes/[sizeId]/route.ts +++ b/app/api/[storeId]/sizes/[sizeId]/route.ts @@ -53,7 +53,6 @@ export async function DELETE( return new NextResponse('Необходим идентификатор размера.', { status: 400 }); } - // Используем транзакцию для обеспечения целостности данных const size = await prismadb.$transaction(async (tx) => { const storeByUserId = await tx.store.findFirst({ where: { diff --git a/next.config.mjs b/next.config.mjs index 25458e7..e12ee0b 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -10,7 +10,7 @@ const nextConfig = { hostname: 'res.cloudinary.com', }, ], - minimumCacheTTL: 1500000 + minimumCacheTTL: 1500000, }, typescript: { ignoreBuildErrors: true, From e9374baa63a2c447e9744b10603b4c4cd50fb1b9 Mon Sep 17 00:00:00 2001 From: Meirbek-dev Date: Mon, 16 Dec 2024 19:24:58 +0500 Subject: [PATCH 4/8] improved error handling for categories route --- .../categories/[categoryId]/route.ts | 196 +++++++++++------- app/api/[storeId]/categories/route.ts | 124 +++++++---- 2 files changed, 204 insertions(+), 116 deletions(-) diff --git a/app/api/[storeId]/categories/[categoryId]/route.ts b/app/api/[storeId]/categories/[categoryId]/route.ts index 93ff5e0..6c42030 100644 --- a/app/api/[storeId]/categories/[categoryId]/route.ts +++ b/app/api/[storeId]/categories/[categoryId]/route.ts @@ -1,30 +1,49 @@ import { auth } from '@clerk/nextjs/server'; import { NextResponse } from 'next/server'; +import { z } from 'zod'; import prismadb from '@/lib/prismadb'; +import { errorResponses } from '@/lib/error-responses'; + +// Схема валидации для обновления категории +const updateCategorySchema = z.object({ + name: z.string().min(2, 'Название должно содержать минимум 2 символа'), + billboardId: z.string().uuid('Неверный формат идентификатора билборда'), +}); export async function GET(request: Request, props: { params: Promise<{ categoryId: string }> }) { - const params = await props.params; try { + const params = await props.params; + if (!params.categoryId) { - return new NextResponse('Необходим идентификатор категории.', { - status: 400, - }); + return errorResponses.badRequest('Необходим идентификатор категории'); } const category = await prismadb.category.findUnique({ - where: { - id: params.categoryId, - }, - include: { - billboard: true, + where: { id: params.categoryId }, + select: { + id: true, + name: true, + billboard: { + select: { + id: true, + label: true, + imageUrl: true, + }, + }, + createdAt: true, + updatedAt: true, }, }); + if (!category) { + return errorResponses.notFound('Категория не найдена'); + } + return NextResponse.json(category); } catch (error) { - console.log('[CATEGORY_GET]', error); - return new NextResponse('Ошибка сервера', { status: 500 }); + console.error('[CATEGORY_GET]', error); + return errorResponses.serverError(error); } } @@ -32,43 +51,62 @@ export async function DELETE( request: Request, props: { params: Promise<{ categoryId: string; storeId: string }> }, ) { - const params = await props.params; try { - const { userId }: { userId: string | null } = await auth(); + const params = await props.params; + const { userId } = await auth(); if (!userId) { - return new NextResponse('Пользователь не аутентифицирован', { - status: 403, - }); + return errorResponses.unauthorized(); } if (!params.categoryId) { - return new NextResponse('Необходим идентификатор категории.', { - status: 400, - }); + return errorResponses.badRequest('Необходим идентификатор категории'); } - const storeByUserId = await prismadb.store.findFirst({ - where: { - id: params.storeId, - userId, - }, - }); + const category = await prismadb.$transaction(async (tx) => { + const storeByUserId = await tx.store.findFirst({ + where: { + id: params.storeId, + userId, + }, + select: { id: true }, + }); - if (!storeByUserId) { - return new NextResponse('Не авторизованный доступ', { status: 405 }); - } + if (!storeByUserId) { + throw new Error('Не авторизованный доступ'); + } - const category = await prismadb.category.delete({ - where: { - id: params.categoryId, - }, + // Проверяем наличие связанных продуктов + const productsCount = await tx.product.count({ + where: { categoryId: params.categoryId }, + }); + + if (productsCount > 0) { + throw new Error('Нельзя удалить категорию с привязанными товарами'); + } + + return tx.category.delete({ + where: { id: params.categoryId }, + select: { + id: true, + name: true, + createdAt: true, + }, + }); }); return NextResponse.json(category); } catch (error) { - console.log('[CATEGORY_DELETE]', error); - return new NextResponse('Ошибка сервера', { status: 500 }); + console.error('[CATEGORY_DELETE]', error); + if (error instanceof Error) { + if (error.message === 'Не авторизованный доступ') { + return errorResponses.forbidden(); + } + if (error.message === 'Нельзя удалить категорию с привязанными товарами') { + return errorResponses.badRequest(error.message); + } + } + return errorResponses.serverError(error); } } @@ -76,60 +114,72 @@ export async function PATCH( request: Request, props: { params: Promise<{ categoryId: string; storeId: string }> }, ) { - const params = await props.params; try { - const { userId }: { userId: string | null } = await auth(); - - const body = await request.json(); - - const { name, billboardId } = body; + const params = await props.params; + const { userId } = await auth(); if (!userId) { - return new NextResponse('Пользователь не аутентифицирован', { - status: 403, - }); + return errorResponses.unauthorized(); } - if (!billboardId) { - return new NextResponse('Необходим идентификатор билборда.', { - status: 400, - }); - } + const body = await request.json(); + const validationResult = updateCategorySchema.safeParse(body); - if (!name) { - return new NextResponse('Укажите название.', { status: 400 }); + if (!validationResult.success) { + return errorResponses.validationError(validationResult.error.message); } - if (!params.categoryId) { - return new NextResponse('Необходим идентификатор категории.', { - status: 400, + const category = await prismadb.$transaction(async (tx) => { + const storeByUserId = await tx.store.findFirst({ + where: { + id: params.storeId, + userId, + }, + select: { id: true }, }); - } - const storeByUserId = await prismadb.store.findFirst({ - where: { - id: params.storeId, - userId, - }, - }); + if (!storeByUserId) { + throw new Error('Не авторизованный доступ'); + } - if (!storeByUserId) { - return new NextResponse('Не авторизованный доступ', { status: 405 }); - } + // Проверяем существование билборда + const billboard = await tx.billboard.findUnique({ + where: { id: validationResult.data.billboardId }, + select: { id: true }, + }); - const category = await prismadb.category.update({ - where: { - id: params.categoryId, - }, - data: { - name, - billboardId, - }, + if (!billboard) { + throw new Error('Билборд не найден'); + } + + return tx.category.update({ + where: { id: params.categoryId }, + data: validationResult.data, + select: { + id: true, + name: true, + billboard: { + select: { + id: true, + label: true, + }, + }, + updatedAt: true, + }, + }); }); return NextResponse.json(category); } catch (error) { - console.log('[CATEGORY_PATCH]', error); - return new NextResponse('Ошибка сервера', { status: 500 }); + console.error('[CATEGORY_PATCH]', error); + if (error instanceof Error) { + if (error.message === 'Не авторизованный доступ') { + return errorResponses.forbidden(); + } + if (error.message === 'Билборд не найден') { + return errorResponses.notFound(error.message); + } + } + return errorResponses.serverError(error); } } diff --git a/app/api/[storeId]/categories/route.ts b/app/api/[storeId]/categories/route.ts index 1176731..9ee5e83 100644 --- a/app/api/[storeId]/categories/route.ts +++ b/app/api/[storeId]/categories/route.ts @@ -1,83 +1,121 @@ import { auth } from '@clerk/nextjs/server'; import { NextResponse } from 'next/server'; +import { z } from 'zod'; import prismadb from '@/lib/prismadb'; +import { errorResponses } from '@/lib/error-responses'; + +// Схема валидации для создания категории +const categorySchema = z.object({ + name: z.string().min(2, 'Название должно содержать минимум 2 символа'), + billboardId: z.string().uuid('Неверный формат идентификатора билборда'), +}); export async function POST(request: Request, props: { params: Promise<{ storeId: string }> }) { - const params = await props.params; try { - const { userId }: { userId: string | null } = await auth(); - - const body = await request.json(); - - const { name, billboardId } = body; + const params = await props.params; + const { userId } = await auth(); if (!userId) { - return new NextResponse('Пользователь не аутентифицирован', { - status: 403, - }); + return errorResponses.unauthorized(); } - if (!name) { - return new NextResponse('Укажите название.', { status: 400 }); + if (!params.storeId) { + return errorResponses.badRequest('Необходим идентификатор магазина'); } - if (!billboardId) { - return new NextResponse('Необходим идентификатор билборда.', { - status: 400, - }); + const body = await request.json(); + const validationResult = categorySchema.safeParse(body); + + if (!validationResult.success) { + return errorResponses.validationError(validationResult.error.message); } - if (!params.storeId) { - return new NextResponse('Необходим идентификатор магазина.', { - status: 400, + // Используем транзакцию для проверки прав и создания категории + const category = await prismadb.$transaction(async (tx) => { + const storeByUserId = await tx.store.findFirst({ + where: { + id: params.storeId, + userId, + }, + select: { id: true }, }); - } - const storeByUserId = await prismadb.store.findFirst({ - where: { - id: params.storeId, - userId, - }, - }); + if (!storeByUserId) { + throw new Error('Не авторизованный доступ'); + } - if (!storeByUserId) { - return new NextResponse('Не авторизованный доступ', { status: 405 }); - } + // Проверяем существование билборда + const billboard = await tx.billboard.findUnique({ + where: { id: validationResult.data.billboardId }, + select: { id: true }, + }); - const category = await prismadb.category.create({ - data: { - name, - billboardId, - storeId: params.storeId, - }, + if (!billboard) { + throw new Error('Билборд не найден'); + } + + return tx.category.create({ + data: { + name: validationResult.data.name, + billboardId: validationResult.data.billboardId, + storeId: params.storeId, + }, + select: { + id: true, + name: true, + billboardId: true, + createdAt: true, + }, + }); }); - return NextResponse.json(category); + return NextResponse.json(category, { status: 201 }); } catch (error) { - console.log('[CATEGORIES_POST]', error); - return new NextResponse('Ошибка сервера', { status: 500 }); + console.error('[CATEGORIES_POST]', error); + if (error instanceof Error) { + if (error.message === 'Не авторизованный доступ') { + return errorResponses.forbidden(); + } + if (error.message === 'Билборд не найден') { + return errorResponses.notFound('Указанный билборд не найден'); + } + } + return errorResponses.serverError(error); } } export async function GET(request: Request, props: { params: Promise<{ storeId: string }> }) { - const params = await props.params; try { + const params = await props.params; + if (!params.storeId) { - return new NextResponse('Необходим идентификатор магазина.', { - status: 400, - }); + return errorResponses.badRequest('Необходим идентификатор магазина'); } const categories = await prismadb.category.findMany({ where: { storeId: params.storeId, }, + select: { + id: true, + name: true, + billboard: { + select: { + id: true, + label: true, + }, + }, + createdAt: true, + }, + orderBy: { + createdAt: 'desc', + }, }); return NextResponse.json(categories); } catch (error) { - console.log('[CATEGORIES_GET]', error); - return new NextResponse('Ошибка сервера', { status: 500 }); + console.error('[CATEGORIES_GET]', error); + return errorResponses.serverError(error); } } From 60fcc021fbe10183e78e0ba0f150acfc8b40eedc Mon Sep 17 00:00:00 2001 From: Meirbek-dev Date: Mon, 16 Dec 2024 19:31:46 +0500 Subject: [PATCH 5/8] added caching and improved error handling for billboards route --- .../billboards/[billboardId]/route.ts | 151 ++++++++++++++++-- app/api/[storeId]/billboards/route.ts | 103 +++++++----- 2 files changed, 199 insertions(+), 55 deletions(-) diff --git a/app/api/[storeId]/billboards/[billboardId]/route.ts b/app/api/[storeId]/billboards/[billboardId]/route.ts index 9f6a4ce..2d02f3d 100644 --- a/app/api/[storeId]/billboards/[billboardId]/route.ts +++ b/app/api/[storeId]/billboards/[billboardId]/route.ts @@ -1,27 +1,152 @@ import { auth } from '@clerk/nextjs/server'; import { NextResponse } from 'next/server'; +import { z } from 'zod'; +import { cache } from 'react'; import prismadb from '@/lib/prismadb'; - -export async function GET(request: Request, props: { params: Promise<{ billboardId: string }> }) { - const params = await props.params; +import { errorResponses } from '@/lib/error-responses'; + +// Кэширование запросов на получение билбордов +const getBillboards = cache(async (storeId: string) => { + return prismadb.billboard.findMany({ + where: { storeId }, + select: { + id: true, + label: true, + imageUrl: true, + createdAt: true, + }, + orderBy: { createdAt: 'desc' }, + }); +}); + +// Расширенная схема валидации для создания билборда +const billboardSchema = z.object({ + label: z + .string() + .min(1, 'Укажите метку') + .max(100, 'Метка слишком длинная') + .trim(), + imageUrl: z + .string() + .url('Укажите корректный URL изображения') + .max(500, 'URL слишком длинный') + .refine((url) => url.startsWith('https://'), 'Разрешены только HTTPS URL'), +}); + +export async function POST(request: Request, props: { params: Promise<{ storeId: string }> }) { try { - if (!params.billboardId) { - return new NextResponse('Необходим идентификатор билборда.', { - status: 400, + const params = await props.params; + const { userId } = await auth(); + + if (!userId) { + return errorResponses.unauthorized(); + } + + if (!params.storeId) { + return errorResponses.badRequest('Необходим идентификатор магазина'); + } + + const body = await request.json(); + const validationResult = billboardSchema.safeParse(body); + + if (!validationResult.success) { + return errorResponses.validationError(validationResult.error.message); + } + + // Проверка на лимит билбордов для магазина + const billboardCount = await prismadb.billboard.count({ + where: { storeId: params.storeId }, + }); + + if (billboardCount >= 50) { + return errorResponses.badRequest('Достигнут лимит билбордов для магазина'); + } + + // Используем транзакцию для проверки прав и создания билборда + const billboard = await prismadb.$transaction(async (tx) => { + const storeByUserId = await tx.store.findFirst({ + where: { + id: params.storeId, + userId, + }, + select: { id: true }, + }); + + if (!storeByUserId) { + throw new Error('Не авторизованный доступ'); + } + + // Проверка на дубликаты + const existingBillboard = await tx.billboard.findFirst({ + where: { + storeId: params.storeId, + label: validationResult.data.label, + }, + select: { id: true }, + }); + + if (existingBillboard) { + throw new Error('Билборд с такой меткой уже существует'); + } + + return tx.billboard.create({ + data: { + ...validationResult.data, + storeId: params.storeId, + }, + select: { + id: true, + label: true, + imageUrl: true, + createdAt: true, + }, }); + }); + + return NextResponse.json(billboard, { + status: 201, + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + } + }); + } catch (error) { + console.error('[BILLBOARDS_POST]', error); + if (error instanceof Error) { + switch (error.message) { + case 'Не авторизованный доступ': + return errorResponses.forbidden(); + case 'Билборд с такой меткой уже существует': + return errorResponses.conflict(error.message); + default: + return errorResponses.serverError(error); + } } + return errorResponses.serverError(error); + } +} - const billboard = await prismadb.billboard.findUnique({ - where: { - id: params.billboardId, +export async function GET(request: Request, props: { params: Promise<{ storeId: string }> }) { + try { + const params = await props.params; + + if (!params.storeId) { + return errorResponses.badRequest('Необходим идентификатор магазина'); + } + + // Используем кэшированную функцию для получения билбордов + const billboards = await getBillboards(params.storeId); + + return NextResponse.json(billboards, { + headers: { + 'Cache-Control': 'public, max-age=60, stale-while-revalidate=30', }, }); - - return NextResponse.json(billboard); } catch (error) { - console.log('[BILLBOARD_GET]', error); - return new NextResponse('Ошибка сервера', { status: 500 }); + console.error('[BILLBOARDS_GET]', error); + return errorResponses.serverError(error); } } diff --git a/app/api/[storeId]/billboards/route.ts b/app/api/[storeId]/billboards/route.ts index 286c5b7..467cab0 100644 --- a/app/api/[storeId]/billboards/route.ts +++ b/app/api/[storeId]/billboards/route.ts @@ -1,81 +1,100 @@ import { auth } from '@clerk/nextjs/server'; import { NextResponse } from 'next/server'; +import { z } from 'zod'; import prismadb from '@/lib/prismadb'; +import { errorResponses } from '@/lib/error-responses'; + +// Схема валидации для создания билборда +const billboardSchema = z.object({ + label: z.string().min(1, 'Укажите метку'), + imageUrl: z.string().url('Укажите корректный URL изображения'), +}); export async function POST(request: Request, props: { params: Promise<{ storeId: string }> }) { - const params = await props.params; try { - const { userId }: { userId: string | null } = await auth(); - - const body = await request.json(); - - const { label, imageUrl } = body; + const params = await props.params; + const { userId } = await auth(); if (!userId) { - return new NextResponse('Пользователь не аутентифицирован', { - status: 403, - }); + return errorResponses.unauthorized(); } - if (!label) { - return new NextResponse('Укажите метку', { status: 400 }); + if (!params.storeId) { + return errorResponses.badRequest('Необходим идентификатор магазина'); } - if (!imageUrl) { - return new NextResponse('Необходимо URL изображения', { status: 400 }); - } + const body = await request.json(); + const validationResult = billboardSchema.safeParse(body); - if (!params.storeId) { - return new NextResponse('Необходим идентификатор магазина.', { - status: 400, - }); + if (!validationResult.success) { + return errorResponses.validationError(validationResult.error.message); } - const storeByUserId = await prismadb.store.findFirst({ - where: { - id: params.storeId, - userId, - }, - }); + // Используем транзакцию для проверки прав и создания билборда + const billboard = await prismadb.$transaction(async (tx) => { + const storeByUserId = await tx.store.findFirst({ + where: { + id: params.storeId, + userId, + }, + select: { id: true }, + }); - if (!storeByUserId) { - return new NextResponse('Не авторизованный доступ', { status: 405 }); - } + if (!storeByUserId) { + throw new Error('Не авторизованный доступ'); + } - const billboard = await prismadb.billboard.create({ - data: { - label, - imageUrl, - storeId: params.storeId, - }, + return tx.billboard.create({ + data: { + ...validationResult.data, + storeId: params.storeId, + }, + select: { + id: true, + label: true, + imageUrl: true, + createdAt: true, + }, + }); }); - return NextResponse.json(billboard); + return NextResponse.json(billboard, { status: 201 }); } catch (error) { - console.log('[BILLBOARDS_POST]', error); - return new NextResponse('Ошибка сервера', { status: 500 }); + console.error('[BILLBOARDS_POST]', error); + if (error instanceof Error && error.message === 'Не авторизованный доступ') { + return errorResponses.forbidden(); + } + return errorResponses.serverError(error); } } export async function GET(request: Request, props: { params: Promise<{ storeId: string }> }) { - const params = await props.params; try { + const params = await props.params; + if (!params.storeId) { - return new NextResponse('Необходим идентификатор магазина.', { - status: 400, - }); + return errorResponses.badRequest('Необходим идентификатор магазина'); } const billboards = await prismadb.billboard.findMany({ where: { storeId: params.storeId, }, + select: { + id: true, + label: true, + imageUrl: true, + createdAt: true, + }, + orderBy: { + createdAt: 'desc', + }, }); return NextResponse.json(billboards); } catch (error) { - console.log('[BILLBOARDS_GET]', error); - return new NextResponse('Ошибка сервера', { status: 500 }); + console.error('[BILLBOARDS_GET]', error); + return errorResponses.serverError(error); } } From b2b5d34d1722878dddc65031ccca584f41c416b0 Mon Sep 17 00:00:00 2001 From: Meirbek-dev Date: Mon, 16 Dec 2024 19:41:59 +0500 Subject: [PATCH 6/8] improved error handling for root route --- app/(root)/(routes)/page.tsx | 30 +++++++++--- app/(root)/layout.tsx | 49 ++++++++++++++----- .../billboards/[billboardId]/route.ts | 12 ++--- app/api/[storeId]/sizes/[sizeId]/route.ts | 20 ++++++-- app/api/webhook/route.ts | 4 +- 5 files changed, 83 insertions(+), 32 deletions(-) diff --git a/app/(root)/(routes)/page.tsx b/app/(root)/(routes)/page.tsx index 9d425ea..63b7f97 100644 --- a/app/(root)/(routes)/page.tsx +++ b/app/(root)/(routes)/page.tsx @@ -1,19 +1,37 @@ 'use client'; -import { useEffect } from 'react'; +import { useEffect, useCallback } from 'react'; +import { useRouter } from 'next/navigation'; import { useStoreModal } from '@/hooks/use-store-modal'; +// Компонент настройки магазина const SetupPage = () => { - const onOpen = useStoreModal((state) => state.onOpen); - const isOpen = useStoreModal((state) => state.isOpen); + const router = useRouter(); + + // Используем селекторы из хука с мемоизацией + const { onOpen, isOpen } = useStoreModal( + useCallback( + (state) => ({ + onOpen: state.onOpen, + isOpen: state.isOpen, + }), + [], + ), + ); useEffect(() => { - if (!isOpen) { - onOpen(); + try { + if (!isOpen) { + onOpen(); + } + } catch (error) { + console.error('[SETUP_PAGE]', error); } }, [isOpen, onOpen]); - return null; + // Возвращаем пустой фрагмент вместо null для лучшей производительности + return <>; }; + export default SetupPage; diff --git a/app/(root)/layout.tsx b/app/(root)/layout.tsx index 5355bd6..c2f4adb 100644 --- a/app/(root)/layout.tsx +++ b/app/(root)/layout.tsx @@ -1,24 +1,47 @@ import { auth } from '@clerk/nextjs/server'; import { redirect } from 'next/navigation'; +import { cache } from 'react'; +import { z } from 'zod'; import prismadb from '@/lib/prismadb'; +import { errorResponses } from '@/lib/error-responses'; + +// Кэширование запроса на получение магазина +const getStore = cache(async (userId: string) => { + return prismadb.store.findFirst({ + where: { userId }, + select: { + id: true, + name: true, + createdAt: true, + }, + }); +}); + +// Схема валидации для userId +const userIdSchema = z.string().min(1, 'ID пользователя обязателен'); export default async function SetupLayout({ children }: { children: React.ReactNode }) { - const { userId }: { userId: string | null } = await auth(); + try { + const { userId } = await auth(); - if (!userId) { - redirect('/sign-in'); - } + // Валидация userId + const validationResult = userIdSchema.safeParse(userId); - const store = await prismadb.store.findFirst({ - where: { - userId, - }, - }); + if (!validationResult.success) { + redirect('/sign-in'); + } - if (store) { - redirect(`/${store.id}`); - } + // Используем кэшированную функцию для получения магазина + const store = await getStore(validationResult.data); - return <>{children}; + if (store) { + redirect(`/${store.id}`); + } + + return
{children}
; + } catch (error) { + console.error('[SETUP_LAYOUT]', error); + redirect('/error'); + } } diff --git a/app/api/[storeId]/billboards/[billboardId]/route.ts b/app/api/[storeId]/billboards/[billboardId]/route.ts index 2d02f3d..8f831bb 100644 --- a/app/api/[storeId]/billboards/[billboardId]/route.ts +++ b/app/api/[storeId]/billboards/[billboardId]/route.ts @@ -22,11 +22,7 @@ const getBillboards = cache(async (storeId: string) => { // Расширенная схема валидации для создания билборда const billboardSchema = z.object({ - label: z - .string() - .min(1, 'Укажите метку') - .max(100, 'Метка слишком длинная') - .trim(), + label: z.string().min(1, 'Укажите метку').max(100, 'Метка слишком длинная').trim(), imageUrl: z .string() .url('Укажите корректный URL изображения') @@ -108,9 +104,9 @@ export async function POST(request: Request, props: { params: Promise<{ storeId: status: 201, headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate', - 'Pragma': 'no-cache', - 'Expires': '0', - } + Pragma: 'no-cache', + Expires: '0', + }, }); } catch (error) { console.error('[BILLBOARDS_POST]', error); diff --git a/app/api/[storeId]/sizes/[sizeId]/route.ts b/app/api/[storeId]/sizes/[sizeId]/route.ts index 86d6133..6e9e5fc 100644 --- a/app/api/[storeId]/sizes/[sizeId]/route.ts +++ b/app/api/[storeId]/sizes/[sizeId]/route.ts @@ -17,12 +17,20 @@ export async function GET(request: Request, props: { params: Promise<{ sizeId: s const params = await props.params; if (!params.sizeId) { - return new NextResponse('Необходим идентификатор размера.', { status: 400 }); + return new NextResponse('Необходим идентификатор размера.', { + status: 400, + }); } const size = await prismadb.size.findUnique({ where: { id: params.sizeId }, - select: { id: true, name: true, value: true, createdAt: true, updatedAt: true }, + select: { + id: true, + name: true, + value: true, + createdAt: true, + updatedAt: true, + }, }); if (!size) { @@ -50,7 +58,9 @@ export async function DELETE( } if (!params.sizeId) { - return new NextResponse('Необходим идентификатор размера.', { status: 400 }); + return new NextResponse('Необходим идентификатор размера.', { + status: 400, + }); } const size = await prismadb.$transaction(async (tx) => { @@ -95,7 +105,9 @@ export async function PATCH( } if (!params.sizeId) { - return new NextResponse('Необходим идентификатор размера.', { status: 400 }); + return new NextResponse('Необходим идентификатор размера.', { + status: 400, + }); } // Валидация входных данных diff --git a/app/api/webhook/route.ts b/app/api/webhook/route.ts index a0385d6..1a3cd46 100644 --- a/app/api/webhook/route.ts +++ b/app/api/webhook/route.ts @@ -39,7 +39,9 @@ export async function POST(request: Request) { // Validate session and metadata if (!session?.metadata?.orderId) { - return new NextResponse('Missing order ID in session metadata', { status: 400 }); + return new NextResponse('Missing order ID in session metadata', { + status: 400, + }); } const orderId = session.metadata.orderId; From 123e10cb0cce11228ad7b2f46e4b3aee74136e69 Mon Sep 17 00:00:00 2001 From: Meirbek-dev Date: Mon, 16 Dec 2024 19:53:32 +0500 Subject: [PATCH 7/8] improved error handling for stores route --- app/(root)/(routes)/page.tsx | 2 - app/(root)/layout.tsx | 1 - app/api/stores/route.ts | 81 ++++++++++++++++++++++++++---------- 3 files changed, 58 insertions(+), 26 deletions(-) diff --git a/app/(root)/(routes)/page.tsx b/app/(root)/(routes)/page.tsx index 63b7f97..f8f800d 100644 --- a/app/(root)/(routes)/page.tsx +++ b/app/(root)/(routes)/page.tsx @@ -1,13 +1,11 @@ 'use client'; import { useEffect, useCallback } from 'react'; -import { useRouter } from 'next/navigation'; import { useStoreModal } from '@/hooks/use-store-modal'; // Компонент настройки магазина const SetupPage = () => { - const router = useRouter(); // Используем селекторы из хука с мемоизацией const { onOpen, isOpen } = useStoreModal( diff --git a/app/(root)/layout.tsx b/app/(root)/layout.tsx index c2f4adb..a8af0c3 100644 --- a/app/(root)/layout.tsx +++ b/app/(root)/layout.tsx @@ -4,7 +4,6 @@ import { cache } from 'react'; import { z } from 'zod'; import prismadb from '@/lib/prismadb'; -import { errorResponses } from '@/lib/error-responses'; // Кэширование запроса на получение магазина const getStore = cache(async (userId: string) => { diff --git a/app/api/stores/route.ts b/app/api/stores/route.ts index b3be671..d8f0bba 100644 --- a/app/api/stores/route.ts +++ b/app/api/stores/route.ts @@ -3,51 +3,86 @@ import { NextResponse } from 'next/server'; import { z } from 'zod'; import prismadb from '@/lib/prismadb'; +import { errorResponses } from '@/lib/error-responses'; +// схема валидации для создания магазина const createStoreSchema = z.object({ - name: z.string().min(2, 'Имя должно содержать минимум 2 символа'), + name: z + .string() + .min(2, 'Имя должно содержать минимум 2 символа') + .max(50, 'Имя слишком длинное') + .trim() + .regex(/^[\p{L}\s-]+$/u, 'Имя может содержать только буквы, пробелы и дефисы'), }); export async function POST(request: Request) { try { - // Get user authentication status const { userId } = await auth(); if (!userId) { - return new NextResponse('Unauthorized', { status: 401 }); + return errorResponses.unauthorized(); } - // Parse and validate request body const body = await request.json(); const validationResult = createStoreSchema.safeParse(body); if (!validationResult.success) { - return new NextResponse(validationResult.error.message, { status: 400 }); + return errorResponses.validationError(validationResult.error.message); } - // Create store with validated data - const store = await prismadb.store.create({ - data: { - name: validationResult.data.name, - userId, - }, - select: { - id: true, - name: true, - createdAt: true, - }, + // Проверка на лимит магазинов + const storesCount = await prismadb.store.count({ + where: { userId }, }); - return NextResponse.json(store, { status: 201 }); - } catch (error) { - // Log error for monitoring but don't expose details - console.error('[STORES_POST]', error); + if (storesCount >= 7) { + return errorResponses.forbidden('Достигнут лимит магазинов (максимум 3)'); + } + + // Используем транзакцию для проверки дубликатов и создания магазина + const store = await prismadb.$transaction(async (tx) => { + // Проверка на дубликаты + const existingStore = await tx.store.findFirst({ + where: { + userId, + name: validationResult.data.name, + }, + select: { id: true }, + }); - return new NextResponse('Internal Server Error', { - status: 500, + if (existingStore) { + throw new Error('Магазин с таким именем уже существует'); + } + + return tx.store.create({ + data: { + name: validationResult.data.name, + userId, + }, + select: { + id: true, + name: true, + createdAt: true, + }, + }); + }); + + return NextResponse.json(store, { + status: 201, headers: { - 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache, no-store, must-revalidate', }, }); + } catch (error) { + console.error('[STORES_POST]', error); + if (error instanceof Error) { + switch (error.message) { + case 'Магазин с таким именем уже существует': + return errorResponses.conflict(error.message); + default: + return errorResponses.serverError(error); + } + } + return errorResponses.serverError(error); } } From 6000aea35053e33b8ab415312cb572260624dfb6 Mon Sep 17 00:00:00 2001 From: Meirbek-dev Date: Mon, 16 Dec 2024 19:54:05 +0500 Subject: [PATCH 8/8] added caching and improved error handling for get-total-revenue action --- actions/get-total-revenue.ts | 70 +++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 20 deletions(-) diff --git a/actions/get-total-revenue.ts b/actions/get-total-revenue.ts index c8e59e4..2181783 100644 --- a/actions/get-total-revenue.ts +++ b/actions/get-total-revenue.ts @@ -1,25 +1,55 @@ +import { cache } from 'react'; +import { z } from 'zod'; + import prismadb from '@/lib/prismadb'; -export const getTotalRevenue = async (storeId: string) => { - const paidOrders = await prismadb.order.findMany({ - where: { - storeId, - isPaid: true, - }, - include: { - orderItems: { - include: { - product: true, +const storeIdSchema = z.string().uuid('Неверный формат идентификатора магазина'); + +interface PrismaOrder { + orderItems: { + product: { + price: number; + }; + }[]; +} + +// Кэшированная функция получения выручки +export const getTotalRevenue = cache(async (storeId: string) => { + try { + const validationResult = storeIdSchema.safeParse(storeId); + + if (!validationResult.success) { + throw new Error('Неверный идентификатор магазина'); + } + + const paidOrders = await prismadb.order.findMany({ + where: { + storeId: validationResult.data, + isPaid: true, + }, + select: { + orderItems: { + select: { + product: { + select: { + price: true, + }, + }, + }, }, }, - }, - }); + }); - return paidOrders.reduce((total: any, order: any) => { - const orderTotal = order.orderItems.reduce( - (orderSum: any, item: any) => orderSum + item.product.price.toNumber(), - 0, - ); - return total + orderTotal; - }, 0); -}; + return paidOrders.reduce((total: number, order: PrismaOrder) => { + const orderTotal = order.orderItems.reduce( + (orderSum: number, item) => + orderSum + item.product.price, + 0, + ); + return total + orderTotal; + }, 0); + } catch (error) { + console.error('[GET_TOTAL_REVENUE]', error); + return 0; + } +});