feat: estoque negativo, UTC-3, dd/mm/yyyy, estorno de pagamento
This commit is contained in:
@@ -133,3 +133,13 @@ export async function deleteItemFromOrderController(request: FastifyRequest, rep
|
||||
return reply.status(400).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function reversePaymentController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const { id } = z.object({ id: z.string().uuid() }).parse(request.params);
|
||||
try {
|
||||
const pagamento = await orderService.reversePayment(id);
|
||||
return reply.send(pagamento);
|
||||
} catch (error: any) {
|
||||
return reply.status(400).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import * as reportService from '../services/report.service';
|
||||
import { z } from 'zod';
|
||||
import { dateTime, nowUTC3, todayStrUTC3 } from '../lib/timezone';
|
||||
|
||||
export async function salesReportController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const querySchema = z.object({
|
||||
inicio: z.string().optional(),
|
||||
fim: z.string().optional(),
|
||||
horaInicio: z.string().optional(),
|
||||
horaFim: z.string().optional(),
|
||||
});
|
||||
const query = querySchema.parse(request.query);
|
||||
|
||||
const inicio = query.inicio ? new Date(query.inicio) : new Date(new Date().setDate(new Date().getDate() - 30));
|
||||
const fim = query.fim ? new Date(query.fim + 'T23:59:59') : new Date();
|
||||
const today = todayStrUTC3();
|
||||
const inicioStr = query.inicio || today;
|
||||
const horaInicio = query.horaInicio || '00:00';
|
||||
const inicio = dateTime(inicioStr, horaInicio);
|
||||
|
||||
const fimStr = query.fim || today;
|
||||
const horaFim = query.horaFim || '23:59';
|
||||
const fim = dateTime(fimStr, horaFim, 59);
|
||||
|
||||
const report = await reportService.getSalesReport(inicio, fim);
|
||||
return reply.send(report);
|
||||
|
||||
62
src/lib/timezone.ts
Normal file
62
src/lib/timezone.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
const OFFSET_HOURS = -3;
|
||||
|
||||
/** Componentes de data/hora em UTC-3 */
|
||||
export interface UTC3 {
|
||||
year: number;
|
||||
month: number; // 0-indexed
|
||||
date: number;
|
||||
hours: number;
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
}
|
||||
|
||||
/** Retorna componentes de agora em UTC-3 */
|
||||
export function nowUTC3(): UTC3 {
|
||||
const d = new Date(Date.now() + OFFSET_HOURS * 60 * 60 * 1000);
|
||||
return {
|
||||
year: d.getUTCFullYear(),
|
||||
month: d.getUTCMonth(),
|
||||
date: d.getUTCDate(),
|
||||
hours: d.getUTCHours(),
|
||||
minutes: d.getUTCMinutes(),
|
||||
seconds: d.getUTCSeconds(),
|
||||
};
|
||||
}
|
||||
|
||||
/** Cria Date cuja representação UTC = meia-noite UTC-3 do dateStr (YYYY-MM-DD) */
|
||||
export function dateStart(dateStr: string): Date {
|
||||
const [y, m, d] = dateStr.split('-').map(Number);
|
||||
return new Date(Date.UTC(y, m - 1, d, 0 - OFFSET_HOURS, 0, 0, 0));
|
||||
}
|
||||
|
||||
/** Cria Date cuja representação UTC = 23:59:59.999 UTC-3 do dateStr */
|
||||
export function dateEnd(dateStr: string): Date {
|
||||
const [y, m, d] = dateStr.split('-').map(Number);
|
||||
return new Date(Date.UTC(y, m - 1, d, 23 - OFFSET_HOURS, 59, 59, 999));
|
||||
}
|
||||
|
||||
/** Date + hora em UTC-3. dateStr = YYYY-MM-DD, timeStr = HH:mm */
|
||||
export function dateTime(dateStr: string, timeStr: string, seconds = 0): Date {
|
||||
const [y, m, d] = dateStr.split('-').map(Number);
|
||||
const [hh, mm] = timeStr.split(':').map(Number);
|
||||
return new Date(Date.UTC(y, m - 1, d, hh - OFFSET_HOURS, mm, seconds, 0));
|
||||
}
|
||||
|
||||
/** Converte Date (UTC no banco) para UTC-3 para exibição */
|
||||
export function toUTC3(d: Date): Date {
|
||||
return new Date(d.getTime() + OFFSET_HOURS * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
/** YYYY-MM-DD em UTC-3 */
|
||||
export function todayStrUTC3(): string {
|
||||
const c = nowUTC3();
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${c.year}-${pad(c.month + 1)}-${pad(c.date)}`;
|
||||
}
|
||||
|
||||
/** YYYY-MM-DD do primeiro dia do mês atual em UTC-3 */
|
||||
export function firstDayOfMonthUTC3(): string {
|
||||
const c = nowUTC3();
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${c.year}-${pad(c.month + 1)}-01`;
|
||||
}
|
||||
@@ -27,4 +27,7 @@ export async function orderRoutes(app: FastifyInstance) {
|
||||
|
||||
// Excluir item de comanda (apenas CAIXA e ADMIN)
|
||||
app.delete('/comandas/itens/:id', { preHandler: [requireRoles(['CAIXA', 'ADMIN'])] }, orderController.deleteItemFromOrderController);
|
||||
|
||||
// Estornar pagamento (Caixa, Admin)
|
||||
app.patch('/pagamentos/:id/estornar', { preHandler: [requireRoles(['CAIXA', 'ADMIN'])] }, orderController.reversePaymentController);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ export const productSchema = z.object({
|
||||
fotoUrl: z.string().url().optional(),
|
||||
preco: z.number().positive(),
|
||||
custo: z.number().positive().optional(),
|
||||
estoqueAtual: z.number().int().nonnegative().default(0),
|
||||
estoqueMinimo: z.number().int().nonnegative().default(0),
|
||||
estoqueAtual: z.number().int().default(0),
|
||||
estoqueMinimo: z.number().int().default(0),
|
||||
itemCozinha: z.boolean(),
|
||||
ativo: z.boolean().default(true),
|
||||
categoriaId: z.string().uuid(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { prisma } from '../lib/prisma';
|
||||
import { CreateOrderInput, AddItemOrderInput, UpdateItemStatusInput, UpdateItemObservacaoInput, UpdateOrderInput, PayOrderInput, PartialPaymentInput } from '../schemas/order.schema';
|
||||
import { dateStart, dateEnd, nowUTC3 } from '../lib/timezone';
|
||||
|
||||
export interface ListOrdersFilter {
|
||||
statuses?: string[];
|
||||
@@ -17,14 +18,8 @@ export async function listPayments(filter?: ListPaymentsFilter) {
|
||||
|
||||
if (filter?.dataInicio || filter?.dataFim) {
|
||||
where.criadoEm = {};
|
||||
if (filter.dataInicio) {
|
||||
const [y, m, d] = filter.dataInicio.split('-').map(Number);
|
||||
where.criadoEm.gte = new Date(y, m - 1, d, 0, 0, 0, 0);
|
||||
}
|
||||
if (filter.dataFim) {
|
||||
const [y, m, d] = filter.dataFim.split('-').map(Number);
|
||||
where.criadoEm.lte = new Date(y, m - 1, d, 23, 59, 59, 999);
|
||||
}
|
||||
if (filter.dataInicio) where.criadoEm.gte = dateStart(filter.dataInicio);
|
||||
if (filter.dataFim) where.criadoEm.lte = dateEnd(filter.dataFim);
|
||||
}
|
||||
|
||||
return prisma.pagamento.findMany({
|
||||
@@ -52,12 +47,8 @@ export async function listOrders(filter?: ListOrdersFilter) {
|
||||
|
||||
if (filter?.dataInicio || filter?.dataFim) {
|
||||
where.criadoEm = {};
|
||||
if (filter.dataInicio) where.criadoEm.gte = new Date(filter.dataInicio);
|
||||
if (filter.dataFim) {
|
||||
const end = new Date(filter.dataFim);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
where.criadoEm.lte = end;
|
||||
}
|
||||
if (filter.dataInicio) where.criadoEm.gte = dateStart(filter.dataInicio);
|
||||
if (filter.dataFim) where.criadoEm.lte = dateEnd(filter.dataFim);
|
||||
}
|
||||
|
||||
return prisma.comanda.findMany({
|
||||
@@ -101,10 +92,6 @@ export async function addItemToOrder(comandaId: string, data: AddItemOrderInput,
|
||||
throw new Error('Produto não encontrado');
|
||||
}
|
||||
|
||||
if (produto.estoqueAtual < data.quantidade) {
|
||||
throw new Error(`Estoque insuficiente. Disponível: ${produto.estoqueAtual}`);
|
||||
}
|
||||
|
||||
const precoTotalItem = produto.preco * data.quantidade;
|
||||
|
||||
const [, itemPedido] = await prisma.$transaction([
|
||||
@@ -134,11 +121,15 @@ export async function addItemToOrder(comandaId: string, data: AddItemOrderInput,
|
||||
|
||||
export async function listKitchenBarItems(itemCozinha?: boolean, status?: string) {
|
||||
const filter: any = {};
|
||||
|
||||
|
||||
const now = nowUTC3();
|
||||
const dozeHorasAtrasMs = Date.UTC(now.year, now.month, now.date, now.hours - 12, now.minutes, now.seconds, 0);
|
||||
filter.criadoEm = { gte: new Date(dozeHorasAtrasMs) };
|
||||
|
||||
if (status) {
|
||||
filter.status = status;
|
||||
}
|
||||
|
||||
|
||||
if (typeof itemCozinha === 'boolean') {
|
||||
filter.produto = {
|
||||
itemCozinha,
|
||||
@@ -396,3 +387,34 @@ export async function deleteItemFromOrder(itemId: string) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function reversePayment(pagamentoId: string) {
|
||||
const pagamento = await prisma.pagamento.findUnique({
|
||||
where: { id: pagamentoId },
|
||||
include: { comanda: true },
|
||||
});
|
||||
|
||||
if (!pagamento) throw new Error('Pagamento não encontrado');
|
||||
if (pagamento.estornado) throw new Error('Pagamento já foi estornado');
|
||||
|
||||
const novoValorPago = Math.max(pagamento.comanda.valorPago - pagamento.valor, 0);
|
||||
|
||||
const statusComanda = pagamento.comanda.status === 'PAGA' ? 'FECHADA' : pagamento.comanda.status;
|
||||
|
||||
const [, pagamentoEstornado] = await prisma.$transaction([
|
||||
prisma.pagamento.update({
|
||||
where: { id: pagamentoId },
|
||||
data: { estornado: true },
|
||||
}),
|
||||
prisma.comanda.update({
|
||||
where: { id: pagamento.comandaId },
|
||||
data: {
|
||||
valorPago: novoValorPago,
|
||||
status: statusComanda,
|
||||
...(pagamento.comanda.status === 'PAGA' ? { fechadoEm: null, formaPagamento: null } : {}),
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return pagamentoEstornado;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { prisma } from '../lib/prisma';
|
||||
import { nowUTC3, toUTC3 } from '../lib/timezone';
|
||||
|
||||
export async function getSalesReport(inicio: Date, fim: Date) {
|
||||
const comandas = await prisma.comanda.findMany({
|
||||
@@ -39,7 +40,7 @@ export async function getSalesReport(inicio: Date, fim: Date) {
|
||||
// Vendas por dia
|
||||
const vendasPorDia: Record<string, { vendas: number; comandas: number }> = {};
|
||||
for (const c of comandas) {
|
||||
const dia = c.fechadoEm!.toISOString().split('T')[0];
|
||||
const dia = toUTC3(c.fechadoEm!).toISOString().split('T')[0];
|
||||
if (!vendasPorDia[dia]) vendasPorDia[dia] = { vendas: 0, comandas: 0 };
|
||||
vendasPorDia[dia].vendas += c.total;
|
||||
vendasPorDia[dia].comandas += 1;
|
||||
@@ -80,12 +81,11 @@ export async function getSalesReport(inicio: Date, fim: Date) {
|
||||
}
|
||||
|
||||
export async function getDashboardData() {
|
||||
const now = new Date();
|
||||
const inicioDia = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const inicioSemana = new Date(now);
|
||||
inicioSemana.setDate(now.getDate() - now.getDay());
|
||||
inicioSemana.setHours(0, 0, 0, 0);
|
||||
const inicioMes = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const now = nowUTC3();
|
||||
const inicioDia = new Date(Date.UTC(now.year, now.month, now.date, 0, 0, 0, 0));
|
||||
const diaSemana = new Date(Date.UTC(now.year, now.month, now.date)).getUTCDay();
|
||||
const inicioSemana = new Date(Date.UTC(now.year, now.month, now.date - diaSemana, 0, 0, 0, 0));
|
||||
const inicioMes = new Date(Date.UTC(now.year, now.month, 1, 0, 0, 0, 0));
|
||||
|
||||
const [comandasHoje, comandasSemana, comandasMes, totalProdutos, produtosEstoqueBaixo, comandasAbertas] = await Promise.all([
|
||||
prisma.comanda.findMany({
|
||||
|
||||
Reference in New Issue
Block a user