Saltar al contenidoPedro Farbo
Lección 25 / 2555 min

Proyecto E-commerce - Parte 3

Proyecto E-commerce - Parte 3

Finalizamos con el módulo de pedidos y checkout.

Schema de Pedidos

prisma
model Order {  id        String      @id @default(uuid())  user      User        @relation(fields: [userId], references: [id])  userId    String  items     OrderItem[]  address   Address     @relation(fields: [addressId], references: [id])  addressId String  subtotal  Decimal     @db.Decimal(10, 2)  shipping  Decimal     @db.Decimal(10, 2)  total     Decimal     @db.Decimal(10, 2)  status    OrderStatus @default(PENDING)  paymentId String?  createdAt DateTime    @default(now())  updatedAt DateTime    @updatedAt   @@index([userId])  @@index([status])} model OrderItem {  id        String  @id @default(uuid())  order     Order   @relation(fields: [orderId], references: [id], onDelete: Cascade)  orderId   String  product   Product @relation(fields: [productId], references: [id])  productId String  name      String  price     Decimal @db.Decimal(10, 2)  quantity  Int   @@unique([orderId, productId])} model Address {  id      String  @id @default(uuid())  user    User    @relation(fields: [userId], references: [id])  userId  String  name    String  street  String  city    String  state   String  zipCode String  country String  phone   String?  orders  Order[]} enum OrderStatus {  PENDING  PAID  PROCESSING  SHIPPED  DELIVERED  CANCELLED}

Order Service

typescript
// src/services/order.service.tsimport { prisma } from '../config/database';import { AppError } from '../errors/AppError';import { mailService } from './mail.service'; interface CreateOrderDTO {  items: { productId: string; quantity: number }[];  addressId: string;} export class OrderService {  async create(userId: string, data: CreateOrderDTO) {    return prisma.$transaction(async (tx) => {      // Verificar stock y calcular precios      const orderItems = [];      let subtotal = 0;       for (const item of data.items) {        const product = await tx.product.findUnique({          where: { id: item.productId },        });         if (!product) {          throw new AppError(`Producto no encontrado: ${item.productId}`, 404);        }         if (product.stock < item.quantity) {          throw new AppError(`Stock insuficiente: ${product.name}`, 400);        }         const itemTotal = Number(product.price) * item.quantity;        subtotal += itemTotal;         orderItems.push({          productId: product.id,          name: product.name,          price: product.price,          quantity: item.quantity,        });         // Reservar stock        await tx.product.update({          where: { id: product.id },          data: { stock: { decrement: item.quantity } },        });      }       // Calcular envío      const shipping = subtotal >= 100 ? 0 : 9.99;      const total = subtotal + shipping;       // Crear pedido      const order = await tx.order.create({        data: {          userId,          addressId: data.addressId,          subtotal,          shipping,          total,          items: { create: orderItems },        },        include: {          items: { include: { product: true } },          address: true,          user: { select: { id: true, name: true, email: true } },        },      });       // Enviar email      await mailService.sendOrderConfirmation(order);       return order;    });  }   async findUserOrders(userId: string, page = 1, limit = 10) {    const skip = (page - 1) * limit;     const [orders, total] = await Promise.all([      prisma.order.findMany({        where: { userId },        skip,        take: limit,        orderBy: { createdAt: 'desc' },        include: {          items: { include: { product: { select: { images: true } } } },        },      }),      prisma.order.count({ where: { userId } }),    ]);     return { data: orders, pagination: { page, limit, total, totalPages: Math.ceil(total / limit) } };  }   async findById(id: string, userId: string, isAdmin: boolean) {    const order = await prisma.order.findUnique({      where: { id },      include: {        items: { include: { product: true } },        address: true,        user: { select: { id: true, name: true, email: true } },      },    });     if (!order) throw new AppError('Pedido no encontrado', 404);    if (!isAdmin && order.userId !== userId) {      throw new AppError('Acceso denegado', 403);    }     return order;  }   async updateStatus(id: string, status: OrderStatus) {    const order = await prisma.order.update({      where: { id },      data: { status },      include: { user: true },    });     // Notificar al usuario    if (status === 'SHIPPED') {      await mailService.sendShippingNotification(order);    }     return order;  }   async cancel(id: string, userId: string) {    const order = await this.findById(id, userId, false);     if (order.status !== 'PENDING') {      throw new AppError('Solo se pueden cancelar pedidos pendientes', 400);    }     return prisma.$transaction(async (tx) => {      // Restaurar stock      for (const item of order.items) {        await tx.product.update({          where: { id: item.productId },          data: { stock: { increment: item.quantity } },        });      }       // Cancelar pedido      return tx.order.update({        where: { id },        data: { status: 'CANCELLED' },      });    });  }}

Order Controller

typescript
// src/controllers/order.controller.tsexport class OrderController {  async index(req: Request, res: Response) {    const { page, limit } = req.query;    const orders = await orderService.findUserOrders(      req.user!.id,      Number(page) || 1,      Number(limit) || 10    );    return res.json(orders);  }   async show(req: Request, res: Response) {    const order = await orderService.findById(      req.params.id,      req.user!.id,      req.user!.role === 'ADMIN'    );    return res.json(order);  }   async store(req: Request, res: Response) {    const order = await orderService.create(req.user!.id, req.body);    return res.status(201).json(order);  }   async cancel(req: Request, res: Response) {    const order = await orderService.cancel(req.params.id, req.user!.id);    return res.json(order);  }   // Admin  async updateStatus(req: Request, res: Response) {    const order = await orderService.updateStatus(req.params.id, req.body.status);    return res.json(order);  }   async adminIndex(req: Request, res: Response) {    const { page, limit, status } = req.query;    const orders = await orderService.findAll({      page: Number(page) || 1,      limit: Number(limit) || 20,      status: status as OrderStatus,    });    return res.json(orders);  }}

Rutas de Pedidos

typescript
// src/routes/order.routes.tsconst router = Router(); router.use(authenticate); router.get('/', asyncHandler(controller.index));router.post('/', validate(createOrderSchema), asyncHandler(controller.store));router.get('/:id', asyncHandler(controller.show));router.post('/:id/cancel', asyncHandler(controller.cancel)); // Adminrouter.get('/admin/all', authorize('ADMIN'), asyncHandler(controller.adminIndex));router.patch('/:id/status', authorize('ADMIN'), asyncHandler(controller.updateStatus)); export { router as orderRoutes };

Dashboard Admin

typescript
// src/services/dashboard.service.tsexport class DashboardService {  async getStats() {    const [totalOrders, totalRevenue, totalUsers, recentOrders] = await Promise.all([      prisma.order.count({ where: { status: { not: 'CANCELLED' } } }),      prisma.order.aggregate({        where: { status: { in: ['PAID', 'SHIPPED', 'DELIVERED'] } },        _sum: { total: true },      }),      prisma.user.count(),      prisma.order.findMany({        take: 10,        orderBy: { createdAt: 'desc' },        include: { user: { select: { name: true } } },      }),    ]);     return {      totalOrders,      totalRevenue: totalRevenue._sum.total || 0,      totalUsers,      recentOrders,    };  }}

🎉 Proyecto Completo

Tu API de e-commerce ahora tiene:

  • Autenticación - Registro, login, JWT
  • Usuarios - Perfil, direcciones
  • Categorías - CRUD completo
  • Productos - CRUD con filtros, búsqueda, cache
  • Reviews - Sistema de valoraciones
  • Pedidos - Checkout, historial, cancelación
  • Admin - Dashboard, gestión de pedidos
  • Seguridad - RBAC, rate limiting, validación
  • Deploy - Docker, CI/CD

Próximos Pasos

  1. Integrar pasarela de pagos (Stripe)
  2. Implementar websockets para notificaciones
  3. Agregar sistema de cupones
  4. Optimizar con GraphQL

¡Felicidades por completar el curso! 🚀

¿Te gustó el contenido? ¡Tu contribución ayuda a mantener todo online y gratuito!

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