import { randomUUID } from 'node:crypto'; import { ValidationPipe } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { NestExpressApplication } from '@nestjs/platform-express'; import { Test, TestingModule } from '@nestjs/testing'; import request from 'supertest'; import { AppModule } from '../src/app.module'; import { PrismaService } from '../src/config/prisma.service'; import { applySecurityConfig } from '../src/security.util'; import { hashPassword } from '../src/modules/auth/password.util'; interface TokenPair { access_token: string; refresh_token: string; } /** * Etapa 8, critério de aceite: "escrever um teste E2E que cobre o fluxo * completo mínimo: master cria tenant → tenant faz login → cadastra * customer e produto → faz uma venda com parcelas → marca uma parcela * como paga → confere dashboard financeiro refletindo o pagamento." * * De propósito, não reexercita casos de erro/borda (já cobertos nos specs * dedicados de cada etapa) — só o caminho feliz de ponta a ponta, contra * Postgres/Redis reais, pra provar que os módulos das 8 etapas realmente * se encaixam quando usados em sequência por um único tenant. */ describe('Fluxo completo — do zero ao dashboard financeiro (e2e)', () => { let app: NestExpressApplication; let prisma: PrismaService; let httpServer: Parameters[0]; const senha = 'SenhaForteDemo#26'; const suffix = randomUUID().slice(0, 8); const masterEmail = `master-fluxo-${suffix}@teste.com`; let masterId: string; let masterToken: string; const tenantIds: string[] = []; const planoIds: string[] = []; const tenantUserIds: string[] = []; function asMaster() { return { post: (url: string) => request(httpServer).post(url).set('Authorization', `Bearer ${masterToken}`), }; } function asTenant(token: string) { return { get: (url: string) => request(httpServer).get(url).set('Authorization', `Bearer ${token}`), post: (url: string) => request(httpServer).post(url).set('Authorization', `Bearer ${token}`), patch: (url: string) => request(httpServer).patch(url).set('Authorization', `Bearer ${token}`), }; } beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); applySecurityConfig(app, app.get(ConfigService)); app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }), ); await app.init(); httpServer = app.getHttpServer(); prisma = app.get(PrismaService); const senha_hash = await hashPassword(senha); const master = await prisma.master_admins.create({ data: { nome: 'Master Fluxo Completo', email: masterEmail, senha_hash }, }); masterId = master.id; }); afterAll(async () => { await prisma.sales.deleteMany({ where: { tenant_id: { in: tenantIds } } }); await prisma.tenants.deleteMany({ where: { id: { in: tenantIds } } }); await prisma.planos.deleteMany({ where: { id: { in: planoIds } } }); await prisma.auth_refresh_tokens.deleteMany({ where: { usuario_id: { in: [masterId, ...tenantUserIds] } }, }); await prisma.login_attempts.deleteMany({ where: { usuario_tipo: 'master', email: masterEmail }, }); await prisma.audit_logs.deleteMany({ where: { usuario_id: masterId } }); await prisma.master_admins.deleteMany({ where: { id: masterId } }); await app.close(); }); it('do master criando o tenant até o dashboard financeiro refletir o pagamento', async () => { // 1) master autentica const masterLogin = await request(httpServer) .post('/auth/master/login') .send({ email: masterEmail, senha }) .expect(200); masterToken = (masterLogin.body as TokenPair).access_token; // 2) master cria um plano e, com ele, um tenant novo (já com o primeiro tenant_admin) const plano = await asMaster() .post('/master/planos') .send({ nome: `Plano Fluxo ${suffix}`, valor_mensal: 149.9 }) .expect(201); planoIds.push((plano.body as { id: string }).id); const emailAdmin = `admin-fluxo-${suffix}@teste.com`; const criarTenant = await asMaster() .post('/master/tenants') .send({ razao_social: `Tenant Fluxo Completo ${suffix}`, email_contato: `contato-fluxo-${suffix}@teste.com`, plano_id: (plano.body as { id: string }).id, admin: { nome: 'Admin Fluxo Completo', email: emailAdmin }, }) .expect(201); const { tenant, admin } = criarTenant.body as { tenant: { id: string }; admin: { id: string; senha_provisoria: string }; }; tenantIds.push(tenant.id); tenantUserIds.push(admin.id); // tenant criado nasce em `trial`; ativa pra poder logar sem bloqueio await prisma.tenants.update({ where: { id: tenant.id }, data: { status: 'ativo' } }); // 3) tenant_admin faz login com a senha provisória const tenantLogin = await request(httpServer) .post('/auth/tenant/login') .send({ email: emailAdmin, senha: admin.senha_provisoria }) .expect(200); const tenantToken = (tenantLogin.body as TokenPair).access_token; // 4) cadastra um customer const customer = await asTenant(tenantToken) .post('/tenant/customers') .send({ razao_social: 'Cliente Fluxo Completo Ltda', email: 'cliente@fluxo-completo.teste' }) .expect(201); const customerId = (customer.body as { id: string }).id; // 5) cadastra um produto const produto = await asTenant(tenantToken) .post('/tenant/produtos') .send({ tipo: 'produto', nome: 'Produto Fluxo Completo', valor_venda: 300 }) .expect(201); const produtoId = (produto.body as { id: string }).id; // 6) faz uma venda com 2 parcelas const venda = await asTenant(tenantToken) .post('/tenant/vendas') .send({ customer_id: customerId, itens: [{ product_service_id: produtoId, quantidade: 1, valor_unitario: 300 }], numero_parcelas: 2, }) .expect(201); const vendaBody = venda.body as { id: string; valor_total: string; parcelas: Array<{ id: string; valor: string }>; }; expect(Number(vendaBody.valor_total)).toBe(300); expect(vendaBody.parcelas).toHaveLength(2); // 7) confere o financeiro ANTES de pagar (delta-based: tenant novo, começa em zero) const financeiroAntes = await asTenant(tenantToken) .get('/tenant/dashboard/financeiro') .expect(200); expect((financeiroAntes.body as { total_recebido_mes: string }).total_recebido_mes).toBe( '0.00', ); expect(Number((financeiroAntes.body as { total_a_receber: string }).total_a_receber)).toBe(300); // 8) marca a primeira parcela como paga const primeiraParcela = vendaBody.parcelas[0]; await asTenant(tenantToken) .patch(`/tenant/recebimentos/${primeiraParcela.id}/marcar-pago`) .send({ forma_pagamento: 'pix' }) .expect(200); // 9) dashboard financeiro reflete o pagamento const financeiroDepois = await asTenant(tenantToken) .get('/tenant/dashboard/financeiro') .expect(200); const depois = financeiroDepois.body as { total_recebido_mes: string; total_a_receber: string; proximas_parcelas: Array<{ id: string }>; }; expect(Number(depois.total_recebido_mes)).toBeCloseTo(Number(primeiraParcela.valor), 2); expect(Number(depois.total_a_receber)).toBeCloseTo(300 - Number(primeiraParcela.valor), 2); expect(depois.proximas_parcelas.some((p) => p.id === primeiraParcela.id)).toBe(false); // 10) a venda em si também reflete a parcela paga const vendaDetalhe = await asTenant(tenantToken) .get(`/tenant/vendas/${vendaBody.id}`) .expect(200); const parcelasDetalhe = ( vendaDetalhe.body as { parcelas: Array<{ id: string; status: string }> } ).parcelas; expect(parcelasDetalhe.find((p) => p.id === primeiraParcela.id)?.status).toBe('pago'); }); });