Pular para o conteúdoPedro Farbo
Lição 25 / 2560 min

Projeto E-commerce: Carrinho e Pedidos

Projeto E-commerce - Parte 3

Finalizando o projeto com pagamentos, reviews e painel admin.

Integração com Stripe

bash
npm install stripe
typescript
// src/modules/payments/payment.service.tsimport Stripe from 'stripe';import { prisma } from '../../config/database';import { AppError } from '../../shared/errors/AppError';import { OrderService } from '../orders/order.service'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {  apiVersion: '2023-10-16',}); export class PaymentService {  private orderService = new OrderService();   async createCheckoutSession(orderId: string, userId: string) {    const order = await prisma.order.findFirst({      where: { id: orderId, userId, status: 'PENDING' },      include: { items: true },    });     if (!order) {      throw new AppError('Pedido não encontrado', 404);    }     const session = await stripe.checkout.sessions.create({      payment_method_types: ['card', 'boleto', 'pix'],      mode: 'payment',      client_reference_id: order.id,      customer_email: (await prisma.user.findUnique({ where: { id: userId } }))?.email,      line_items: order.items.map((item) => ({        price_data: {          currency: 'brl',          product_data: {            name: item.name,          },          unit_amount: Math.round(Number(item.price) * 100),        },        quantity: item.quantity,      })),      success_url: `${process.env.FRONTEND_URL}/orders/${order.id}?success=true`,      cancel_url: `${process.env.FRONTEND_URL}/orders/${order.id}?canceled=true`,      metadata: {        orderId: order.id,      },    });     return { url: session.url };  }   async handleWebhook(payload: Buffer, signature: string) {    const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;     let event: Stripe.Event;     try {      event = stripe.webhooks.constructEvent(payload, signature, webhookSecret);    } catch (err) {      throw new AppError('Webhook signature inválida', 400);    }     switch (event.type) {      case 'checkout.session.completed': {        const session = event.data.object as Stripe.Checkout.Session;        const orderId = session.metadata?.orderId;         if (orderId) {          await prisma.order.update({            where: { id: orderId },            data: {              status: 'PAID',              paymentId: session.payment_intent as string,            },          });        }        break;      }       case 'payment_intent.payment_failed': {        const paymentIntent = event.data.object as Stripe.PaymentIntent;        console.log('Pagamento falhou:', paymentIntent.id);        break;      }    }     return { received: true };  }   async refund(orderId: string) {    const order = await prisma.order.findUnique({      where: { id: orderId },    });     if (!order || !order.paymentId) {      throw new AppError('Pedido não encontrado ou sem pagamento', 404);    }     await stripe.refunds.create({      payment_intent: order.paymentId,    });     await prisma.order.update({      where: { id: orderId },      data: { status: 'CANCELLED' },    });     return { success: true };  }}

Módulo de Reviews

typescript
// src/modules/reviews/review.service.tsimport { prisma } from '../../config/database';import { AppError } from '../../shared/errors/AppError';import { CacheService } from '../../shared/services/cache.service'; const cache = new CacheService(); interface CreateReviewDTO {  userId: string;  productId: string;  rating: number;  comment: string;} export class ReviewService {  async create(data: CreateReviewDTO) {    // Verifica se usuário comprou o produto    const hasPurchased = await prisma.orderItem.findFirst({      where: {        productId: data.productId,        order: {          userId: data.userId,          status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] },        },      },    });     if (!hasPurchased) {      throw new AppError('Você precisa comprar o produto para avaliá-lo', 403);    }     // Verifica se já avaliou    const existingReview = await prisma.review.findUnique({      where: {        userId_productId: {          userId: data.userId,          productId: data.productId,        },      },    });     if (existingReview) {      throw new AppError('Você já avaliou este produto', 400);    }     const review = await prisma.review.create({      data,      include: {        user: { select: { name: true } },      },    });     // Invalida cache do produto    const product = await prisma.product.findUnique({      where: { id: data.productId },    });    if (product) {      await cache.delete(`product:${product.slug}`);    }     return review;  }   async findByProduct(productId: string, page = 1, limit = 10) {    const skip = (page - 1) * limit;     const [reviews, total, stats] = await Promise.all([      prisma.review.findMany({        where: { productId },        skip,        take: limit,        orderBy: { createdAt: 'desc' },        include: {          user: { select: { name: true } },        },      }),      prisma.review.count({ where: { productId } }),      prisma.review.aggregate({        where: { productId },        _avg: { rating: true },        _count: true,      }),    ]);     return {      data: reviews,      stats: {        average: stats._avg.rating || 0,        count: stats._count,      },      pagination: {        page,        limit,        total,        totalPages: Math.ceil(total / limit),      },    };  }}

Painel Admin

typescript
// src/modules/admin/admin.service.tsimport { prisma } from '../../config/database'; export class AdminService {  async getDashboard() {    const now = new Date();    const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);    const startOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);    const endOfLastMonth = new Date(now.getFullYear(), now.getMonth(), 0);     const [      totalUsers,      totalProducts,      totalOrders,      monthlyRevenue,      lastMonthRevenue,      recentOrders,      topProducts,    ] = await Promise.all([      prisma.user.count(),      prisma.product.count({ where: { active: true } }),      prisma.order.count(),      prisma.order.aggregate({        where: {          status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] },          createdAt: { gte: startOfMonth },        },        _sum: { total: true },      }),      prisma.order.aggregate({        where: {          status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] },          createdAt: { gte: startOfLastMonth, lte: endOfLastMonth },        },        _sum: { total: true },      }),      prisma.order.findMany({        take: 10,        orderBy: { createdAt: 'desc' },        include: {          user: { select: { name: true, email: true } },        },      }),      prisma.orderItem.groupBy({        by: ['productId'],        _sum: { quantity: true },        orderBy: { _sum: { quantity: 'desc' } },        take: 5,      }),    ]);     // Busca detalhes dos top produtos    const topProductsDetails = await prisma.product.findMany({      where: { id: { in: topProducts.map((p) => p.productId) } },      select: { id: true, name: true, images: true },    });     const monthlyTotal = Number(monthlyRevenue._sum.total || 0);    const lastMonthTotal = Number(lastMonthRevenue._sum.total || 0);    const revenueGrowth = lastMonthTotal      ? ((monthlyTotal - lastMonthTotal) / lastMonthTotal) * 100      : 0;     return {      stats: {        totalUsers,        totalProducts,        totalOrders,        monthlyRevenue: monthlyTotal,        revenueGrowth: revenueGrowth.toFixed(1),      },      recentOrders,      topProducts: topProducts.map((p) => ({        ...topProductsDetails.find((d) => d.id === p.productId),        totalSold: p._sum.quantity,      })),    };  }   async getOrdersReport(startDate: Date, endDate: Date) {    const orders = await prisma.order.groupBy({      by: ['status'],      where: {        createdAt: { gte: startDate, lte: endDate },      },      _count: true,      _sum: { total: true },    });     const dailySales = await prisma.$queryRaw`      SELECT        DATE(created_at) as date,        COUNT(*) as orders,        SUM(total) as revenue      FROM orders      WHERE created_at >= ${startDate} AND created_at <= ${endDate}        AND status IN ('PAID', 'SHIPPED', 'DELIVERED')      GROUP BY DATE(created_at)      ORDER BY date    `;     return { orders, dailySales };  }}

Rotas Admin

typescript
// src/modules/admin/admin.routes.tsimport { Router } from 'express';import { AdminController } from './admin.controller';import { authenticate, authorize } from '../../shared/middlewares/auth.middleware'; const router = Router();const controller = new AdminController(); router.use(authenticate, authorize('ADMIN')); router.get('/dashboard', controller.dashboard);router.get('/reports/orders', controller.ordersReport);router.get('/orders', controller.listOrders);router.patch('/orders/:id/status', controller.updateOrderStatus);router.get('/users', controller.listUsers);router.patch('/users/:id/role', controller.updateUserRole); export default router;

Webhook Route

typescript
// src/modules/payments/payment.routes.tsimport { Router } from 'express';import express from 'express';import { PaymentController } from './payment.controller';import { authenticate } from '../../shared/middlewares/auth.middleware'; const router = Router();const controller = new PaymentController(); // Webhook precisa do body rawrouter.post(  '/webhook',  express.raw({ type: 'application/json' }),  controller.webhook); router.use(authenticate);router.post('/checkout/:orderId', controller.createCheckout);router.post('/refund/:orderId', controller.refund); export default router;

Conclusão do Projeto

Parabéns! Você construiu uma API de e-commerce completa com:

  • Autenticação JWT com registro e login
  • CRUD de produtos com filtros e paginação
  • Carrinho de compras persistente
  • Sistema de pedidos com transações
  • Pagamentos Stripe com checkout e webhooks
  • Reviews de produtos com validação de compra
  • Painel administrativo com dashboard
  • Cache Redis para performance
  • Testes automatizados
  • Documentação Swagger
  • Deploy com Docker

Próximos Passos

  • Adicionar busca full-text com Elasticsearch
  • Implementar notificações push
  • Criar sistema de wishlist
  • Adicionar múltiplos métodos de pagamento
  • Implementar programa de fidelidade

🎉 Faça a prova final para receber seu certificado!

Gostou do conteúdo? Sua contribuição ajuda a manter tudo online e gratuito!

PIX:0737160d-e98f-4a65-8392-5dba70e7ff3e