From 1eac71790cca7425abf7cb6a82c668e728c7f48a Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Tue, 17 Feb 2026 06:34:01 +0000 Subject: [PATCH] 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 --- src/routes/facturas.js | 53 +++++++++++++ tests/routes-facturas.test.js | 144 ++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 src/routes/facturas.js create mode 100644 tests/routes-facturas.test.js diff --git a/src/routes/facturas.js b/src/routes/facturas.js new file mode 100644 index 0000000..1f46120 --- /dev/null +++ b/src/routes/facturas.js @@ -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 }; diff --git a/tests/routes-facturas.test.js b/tests/routes-facturas.test.js new file mode 100644 index 0000000..e42eee1 --- /dev/null +++ b/tests/routes-facturas.test.js @@ -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'); + }); +});