Add API routes for facturas with filtering and detail endpoints

Implements GET /api/facturas (list with optional proveedor, fecha_inicio,
fecha_fin, and q filters) and GET /api/facturas/:id (single factura with
its conceptos). Includes 10 tests covering all filters and 404 handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
consultoria-as
2026-02-17 06:34:01 +00:00
parent bec6599c5f
commit 1eac71790c
2 changed files with 197 additions and 0 deletions

53
src/routes/facturas.js Normal file
View File

@@ -0,0 +1,53 @@
const { Router } = require('express');
function facturasRouter(db) {
const router = Router();
router.get('/', (req, res) => {
const { proveedor, fecha_inicio, fecha_fin, q } = req.query;
let sql = `
SELECT f.*, COUNT(c.id) as num_conceptos
FROM facturas f
LEFT JOIN conceptos c ON c.factura_id = f.id
`;
const conditions = [];
const params = [];
if (proveedor) {
conditions.push('f.nombre_emisor LIKE ?');
params.push(`%${proveedor}%`);
}
if (fecha_inicio) {
conditions.push('f.fecha >= ?');
params.push(fecha_inicio);
}
if (fecha_fin) {
conditions.push('f.fecha <= ?');
params.push(fecha_fin);
}
if (q) {
conditions.push('(f.uuid LIKE ? OR f.nombre_emisor LIKE ?)');
params.push(`%${q}%`, `%${q}%`);
}
if (conditions.length > 0) {
sql += ' WHERE ' + conditions.join(' AND ');
}
sql += ' GROUP BY f.id ORDER BY f.fecha DESC';
const facturas = db.prepare(sql).all(...params);
res.json(facturas);
});
router.get('/:id', (req, res) => {
const factura = db.prepare('SELECT * FROM facturas WHERE id = ?').get(req.params.id);
if (!factura) return res.status(404).json({ error: 'Factura no encontrada' });
const conceptos = db.prepare('SELECT * FROM conceptos WHERE factura_id = ?').all(factura.id);
res.json({ factura, conceptos });
});
return router;
}
module.exports = { facturasRouter };

View File

@@ -0,0 +1,144 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createDatabase } from '../src/services/database.js';
import express from 'express';
import { facturasRouter } from '../src/routes/facturas.js';
let db;
let server;
let baseUrl;
beforeAll(async () => {
db = createDatabase(':memory:');
// Seed test data: two facturas from different proveedores and dates
db.prepare(`
INSERT INTO facturas (uuid, rfc_emisor, nombre_emisor, fecha, subtotal, total, xml_path)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run('UUID-001', 'RFC-A', 'Proveedor Uno', '2026-01-10', 100, 116, '/a.xml');
db.prepare(`
INSERT INTO facturas (uuid, rfc_emisor, nombre_emisor, fecha, subtotal, total, xml_path)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run('UUID-002', 'RFC-B', 'Proveedor Dos', '2026-02-15', 200, 232, '/b.xml');
const factura1 = db.prepare('SELECT id FROM facturas WHERE uuid = ?').get('UUID-001');
const factura2 = db.prepare('SELECT id FROM facturas WHERE uuid = ?').get('UUID-002');
// Add conceptos to factura 1
db.prepare(`
INSERT INTO conceptos (factura_id, clave_prod_serv, no_identificacion, descripcion, cantidad, valor_unitario, importe)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(factura1.id, '25101503', 'BK-001', 'Balata delantera', 4, 25, 100);
db.prepare(`
INSERT INTO conceptos (factura_id, clave_prod_serv, no_identificacion, descripcion, cantidad, valor_unitario, importe)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(factura1.id, '25101504', 'BK-002', 'Balata trasera', 2, 50, 100);
// No conceptos for factura 2 — intentional
const app = express();
app.use(express.json());
app.use('/api/facturas', facturasRouter(db));
await new Promise((resolve) => {
server = app.listen(0, '127.0.0.1', () => {
const addr = server.address();
baseUrl = `http://127.0.0.1:${addr.port}`;
resolve();
});
});
});
afterAll(async () => {
if (server) {
await new Promise((resolve) => server.close(resolve));
}
if (db) {
db.close();
}
});
describe('GET /api/facturas', () => {
it('should return all facturas', async () => {
const res = await fetch(`${baseUrl}/api/facturas`);
expect(res.status).toBe(200);
const data = await res.json();
expect(data).toHaveLength(2);
// Ordered by fecha DESC, so Proveedor Dos (2026-02-15) first
expect(data[0].nombre_emisor).toBe('Proveedor Dos');
expect(data[1].nombre_emisor).toBe('Proveedor Uno');
});
it('should include num_conceptos count', async () => {
const res = await fetch(`${baseUrl}/api/facturas`);
const data = await res.json();
const uno = data.find((f) => f.nombre_emisor === 'Proveedor Uno');
const dos = data.find((f) => f.nombre_emisor === 'Proveedor Dos');
expect(uno.num_conceptos).toBe(2);
expect(dos.num_conceptos).toBe(0);
});
it('should filter by proveedor', async () => {
const res = await fetch(`${baseUrl}/api/facturas?proveedor=Uno`);
expect(res.status).toBe(200);
const data = await res.json();
expect(data).toHaveLength(1);
expect(data[0].nombre_emisor).toBe('Proveedor Uno');
});
it('should filter by fecha_inicio', async () => {
const res = await fetch(`${baseUrl}/api/facturas?fecha_inicio=2026-02-01`);
const data = await res.json();
expect(data).toHaveLength(1);
expect(data[0].uuid).toBe('UUID-002');
});
it('should filter by fecha_fin', async () => {
const res = await fetch(`${baseUrl}/api/facturas?fecha_fin=2026-01-31`);
const data = await res.json();
expect(data).toHaveLength(1);
expect(data[0].uuid).toBe('UUID-001');
});
it('should filter by fecha_inicio and fecha_fin combined', async () => {
const res = await fetch(`${baseUrl}/api/facturas?fecha_inicio=2026-01-01&fecha_fin=2026-01-31`);
const data = await res.json();
expect(data).toHaveLength(1);
expect(data[0].uuid).toBe('UUID-001');
});
it('should search by q matching uuid', async () => {
const res = await fetch(`${baseUrl}/api/facturas?q=UUID-002`);
const data = await res.json();
expect(data).toHaveLength(1);
expect(data[0].uuid).toBe('UUID-002');
});
it('should search by q matching nombre_emisor', async () => {
const res = await fetch(`${baseUrl}/api/facturas?q=Dos`);
const data = await res.json();
expect(data).toHaveLength(1);
expect(data[0].nombre_emisor).toBe('Proveedor Dos');
});
});
describe('GET /api/facturas/:id', () => {
it('should return factura with its conceptos', async () => {
const factura = db.prepare('SELECT id FROM facturas WHERE uuid = ?').get('UUID-001');
const res = await fetch(`${baseUrl}/api/facturas/${factura.id}`);
expect(res.status).toBe(200);
const data = await res.json();
expect(data.factura).toBeDefined();
expect(data.factura.uuid).toBe('UUID-001');
expect(data.conceptos).toHaveLength(2);
expect(data.conceptos[0].descripcion).toBe('Balata delantera');
});
it('should return 404 for non-existent factura', async () => {
const res = await fetch(`${baseUrl}/api/facturas/999`);
expect(res.status).toBe(404);
const data = await res.json();
expect(data.error).toBe('Factura no encontrada');
});
});