Files
SeConectai_Vendas/test/fluxo-completo.e2e-spec.ts
T
tiagoandClaude Sonnet 5 f9738e54da Etapa 8 — Personalização whitelabel e ajustes finais
Adiciona upload/remoção de logo e cor primária por tenant (módulo
tenant-profile, armazenamento local atrás de uma interface StorageService
trocável por object storage depois) e um endpoint público de tema para a
tela de login carregar antes da autenticação.

Revisão de segurança: Helmet e CORS explícito (extraídos para
security.util.ts, reutilizado pelos testes e2e que verificam os
cabeçalhos), Swagger em /docs cobrindo todos os módulos via o plugin
oficial, paginação em alert_templates. Corrige um achado real: o reset de
senha logava o token em texto puro desde a Etapa 2 — agora envia por
e-mail de verdade.

Build de produção do Docker validado (imagem sem devDependencies, app e
worker rodando contra Postgres/Redis reais) e README consolidado com o
processo de deploy. Fecha o backlog com um teste e2e do fluxo completo:
master cria tenant → login → customer/produto → venda com parcelas →
parcela paga → dashboard financeiro reflete o pagamento.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 23:38:37 -03:00

209 lines
8.0 KiB
TypeScript

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<typeof request>[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<NestExpressApplication>();
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');
});
});