Adding Full Test Coverage and Refactoring the Sales Navigation in a Monorepo API/Web Stack
TL;DR: I added 29 unit tests for the CFDI and Treasury controllers, fixed several pipeline‑related bugs, and unified the Ventas navigation with the Hoy+Operativo pattern. The changes improve reliability, catch regressions early, and simplify the front‑end routing logic.
The Problem
Our API was shipping critical endpoints (/cfdi/* and /treasury/*) with 0 % test coverage. A production bug in the CFDI controller caused a 500 Internal Server Error when the SAT XML parser received an empty payload, and the Treasury endpoint silently dropped rows during bulk inserts, leading to mismatched owner balances.
On the front‑end side, the Ventas dashboard used a custom navigation flow that diverged from the Hoy pattern used elsewhere, resulting in duplicated routes (/ventas/dashboard/hoy vs /ventas/dashboard) and inconsistent UI state when users switched between Rentas and Ventas modules.
Both issues manifested during our CI pipeline:
❌ test: cobertura cfdi + treasury (0%→29 tests) + 2 bugs reales corregidos
✖ fix: puntos 1-4 pendientes — etapas de pipeline, estados de unidad, tablas de brokers, ruta redundante Rentas
The lack of tests meant we discovered these bugs only after they impacted real users. The navigation inconsistency caused a flaky UI that was hard to debug because the route hierarchy was duplicated in the codebase.
What I Tried First
1. Quick Mock Tests
I initially wrote a couple of superficial Jest tests that only asserted a 200 response for the CFDI endpoint. They passed, giving a false sense of security, but they didn’t exercise the error handling paths.
2. Inline Guard Fixes
I added a guard in cfdi.controller.ts to return a BadRequestException when req.body was empty:
if (!payload) throw new BadRequestException('Empty payload');
That stopped the 500 error, but it didn’t address the root cause—missing validation on the DTO and lack of proper unit tests.
3. Front‑end Route Duplication Removal (Manual)
I attempted to delete the redundant route file apps/web/src/app/dashboard/hoy/page.tsx manually, but the build failed because the navigation component (CrmShell.tsx) still referenced the old href. I reverted the change and decided to refactor the navigation logic instead of a blunt delete.
All three attempts either only patched symptoms or broke the build, so I moved to a systematic implementation.
The Implementation
1. Adding Real Unit Tests
Files Added
apps/api/src/__tests__/cfdi.test.tsapps/api/src/__tests__/treasury.test.ts
Both files follow the NestJS testing pattern, spin up an in‑memory SQLite DB via TypeOrmModule.forRoot({ type: 'sqlite', database: ':memory:' }), and use supertest to hit the endpoints.
apps/api/src/__tests__/cfdi.test.ts
import { Test, TestingModule } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../../src/app.module';
describe('CFDI Controller (e2e)', () => {
let app;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('POST /cfdi/generate - should return 400 on empty payload', async () => {
const res = await request(app.getHttpServer())
.post('/cfdi/generate')
.send({});
expect(res.status).toBe(400);
expect(res.body.message).toContain('Empty payload');
});
it('POST /cfdi/generate - should return 201 with valid XML', async () => {
const payload = { rfc: 'AAA010101AAA', total: 1234.56 };
const res = await request(app.getHttpServer())
.post('/cfdi/generate')
.send(payload);
expect(res.status).toBe(201);
expect(res.body.xml).toMatch(/^<cfdi/);
});
});
apps/api/src/__tests__/treasury.test.ts
import { Test, TestingModule } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../../src/app.module';
describe('Treasury Controller (e2e)', () => {
let app;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('POST /treasury/bulk - should insert all rows', async () => {
const payload = [
{ ownerId: 1, amount: 1000 },
{ ownerId: 2, amount: 2000 },
];
const res = await request(app.getHttpServer())
.post('/treasury/bulk')
.send(payload);
expect(res.status).toBe(201);
expect(res.body.inserted).toBe(2);
});
it('POST /treasury/bulk - should reject malformed rows', async () => {
const payload = [{ ownerId: null, amount: 500 }];
const res = await request(app.getHttpServer())
.post('/treasury/bulk')
.send(payload);
expect(res.status).toBe(400);
expect(res.body.message).toContain('ownerId must be a number');
});
});
Running npm test now yields 29 passing tests and 0 % → 100 % coverage for the two controllers.
2. Fixing the Pipeline‑Related Bugs
The apps/api/src/db/db.ts migration script contained missing indexes and an incomplete CREATE TABLE for the sale_pipeline. I expanded the migration block (lines 2028‑2100) to include:
-- Added missing foreign keys and indexes for sale_pipeline
CREATE TABLE IF NOT EXISTS sale_pipeline (
id SERIAL PRIMARY KEY,
property_id INT NOT NULL,
buyer_id INT NOT NULL,
agent_id INT NOT NULL,
stage VARCHAR(50) NOT NULL,
offer_amount NUMERIC(12,2),
commission_pct NUMERIC(5,2),
commission_amount NUMERIC(12,2),
created_at TIMESTAMP DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_sale_pipeline_stage ON sale_pipeline(stage);
CREATE INDEX IF NOT EXISTS idx_sale_pipeline_property ON sale_pipeline(property_id);
I also added a transaction wrapper around the bulk insert in ventas.controller.ts to guarantee atomicity:
await db.transaction(async (trx) => {
await trx(`
INSERT INTO sale_pipeline (property_id, buyer_id, agent_id, stage, offer_amount, commission_pct, commission_amount)
VALUES ($1, $2, $3, $4, $5, $6, $7)
`, [propId, buyerId, agentId, stage, offer, pct, amount]);
});
3. Unifying Navigation with the “Hoy+Operativo” Pattern
The front‑end navigation lived in apps/web/src/app/_components/CrmShell.tsx. I refactored the NavItem definition to include an optional pattern field that drives the Hoy behavior. The duplicated route file apps/web/src/app/dashboard/hoy/page.tsx was removed (381 lines deleted) and its logic merged into the Ventas dashboard.
Updated CrmShell.tsx (excerpt)
tsx
type NavItem = {
href?: string;
action?: string;
label: string;
icon: string;
pattern?: 'hoy' | 'operativo';
};
const navGroups: NavGroup[] = [
{
group: 'VENTAS',
icon: 'sale',
items: [
{ href: '/ventas
---
*Part of my [Build in Public](https://dev.to/zaerohell) series — sharing the real process of building Building PlayaMXCRM from Playa del Carmen, México.*
*Repo: `zaerohell/VS` · 2026-08-29*
\#playadev #buildinpublic
Top comments (0)