Spanish invoices from paid orders: legal numbering, simplified/full, credit notes and VeriFactu-style hash chain
Install
genpm add @core/invoicesWhat you get
- Source in src/lib/invoices/, 8 files. (30.3 kB)
- AI rules in src/lib/invoices/AGENTS.md, plus IDE rule files.
- Env vars added to .env.example: INVOICE_ISSUER_NIF, INVOICE_ISSUER_NAME, INVOICE_ISSUER_ADDRESS, INVOICE_SERIES, INVOICE_VAT_RATE, INVOICE_RETAIL, INVOICE_QR_ENV.
- Resolves @core/contracts, @core/db, @core/orders for you.
README
This package has no README.
This is exactly what your AI reads when it works in src/lib/invoices. Nothing else is added to its context.
@core/invoices — rules for AI agents
Purpose
Spanish invoices for paid orders (@core/orders): correlative numbering per series and year without gaps, simplified
invoice up to the legal limit and full invoice above it, credit notes (rectificativas por diferencias) for refunds in
their own series, full invoices that replace simplified ones when the customer gives their tax details, printable HTML
with the mandatory content, and VeriFactu-style registration records: each record carries the SHA-256 hash of the
previous one (format verified against the AEAT example) and the AEAT verification QR URL. Invoices are never edited
or deleted. Tables: invoices, invoice_counters, invoice_records.
Scope of 1.0 (tell the person): this package prepares the data required by RD 1007/2023 and Order HAC/1177/2024, but it does not yet sign records (non-VeriFactu mode) nor send them to the AEAT (VeriFactu mode), and it is not a certified billing system. Deadlines changed several times (RD-ley 15/2025 set 2027; in October 2026 the Ministry announced a move to October 2028): check the current official calendar with the AEAT or a tax advisor.
Map
index.ts—invoiceOrder,rectifyRefund,rectifyInFull,remainingToRectify,replaceSimplified,verifyChain,invoiceHtml,invoiceQrUrl,invoicesAdminResource.invoices.ts— issuing and order events.verifactu.ts— hash input, hash, Madrid dates, amounts, QR URL.html.ts— printable invoice.
Integration
- Env:
INVOICE_ISSUER_NIF,INVOICE_ISSUER_NAME,INVOICE_ISSUER_ADDRESS; optionalINVOICE_SERIES(defaultA; credit notes use<series>R),INVOICE_VAT_RATE(default 21, prices include VAT),INVOICE_RETAIL=1(simplified limit 3,000 € instead of 400 €),INVOICE_QR_ENV=testwhile testing. - Migrations as in
src/lib/db/AGENTS.md. Import@/lib/invoicesonce at startup (e.g.src/genpm/commerce.ts): it issues invoices when orders are paid and credit notes when they are refunded (background jobs, retried). - Admin: add
invoicesAdminResource({ printPath: (i) => \/invoices/${i.number}` })tosrc/genpm/admin.ts`. - Printable page for the owner (protected by
invoices:read) or the customer (signed order link): returnnew Response(invoiceHtml(inv, { qrImageSrc }), { headers: { 'content-type': 'text/html' } }). Generate the QR image frominvoiceQrUrl(inv)with a QR library in the app. - Verify: pay a test order → invoice
A<year>-000001; refund part → credit noteAR<year>-000001with a negative total;verifyChain(nif)returnsok: true.
Conventions
- Lines add up to the total: products at their pre-discount amount, one "Discount" line, "Shipping (<method>)" and "Tax" if added on top.
- "Rectify in full" (admin,
rectifyInFull) credits only what earlier credit notes left; it refuses when nothing remains. - All lines use one VAT rate in 1.0; mixed rates (reduced, exempt) need per-line taxes before using this package.
- Issue invoices in EUR only; convert other currencies before.
- Keep invoices and records at least as long as tax law requires (typically 4–6 years); never prune these tables.
Don't
- Don't edit, renumber or delete invoices or records; correct with credit notes.
- Don't print "VERI*FACTU" on invoices until records are actually sent to the AEAT.
- Don't present this package as certified or as legal/tax advice.
# @core/invoices — rules for AI agents
## Purpose
Spanish invoices for paid orders (@core/orders): correlative numbering per series and year without gaps, simplified
invoice up to the legal limit and full invoice above it, credit notes (rectificativas por diferencias) for refunds in
their own series, full invoices that replace simplified ones when the customer gives their tax details, printable HTML
with the mandatory content, and VeriFactu-style registration records: each record carries the SHA-256 hash of the
previous one (format verified against the AEAT example) and the AEAT verification QR URL. Invoices are never edited
or deleted. Tables: `invoices`, `invoice_counters`, `invoice_records`.
**Scope of 1.0 (tell the person):** this package prepares the data required by RD 1007/2023 and Order HAC/1177/2024,
but it does not yet sign records (non-VeriFactu mode) nor send them to the AEAT (VeriFactu mode), and it is not a
certified billing system. Deadlines changed several times (RD-ley 15/2025 set 2027; in October 2026 the Ministry
announced a move to October 2028): check the current official calendar with the AEAT or a tax advisor.
## Map
- `index.ts` — `invoiceOrder`, `rectifyRefund`, `rectifyInFull`, `remainingToRectify`, `replaceSimplified`, `verifyChain`, `invoiceHtml`, `invoiceQrUrl`, `invoicesAdminResource`.
- `invoices.ts` — issuing and order events. `verifactu.ts` — hash input, hash, Madrid dates, amounts, QR URL. `html.ts` — printable invoice.
## Integration
1. Env: `INVOICE_ISSUER_NIF`, `INVOICE_ISSUER_NAME`, `INVOICE_ISSUER_ADDRESS`; optional `INVOICE_SERIES` (default `A`;
credit notes use `<series>R`), `INVOICE_VAT_RATE` (default 21, prices include VAT), `INVOICE_RETAIL=1` (simplified
limit 3,000 € instead of 400 €), `INVOICE_QR_ENV=test` while testing.
2. Migrations as in `src/lib/db/AGENTS.md`. Import `@/lib/invoices` once at startup (e.g. `src/genpm/commerce.ts`):
it issues invoices when orders are paid and credit notes when they are refunded (background jobs, retried).
3. Admin: add `invoicesAdminResource({ printPath: (i) => \`/invoices/${i.number}\` })` to `src/genpm/admin.ts`.
4. Printable page for the owner (protected by `invoices:read`) or the customer (signed order link):
return `new Response(invoiceHtml(inv, { qrImageSrc }), { headers: { 'content-type': 'text/html' } })`. Generate the QR
image from `invoiceQrUrl(inv)` with a QR library in the app.
5. Verify: pay a test order → invoice `A<year>-000001`; refund part → credit note `AR<year>-000001` with a negative total;
`verifyChain(nif)` returns `ok: true`.
## Conventions
- Lines add up to the total: products at their pre-discount amount, one "Discount" line, "Shipping (<method>)" and "Tax" if added on top.
- "Rectify in full" (admin, `rectifyInFull`) credits only what earlier credit notes left; it refuses when nothing remains.
- All lines use one VAT rate in 1.0; mixed rates (reduced, exempt) need per-line taxes before using this package.
- Issue invoices in EUR only; convert other currencies before.
- Keep invoices and records at least as long as tax law requires (typically 4–6 years); never prune these tables.
## Don't
- Don't edit, renumber or delete invoices or records; correct with credit notes.
- Don't print "VERI*FACTU" on invoices until records are actually sent to the AEAT.
- Don't present this package as certified or as legal/tax advice.
The exact tree that will be injected, after .genpmignore. Pinned to
// Factura en HTML imprimible (también para PDF con el navegador) con el contenido obligatorio del RD 1619/2012.
// Todos los textos se escapan; el QR se pinta con la URL de cotejo (genera la imagen con una librería de QR en la app).
import { type Invoice } from './schema.ts';
import { invoiceQrUrl } from './invoices.ts';
import { amountText, spanishDate } from './verifactu.ts';
const esc = (s: unknown) => String(s ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"');
const eur = (c: number) => `${amountText(c).replace('.', ',')} €`;
export const INVOICE_TITLES: Record<Invoice['type'], string> = {
F1: 'Factura',
F2: 'Factura simplificada',
F3: 'Factura (en sustitución de factura simplificada)',
R1: 'Factura rectificativa',
R5: 'Factura rectificativa (de factura simplificada)',
};
export function invoiceHtml(inv: Invoice, opts: { rectifiedNumber?: string; qrImageSrc?: string } = {}): string {
const r = inv.recipient;
return `<!doctype html><html lang="es"><head><meta charset="utf-8"><title>${esc(INVOICE_TITLES[inv.type])} ${esc(inv.number)}</title>
<style>body{font:14px/1.5 system-ui,sans-serif;max-width:48rem;margin:2rem auto;color:#111}table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:.4rem;border-bottom:1px solid #ddd}td.n,th.n{text-align:right}.grid{display:grid;grid-template-columns:1fr 1fr;gap:1rem}</style></head><body>
<h1>${esc(INVOICE_TITLES[inv.type])} ${esc(inv.number)}</h1>
<p>Fecha de expedición: ${esc(spanishDate(inv.issuedAt))}${opts.rectifiedNumber ? ` · Rectifica/sustituye a: ${esc(opts.rectifiedNumber)}` : ''}</p>
<div class="grid"><section><h2>Emisor</h2><p>${esc(inv.issuer.name)}<br>NIF: ${esc(inv.issuer.taxId)}${inv.issuer.address ? `<br>${esc(inv.issuer.address)}` : ''}</p></section>
${r ? `<section><h2>Destinatario</h2><p>${esc(r.name)}${r.taxId ? `<br>NIF: ${esc(r.taxId)}` : ''}${r.address ? `<br>${esc(r.address)}` : ''}</p></section>` : ''}</div>
<table><thead><tr><th>Concepto</th><th class="n">Cant.</th><th class="n">Precio (IVA incl.)</th><th class="n">Importe</th></tr></thead><tbody>
${inv.lines.map((l) => `<tr><td>${esc(l.description)}</td><td class="n">${l.quantity}</td><td class="n">${eur(l.unitPrice)}</td><td class="n">${eur(l.total)}</td></tr>`).join('')}
</tbody></table>
<table><tbody><tr><th>Base imponible</th><td class="n">${eur(inv.base)}</td></tr><tr><th>IVA ${inv.vatRate} %</th><td class="n">${eur(inv.vat)}</td></tr><tr><th>Total</th><td class="n"><strong>${eur(inv.total)}</strong></td></tr></tbody></table>
<p>${opts.qrImageSrc ? `<img src="${esc(opts.qrImageSrc)}" alt="Código QR de cotejo de la factura en la AEAT" width="120" height="120"><br>` : ''}Cotejo: <a href="${esc(invoiceQrUrl(inv))}">${esc(invoiceQrUrl(inv))}</a></p>
</body></html>`;
}
This package declares no MCP servers.
| Version | Commit | Published | Scan |
|---|---|---|---|
| 1.0.1 | 6912dca | 6 hours ago | scan passed |
- npm
- zod ^4.0.0
- proposed
- GenPM proposes the npm command and runs it only if you say yes.
- Used by (0)
- No public package depends on it yet.
- scan
- scan passed · 0 findings
- commit
- v1.0.1 → 6912dca67ec74a456be4abfe09c9584156cc7e4a · verified after fetch
- scripts
- None. GenPM never runs package code.
- license
- MIT
- Quality
- 100/100
- Recognized licensepassed
- AGENTS.md explains its purposepassed
- AGENTS.md has integration stepspassed
- AGENTS.md lists conventions or don'tspassed
- Includes testspassed
- Security scan passedpassed
- Released in the last 6 monthspassed
- Verified publisherpassed
- Summary and keywordspassed
- report
- See something wrong?