Projeto E-commerce - Parte 3
Finalizando o projeto com pagamentos, reviews e painel admin.
Integração com Stripe
bash
npm install stripetypescript
// 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!