🚀 Oferta especial: 60% OFF no CrazyStack - Últimas vagas!Garantir vaga →

Aula 9: Gerenciamento de Pedidos - Status e Detalhes

Módulo 3⏱️ 45 minutos📚 Avançado

🎯 Objetivo da Aula

Vamos implementar o sistema completo de gerenciamento de pedidos: obter detalhes, aprovar, despachar, entregar e cancelar pedidos.

📋 Rotas que vamos criar

  • GET /orders/:orderId - Detalhes do pedido
  • PATCH /orders/:orderId/approve - Aprovar pedido
  • PATCH /orders/:orderId/dispatch - Despachar pedido
  • PATCH /orders/:orderId/deliver - Entregar pedido
  • PATCH /orders/:orderId/cancel - Cancelar pedido

📊 Fluxo de Status dos Pedidos

🔄 FLUXO DE STATUS DOS PEDIDOS

pending ──────► preparing ──────► ready ──────► delivered
   │                │                              
   │                │                              
   └──────► cancelled ◄──────────┘                

📝 REGRAS DE TRANSIÇÃO:
• pending → preparing (approve)
• pending → cancelled (cancel)
• preparing → ready (dispatch)
• preparing → cancelled (cancel)
• ready → delivered (deliver)
• delivered/cancelled = FINAL (não pode mudar)

🔍 Rota: Detalhes do Pedido

Rota com joins complexos para obter todos os dados do pedido:

// src/http/routes/get-order-details.ts
import { eq, and } from "drizzle-orm";
import { db } from "@/application/infra/db/connection";
import { orders } from "@/application/infra/db/schema";
import { auth } from "../auth";
import { UnauthorizedError } from "../errors/unauthorized-error";
import { NotFoundError, t } from "elysia";

export const getOrderDetails = auth.get(
  "/orders/:orderId",
  async ({ params, getCurrentUser }) => {
    const { orderId } = params;
    const { restaurantId } = await getCurrentUser();
    
    if (!restaurantId) {
      throw new UnauthorizedError();
    }

    // Query com joins usando Drizzle Query API
    const order = await db.query.orders.findFirst({
      columns: {
        id: true,
        status: true,
        priceInCents: true,
        createdAt: true,
        restaurantId: true
      },
      with: {
        // Join com customer
        customer: {
          columns: {
            phone: true,
            name: true,
            email: true
          }
        },
        // Join com order items e products
        orderItems: {
          columns: {
            id: true,
            priceInCents: true,
            quantity: true
          },
          with: {
            product: {
              columns: {
                name: true
              }
            }
          }
        }
      },
      where: and(
        eq(orders.id, orderId),
        eq(orders.restaurantId, restaurantId)
      )
    });

    if (!order) {
      throw new NotFoundError("Order not found");
    }

    if (order.restaurantId !== restaurantId) {
      throw new UnauthorizedError();
    }

    return { order };

export default
  },
  {
    params: t.Object({
      orderId: t.String()
    })
  },
);

Estrutura de Resposta

📋 ESTRUTURA DA RESPOSTA

{
  "order": {
    "id": "cm5abc123",
    "status": "preparing",
    "priceInCents": 2500,
    "createdAt": "2025-01-01T10:00:00Z",
    "restaurantId": "cm5rest123",
    "customer": {
      "name": "João Silva",
      "email": "joao@email.com",
      "phone": "(11) 99999-9999"
    },
    "orderItems": [
      {
        "id": "cm5item1",
        "quantity": 2,
        "priceInCents": 1200,
        "product": {
          "name": "Pizza Margherita"
        }
      },
      {
        "id": "cm5item2", 
        "quantity": 1,
        "priceInCents": 1300,
        "product": {
          "name": "Refrigerante"
        }
      }
    ]
  }
}

✅ Rota: Aprovar Pedido

Muda status de "pending" para "preparing":

// src/http/routes/approve-order.ts
import { and, eq } from "drizzle-orm";
import { db } from "@/application/infra/db/connection";
import { orders } from "@/application/infra/db/schema";
import { auth } from "../auth";
import { UnauthorizedError } from "../errors/unauthorized-error";
import { NotFoundError, t } from "elysia";

export const approveOrder = auth.patch(
  "/orders/:orderId/approve",
  async ({ params, getCurrentUser, set }) => {
    const { orderId } = params;
    const { restaurantId } = await getCurrentUser();
    
    if (!restaurantId) {
      throw new UnauthorizedError();
    }

    // 1. Buscar pedido e verificar status atual
    const order = await db.query.orders.findFirst({
      columns: {
        id: true,
        status: true,
        restaurantId: true
      },
      where: and(
        eq(orders.id, orderId),
        eq(orders.restaurantId, restaurantId)
      )
    });

    if (!order) {
      throw new NotFoundError("Order not found");
    }

    // 2. Validar transição de status
    if (order.status !== "pending") {
      set.status = 400;
      return { message: "You can only approve pending orders" };
    }

    if (order.restaurantId !== restaurantId) {
      throw new UnauthorizedError();
    }

    // 3. Atualizar status para "preparing"
    const updatedOrder = await db
      .update(orders)
      .set({ status: "preparing" })
      .where(eq(orders.id, orderId))
      .returning({
        id: orders.id,
        status: orders.status,
        restaurantId: orders.restaurantId
      });

    return { order: updatedOrder };
  },
  {
    params: t.Object({
      orderId: t.String()
    })
  },
);

🚚 Rota: Despachar Pedido

Muda status de "preparing" para "ready":

// src/http/routes/dispatch-order.ts
import { and, eq } from "drizzle-orm";
import { db } from "@/application/infra/db/connection";
import { orders } from "@/application/infra/db/schema";
import { auth } from "../auth";
import { UnauthorizedError } from "../errors/unauthorized-error";
import { NotFoundError, t } from "elysia";

export const dispatchOrder = auth.patch(
  "/orders/:orderId/dispatch",
  async ({ params, getCurrentUser, set }) => {
    const { orderId } = params;
    const { restaurantId } = await getCurrentUser();
    
    if (!restaurantId) {
      throw new UnauthorizedError();
    }

    const order = await db.query.orders.findFirst({
      columns: {
        id: true,
        status: true,
        restaurantId: true
      },
      where: and(
        eq(orders.id, orderId),
        eq(orders.restaurantId, restaurantId)
      )
    });

    if (!order) {
      throw new NotFoundError("Order not found");
    }

    // Validar transição: só pode despachar pedidos em preparo
    if (order.status !== "preparing") {
      set.status = 400;
      return { message: "You can only dispatch preparing orders" };
    }

    if (order.restaurantId !== restaurantId) {
      throw new UnauthorizedError();
    }

    const updatedOrder = await db
      .update(orders)
      .set({ status: "ready" })
      .where(eq(orders.id, orderId))
      .returning({
        id: orders.id,
        status: orders.status,
        restaurantId: orders.restaurantId
      });

    return { order: updatedOrder };
  },
  {
    params: t.Object({
      orderId: t.String()
    })
  },
);

📦 Rota: Entregar Pedido

Muda status de "ready" para "delivered":

// src/http/routes/deliver-order.ts
import { and, eq } from "drizzle-orm";
import { db } from "@/application/infra/db/connection";
import { orders } from "@/application/infra/db/schema";
import { auth } from "../auth";
import { UnauthorizedError } from "../errors/unauthorized-error";
import { NotFoundError, t } from "elysia";

export const deliverOrder = auth.patch(
  "/orders/:orderId/deliver",
  async ({ params, getCurrentUser, set }) => {
    const { orderId } = params;
    const { restaurantId } = await getCurrentUser();
    
    if (!restaurantId) {
      throw new UnauthorizedError();
    }

    const order = await db.query.orders.findFirst({
      columns: {
        id: true,
        status: true,
        restaurantId: true
      },
      where: and(
        eq(orders.id, orderId),
        eq(orders.restaurantId, restaurantId)
      )
    });

    if (!order) {
      throw new NotFoundError("Order not found");
    }

    // Validar transição: só pode entregar pedidos prontos
    if (order.status !== "ready") {
      set.status = 400;
      return { message: "You can only deliver ready orders" };
    }

    if (order.restaurantId !== restaurantId) {
      throw new UnauthorizedError();
    }

    const updatedOrder = await db
      .update(orders)
      .set({ status: "delivered" })
      .where(eq(orders.id, orderId))
      .returning({
        id: orders.id,
        status: orders.status,
        restaurantId: orders.restaurantId
      });

    return { order: updatedOrder };
  },
  {
    params: t.Object({
      orderId: t.String()
    })
  },
);

❌ Rota: Cancelar Pedido

Cancela pedidos que ainda não foram despachados:

// src/http/routes/cancel-order.ts
import { and, eq } from "drizzle-orm";
import { orders } from "@/application/infra/db/schema";
import { auth } from "../auth";
import { UnauthorizedError } from "../errors/unauthorized-error";
import { NotFoundError, t } from "elysia";
import { db } from "@/application/infra/db/connection";

export const cancelOrder = auth.patch(
  "/orders/:orderId/cancel",
  async ({ params, getCurrentUser, set }) => {
    const { orderId } = params;
    const { restaurantId } = await getCurrentUser();
    
    if (!restaurantId) {
      throw new UnauthorizedError();
    }

    const order = await db.query.orders.findFirst({
      columns: {
        id: true,
        status: true,
        restaurantId: true
      },
      where: and(
        eq(orders.id, orderId),
        eq(orders.restaurantId, restaurantId)
      )
    });

    if (!order) {
      throw new NotFoundError("Order not found");
    }

    // Validar transição: só pode cancelar pedidos pending ou preparing
    if (!["preparing", "pending"].includes(order.status)) {
      set.status = 400;
      return {
        message: "You cannot cancel orders after dispatching"
      };
    }

    if (order.restaurantId !== restaurantId) {
      throw new UnauthorizedError();
    }

    const updatedOrder = await db
      .update(orders)
      .set({ status: "cancelled" })
      .where(eq(orders.id, orderId))
      .returning({
        id: orders.id,
        status: orders.status,
        restaurantId: orders.restaurantId
      });

    return { order: updatedOrder };
  },
  {
    params: t.Object({
      orderId: t.String()
    })
  },
);

📝 Registrar Rotas no Servidor

Vamos adicionar todas as rotas de gerenciamento de pedidos:

// src/http/server.ts (atualização)
import { getOrderDetails } from "./routes/get-order-details";
import { approveOrder } from "./routes/approve-order";
import { dispatchOrder } from "./routes/dispatch-order";
import { deliverOrder } from "./routes/deliver-order";
import { cancelOrder } from "./routes/cancel-order";

import { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Aula 9 | CrazyStack',
  description: 'Curso completo sobre aula 9. Aprenda técnicas práticas e melhores práticas para desenvolvimento moderno. Guia atualizado 2025.',
  keywords: ['aula', '9', 'programação', 'desenvolvimento', 'tutorial', 'crazystack'],
  openGraph: {
    title: 'Aula 9',
    description: 'Curso completo sobre aula 9. Aprenda técnicas práticas e melhores práticas para desenvolvimento moderno. Guia atualizado 2025.',
    type: 'article'
  } // ... outras importações

const app = new Elysia()
  .use(cors())
  .use(csrf)
  .use(auth)
  .use(getCsrfToken)
  .use(addRestaurant)
  .use(getManagedRestaurant)
  .use(sendAuthLink)
  .use(authenticateFromLink)
  .use(signOut)
  .use(getProfile)
  .use(getOrderDetails)      // ← Nova rota
  .use(approveOrder)         // ← Nova rota
  .use(dispatchOrder)        // ← Nova rota
  .use(deliverOrder)         // ← Nova rota
  .use(cancelOrder)          // ← Nova rota
  .use(getOrders)            // Já existia
  // ... outras rotas

🧪 Testando o Fluxo Completo

1. Obter Detalhes do Pedido

# Obter detalhes de um pedido específico
GET http://localhost:3000/orders/cm5abc123
Cookie: auth=[jwt_token]

2. Fluxo de Aprovação e Entrega

# 1. Aprovar pedido (pending → preparing)
PATCH http://localhost:3000/orders/cm5abc123/approve
Cookie: auth=[jwt_token]

# 2. Despachar pedido (preparing → ready)
PATCH http://localhost:3000/orders/cm5abc123/dispatch
Cookie: auth=[jwt_token]

# 3. Entregar pedido (ready → delivered)
PATCH http://localhost:3000/orders/cm5abc123/deliver
Cookie: auth=[jwt_token]

3. Cancelamento

# Cancelar pedido (pending/preparing → cancelled)
PATCH http://localhost:3000/orders/cm5abc123/cancel
Cookie: auth=[jwt_token]

🔍 Padrões Implementados

🏗️ PADRÕES DE ARQUITETURA

✅ Autorização Consistente
   - Todas as rotas verificam restaurantId
   - Manager só acessa pedidos do seu restaurante

✅ Validação de Estado
   - Transições de status validadas
   - Mensagens de erro claras

✅ Tratamento de Erros
   - NotFoundError para pedidos inexistentes
   - UnauthorizedError para acesso negado
   - Status codes HTTP apropriados

✅ Queries Otimizadas
   - Drizzle Query API para joins
   - Seleção específica de colunas
   - Returning clause para updates

✅ Segurança
   - Autenticação obrigatória
   - Verificação de ownership
   - Validação de parâmetros

🎯 Próximos Passos

Na próxima aula vamos implementar as métricas restantes e o script de seed completo para popular o banco com dados realistas.

  • Métricas de pedidos por dia e mês
  • Receitas diárias em período customizado
  • Script de seed com 200 pedidos
  • Dados faker realistas