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.