313 lines
14 KiB
HTML
313 lines
14 KiB
HTML
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="es">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<title>VeriFactu — Presentación del proyecto</title>
|
||
|
|
<style>
|
||
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
|
body { font-family: 'Segoe UI', sans-serif; background: #0f1117; color: #e8eaf0; overflow: hidden; }
|
||
|
|
|
||
|
|
.slide {
|
||
|
|
display: none;
|
||
|
|
width: 100vw; height: 100vh;
|
||
|
|
padding: 60px 80px;
|
||
|
|
flex-direction: column;
|
||
|
|
justify-content: center;
|
||
|
|
}
|
||
|
|
.slide.active { display: flex; }
|
||
|
|
|
||
|
|
h1 { font-size: 2.6rem; font-weight: 700; color: #fff; margin-bottom: 12px; }
|
||
|
|
h2 { font-size: 1.9rem; font-weight: 600; color: #7eb8f7; margin-bottom: 28px; border-bottom: 2px solid #2a3050; padding-bottom: 12px; }
|
||
|
|
h3 { font-size: 1.2rem; color: #a0b4cc; margin-bottom: 10px; }
|
||
|
|
p { font-size: 1.15rem; line-height: 1.7; color: #c8d0dc; max-width: 860px; }
|
||
|
|
ul { list-style: none; padding: 0; }
|
||
|
|
ul li { font-size: 1.1rem; padding: 7px 0 7px 22px; color: #c8d0dc; position: relative; line-height: 1.5; }
|
||
|
|
ul li::before { content: "→"; position: absolute; left: 0; color: #7eb8f7; }
|
||
|
|
|
||
|
|
.subtitle { font-size: 1.3rem; color: #7eb8f7; margin-bottom: 40px; }
|
||
|
|
.tag { display: inline-block; background: #1e2d45; color: #7eb8f7; border: 1px solid #2e4a6e; border-radius: 6px; padding: 3px 12px; font-size: 0.85rem; margin: 3px; }
|
||
|
|
.tag.green { background: #1a2e1a; color: #6fcf97; border-color: #2e6e2e; }
|
||
|
|
.tag.amber { background: #2e2208; color: #f2c94c; border-color: #6e4e08; }
|
||
|
|
.tag.red { background: #2e1010; color: #eb5757; border-color: #6e1010; }
|
||
|
|
|
||
|
|
.cols { display: grid; gap: 40px; }
|
||
|
|
.cols-2 { grid-template-columns: 1fr 1fr; }
|
||
|
|
.cols-3 { grid-template-columns: 1fr 1fr 1fr; }
|
||
|
|
|
||
|
|
.card {
|
||
|
|
background: #181d2e;
|
||
|
|
border: 1px solid #2a3050;
|
||
|
|
border-radius: 12px;
|
||
|
|
padding: 24px 28px;
|
||
|
|
}
|
||
|
|
.card h3 { color: #7eb8f7; margin-bottom: 12px; font-size: 1rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
||
|
|
|
||
|
|
.flow {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0;
|
||
|
|
margin: 30px 0;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
}
|
||
|
|
.flow-step {
|
||
|
|
background: #181d2e;
|
||
|
|
border: 1px solid #2a3050;
|
||
|
|
border-radius: 10px;
|
||
|
|
padding: 16px 22px;
|
||
|
|
font-size: 1rem;
|
||
|
|
color: #e8eaf0;
|
||
|
|
text-align: center;
|
||
|
|
min-width: 130px;
|
||
|
|
}
|
||
|
|
.flow-arrow {
|
||
|
|
font-size: 1.6rem;
|
||
|
|
color: #7eb8f7;
|
||
|
|
padding: 0 10px;
|
||
|
|
}
|
||
|
|
|
||
|
|
table { border-collapse: collapse; width: 100%; margin-top: 10px; }
|
||
|
|
th { text-align: left; padding: 10px 16px; background: #181d2e; color: #7eb8f7; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
||
|
|
td { padding: 10px 16px; border-top: 1px solid #1e2535; font-size: 1rem; color: #c8d0dc; }
|
||
|
|
td code { background: #1e2535; padding: 2px 8px; border-radius: 4px; font-size: 0.9rem; color: #9ecbff; }
|
||
|
|
|
||
|
|
.status-ok { color: #6fcf97; font-weight: 600; }
|
||
|
|
.status-wip { color: #f2c94c; font-weight: 600; }
|
||
|
|
.status-no { color: #eb5757; font-weight: 600; }
|
||
|
|
|
||
|
|
.nav {
|
||
|
|
position: fixed;
|
||
|
|
bottom: 24px; right: 32px;
|
||
|
|
display: flex; gap: 12px; align-items: center;
|
||
|
|
}
|
||
|
|
.nav span { color: #4a5568; font-size: 0.9rem; }
|
||
|
|
#counter { color: #7eb8f7; font-size: 0.95rem; min-width: 60px; text-align: right; }
|
||
|
|
|
||
|
|
.note {
|
||
|
|
margin-top: 28px;
|
||
|
|
background: #1a1e2e;
|
||
|
|
border-left: 3px solid #f2c94c;
|
||
|
|
padding: 14px 20px;
|
||
|
|
border-radius: 0 8px 8px 0;
|
||
|
|
font-size: 0.95rem;
|
||
|
|
color: #b0b8c8;
|
||
|
|
max-width: 860px;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Slide 1 — portada */
|
||
|
|
#s1 { justify-content: center; align-items: flex-start; background: radial-gradient(ellipse at 70% 40%, #0d2140 0%, #0f1117 60%); }
|
||
|
|
#s1 h1 { font-size: 3.2rem; }
|
||
|
|
#s1 .meta { margin-top: 48px; font-size: 0.95rem; color: #4a5568; }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
|
||
|
|
<!-- ═══════════════════════════════════════════════════ SLIDE 1 — PORTADA -->
|
||
|
|
<div class="slide active" id="s1">
|
||
|
|
<div class="tag">BYolivia · 2025</div>
|
||
|
|
<br>
|
||
|
|
<h1>Integración VeriFactu</h1>
|
||
|
|
<p class="subtitle">Facturación electrónica obligatoria con la AEAT</p>
|
||
|
|
<p>Presentación del sistema de envío de facturas al sistema VeriFactu de la Agencia Tributaria, integrado en la suite de gestión empresarial.</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ═══════════════════════════════════════════════════ SLIDE 2 — CONTEXTO -->
|
||
|
|
<div class="slide" id="s2">
|
||
|
|
<h2>¿Por qué VeriFactu?</h2>
|
||
|
|
<div class="cols cols-2">
|
||
|
|
<div class="card">
|
||
|
|
<h3>Obligación legal</h3>
|
||
|
|
<ul>
|
||
|
|
<li>Reglamento de facturación electrónica (RD 1007/2023)</li>
|
||
|
|
<li>Obligatorio para empresas y autónomos en España</li>
|
||
|
|
<li>Cada factura debe enviarse a la AEAT en tiempo real</li>
|
||
|
|
<li>Las facturas se encadenan con hash para garantizar integridad</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
<div class="card">
|
||
|
|
<h3>El reto técnico</h3>
|
||
|
|
<ul>
|
||
|
|
<li>La AEAT usa SOAP/XML con firma digital</li>
|
||
|
|
<li>Requiere certificado digital de empresa (FNMT/ACCV)</li>
|
||
|
|
<li>TLS mútuo (mTLS) para autenticarse</li>
|
||
|
|
<li>El software ERP (Dolibarr) no lo soporta de forma nativa</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ═══════════════════════════════════════════════════ SLIDE 3 — ECOSISTEMA -->
|
||
|
|
<div class="slide" id="s3">
|
||
|
|
<h2>El ecosistema del proyecto</h2>
|
||
|
|
|
||
|
|
<!-- Flujo principal -->
|
||
|
|
<div class="flow" style="font-size:0.9rem; margin-bottom:8px; margin-top:16px">
|
||
|
|
<div class="flow-step">Dolibarr ERP<br><small style="color:#4a5568">datos de facturas</small></div>
|
||
|
|
<div class="flow-arrow">→</div>
|
||
|
|
<div class="flow-step">Backend<br><small style="color:#4a5568">dolibarr-bff :5269</small></div>
|
||
|
|
<div class="flow-arrow">→</div>
|
||
|
|
<div class="flow-step">Front<br><small style="color:#4a5568">doli-front :5173</small></div>
|
||
|
|
<div class="flow-arrow">→</div>
|
||
|
|
<div class="flow-step">VerifactuMidAPI<br><small style="color:#4a5568">Go :6789</small></div>
|
||
|
|
<div class="flow-arrow" style="color:#f2c94c">- - →</div>
|
||
|
|
<div class="flow-step" style="border-color:#f2c94c; color:#f2c94c">AEAT<br><small style="color:#4a5568">VeriFactu</small></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style="margin-top:20px; display:flex; flex-direction:column; gap:10px; max-width:860px">
|
||
|
|
<div style="display:flex; align-items:flex-start; gap:10px">
|
||
|
|
<span class="tag green" style="white-space:nowrap">Implementado</span>
|
||
|
|
<span style="font-size:0.9rem; color:#c8d0dc">Dolibarr → Backend → Front → VerifactuMidAPI. El backend almacena el token que devuelve la API tras registrar el certificado.</span>
|
||
|
|
</div>
|
||
|
|
<div style="display:flex; align-items:flex-start; gap:10px">
|
||
|
|
<span class="tag amber" style="white-space:nowrap">Sin probar</span>
|
||
|
|
<span style="font-size:0.9rem; color:#c8d0dc">VerifactuMidAPI → AEAT. El envío real a VeriFactu no ha podido validarse por el tema del certificado. Es el único tramo pendiente de verificar.</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ═══════════════════════════════════════════════════ SLIDE 4 — VERIFACTU API -->
|
||
|
|
<div class="slide" id="s4">
|
||
|
|
<h2>VerifactuMidAPI — Qué hace</h2>
|
||
|
|
<div class="cols cols-2">
|
||
|
|
<ul style="gap:4px">
|
||
|
|
<li>Recibe la factura en JSON (formato nativo o Dolibarr)</li>
|
||
|
|
<li>Calcula el hash SHA-256 encadenado (norma AEAT)</li>
|
||
|
|
<li>Genera el XML SOAP con todos los campos requeridos</li>
|
||
|
|
<li>Firma digitalmente con el certificado de empresa</li>
|
||
|
|
<li>Envía a la AEAT por SOAP con mTLS (TLS 1.2)</li>
|
||
|
|
<li>Si la AEAT no responde → guarda en fallback local</li>
|
||
|
|
<li>Devuelve el resultado al BFF</li>
|
||
|
|
</ul>
|
||
|
|
<div>
|
||
|
|
<div class="card">
|
||
|
|
<h3>Endpoints</h3>
|
||
|
|
<table>
|
||
|
|
<tr><td><code>GET /health</code></td><td>Estado</td></tr>
|
||
|
|
<tr><td><code>GET /auth/public-key</code></td><td>Clave RSA pública</td></tr>
|
||
|
|
<tr><td><code>POST /auth/register</code></td><td>Subir certificado P12</td></tr>
|
||
|
|
<tr><td><code>GET /formats</code></td><td>Formatos aceptados</td></tr>
|
||
|
|
<tr><td><code>POST /facturas</code></td><td>Alta de factura</td></tr>
|
||
|
|
<tr><td><code>POST /facturas/anular</code></td><td>Anulación</td></tr>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ═══════════════════════════════════════════════════ SLIDE 5 — SEGURIDAD CERTS -->
|
||
|
|
<div class="slide" id="s5">
|
||
|
|
<h2>Seguridad — Certificados y tokens</h2>
|
||
|
|
<div class="cols cols-2">
|
||
|
|
<div class="card">
|
||
|
|
<h3>Registro del certificado</h3>
|
||
|
|
<ul>
|
||
|
|
<li>El front cifra la contraseña del .p12 con RSA (clave pública de la API)</li>
|
||
|
|
<li>La API descifra, valida el P12 y lo almacena</li>
|
||
|
|
<li>Soporta certificados FNMT y ACCV (multi-cert)</li>
|
||
|
|
<li>La API extrae automáticamente los datos del emisor del propio certificado</li>
|
||
|
|
<li>Devuelve un <strong>token</strong> de sesión para usar en los envíos</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
<div class="card">
|
||
|
|
<h3>Flujo de registro</h3>
|
||
|
|
<div class="flow" style="flex-direction:column; align-items:flex-start; gap:8px; margin:0">
|
||
|
|
<div class="flow-step" style="width:100%">Front: selecciona .p12 + contraseña</div>
|
||
|
|
<div class="flow-arrow" style="padding:0 0 0 20px">↓</div>
|
||
|
|
<div class="flow-step" style="width:100%">Front: cifra pass con RSA pública</div>
|
||
|
|
<div class="flow-arrow" style="padding:0 0 0 20px">↓</div>
|
||
|
|
<div class="flow-step" style="width:100%">API: valida, almacena, genera token</div>
|
||
|
|
<div class="flow-arrow" style="padding:0 0 0 20px">↓</div>
|
||
|
|
<div class="flow-step" style="width:100%">BFF: guarda el token para futuros envíos</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ═══════════════════════════════════════════════════ SLIDE 6 — ESTADO -->
|
||
|
|
<div class="slide" id="s6">
|
||
|
|
<h2>Estado actual del proyecto</h2>
|
||
|
|
<div class="cols cols-2">
|
||
|
|
<div class="card">
|
||
|
|
<h3>VerifactuMidAPI (este repo — rama main)</h3>
|
||
|
|
<table>
|
||
|
|
<tr><td class="status-ok">✓</td><td>Alta de facturas con hash encadenado</td></tr>
|
||
|
|
<tr><td class="status-ok">✓</td><td>Anulación de facturas</td></tr>
|
||
|
|
<tr><td class="status-ok">✓</td><td>Registro y validación de certificados P12</td></tr>
|
||
|
|
<tr><td class="status-ok">✓</td><td>Soporte multi-cert (FNMT / ACCV)</td></tr>
|
||
|
|
<tr><td class="status-ok">✓</td><td>Fallback local si AEAT no responde</td></tr>
|
||
|
|
<tr><td class="status-ok">✓</td><td>TLS 1.2 + auto-fill datos emisor del cert</td></tr>
|
||
|
|
<tr><td class="status-no">✗</td><td>Consultas y subsanación (pendiente)</td></tr>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
<div class="card">
|
||
|
|
<h3>Integración en el ecosistema (rama verifactu)</h3>
|
||
|
|
<table>
|
||
|
|
<tr><td class="status-ok">✓</td><td>Front: subida del certificado P12</td></tr>
|
||
|
|
<tr><td class="status-ok">✓</td><td>Front: cifrado RSA de contraseña</td></tr>
|
||
|
|
<tr><td class="status-ok">✓</td><td>Front: recepción y uso del token</td></tr>
|
||
|
|
<tr><td class="status-ok">✓</td><td>BFF: guardado del token en backend</td></tr>
|
||
|
|
<tr><td class="status-wip">⏳</td><td>Pendiente de merge a main</td></tr>
|
||
|
|
</table>
|
||
|
|
<div class="note" style="margin-top:16px">La integración completa estaba bloqueada por el problema del certificado en la API, ya resuelto.</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
|
||
|
|
<!-- ═══════════════════════════════════════════════════ SLIDE 8 — PRÓXIMOS PASOS -->
|
||
|
|
<div class="slide" id="s8">
|
||
|
|
<h2>Próximos pasos</h2>
|
||
|
|
<div class="cols cols-2">
|
||
|
|
<div>
|
||
|
|
<h3 style="color:#6fcf97; margin-bottom:12px">Inmediatos</h3>
|
||
|
|
<ul>
|
||
|
|
<li>Merge de rama <code>verifactu</code> a main en front y BFF</li>
|
||
|
|
<li>Pruebas end-to-end con el ecosistema completo</li>
|
||
|
|
<li>Validar con certificado real en entorno de pruebas AEAT</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<h3 style="color:#f2c94c; margin-bottom:12px">Siguientes funcionalidades</h3>
|
||
|
|
<ul>
|
||
|
|
<li>Consultas a la AEAT (estado de facturas enviadas)</li>
|
||
|
|
<li>Subsanación de facturas incorrectas</li>
|
||
|
|
<li>Panel de estado VeriFactu en el front</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="note" style="margin-top:36px">El núcleo de la integración está completo y probado. El bloqueo original (gestión del certificado) está resuelto.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- NAV -->
|
||
|
|
<div class="nav">
|
||
|
|
<span id="counter">1 / 7</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
const slides = document.querySelectorAll('.slide');
|
||
|
|
let cur = 0;
|
||
|
|
|
||
|
|
function show(n) {
|
||
|
|
slides[cur].classList.remove('active');
|
||
|
|
cur = Math.max(0, Math.min(n, slides.length - 1));
|
||
|
|
slides[cur].classList.add('active');
|
||
|
|
document.getElementById('counter').textContent = (cur + 1) + ' / ' + slides.length;
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
document.addEventListener('keydown', e => {
|
||
|
|
if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ') show(cur + 1);
|
||
|
|
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') show(cur - 1);
|
||
|
|
if (e.key === 'f' || e.key === 'F') document.documentElement.requestFullscreen?.();
|
||
|
|
if (e.key === 'Escape' && document.fullscreenElement) document.exitFullscreen?.();
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener('click', e => {
|
||
|
|
if (e.target.closest('a, button, code')) return;
|
||
|
|
const mitad = window.innerWidth / 2;
|
||
|
|
if (e.clientX >= mitad) show(cur + 1);
|
||
|
|
else show(cur - 1);
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|