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.