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:
53
src/routes/facturas.js
Normal file
53
src/routes/facturas.js
Normal 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 };
|
||||
144
tests/routes-facturas.test.js
Normal file
144
tests/routes-facturas.test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user