Finalize seu SaaS com Bun.js! Implemente métricas avançadas (pedidos, receitas), crie script de seed completo e valide seu Restaurantix. Guia final para deploy.
get-day-orders-amount
- Pedidos por dia vs ontemget-month-orders-amount
- Pedidos por mês com filtroget-daily-receipt-in-period
- Receitas em períodoseed.ts
- Script completo com 200 pedidosCompara a quantidade de pedidos de hoje com ontem:
// src/http/routes/get-day-orders-amount.ts import { and, eq, gte, sql, count } 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 dayjs from "dayjs"; export const getDayOrdersAmount = auth.get( "/metrics/day-orders-amount", async ({ getCurrentUser }) => { const { restaurantId } = await getCurrentUser(); if (!restaurantId) { throw new UnauthorizedError(); } // Configurar datas const today = dayjs(); const yesterday = today.subtract(1, "day"); const startOfYesterday = yesterday.startOf("day"); // Query agrupada por dia const ordersAmount = await db .select({ dayWithMonthAndYear: sql<string>`TO_CHAR(${orders.createdAt}, 'YYYY-MM-DD')`, amount: count() }) .from(orders) .where( and( eq(orders.restaurantId, restaurantId), gte(orders.createdAt, startOfYesterday.toDate()), ), ) .groupBy(sql`TO_CHAR(${orders.createdAt}, 'YYYY-MM-DD')`); // Extrair dados de hoje e ontem const todayWithMonthAndYear = today.format("YYYY-MM-DD"); const yesterdayWithMonthAndYear = yesterday.format("YYYY-MM-DD"); const todayOrdersAmount = ordersAmount.find( (order) => order.dayWithMonthAndYear === todayWithMonthAndYear, ); const yesterdayOrdersAmount = ordersAmount.find( (order) => order.dayWithMonthAndYear === yesterdayWithMonthAndYear, ); // Calcular diferença percentual const diffFromYesterday = todayOrdersAmount && yesterdayOrdersAmount ? (todayOrdersAmount.amount * 100) / yesterdayOrdersAmount.amount : null; return { todayOrdersAmount, yesterdayOrdersAmount, diffFromYesterday: diffFromYesterday ? Number((diffFromYesterday - 100).toFixed(2)) : 0 }; export default }, );
Quantidade de pedidos por mês com filtro opcional de status:
// src/http/routes/get-month-orders-amount.ts import { and, eq, gte, sql, count } 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 dayjs from "dayjs"; import { createSelectSchema } from "drizzle-typebox"; import { t } from "elysia"; export const getMonthOrdersAmount = auth.get( "/metrics/month-orders-amount", async ({ getCurrentUser, query }) => { const { restaurantId } = await getCurrentUser(); const { status } = query; // Filtro opcional de status if (!restaurantId) { throw new UnauthorizedError(); } // Configurar período const today = dayjs(); const lastMonth = today.subtract(1, "month"); const startOfLastMonth = lastMonth.startOf("month"); // Construir condições WHERE const whereConditions = [ eq(orders.restaurantId, restaurantId), gte(orders.createdAt, startOfLastMonth.toDate()), ]; // Adicionar filtro de status se fornecido if (status) { whereConditions.push(eq(orders.status, status)); } // Query com agrupamento por mês const ordersAmount = await db .select({ monthWithYear: sql<string>`TO_CHAR(${orders.createdAt}, 'YYYY-MM')`, amount: count() }) .from(orders) .where(and(...whereConditions)) .groupBy(sql`TO_CHAR(${orders.createdAt}, 'YYYY-MM')`); // Extrair dados do mês atual e anterior const currentMonthWithYear = today.format("YYYY-MM"); const lastMonthWithYear = lastMonth.format("YYYY-MM"); const currentMonthOrdersAmount = ordersAmount.find( (order) => order.monthWithYear === currentMonthWithYear, ); const lastMonthOrdersAmount = ordersAmount.find( (order) => order.monthWithYear === lastMonthWithYear, ); // Calcular diferença percentual const diffFromLastMonth = currentMonthOrdersAmount && lastMonthOrdersAmount ? (currentMonthOrdersAmount.amount * 100) / lastMonthOrdersAmount.amount : null; return { currentMonthOrdersAmount, lastMonthOrdersAmount, diffFromLastMonth: diffFromLastMonth ? Number((diffFromLastMonth - 100).toFixed(2)) : 0 }; }, { query: t.Object({ status: t.Optional(createSelectSchema(orders).properties.status) }) }, );
Receitas diárias em um período customizado (máximo 30 dias):
// src/http/routes/get-daily-receipt-in-period.ts import { sum, eq, and, gte, lte, sql } 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 { t } from "elysia"; import dayjs from "dayjs"; export const getDailyReceiptInPeriod = auth.get( "/metrics/daily-receipt-in-period", async ({ getCurrentUser, set, query }) => { const { restaurantId } = await getCurrentUser(); if (!restaurantId) { throw new UnauthorizedError(); } const { from, to } = query; // Configurar período padrão (últimos 7 dias se não especificado) const startDate = from ? dayjs(from) : dayjs().subtract(7, "days"); const endDate = to ? dayjs(to) : from ? dayjs(from).add(7, "days") : dayjs(); // Validar período máximo de 30 dias if (endDate.diff(startDate, "days") > 30) { set.status = 400; return { message: "Period cannot be more than 30 days" }; } // Query de receitas diárias const dailyReceipt = await db .select({ date: sql<string>`TO_CHAR(${orders.createdAt}, 'DD/MM')`, receipt: sum(orders.priceInCents).mapWith(Number) }) .from(orders) .where( and( eq(orders.restaurantId, restaurantId), gte( orders.createdAt, startDate .startOf("day") .add(startDate.utcOffset(), "minutes") .toDate(), ), lte( orders.createdAt, endDate.endOf("day").add(endDate.utcOffset(), "minutes").toDate(), ), ), ) .groupBy(sql`TO_CHAR(${orders.createdAt}, 'DD/MM')`); // Ordenar por data const orderedReceiptPerDay = dailyReceipt.sort((a, b) => { const [dayA = 1, monthA = 1] = a.date.split("/").map(Number); const [dayB = 1, monthB = 1] = b.date.split("/").map(Number); if (monthA === monthB) { return (dayA ?? 0) - (dayB ?? 0); } const dateA = new Date(2025, monthA - 1); const dateB = new Date(2025, monthB - 1); return dateA.getTime() - dateB.getTime(); }); return orderedReceiptPerDay; }, { query: t.Object({ from: t.Optional(t.String()), to: t.Optional(t.String()) }) }, );
Script completo para popular o banco com dados realistas:
// src/application/infra/db/seed.ts /* eslint-disable drizzle/enforce-delete-with-where */ import { faker } from "@faker-js/faker"; import { users, restaurants, authLinks, orderItems, orders, products } from "./schema"; import { db } from "./connection"; import chalk from "chalk"; import { createId } from "@paralleldrive/cuid2"; // 1. LIMPAR BANCO DE DADOS await db.delete(users); await db.delete(restaurants); await db.delete(orderItems); await db.delete(orders); await db.delete(products); await db.delete(authLinks); console.log(chalk.yellow("Database cleaned")); // 2. CRIAR CUSTOMERS const [customer1, customer2] = await db .insert(users) .values([ { name: faker.person.fullName(), email: faker.internet.email(), role: "customer" }, { name: faker.person.fullName(), email: faker.internet.email(), role: "customer" }, ]) .returning(); console.log(chalk.green("Customers inserted")); // 3. CRIAR MANAGER const [manager] = await db .insert(users) .values([ { name: faker.person.fullName(), email: "appbelezix1@gmail.com", // Email fixo para testes role: "manager" }, ]) .returning({ id: users.id }); console.log(chalk.green("Managers inserted")); // 4. CRIAR RESTAURANTE const [restaurant] = await db .insert(restaurants) .values([ { name: faker.company.name(), description: faker.lorem.paragraph(), managerId: manager!.id }, ]) .returning(); console.log(chalk.green("Restaurants inserted")); // 5. FUNÇÃO PARA GERAR PRODUTOS function generateProduct() { return { name: faker.commerce.productName(), description: faker.commerce.productDescription(), priceInCents: Number(faker.commerce.price({ min: 100, max: 1000, dec: 0 })), restaurantId: restaurant!.id }; } // 6. CRIAR PRODUTOS const availableProducts = await db .insert(products) .values([ generateProduct(), generateProduct(), generateProduct(), generateProduct(), generateProduct(), ]) .returning(); console.log(chalk.green("Products inserted")); // 7. GERAR 200 PEDIDOS COM ITENS type OrderItemsInsert = typeof orderItems.$inferInsert; type OrderInsert = typeof orders.$inferInsert; const orderItemsToInsert: OrderItemsInsert[] = []; const ordersToInsert: OrderInsert[] = []; for (let i = 0; i < 200; i++) { const orderId = createId(); // Selecionar produtos aleatórios para o pedido const orderProducts = faker.helpers.arrayElements(availableProducts, { min: 1, max: 3 }); let totalPriceInCents = 0; // Criar itens do pedido orderProducts.forEach((product) => { const quantity = faker.number.int({ min: 1, max: 3 }); totalPriceInCents += product.priceInCents * quantity; orderItemsToInsert.push({ orderId, productId: product.id, quantity, priceInCents: product.priceInCents }); }); // Criar pedido ordersToInsert.push({ id: orderId, customerId: faker.helpers.arrayElement([customer1!.id, customer2!.id]), restaurantId: restaurant!.id, priceInCents: totalPriceInCents, status: faker.helpers.arrayElement([ "pending", "preparing", "ready", "delivered", "cancelled", ]), createdAt: faker.date.recent({ days: 40 }), // Últimos 40 dias }); } // 8. INSERIR PEDIDOS E ITENS await db.insert(orders).values(ordersToInsert); await db.insert(orderItems).values(orderItemsToInsert); console.log(chalk.green("Orders and order items inserted")); console.log(chalk.blue(`Generated ${ordersToInsert.length} orders with ${orderItemsToInsert.length} items`)); process.exit();
Vamos adicionar as últimas métricas no servidor:
// src/http/server.ts (versão final completa) import { addRestaurant } from "./routes/add-restaurant"; import { sendAuthLink } from "./routes/send-auth-link"; import { authenticateFromLink } from "./routes/authenticate-from-link"; import { auth } from "./auth"; import { Elysia } from "elysia"; import { cors } from "@elysiajs/cors"; import { signOut } from "./routes/sign-out"; import { getProfile } from "./routes/get-profile"; import { getManagedRestaurant } from "./routes/get-managed-restaurant"; import { csrf } from "./csrf"; import { getCsrfToken } from "./routes/get-csrf-token"; 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 { getOrders } from "./routes/get-orders"; import { getMonthRevenue } from "./routes/get-month-revenue"; import { getDayOrdersAmount } from "./routes/get-day-orders-amount"; import { getMonthOrdersAmount } from "./routes/get-month-orders-amount"; import { getPopularProducts } from "./routes/get-popular-products"; import { getDailyReceiptInPeriod } from "./routes/get-daily-receipt-in-period"; import { Metadata } from 'next'; export const metadata: Metadata = { title: 'Aula 10 | CrazyStack', description: 'Curso completo sobre aula 10. Aprenda técnicas práticas e melhores práticas para desenvolvimento moderno. Guia atualizado 2025.', keywords: ['aula', '10', 'programação', 'desenvolvimento', 'tutorial', 'crazystack'], openGraph: { title: 'Aula 10', description: 'Curso completo sobre aula 10. Aprenda técnicas práticas e melhores práticas para desenvolvimento moderno. Guia atualizado 2025.', type: 'article' }, }; const app = new Elysia() .use(cors()) .use(csrf) .use(auth) .use(getCsrfToken) .use(addRestaurant) .use(sendAuthLink) .use(authenticateFromLink) .use(signOut) .use(getProfile) .use(getManagedRestaurant) .use(getOrderDetails) .use(approveOrder) .use(dispatchOrder) .use(deliverOrder) .use(cancelOrder) .use(getOrders) .use(getMonthRevenue) .use(getDayOrdersAmount) // ← Nova métrica .use(getMonthOrdersAmount) // ← Nova métrica .use(getPopularProducts) .use(getDailyReceiptInPeriod) // ← Nova métrica .onError(({ error, code, set }) => { console.error(error); switch (code) { case "VALIDATION": set.status = error.status; return error.toResponse(); default: set.status = 500; console.error(error); return new Response(null, { status: 500 }); } }); app.listen(3000, () => { console.log("Server is running on port 3000"); });
# Executar script de seed bun run src/application/infra/db/seed.ts # Verificar dados no Drizzle Studio bun run drizzle-kit studio
# Pedidos por dia GET http://localhost:3000/metrics/day-orders-amount Cookie: auth=[jwt_token] # Pedidos por mês (todos os status) GET http://localhost:3000/metrics/month-orders-amount Cookie: auth=[jwt_token] # Pedidos por mês (apenas entregues) GET http://localhost:3000/metrics/month-orders-amount?status=delivered Cookie: auth=[jwt_token] # Receitas diárias (últimos 7 dias) GET http://localhost:3000/metrics/daily-receipt-in-period Cookie: auth=[jwt_token] # Receitas diárias (período customizado) GET http://localhost:3000/metrics/daily-receipt-in-period?from=2025-01-01&to=2025-01-15 Cookie: auth=[jwt_token]
# 1. Criar restaurante POST /restaurants # 2. Fazer login do manager POST /auth-links/authenticate GET /auth-links/authenticate?code=... # 3. Obter restaurante gerenciado GET /managed-restaurant # 4. Listar pedidos GET /orders # 5. Ver detalhes de um pedido GET /orders/:orderId # 6. Gerenciar pedido PATCH /orders/:orderId/approve PATCH /orders/:orderId/dispatch PATCH /orders/:orderId/deliver # 7. Ver todas as métricas GET /metrics/month-revenue GET /metrics/day-orders-amount GET /metrics/month-orders-amount GET /metrics/popular-products GET /metrics/daily-receipt-in-period
🚀 RESTAURANTIX - SISTEMA COMPLETO ✅ AUTENTICAÇÃO ├─ Magic Links com JWT ├─ Cookies seguros ├─ Middleware de proteção └─ Logout completo ✅ BANCO DE DADOS ├─ 6 tabelas com relações ├─ Enums e validações ├─ Migrações automáticas └─ Seed com 200 pedidos ✅ ROTAS IMPLEMENTADAS (18 total) ├─ Autenticação (4 rotas) ├─ Restaurantes (2 rotas) ├─ Pedidos (6 rotas) └─ Métricas (6 rotas) ✅ FUNCIONALIDADES ├─ CRUD completo de pedidos ├─ Fluxo de status validado ├─ Métricas em tempo real ├─ Sistema de email ├─ Deploy em produção └─ Testes automatizados ✅ TECNOLOGIAS ├─ Bun.js (runtime) ├─ ElysiaJS (framework) ├─ Drizzle ORM (banco) ├─ PostgreSQL (dados) ├─ TypeScript (tipagem) ├─ Zod (validação) ├─ Resend (email) └─ Fly.io (deploy) 🎯 RESULTADO: SaaS completo e funcional!
Você implementou um SaaS completo usando as tecnologias mais modernas do ecossistema JavaScript. O Restaurantix está pronto para produção com todas as funcionalidades de um sistema real.