Vamos implementar o sistema completo de gerenciamento de pedidos: obter detalhes, aprovar, despachar, entregar e cancelar pedidos.
GET /orders/:orderId
- Detalhes do pedidoPATCH /orders/:orderId/approve
- Aprovar pedidoPATCH /orders/:orderId/dispatch
- Despachar pedidoPATCH /orders/:orderId/deliver
- Entregar pedidoPATCH /orders/:orderId/cancel
- Cancelar pedido🔄 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 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 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" } } ] } }
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() }) }, );
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() }) }, );
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() }) }, );
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() }) }, );
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
# Obter detalhes de um pedido específico GET http://localhost:3000/orders/cm5abc123 Cookie: auth=[jwt_token]
# 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]
# Cancelar pedido (pending/preparing → cancelled) PATCH http://localhost:3000/orders/cm5abc123/cancel Cookie: auth=[jwt_token]
🏗️ 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
Na próxima aula vamos implementar as métricas restantes e o script de seed completo para popular o banco com dados realistas.