从订单生成西班牙发票:合法编号、简易/完整发票、更正发票和 VeriFactu 风格哈希链
安装
genpm add @core/invoices你将获得
- 源代码位于 src/lib/invoices/,共 8 个文件。 (30.3 kB)
- AI 规则位于 src/lib/invoices/AGENTS.md,另附 IDE 规则文件。
- 添加到 .env.example 的环境变量:INVOICE_ISSUER_NIF, INVOICE_ISSUER_NAME, INVOICE_ISSUER_ADDRESS, INVOICE_SERIES, INVOICE_VAT_RATE, INVOICE_RETAIL, INVOICE_QR_ENV。
- 自动为你解析 @core/contracts, @core/db, @core/orders。
README
此包没有 README。
这正是你的 AI 在 src/lib/invoices 中工作时读取的内容。不会向其上下文添加其他任何内容。
@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.
应用 .genpmignore 后将被注入的确切目录树。固定于
// Huella (hash) encadenada y código QR de los registros de facturación según las especificaciones técnicas de la AEAT
// (Orden HAC/1177/2024). Comprueba siempre la versión vigente de los documentos técnicos antes de producción.
/** dd-mm-aaaa en hora de Madrid. */
export function spanishDate(d: Date): string {
const p = new Intl.DateTimeFormat('es-ES', { timeZone: 'Europe/Madrid', day: '2-digit', month: '2-digit', year: 'numeric' }).formatToParts(d);
const v = (t: string) => p.find((x) => x.type === t)!.value;
return `${v('day')}-${v('month')}-${v('year')}`;
}
/** `2024-01-01T19:20:30+01:00`: fecha, hora y huso horario de Madrid (sin milisegundos). */
export function madridTimestamp(d: Date): string {
const p = new Intl.DateTimeFormat('en-GB', { timeZone: 'Europe/Madrid', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23', timeZoneName: 'longOffset' }).formatToParts(d);
const v = (t: string) => p.find((x) => x.type === t)!.value;
const off = v('timeZoneName').replace('GMT', '') || '+00:00';
return `${v('year')}-${v('month')}-${v('day')}T${v('hour')}:${v('minute')}:${v('second')}${off}`;
}
/** Importe en céntimos → texto con punto decimal y dos decimales (`123.45`, `-5.00`). */
export const amountText = (cents: number) => `${cents < 0 ? '-' : ''}${Math.floor(Math.abs(cents) / 100)}.${String(Math.abs(cents) % 100).padStart(2, '0')}`;
export type AltaFields = {
IDEmisorFactura: string;
NumSerieFactura: string;
FechaExpedicionFactura: string;
TipoFactura: string;
CuotaTotal: string;
ImporteTotal: string;
Huella: string;
FechaHoraHusoGenRegistro: string;
};
export type AnulacionFields = {
IDEmisorFacturaAnulada: string;
NumSerieFacturaAnulada: string;
FechaExpedicionFacturaAnulada: string;
Huella: string;
FechaHoraHusoGenRegistro: string;
};
const ORDER_ALTA: Array<keyof AltaFields> = ['IDEmisorFactura', 'NumSerieFactura', 'FechaExpedicionFactura', 'TipoFactura', 'CuotaTotal', 'ImporteTotal', 'Huella', 'FechaHoraHusoGenRegistro'];
const ORDER_ANULACION: Array<keyof AnulacionFields> = ['IDEmisorFacturaAnulada', 'NumSerieFacturaAnulada', 'FechaExpedicionFacturaAnulada', 'Huella', 'FechaHoraHusoGenRegistro'];
/** Cadena `campo=valor&…` (valores sin espacios iniciales/finales) en el orden de la especificación. */
export function huellaInput(fields: AltaFields | AnulacionFields): string {
const order = 'TipoFactura' in fields ? ORDER_ALTA : ORDER_ANULACION;
return order.map((k) => `${k}=${String((fields as Record<string, string>)[k] ?? '').trim()}`).join('&');
}
/** SHA-256 en hexadecimal en mayúsculas. */
export async function huella(fields: AltaFields | AnulacionFields): Promise<string> {
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(huellaInput(fields)));
return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, '0')).join('').toUpperCase();
}
/** URL del código QR de cotejo de la AEAT (`test` usa el entorno de pruebas). */
export function qrUrl(p: { nif: string; number: string; date: string; total: string }, env: 'production' | 'test' = 'production'): string {
const base = env === 'production' ? 'https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR' : 'https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR';
const q = new URLSearchParams({ nif: p.nif, numserie: p.number, fecha: p.date, importe: p.total });
return `${base}?${q}`;
}
此包未声明 MCP 服务器。
| 版本 | 提交 | 发布时间 | 扫描 |
|---|---|---|---|
| 1.0.1 | 6912dca | 6小时前 | 扫描通过 |
- npm
- zod ^4.0.0
- 建议
- GenPM 会给出 npm 命令建议,只有你同意时才会运行。
- 被以下包使用(0)
- 目前还没有公开软件包依赖它。
- 扫描
- 扫描通过 · 0 个问题
- 提交
- v1.0.1 → 6912dca67ec74a456be4abfe09c9584156cc7e4a · 获取后已校验
- 脚本
- 无。GenPM 从不运行包中的代码。
- 许可证
- MIT
- 质量
- 100/100
- 可识别的许可证已满足
- AGENTS.md 说明了用途已满足
- AGENTS.md 包含集成步骤已满足
- AGENTS.md 列出约定或禁止事项已满足
- 包含测试已满足
- 通过安全扫描已满足
- 最近 6 个月内发布已满足
- 已验证的发布者已满足
- 摘要和关键词已满足
- 举报
- 发现问题了吗?