# Generatore PDF fattura di cortesia — Piano di implementazione

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Pagina web su Vercel che riceve un XML FatturaPA (`.xml`) e restituisce il PDF "fattura di cortesia" impaginato come `template/fattura.pdf`.

**Architecture:** App Next.js (App Router, TypeScript): una pagina con drag & drop che fa POST a `/api/genera-pdf`; la route fa parsing dell'XML (`fast-xml-parser`), decodifica i codici FatturaPA e compone il PDF con `@react-pdf/renderer` usando Trebuchet MS incorporato. Nessun dato viene salvato.

**Tech Stack:** Next.js 15 (App Router), TypeScript, `@react-pdf/renderer` v4, `fast-xml-parser` v5, Vitest.

**Spec di riferimento:** `docs/superpowers/specs/2026-07-21-xml-pdf-fattura-design.md` (leggerla prima di iniziare: contiene mappatura campi e specifica tipografica completa).

## Global Constraints

- Lingua UI e messaggi di errore: italiano.
- Font PDF: Trebuchet MS (4 face in `fonts/`, incorporate; niente font di sistema react-pdf).
- Colori PDF: etichette `#828282`, testo `#6a6a6a`, bordi `#cecece` (0,75 pt tabella; 0,59 pt blocco basso), righe alternate `#ededed`.
- Corpi testo PDF (pt): header cedente 8,25; riga FATTURA 10,5; anagrafiche e tabella 9; etichette colonne 6; etichetta NOTE/DESTINATARIO 6,75; etichette blocco basso 5,34; contenuti blocco basso 6,53; totale grande 17,81; piè di pagina 7,5.
- Pagina A4, margini orizzontali 28,3 pt.
- Importi: sempre in centesimi (integer) nel modello dati; formattazione it-IT solo in presentazione ("€ 6.900,00"). Date `gg/mm/aaaa`.
- Nessuna persistenza server-side del file caricato.
- API route con `runtime = 'nodejs'`.
- Tutti i valori XML letti come stringhe (`parseTagValue: false` — mai perdere gli zeri iniziali di P.IVA/CAP).
- Commit frequenti; messaggi commit in italiano, prefissi `feat:`/`test:`/`fix:`/`chore:`.

---

### Task 1: Scaffold del progetto Next.js + font

**Files:**
- Create: `package.json`, `tsconfig.json`, `next.config.ts`, `.gitignore`, `app/layout.tsx`, `app/globals.css`, `app/page.tsx` (placeholder), `vitest.config.ts`, `fonts/*.ttf` (copiati dal sistema)

**Interfaces:**
- Consumes: niente (primo task).
- Produces: progetto che builda; alias `@/*` → radice repo; script npm `dev`, `build`, `test`; font in `fonts/TrebuchetMS.ttf`, `fonts/TrebuchetMS-Bold.ttf`, `fonts/TrebuchetMS-Italic.ttf`, `fonts/TrebuchetMS-BoldItalic.ttf`.

- [ ] **Step 1: Crea `package.json`**

```json
{
  "name": "xml-pdf",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "test": "vitest run"
  },
  "dependencies": {
    "@react-pdf/renderer": "^4.3.0",
    "fast-xml-parser": "^5.2.0",
    "next": "^15.3.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "@types/node": "^22",
    "@types/react": "^19",
    "typescript": "^5",
    "vitest": "^3"
  }
}
```

- [ ] **Step 2: Crea `tsconfig.json`**

```json
{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [{ "name": "next" }],
    "paths": { "@/*": ["./*"] }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}
```

- [ ] **Step 3: Crea `next.config.ts`** (i font vanno inclusi nel bundle serverless su Vercel)

```ts
import type { NextConfig } from 'next';

const config: NextConfig = {
  outputFileTracingIncludes: {
    '/api/genera-pdf': ['./fonts/**'],
  },
};

export default config;
```

- [ ] **Step 4: Crea `.gitignore`**

```
node_modules/
.next/
.vercel
next-env.d.ts
*.tsbuildinfo
```

- [ ] **Step 5: Crea `app/layout.tsx`**

```tsx
import './globals.css';
import type { ReactNode } from 'react';

export const metadata = {
  title: 'Fattura XML → PDF',
  description: 'Genera la fattura di cortesia in PDF da un XML FatturaPA',
};

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="it">
      <body>{children}</body>
    </html>
  );
}
```

- [ ] **Step 6: Crea `app/globals.css`** (minimo per ora; la UI vera arriva nel Task 7)

```css
* { box-sizing: border-box; }
body { margin: 0; font-family: "Trebuchet MS", Verdana, sans-serif; }
```

- [ ] **Step 7: Crea `app/page.tsx` placeholder**

```tsx
export default function Pagina() {
  return (
    <main>
      <h1>Fattura XML → PDF</h1>
    </main>
  );
}
```

- [ ] **Step 8: Crea `vitest.config.ts`**

Attenzione: il `jsx: "preserve"` del tsconfig Next romperebbe i test sui `.tsx`; l'override esbuild qui sotto è obbligatorio.

```ts
import path from 'node:path';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  resolve: { alias: { '@': path.resolve(__dirname) } },
  esbuild: { jsx: 'automatic' },
  test: { environment: 'node' },
});
```

- [ ] **Step 9: Copia i font Trebuchet MS dal sistema**

```bash
mkdir -p fonts
cp "/System/Library/Fonts/Supplemental/Trebuchet MS.ttf" fonts/TrebuchetMS.ttf
cp "/System/Library/Fonts/Supplemental/Trebuchet MS Bold.ttf" fonts/TrebuchetMS-Bold.ttf
cp "/System/Library/Fonts/Supplemental/Trebuchet MS Italic.ttf" fonts/TrebuchetMS-Italic.ttf
cp "/System/Library/Fonts/Supplemental/Trebuchet MS Bold Italic.ttf" fonts/TrebuchetMS-BoldItalic.ttf
ls -la fonts/
```

Expected: 4 file `.ttf` (~126–141 KB l'uno).

- [ ] **Step 10: Installa le dipendenze e verifica la build**

```bash
npm install
npm run build
```

Expected: `npm run build` termina con "Compiled successfully" (o "✓ Compiled"), nessun errore.

- [ ] **Step 11: Commit**

```bash
git add -A
git commit -m "chore: scaffold Next.js con font Trebuchet MS e Vitest"
```

---

### Task 2: Modello dati e calcoli (`lib/tipi.ts`, `lib/calcoli.ts`)

**Files:**
- Create: `lib/tipi.ts`, `lib/calcoli.ts`
- Test: `test/calcoli.test.ts`

**Interfaces:**
- Consumes: niente.
- Produces (usati dai Task 4, 5, 6):
  - Tipi: `Soggetto`, `Linea`, `Riepilogo`, `Cassa`, `Pagamento`, `Fattura` (vedi Step 1).
  - Funzioni: `centesimi(importo: string): number`, `numeroIt(cent: number): string`, `euro(cent: number): string`, `formattaData(iso: string): string`, `percentuale(valore: string): string`, `numeroDocumento(numero: string, dataIso: string): string`, `capitalizza(testo: string): string`, `totaleOnorario(f: Fattura): number`, `totaleImponibile(f: Fattura): number`, `totaleDocumento(f: Fattura): number`.

- [ ] **Step 1: Crea `lib/tipi.ts`**

```ts
export interface Soggetto {
  denominazione: string; // Denominazione, oppure "Nome Cognome"
  indirizzo: string;
  civico?: string;
  cap: string;
  comune: string;
  provincia?: string;
  partitaIva?: string;
  codiceFiscale?: string;
  email?: string;
}

export interface Linea {
  descrizione: string;
  prezzoTotaleCent: number;
  aliquotaIva: string; // es. "0.00", "22.00"
}

export interface Riepilogo {
  aliquotaIva: string;
  natura?: string; // es. "N2.2"
  imponibileCent: number;
  impostaCent: number;
}

export interface Cassa {
  tipo: string; // es. "TC04"
  aliquota: string; // es. "4.00"
  importoCent: number;
}

export interface Pagamento {
  modalita?: string; // es. "MP05"
  iban?: string;
  beneficiario?: string;
  dataIso?: string; // scadenza
  importoCent: number;
}

export interface Fattura {
  tipoDocumento: string; // es. "TD01"
  dataIso: string; // es. "2026-07-20"
  numero: string;
  totaleDocumentoCent?: number;
  bolloCent?: number;
  regimeFiscale?: string; // es. "RF19"
  emittente: Soggetto;
  destinatario: Soggetto;
  linee: Linea[];
  riepiloghi: Riepilogo[];
  casse: Cassa[];
  pagamenti: Pagamento[];
}
```

- [ ] **Step 2: Scrivi i test in `test/calcoli.test.ts`**

```ts
import { describe, expect, it } from 'vitest';
import {
  capitalizza, centesimi, euro, formattaData, numeroDocumento, numeroIt,
  percentuale, totaleDocumento, totaleImponibile, totaleOnorario,
} from '@/lib/calcoli';
import type { Fattura } from '@/lib/tipi';

describe('centesimi', () => {
  it('converte importi decimali in centesimi', () => {
    expect(centesimi('6900.00')).toBe(690000);
    expect(centesimi('2.00')).toBe(200);
    expect(centesimi('0.1')).toBe(10);
    expect(centesimi('7178')).toBe(717800);
    expect(centesimi('-15.50')).toBe(-1550);
  });
  it('rifiuta importi non numerici', () => {
    expect(() => centesimi('abc')).toThrow();
  });
});

describe('formattazione', () => {
  it('numeroIt: separatore migliaia e virgola', () => {
    expect(numeroIt(696800)).toBe('6.968,00');
    expect(numeroIt(200)).toBe('2,00');
    expect(numeroIt(123456789)).toBe('1.234.567,89');
  });
  it('euro antepone il simbolo', () => {
    expect(euro(690000)).toBe('€ 6.900,00');
  });
  it('formattaData: da ISO a gg/mm/aaaa', () => {
    expect(formattaData('2026-07-20')).toBe('20/07/2026');
  });
  it('percentuale: intero senza decimali, altrimenti virgola', () => {
    expect(percentuale('0.00')).toBe('0');
    expect(percentuale('4.00')).toBe('4');
    expect(percentuale('4.50')).toBe('4,5');
    expect(percentuale('22.00')).toBe('22');
  });
  it('numeroDocumento: numero/anno', () => {
    expect(numeroDocumento('4', '2026-07-20')).toBe('4/2026');
  });
  it('capitalizza: da maiuscolo a Iniziali Maiuscole', () => {
    expect(capitalizza('VIA BEGLINI')).toBe('Via Beglini');
    expect(capitalizza('CINZIA LOIACONO')).toBe('Cinzia Loiacono');
  });
});

const FATTURA_BASE: Fattura = {
  tipoDocumento: 'TD01',
  dataIso: '2026-07-20',
  numero: '4',
  bolloCent: 200,
  emittente: { denominazione: 'X', indirizzo: '', cap: '', comune: '' },
  destinatario: { denominazione: 'Y', indirizzo: '', cap: '', comune: '' },
  linee: [{ descrizione: 'A', prezzoTotaleCent: 690000, aliquotaIva: '0.00' }],
  riepiloghi: [{ aliquotaIva: '0.00', natura: 'N2.2', imponibileCent: 717600, impostaCent: 0 }],
  casse: [{ tipo: 'TC04', aliquota: '4.00', importoCent: 27600 }],
  pagamenti: [],
};

describe('totali', () => {
  it('totaleOnorario somma le linee', () => {
    expect(totaleOnorario(FATTURA_BASE)).toBe(690000);
  });
  it('totaleImponibile somma i riepiloghi', () => {
    expect(totaleImponibile(FATTURA_BASE)).toBe(717600);
  });
  it('totaleDocumento usa ImportoTotaleDocumento se presente', () => {
    expect(totaleDocumento({ ...FATTURA_BASE, totaleDocumentoCent: 717800 })).toBe(717800);
  });
  it('totaleDocumento altrimenti calcola imponibile + imposte + bollo', () => {
    expect(totaleDocumento(FATTURA_BASE)).toBe(717800);
  });
});
```

- [ ] **Step 3: Esegui i test e verifica che falliscano**

Run: `npm test`
Expected: FAIL, `Cannot find module '@/lib/calcoli'` (o simile).

- [ ] **Step 4: Crea `lib/calcoli.ts`**

```ts
import type { Fattura } from './tipi';

/** "6900.00" → 690000. Solo aritmetica su stringhe: niente virgola mobile. */
export function centesimi(importo: string): number {
  const m = importo.trim().match(/^(-?)(\d+)(?:\.(\d{1,2})\d*)?$/);
  if (!m) throw new Error(`Importo non valido: "${importo}"`);
  const segno = m[1] === '-' ? -1 : 1;
  const decimali = (m[3] ?? '').padEnd(2, '0');
  return segno * (parseInt(m[2], 10) * 100 + parseInt(decimali, 10));
}

/** 696800 → "6.968,00" */
export function numeroIt(cent: number): string {
  const negativo = cent < 0 ? '-' : '';
  const assoluto = Math.abs(cent);
  const intero = Math.floor(assoluto / 100)
    .toString()
    .replace(/\B(?=(\d{3})+(?!\d))/g, '.');
  const decimali = (assoluto % 100).toString().padStart(2, '0');
  return `${negativo}${intero},${decimali}`;
}

/** 690000 → "€ 6.900,00" */
export function euro(cent: number): string {
  return `€ ${numeroIt(cent)}`;
}

/** "2026-07-20" → "20/07/2026" */
export function formattaData(iso: string): string {
  const m = iso.match(/^(\d{4})-(\d{2})-(\d{2})/);
  return m ? `${m[3]}/${m[2]}/${m[1]}` : iso;
}

/** "4.00" → "4"; "4.50" → "4,5" */
export function percentuale(valore: string): string {
  const n = parseFloat(valore);
  if (Number.isNaN(n)) return valore;
  return n.toLocaleString('it-IT', { maximumFractionDigits: 2 });
}

/** ("4", "2026-07-20") → "4/2026" */
export function numeroDocumento(numero: string, dataIso: string): string {
  return `${numero}/${dataIso.slice(0, 4)}`;
}

/** "VIA BEGLINI" → "Via Beglini" (anche dopo apostrofo e trattino) */
export function capitalizza(testo: string): string {
  return testo.toLowerCase().replace(/(^|[\s\-'])\p{L}/gu, (c) => c.toUpperCase());
}

export function totaleOnorario(f: Fattura): number {
  return f.linee.reduce((somma, l) => somma + l.prezzoTotaleCent, 0);
}

export function totaleImponibile(f: Fattura): number {
  return f.riepiloghi.reduce((somma, r) => somma + r.imponibileCent, 0);
}

export function totaleDocumento(f: Fattura): number {
  if (f.totaleDocumentoCent != null) return f.totaleDocumentoCent;
  const imposte = f.riepiloghi.reduce((somma, r) => somma + r.impostaCent, 0);
  return totaleImponibile(f) + imposte + (f.bolloCent ?? 0);
}
```

- [ ] **Step 5: Esegui i test e verifica che passino**

Run: `npm test`
Expected: PASS, tutti verdi.

- [ ] **Step 6: Commit**

```bash
git add lib/ test/
git commit -m "feat: modello dati fattura e calcoli con importi in centesimi"
```

---

### Task 3: Tabelle di decodifica FatturaPA (`lib/mappe.ts`)

**Files:**
- Create: `lib/mappe.ts`
- Test: `test/mappe.test.ts`

**Interfaces:**
- Consumes: niente.
- Produces (usati dal Task 5): `etichettaTipoDocumento(codice: string): string` (maiuscolo, es. "FATTURA"), `descrizioneModalitaPagamento(codice: string): string`, `descrizioneCassa(codice: string): string`, `descrizioneNatura(natura: string, regimeFiscale?: string): string`. Tutte con fallback: codice sconosciuto → restituiscono il codice stesso.

- [ ] **Step 1: Scrivi i test in `test/mappe.test.ts`**

```ts
import { describe, expect, it } from 'vitest';
import {
  descrizioneCassa, descrizioneModalitaPagamento, descrizioneNatura, etichettaTipoDocumento,
} from '@/lib/mappe';

describe('mappe FatturaPA', () => {
  it('tipo documento in maiuscolo', () => {
    expect(etichettaTipoDocumento('TD01')).toBe('FATTURA');
    expect(etichettaTipoDocumento('TD06')).toBe('PARCELLA');
    expect(etichettaTipoDocumento('TD99')).toBe('TD99');
  });
  it('modalità di pagamento', () => {
    expect(descrizioneModalitaPagamento('MP05')).toBe('Bonifico Bancario');
    expect(descrizioneModalitaPagamento('MP08')).toBe('Carta di pagamento');
    expect(descrizioneModalitaPagamento('MP99')).toBe('MP99');
  });
  it('cassa previdenziale (dicitura INARCASSA identica al template)', () => {
    expect(descrizioneCassa('TC04')).toBe(
      'Cassa di previdenza e assistenza per gli ingegneri e architetti (INARCASSA)',
    );
    expect(descrizioneCassa('TC22')).toBe('INPS');
  });
  it('natura: caso speciale forfettario RF19 + N2.2', () => {
    expect(descrizioneNatura('N2.2', 'RF19')).toBe(
      "Operazione non soggetta a IVA ai sensi dell'art. 1, commi 54-89, Legge n. 190/2014 e succ. modifiche/integrazioni",
    );
  });
  it('natura: casi generali', () => {
    expect(descrizioneNatura('N2.2')).toBe('Operazione non soggetta a IVA');
    expect(descrizioneNatura('N1')).toBe('Escluso Art.15');
    expect(descrizioneNatura('N4')).toBe('Operazione esente');
    expect(descrizioneNatura('N9')).toBe('N9');
  });
});
```

- [ ] **Step 2: Esegui i test e verifica che falliscano**

Run: `npm test`
Expected: FAIL, `Cannot find module '@/lib/mappe'`.

- [ ] **Step 3: Crea `lib/mappe.ts`**

```ts
const TIPI_DOCUMENTO: Record<string, string> = {
  TD01: 'Fattura',
  TD02: 'Acconto/anticipo su fattura',
  TD03: 'Acconto/anticipo su parcella',
  TD04: 'Nota di credito',
  TD05: 'Nota di debito',
  TD06: 'Parcella',
  TD16: 'Integrazione fattura reverse charge interno',
  TD17: "Integrazione/autofattura per acquisto servizi dall'estero",
  TD18: 'Integrazione per acquisto di beni intracomunitari',
  TD19: 'Integrazione/autofattura per acquisto di beni ex art.17 c.2 DPR 633/72',
  TD20: 'Autofattura per regolarizzazione e integrazione delle fatture',
  TD21: 'Autofattura per splafonamento',
  TD22: 'Estrazione beni da Deposito IVA',
  TD23: "Estrazione beni da Deposito IVA con versamento dell'IVA",
  TD24: 'Fattura differita art.21 c.4 lett. a)',
  TD25: 'Fattura differita art.21 c.4 terzo periodo lett. b)',
  TD26: 'Cessione di beni ammortizzabili e per passaggi interni',
  TD27: 'Fattura per autoconsumo o per cessioni gratuite senza rivalsa',
  TD28: 'Acquisti da San Marino con IVA (fattura cartacea)',
};

const MODALITA_PAGAMENTO: Record<string, string> = {
  MP01: 'Contanti',
  MP02: 'Assegno',
  MP03: 'Assegno circolare',
  MP04: 'Contanti presso Tesoreria',
  MP05: 'Bonifico Bancario',
  MP06: 'Vaglia cambiario',
  MP07: 'Bollettino bancario',
  MP08: 'Carta di pagamento',
  MP09: 'RID',
  MP10: 'RID utenze',
  MP11: 'RID veloce',
  MP12: 'RIBA',
  MP13: 'MAV',
  MP14: 'Quietanza erario',
  MP15: 'Giroconto su conti di contabilità speciale',
  MP16: 'Domiciliazione bancaria',
  MP17: 'Domiciliazione postale',
  MP18: 'Bollettino di c/c postale',
  MP19: 'SEPA Direct Debit',
  MP20: 'SEPA Direct Debit CORE',
  MP21: 'SEPA Direct Debit B2B',
  MP22: 'Trattenuta su somme già riscosse',
  MP23: 'PagoPA',
};

const TIPI_CASSA: Record<string, string> = {
  TC01: 'Cassa nazionale previdenza e assistenza avvocati e procuratori legali',
  TC02: 'Cassa previdenza dottori commercialisti',
  TC03: 'Cassa previdenza e assistenza geometri',
  TC04: 'Cassa di previdenza e assistenza per gli ingegneri e architetti (INARCASSA)',
  TC05: 'Cassa nazionale del notariato',
  TC06: 'Cassa nazionale previdenza e assistenza ragionieri e periti commerciali',
  TC07: 'Ente nazionale assistenza agenti e rappresentanti di commercio (ENASARCO)',
  TC08: 'Ente nazionale previdenza e assistenza consulenti del lavoro (ENPACL)',
  TC09: 'Ente nazionale previdenza e assistenza medici (ENPAM)',
  TC10: 'Ente nazionale previdenza e assistenza farmacisti (ENPAF)',
  TC11: 'Ente nazionale previdenza e assistenza veterinari (ENPAV)',
  TC12: "Ente nazionale previdenza e assistenza impiegati dell'agricoltura (ENPAIA)",
  TC13: 'Fondo previdenza impiegati imprese di spedizione e agenzie marittime',
  TC14: 'Istituto nazionale previdenza giornalisti italiani (INPGI)',
  TC15: 'Opera nazionale assistenza orfani sanitari italiani (ONAOSI)',
  TC16: 'Cassa autonoma assistenza integrativa giornalisti italiani (CASAGIT)',
  TC17: 'Ente previdenza periti industriali e periti industriali laureati (EPPI)',
  TC18: 'Ente previdenza e assistenza pluricategoriale (EPAP)',
  TC19: 'Ente nazionale previdenza e assistenza biologi (ENPAB)',
  TC20: 'Ente nazionale previdenza e assistenza professione infermieristica (ENPAPI)',
  TC21: 'Ente nazionale previdenza e assistenza psicologi (ENPAP)',
  TC22: 'INPS',
};

const NATURE: Record<string, string> = {
  N1: 'Escluso Art.15',
  N2: 'Operazione non soggetta a IVA',
  'N2.1': 'Operazione non soggetta a IVA ai sensi degli artt. da 7 a 7-septies del DPR 633/72',
  'N2.2': 'Operazione non soggetta a IVA',
  N3: 'Operazione non imponibile',
  'N3.1': 'Non imponibile - esportazioni',
  'N3.2': 'Non imponibile - cessioni intracomunitarie',
  'N3.3': 'Non imponibile - cessioni verso San Marino',
  'N3.4': "Non imponibile - operazioni assimilate alle cessioni all'esportazione",
  'N3.5': "Non imponibile - a seguito di dichiarazioni d'intento",
  'N3.6': 'Non imponibile - altre operazioni che non concorrono alla formazione del plafond',
  N4: 'Operazione esente',
  N5: 'Regime del margine / IVA non esposta in fattura',
  N6: 'Inversione contabile',
  'N6.1': 'Inversione contabile - cessione di rottami e altri materiali di recupero',
  'N6.2': 'Inversione contabile - cessione di oro e argento puro',
  'N6.3': 'Inversione contabile - subappalto nel settore edile',
  'N6.4': 'Inversione contabile - cessione di fabbricati',
  'N6.5': 'Inversione contabile - cessione di telefoni cellulari',
  'N6.6': 'Inversione contabile - cessione di prodotti elettronici',
  'N6.7': 'Inversione contabile - prestazioni comparto edile e settori connessi',
  'N6.8': 'Inversione contabile - operazioni settore energetico',
  'N6.9': 'Inversione contabile - altri casi',
  N7: 'IVA assolta in altro stato UE',
};

const NATURA_FORFETTARIO =
  "Operazione non soggetta a IVA ai sensi dell'art. 1, commi 54-89, Legge n. 190/2014 e succ. modifiche/integrazioni";

export function etichettaTipoDocumento(codice: string): string {
  return (TIPI_DOCUMENTO[codice] ?? codice).toUpperCase();
}

export function descrizioneModalitaPagamento(codice: string): string {
  return MODALITA_PAGAMENTO[codice] ?? codice;
}

export function descrizioneCassa(codice: string): string {
  return TIPI_CASSA[codice] ?? codice;
}

export function descrizioneNatura(natura: string, regimeFiscale?: string): string {
  if (regimeFiscale === 'RF19' && natura === 'N2.2') return NATURA_FORFETTARIO;
  return NATURE[natura] ?? natura;
}
```

- [ ] **Step 4: Esegui i test e verifica che passino**

Run: `npm test`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add lib/mappe.ts test/mappe.test.ts
git commit -m "feat: tabelle di decodifica codici FatturaPA"
```

---

### Task 4: Parser XML FatturaPA (`lib/parser.ts`)

**Files:**
- Create: `lib/parser.ts`
- Test: `test/parser.test.ts` (fixture: il file reale `template/IT01511230086_AAAAA.xml`)

**Interfaces:**
- Consumes: `centesimi` da `@/lib/calcoli`; tipi da `@/lib/tipi`.
- Produces (usati dai Task 5 e 6): `parseFatture(xml: string): Fattura[]` (una `Fattura` per ogni `FatturaElettronicaBody`) e `class ErroreFattura extends Error` (errori "di dominio" da mostrare all'utente con status 400).

- [ ] **Step 1: Scrivi i test in `test/parser.test.ts`**

```ts
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { ErroreFattura, parseFatture } from '@/lib/parser';

const XML = readFileSync(
  path.join(process.cwd(), 'template', 'IT01511230086_AAAAA.xml'),
  'utf8',
);

describe('parseFatture con la fattura di esempio', () => {
  const f = parseFatture(XML)[0];

  it('dati documento', () => {
    expect(f.tipoDocumento).toBe('TD01');
    expect(f.numero).toBe('4');
    expect(f.dataIso).toBe('2026-07-20');
    expect(f.totaleDocumentoCent).toBe(717800);
    expect(f.bolloCent).toBe(200);
    expect(f.regimeFiscale).toBe('RF19');
  });

  it('emittente (P.IVA come stringa: zeri iniziali conservati)', () => {
    expect(f.emittente.denominazione).toBe('CINZIA LOIACONO');
    expect(f.emittente.partitaIva).toBe('01511230086');
    expect(f.emittente.codiceFiscale).toBe('LCNCNZ84H68F965B');
    expect(f.emittente.indirizzo).toBe('VIA BEGLINI');
    expect(f.emittente.civico).toBe('34');
    expect(f.emittente.cap).toBe('18018');
    expect(f.emittente.comune).toBe('TAGGIA');
    expect(f.emittente.provincia).toBe('IM');
  });

  it('destinatario', () => {
    expect(f.destinatario.denominazione).toBe("CALVI CESCHIA VIGANO' ARCHITETTI ASSOCIATI");
    expect(f.destinatario.partitaIva).toBe('01680080080');
    expect(f.destinatario.email).toBeUndefined();
  });

  it('linee', () => {
    expect(f.linee).toHaveLength(1);
    expect(f.linee[0].prezzoTotaleCent).toBe(690000);
    expect(f.linee[0].aliquotaIva).toBe('0.00');
    expect(f.linee[0].descrizione).toContain('Onorario per prestazioni professionali');
  });

  it('riepiloghi, cassa, pagamenti', () => {
    expect(f.riepiloghi).toEqual([
      { aliquotaIva: '0.00', natura: 'N2.2', imponibileCent: 717600, impostaCent: 0 },
    ]);
    expect(f.casse).toEqual([{ tipo: 'TC04', aliquota: '4.00', importoCent: 27600 }]);
    expect(f.pagamenti).toEqual([
      {
        modalita: 'MP05',
        iban: 'IT95C0366901600890070575475',
        beneficiario: 'Cinzia Loiacono',
        dataIso: '2026-07-21',
        importoCent: 717800,
      },
    ]);
  });
});

describe('errori', () => {
  it('XML non valido', () => {
    expect(() => parseFatture('non sono xml <<<')).toThrow(ErroreFattura);
  });
  it('XML valido ma non FatturaPA', () => {
    expect(() => parseFatture('<?xml version="1.0"?><altro/>')).toThrow(
      /non sembra una Fattura Elettronica/,
    );
  });
});
```

- [ ] **Step 2: Esegui i test e verifica che falliscano**

Run: `npm test`
Expected: FAIL, `Cannot find module '@/lib/parser'`.

- [ ] **Step 3: Crea `lib/parser.ts`**

```ts
import { XMLParser } from 'fast-xml-parser';
import { centesimi } from './calcoli';
import type { Cassa, Fattura, Linea, Pagamento, Riepilogo, Soggetto } from './tipi';

/** Errore "di dominio": il messaggio è pensato per essere mostrato all'utente. */
export class ErroreFattura extends Error {}

// Nodi che possono ripetersi: senza questo, con un solo elemento
// fast-xml-parser restituirebbe un oggetto invece di un array.
const SEMPRE_ARRAY = new Set([
  'FatturaElettronicaBody',
  'DettaglioLinee',
  'DatiRiepilogo',
  'DatiPagamento',
  'DettaglioPagamento',
  'DatiCassaPrevidenziale',
  'Causale',
]);

export function parseFatture(xml: string): Fattura[] {
  let documento: Record<string, unknown>;
  try {
    documento = new XMLParser({
      removeNSPrefix: true,
      parseTagValue: false, // tutto stringhe: mai perdere zeri iniziali di P.IVA/CAP
      isArray: (nome) => SEMPRE_ARRAY.has(nome),
    }).parse(xml);
  } catch {
    throw new ErroreFattura('Il file non è un XML valido.');
  }

  const radice = documento?.FatturaElettronica as Record<string, unknown> | undefined;
  if (!radice) {
    throw new ErroreFattura(
      'Il file non sembra una Fattura Elettronica (manca il nodo FatturaElettronica).',
    );
  }
  const intestazione = radice.FatturaElettronicaHeader as Record<string, unknown> | undefined;
  const corpi = radice.FatturaElettronicaBody as Record<string, unknown>[] | undefined;
  if (!intestazione || !corpi?.length) {
    throw new ErroreFattura('Fattura Elettronica incompleta: mancano header o body.');
  }

  const emittente = soggetto(intestazione.CedentePrestatore);
  const destinatario = soggetto(intestazione.CessionarioCommittente);
  const trasmissione = intestazione.DatiTrasmissione as Record<string, unknown> | undefined;
  if (trasmissione?.PECDestinatario) destinatario.email = testo(trasmissione.PECDestinatario);
  const anagraficiCedente = (intestazione.CedentePrestatore as Record<string, unknown> | undefined)
    ?.DatiAnagrafici as Record<string, unknown> | undefined;
  const regimeFiscale = testo(anagraficiCedente?.RegimeFiscale) || undefined;

  return corpi.map((corpo) => fattura(corpo, emittente, destinatario, regimeFiscale));
}

function testo(valore: unknown): string {
  return valore == null ? '' : String(valore).trim();
}

function soggetto(nodo: unknown): Soggetto {
  const n = (nodo ?? {}) as Record<string, any>;
  const dati = n.DatiAnagrafici ?? {};
  const anagrafica = dati.Anagrafica ?? {};
  const sede = n.Sede ?? {};
  const denominazione = anagrafica.Denominazione
    ? testo(anagrafica.Denominazione)
    : [testo(anagrafica.Nome), testo(anagrafica.Cognome)].filter(Boolean).join(' ');
  return {
    denominazione,
    indirizzo: testo(sede.Indirizzo),
    civico: testo(sede.NumeroCivico) || undefined,
    cap: testo(sede.CAP),
    comune: testo(sede.Comune),
    provincia: testo(sede.Provincia) || undefined,
    partitaIva: testo(dati.IdFiscaleIVA?.IdCodice) || undefined,
    codiceFiscale: testo(dati.CodiceFiscale) || undefined,
  };
}

function fattura(
  corpo: Record<string, any>,
  emittente: Soggetto,
  destinatario: Soggetto,
  regimeFiscale?: string,
): Fattura {
  const generali = corpo?.DatiGenerali?.DatiGeneraliDocumento;
  if (!generali) {
    throw new ErroreFattura('Fattura Elettronica incompleta: mancano i dati generali del documento.');
  }
  const beni = corpo?.DatiBeniServizi ?? {};
  const dataIso = testo(generali.Data);

  const linee: Linea[] = (beni.DettaglioLinee ?? []).map((l: any) => ({
    descrizione: testo(l.Descrizione),
    prezzoTotaleCent: centesimi(testo(l.PrezzoTotale ?? '0')),
    aliquotaIva: testo(l.AliquotaIVA ?? '0'),
  }));

  const riepiloghi: Riepilogo[] = (beni.DatiRiepilogo ?? []).map((r: any) => ({
    aliquotaIva: testo(r.AliquotaIVA ?? '0'),
    natura: testo(r.Natura) || undefined,
    imponibileCent: centesimi(testo(r.ImponibileImporto ?? '0')),
    impostaCent: centesimi(testo(r.Imposta ?? '0')),
  }));

  const casse: Cassa[] = (generali.DatiCassaPrevidenziale ?? []).map((c: any) => ({
    tipo: testo(c.TipoCassa),
    aliquota: testo(c.AlCassa ?? '0'),
    importoCent: centesimi(testo(c.ImportoContributoCassa ?? '0')),
  }));

  const pagamenti: Pagamento[] = (corpo.DatiPagamento ?? []).flatMap((dp: any) =>
    (dp.DettaglioPagamento ?? []).map((d: any) => ({
      modalita: testo(d.ModalitaPagamento) || undefined,
      iban: testo(d.IBAN) || undefined,
      beneficiario: testo(d.Beneficiario) || undefined,
      dataIso:
        testo(d.DataScadenzaPagamento) ||
        testo(d.DataRiferimentoTerminiPagamento) ||
        dataIso ||
        undefined,
      importoCent: centesimi(testo(d.ImportoPagamento ?? '0')),
    })),
  );

  // Bollo: se c'è l'importo lo usiamo; se c'è solo BolloVirtuale=SI, vale 2 €.
  const bollo = generali.DatiBollo;
  let bolloCent: number | undefined;
  if (bollo?.ImportoBollo != null) bolloCent = centesimi(testo(bollo.ImportoBollo));
  else if (testo(bollo?.BolloVirtuale).toUpperCase() === 'SI') bolloCent = 200;

  return {
    tipoDocumento: testo(generali.TipoDocumento) || 'TD01',
    dataIso,
    numero: testo(generali.Numero),
    totaleDocumentoCent:
      generali.ImportoTotaleDocumento != null
        ? centesimi(testo(generali.ImportoTotaleDocumento))
        : undefined,
    bolloCent,
    regimeFiscale,
    emittente,
    destinatario,
    linee,
    riepiloghi,
    casse,
    pagamenti,
  };
}
```

- [ ] **Step 4: Esegui i test e verifica che passino**

Run: `npm test`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add lib/parser.ts test/parser.test.ts
git commit -m "feat: parser XML FatturaPA verso il modello dati"
```

---

### Task 5: Profilo emittente, font e template PDF (`components/FatturaPDF.tsx`)

**Files:**
- Create: `config/emittente.ts`, `lib/fonts.ts`, `components/FatturaPDF.tsx`
- Test: `test/pdf.test.ts`

**Interfaces:**
- Consumes: tutto dai Task 2, 3, 4 (`Fattura`, calcoli, mappe, `parseFatture` nei test).
- Produces (usati dal Task 6): `FatturaPDF({ fatture }: { fatture: Fattura[] })` componente react-pdf `<Document>`; `EMITTENTE` (`titolo`, `email`, `cellulare`, `note`); `registraFont(): void` idempotente.

- [ ] **Step 1: Crea `config/emittente.ts`**

Nota: l'email nel PDF originale era "loiacono.cinzia@gmail.con", refuso evidente: usiamo `.com`.

```ts
// Dati dell'emittente NON presenti nell'XML FatturaPA.
// Oggi hardcoded per Cinzia; in un futuro multi-utente questo diventa
// il record salvato per ogni utente.
export const EMITTENTE = {
  titolo: 'Architetto',
  email: 'loiacono.cinzia@gmail.com',
  cellulare: '0039 329 715 98 48',
  note:
    "Documento privo di valenza fiscale ai sensi dell'art. 21 Dpr 633/72. " +
    "L'originale è disponibile all'indirizzo telematico da Lei fornito " +
    "oppure nella Sua area riservata dell'Agenzia delle Entrate.",
};
```

- [ ] **Step 2: Crea `lib/fonts.ts`**

```ts
import { Font } from '@react-pdf/renderer';
import path from 'node:path';

let registrati = false;

export function registraFont(): void {
  if (registrati) return;
  registrati = true;
  const cartella = path.join(process.cwd(), 'fonts');
  Font.register({
    family: 'Trebuchet',
    fonts: [
      { src: path.join(cartella, 'TrebuchetMS.ttf') },
      { src: path.join(cartella, 'TrebuchetMS-Bold.ttf'), fontWeight: 'bold' },
      { src: path.join(cartella, 'TrebuchetMS-Italic.ttf'), fontStyle: 'italic' },
      {
        src: path.join(cartella, 'TrebuchetMS-BoldItalic.ttf'),
        fontWeight: 'bold',
        fontStyle: 'italic',
      },
    ],
  });
  // Niente sillabazione: il template originale non spezza le parole.
  Font.registerHyphenationCallback((parola) => [parola]);
}
```

- [ ] **Step 3: Scrivi il test in `test/pdf.test.ts`**

```ts
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { renderToBuffer } from '@react-pdf/renderer';
import { createElement } from 'react';
import { describe, expect, it } from 'vitest';
import { FatturaPDF } from '@/components/FatturaPDF';
import { parseFatture } from '@/lib/parser';

const XML = readFileSync(
  path.join(process.cwd(), 'template', 'IT01511230086_AAAAA.xml'),
  'utf8',
);

describe('FatturaPDF', () => {
  it('genera un PDF valido dalla fattura di esempio', async () => {
    const fatture = parseFatture(XML);
    const buffer = await renderToBuffer(createElement(FatturaPDF, { fatture }) as never);
    expect(buffer.subarray(0, 5).toString()).toBe('%PDF-');
    expect(buffer.length).toBeGreaterThan(10_000); // font incorporati: non può essere minuscolo
  });

  it('genera un PDF anche senza bollo, cassa e pagamenti', async () => {
    const [f] = parseFatture(XML);
    const spoglia = { ...f, bolloCent: undefined, casse: [], pagamenti: [], riepiloghi: [] };
    const buffer = await renderToBuffer(createElement(FatturaPDF, { fatture: [spoglia] }) as never);
    expect(buffer.subarray(0, 5).toString()).toBe('%PDF-');
  });
});
```

- [ ] **Step 4: Esegui il test e verifica che fallisca**

Run: `npm test`
Expected: FAIL, `Cannot find module '@/components/FatturaPDF'`.

- [ ] **Step 5: Crea `components/FatturaPDF.tsx`**

Le misure (pt) vengono dalla spec: colonne tabella 453,9 / 57,8 / 26,9; colonna sinistra blocco basso 256,2; bordi 0,75 e 0,59.

```tsx
import { Document, Page, StyleSheet, Text, View } from '@react-pdf/renderer';
import { EMITTENTE } from '@/config/emittente';
import {
  capitalizza, euro, formattaData, numeroDocumento, numeroIt, percentuale,
  totaleDocumento, totaleImponibile, totaleOnorario,
} from '@/lib/calcoli';
import { registraFont } from '@/lib/fonts';
import {
  descrizioneCassa, descrizioneModalitaPagamento, descrizioneNatura, etichettaTipoDocumento,
} from '@/lib/mappe';
import type { Fattura, Soggetto } from '@/lib/tipi';

const ETICHETTA = '#828282';
const TESTO = '#6a6a6a';
const BORDO = '#cecece';
const RIGA_ALTERNATA = '#ededed';

const stili = StyleSheet.create({
  pagina: {
    fontFamily: 'Trebuchet',
    fontSize: 9,
    color: TESTO,
    paddingTop: 30,
    paddingHorizontal: 28.3,
    paddingBottom: 48,
  },
  bold: { fontWeight: 'bold' },

  header: { textAlign: 'right', color: ETICHETTA, fontSize: 8.25 },
  headerFattura: { fontSize: 10.5, marginTop: 10 },
  separatoreHeader: { borderBottomWidth: 0.75, borderBottomColor: BORDO, marginTop: 26 },

  contraenti: { flexDirection: 'row', marginTop: 9 },
  contraentiSx: { flex: 1 },
  contraentiDx: { width: 255 },
  etichettaMedia: { fontSize: 6.75, color: ETICHETTA, marginBottom: 3 },
  rigaAnagrafica: { marginBottom: 2.5 },

  tabella: { marginTop: 38, borderBottomWidth: 0.75, borderBottomColor: BORDO },
  rigaTabella: { flexDirection: 'row' },
  intestazioneTabella: {
    borderTopWidth: 0.75,
    borderBottomWidth: 0.75,
    borderColor: BORDO,
    fontSize: 6,
    color: ETICHETTA,
  },
  cellaDescrizione: { width: 453.9, paddingVertical: 6, paddingLeft: 7.5, paddingRight: 10 },
  cellaImporto: {
    width: 57.8,
    borderLeftWidth: 0.75,
    borderLeftColor: BORDO,
    paddingVertical: 6,
    paddingRight: 6.4,
    textAlign: 'right',
  },
  cellaIva: {
    width: 26.9,
    borderLeftWidth: 0.75,
    borderLeftColor: BORDO,
    paddingVertical: 6,
    textAlign: 'center',
  },
  cellaIntestazione: { paddingVertical: 4.5, fontWeight: 'bold' },

  note: { marginTop: 18 },
  testoNote: { marginTop: 3 },

  spaziatore: { flexGrow: 1 },

  bloccoBasso: { borderTopWidth: 0.59, borderBottomWidth: 0.59, borderColor: BORDO, fontSize: 6.53 },
  rigaBassa: { flexDirection: 'row' },
  separatoreBasso: { borderTopWidth: 0.59, borderTopColor: BORDO },
  cellaBassaSx: {
    width: 256.2,
    borderRightWidth: 0.59,
    borderRightColor: BORDO,
    paddingVertical: 7,
    paddingRight: 10,
  },
  cellaBassaDx: { flex: 1, paddingVertical: 7, paddingLeft: 9 },
  quadranteAlto: { minHeight: 75 },
  quadranteBasso: { minHeight: 112 },
  etichettaBassa: { fontSize: 5.34, color: ETICHETTA, marginBottom: 3 },
  rigaPagamento: { marginBottom: 1.5 },

  rigaRiepilogo: { flexDirection: 'row', marginBottom: 3 },
  riepilogoDescrizione: { flex: 1, fontStyle: 'italic', paddingRight: 6 },
  riepilogoImponibile: { width: 55, textAlign: 'right' },
  riepilogoImposta: { width: 32, textAlign: 'right' },

  rigaTotale: { flexDirection: 'row', justifyContent: 'flex-end', marginBottom: 2.5 },
  etichettaTotale: { flex: 1, textAlign: 'right' },
  valoreTotale: { width: 44, textAlign: 'right' },
  totaleGrande: {
    fontSize: 17.81,
    fontWeight: 'bold',
    color: ETICHETTA,
    textAlign: 'right',
    marginTop: 5,
  },

  piede: {
    position: 'absolute',
    bottom: 15,
    left: 28.3,
    right: 28.3,
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'flex-end',
    fontSize: 7.5,
    color: ETICHETTA,
  },
  piedeDx: { textAlign: 'right' },
});

function indirizzoEmittente(s: Soggetto): string {
  const via = [capitalizza(s.indirizzo), s.civico].filter(Boolean).join(' ');
  const provincia = s.provincia ? ` (${s.provincia})` : '';
  return `${via} - ${s.cap} - ${capitalizza(s.comune)}${provincia}`;
}

function PaginaFattura({ f }: { f: Fattura }) {
  const etichettaDoc = etichettaTipoDocumento(f.tipoDocumento);
  const numDoc = numeroDocumento(f.numero, f.dataIso);
  const dataDoc = formattaData(f.dataIso);
  const nomeEmittente = `${EMITTENTE.titolo} ${capitalizza(f.emittente.denominazione)}`;
  const pagamento = f.pagamenti[0];

  const righe = [
    ...f.linee.map((l) => ({
      descrizione: l.descrizione,
      importo: euro(l.prezzoTotaleCent),
      aliquota: percentuale(l.aliquotaIva),
    })),
    ...(f.bolloCent != null
      ? [{ descrizione: 'Marca da bollo', importo: euro(f.bolloCent), aliquota: '0' }]
      : []),
  ];

  const righeRiepilogo = [
    ...f.riepiloghi.map((r) => ({
      descrizione: r.natura
        ? `${percentuale(r.aliquotaIva)}% - ${descrizioneNatura(r.natura, f.regimeFiscale)}`
        : `${percentuale(r.aliquotaIva)}%`,
      imponibile: numeroIt(r.imponibileCent),
      imposta: euro(r.impostaCent),
    })),
    ...(f.bolloCent != null
      ? [{ descrizione: '0% - Escluso Art.15', imponibile: numeroIt(f.bolloCent), imposta: euro(0) }]
      : []),
  ];

  const totali: Array<[string, string]> = [
    ['Totale onorario', euro(totaleOnorario(f))],
    ...f.casse.map(
      (c): [string, string] => [
        `${descrizioneCassa(c.tipo)} ${percentuale(c.aliquota)}%`,
        euro(c.importoCent),
      ],
    ),
    ['Imponibile', euro(totaleImponibile(f))],
    ...(f.bolloCent != null
      ? ([
          ['Non imponibile', euro(f.bolloCent)],
          ['Marca da bollo', euro(f.bolloCent)],
        ] as Array<[string, string]>)
      : []),
  ];

  return (
    <Page size="A4" style={stili.pagina} wrap>
      <View style={stili.header}>
        <Text style={stili.bold}>{nomeEmittente}</Text>
        <Text>{indirizzoEmittente(f.emittente)}</Text>
        <Text>
          P.iva {f.emittente.partitaIva} - C.F. {f.emittente.codiceFiscale}
        </Text>
        <Text style={stili.headerFattura}>
          <Text style={stili.bold}>{etichettaDoc}</Text> nr.{' '}
          <Text style={stili.bold}>{numDoc}</Text> del <Text style={stili.bold}>{dataDoc}</Text>
        </Text>
      </View>

      <View style={stili.separatoreHeader} />

      <View style={stili.contraenti}>
        <View style={stili.contraentiSx}>
          {f.destinatario.partitaIva && (
            <Text style={stili.rigaAnagrafica}>
              <Text style={stili.bold}>P.IVA</Text> {f.destinatario.partitaIva}
            </Text>
          )}
          {f.destinatario.codiceFiscale && (
            <Text style={stili.rigaAnagrafica}>
              <Text style={stili.bold}>CF</Text> {f.destinatario.codiceFiscale}
            </Text>
          )}
          {f.destinatario.email && (
            <Text style={stili.rigaAnagrafica}>
              <Text style={stili.bold}>EMAIL</Text> {f.destinatario.email}
            </Text>
          )}
        </View>
        <View style={stili.contraentiDx}>
          <Text style={stili.etichettaMedia}>DESTINATARIO</Text>
          <Text style={[stili.rigaAnagrafica, stili.bold]}>{f.destinatario.denominazione}</Text>
          <Text style={stili.rigaAnagrafica}>
            {[f.destinatario.indirizzo, f.destinatario.civico].filter(Boolean).join(', ')}
          </Text>
          <Text>
            {f.destinatario.cap} {f.destinatario.comune}
            {f.destinatario.provincia ? ` (${f.destinatario.provincia})` : ''}
          </Text>
        </View>
      </View>

      <View style={stili.tabella}>
        <View style={[stili.rigaTabella, stili.intestazioneTabella]}>
          <Text style={[stili.cellaDescrizione, stili.cellaIntestazione]}>DESCRIZIONE</Text>
          <Text style={[stili.cellaImporto, stili.cellaIntestazione]}>IMPORTO</Text>
          <Text style={[stili.cellaIva, stili.cellaIntestazione]}>% IVA</Text>
        </View>
        {righe.map((r, i) => (
          <View
            key={i}
            style={[stili.rigaTabella, i % 2 === 0 ? { backgroundColor: RIGA_ALTERNATA } : {}]}
            wrap={false}
          >
            <Text style={stili.cellaDescrizione}>{r.descrizione}</Text>
            <Text style={stili.cellaImporto}>{r.importo}</Text>
            <Text style={stili.cellaIva}>{r.aliquota}</Text>
          </View>
        ))}
      </View>

      <View style={stili.note}>
        <Text style={stili.etichettaMedia}>NOTE</Text>
        <Text style={stili.testoNote}>{EMITTENTE.note}</Text>
      </View>

      <View style={stili.spaziatore} />

      <View style={stili.bloccoBasso} wrap={false}>
        <View style={stili.rigaBassa}>
          <View style={[stili.cellaBassaSx, stili.quadranteAlto]}>
            {pagamento && (
              <>
                <Text style={stili.etichettaBassa}>MODALITÀ DI PAGAMENTO</Text>
                {pagamento.modalita && (
                  <Text style={[stili.rigaPagamento, stili.bold]}>
                    {descrizioneModalitaPagamento(pagamento.modalita)}
                  </Text>
                )}
                {pagamento.iban && (
                  <Text style={stili.rigaPagamento}>
                    IBAN: <Text style={stili.bold}>{pagamento.iban}</Text>
                  </Text>
                )}
                {pagamento.beneficiario && (
                  <Text style={[stili.rigaPagamento, stili.bold]}>{pagamento.beneficiario}</Text>
                )}
              </>
            )}
          </View>
          <View style={[stili.cellaBassaDx, stili.quadranteAlto]}>
            {f.pagamenti.length > 0 && (
              <>
                <Text style={stili.etichettaBassa}>SCADENZE</Text>
                {f.pagamenti.map((p, i) => (
                  <Text key={i} style={stili.rigaPagamento}>
                    <Text style={stili.bold}>{p.dataIso ? `${formattaData(p.dataIso)}:` : ''}</Text>{' '}
                    {euro(p.importoCent)}
                  </Text>
                ))}
              </>
            )}
          </View>
        </View>
        <View style={[stili.rigaBassa, stili.separatoreBasso]}>
          <View style={[stili.cellaBassaSx, stili.quadranteBasso]}>
            {righeRiepilogo.length > 0 && (
              <>
                <View style={stili.rigaRiepilogo}>
                  <Text style={[stili.etichettaBassa, { flex: 1 }]}>RIEPILOGO IVA</Text>
                  <Text style={[stili.etichettaBassa, stili.riepilogoImponibile]}>IMPONIBILE</Text>
                  <Text style={[stili.etichettaBassa, stili.riepilogoImposta]}>IMPOSTE</Text>
                </View>
                {righeRiepilogo.map((r, i) => (
                  <View key={i} style={stili.rigaRiepilogo}>
                    <Text style={stili.riepilogoDescrizione}>{r.descrizione}</Text>
                    <Text style={stili.riepilogoImponibile}>{r.imponibile}</Text>
                    <Text style={stili.riepilogoImposta}>{r.imposta}</Text>
                  </View>
                ))}
              </>
            )}
          </View>
          <View style={[stili.cellaBassaDx, stili.quadranteBasso]}>
            {totali.map(([etichetta, valore], i) => (
              <View key={i} style={stili.rigaTotale}>
                <Text style={stili.etichettaTotale}>{etichetta}</Text>
                <Text style={stili.valoreTotale}>{valore}</Text>
              </View>
            ))}
            <Text style={stili.totaleGrande}>{euro(totaleDocumento(f))}</Text>
          </View>
        </View>
      </View>

      <View style={stili.piede} fixed>
        <Text
          render={({ pageNumber, totalPages }) =>
            `Fattura nr. ${numDoc} del ${dataDoc} - ${pageNumber} / ${totalPages}`
          }
        />
        <View style={stili.piedeDx}>
          <Text>
            <Text style={stili.bold}>{nomeEmittente}</Text> {EMITTENTE.email}
          </Text>
          <Text>cell: {EMITTENTE.cellulare}</Text>
        </View>
      </View>
    </Page>
  );
}

export function FatturaPDF({ fatture }: { fatture: Fattura[] }) {
  registraFont();
  return (
    <Document>
      {fatture.map((f, i) => (
        <PaginaFattura key={i} f={f} />
      ))}
    </Document>
  );
}
```

- [ ] **Step 6: Esegui i test e verifica che passino**

Run: `npm test`
Expected: PASS (tutti i file di test).

- [ ] **Step 7: Commit**

```bash
git add config/ lib/fonts.ts components/ test/pdf.test.ts
git commit -m "feat: template PDF fedele al layout FattureInCloud con Trebuchet MS"
```

---

### Task 6: API route `POST /api/genera-pdf`

**Files:**
- Create: `app/api/genera-pdf/route.ts`
- Test: `test/api.test.ts`

**Interfaces:**
- Consumes: `parseFatture`, `ErroreFattura` da `@/lib/parser`; `FatturaPDF` da `@/components/FatturaPDF`.
- Produces (usata dal Task 7): endpoint `POST /api/genera-pdf` che accetta `FormData` con campo `file`; risponde 200 con `application/pdf` e header `Content-Disposition: attachment; filename="Fattura_<numero>_<anno>.pdf"`, oppure 400/500 con JSON `{ "errore": "<messaggio in italiano>" }`.

- [ ] **Step 1: Scrivi i test in `test/api.test.ts`**

```ts
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { POST } from '@/app/api/genera-pdf/route';

const XML = readFileSync(
  path.join(process.cwd(), 'template', 'IT01511230086_AAAAA.xml'),
  'utf8',
);

function richiestaConFile(contenuto: string | null): Request {
  const form = new FormData();
  if (contenuto !== null) {
    form.append('file', new File([contenuto], 'fattura.xml', { type: 'text/xml' }));
  }
  return new Request('http://test/api/genera-pdf', { method: 'POST', body: form });
}

describe('POST /api/genera-pdf', () => {
  it('con un XML valido risponde con il PDF', async () => {
    const risposta = await POST(richiestaConFile(XML));
    expect(risposta.status).toBe(200);
    expect(risposta.headers.get('Content-Type')).toBe('application/pdf');
    expect(risposta.headers.get('Content-Disposition')).toBe(
      'attachment; filename="Fattura_4_2026.pdf"',
    );
    const corpo = Buffer.from(await risposta.arrayBuffer());
    expect(corpo.subarray(0, 5).toString()).toBe('%PDF-');
  });

  it('senza file risponde 400', async () => {
    const risposta = await POST(richiestaConFile(null));
    expect(risposta.status).toBe(400);
    expect((await risposta.json()).errore).toBe('Nessun file ricevuto.');
  });

  it('con un file non XML risponde 400 con messaggio in italiano', async () => {
    const risposta = await POST(richiestaConFile('ciao, non sono un xml <<<'));
    expect(risposta.status).toBe(400);
    expect((await risposta.json()).errore).toMatch(/XML valido|Fattura Elettronica/);
  });
});
```

- [ ] **Step 2: Esegui i test e verifica che falliscano**

Run: `npm test`
Expected: FAIL, `Cannot find module '@/app/api/genera-pdf/route'`.

- [ ] **Step 3: Crea `app/api/genera-pdf/route.ts`**

Niente JSX qui (Next accetta solo `route.ts`): si usa `createElement`.

```ts
import { renderToBuffer } from '@react-pdf/renderer';
import { createElement } from 'react';
import { FatturaPDF } from '@/components/FatturaPDF';
import { ErroreFattura, parseFatture } from '@/lib/parser';

export const runtime = 'nodejs';

export async function POST(richiesta: Request): Promise<Response> {
  try {
    const form = await richiesta.formData();
    const file = form.get('file');
    if (!(file instanceof File)) {
      return Response.json({ errore: 'Nessun file ricevuto.' }, { status: 400 });
    }

    const fatture = parseFatture(await file.text());
    const buffer = await renderToBuffer(createElement(FatturaPDF, { fatture }) as never);

    const anno = fatture[0].dataIso.slice(0, 4);
    const numero = fatture[0].numero.replace(/[^\w-]/g, '_');
    return new Response(new Uint8Array(buffer), {
      headers: {
        'Content-Type': 'application/pdf',
        'Content-Disposition': `attachment; filename="Fattura_${numero}_${anno}.pdf"`,
      },
    });
  } catch (errore) {
    if (errore instanceof ErroreFattura) {
      return Response.json({ errore: errore.message }, { status: 400 });
    }
    console.error(errore);
    return Response.json(
      { errore: 'Errore imprevisto durante la generazione del PDF.' },
      { status: 500 },
    );
  }
}
```

- [ ] **Step 4: Esegui i test e verifica che passino**

Run: `npm test`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add app/api/ test/api.test.ts
git commit -m "feat: API route che trasforma l'XML caricato in PDF"
```

---

### Task 7: Interfaccia web (`app/page.tsx`)

**Files:**
- Modify: `app/page.tsx` (sostituisce il placeholder), `app/globals.css` (sostituisce il minimo del Task 1)

**Interfaces:**
- Consumes: endpoint `POST /api/genera-pdf` del Task 6.
- Produces: pagina `/` completa (drag & drop, stato, errori, download automatico).

- [ ] **Step 1: Sostituisci `app/globals.css`**

```css
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: "Trebuchet MS", Verdana, sans-serif;
  background: #f4f4f4;
  color: #4a4a4a;
}

main {
  width: min(480px, 92vw);
  background: #fff;
  border: 1px solid #dcdcdc;
  border-radius: 10px;
  padding: 36px 32px;
  text-align: center;
}

h1 { font-size: 22px; margin: 0 0 6px; color: #6a6a6a; }

.sottotitolo { margin: 0 0 24px; font-size: 14px; color: #828282; }

.zona {
  border: 2px dashed #cecece;
  border-radius: 8px;
  padding: 40px 20px;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
  font-size: 14px;
  color: #828282;
}

.zona.attiva,
.zona:hover { border-color: #8a8a8a; background: #fafafa; }

.errore { margin-top: 16px; color: #b3261e; font-size: 13px; }

.stato { margin-top: 16px; color: #828282; font-size: 13px; }
```

- [ ] **Step 2: Sostituisci `app/page.tsx`**

```tsx
'use client';

import { useRef, useState } from 'react';

export default function Pagina() {
  const inputRef = useRef<HTMLInputElement>(null);
  const [trascinando, setTrascinando] = useState(false);
  const [generazione, setGenerazione] = useState(false);
  const [errore, setErrore] = useState<string | null>(null);

  async function genera(file: File) {
    setErrore(null);
    if (!file.name.toLowerCase().endsWith('.xml')) {
      setErrore('Seleziona un file .xml di Fattura Elettronica.');
      return;
    }
    setGenerazione(true);
    try {
      const dati = new FormData();
      dati.append('file', file);
      const risposta = await fetch('/api/genera-pdf', { method: 'POST', body: dati });
      if (!risposta.ok) {
        const corpo = await risposta.json().catch(() => null);
        setErrore(corpo?.errore ?? 'Errore durante la generazione del PDF.');
        return;
      }
      const nome =
        risposta.headers.get('Content-Disposition')?.match(/filename="(.+)"/)?.[1] ??
        'Fattura.pdf';
      const url = URL.createObjectURL(await risposta.blob());
      const collegamento = document.createElement('a');
      collegamento.href = url;
      collegamento.download = nome;
      collegamento.click();
      URL.revokeObjectURL(url);
    } catch {
      setErrore('Errore di rete durante la generazione del PDF.');
    } finally {
      setGenerazione(false);
    }
  }

  return (
    <main>
      <h1>Fattura XML → PDF</h1>
      <p className="sottotitolo">
        Carica la Fattura Elettronica (.xml) e scarica la fattura di cortesia in PDF.
      </p>
      <div
        className={`zona${trascinando ? ' attiva' : ''}`}
        onClick={() => inputRef.current?.click()}
        onDragOver={(e) => {
          e.preventDefault();
          setTrascinando(true);
        }}
        onDragLeave={() => setTrascinando(false)}
        onDrop={(e) => {
          e.preventDefault();
          setTrascinando(false);
          const file = e.dataTransfer.files[0];
          if (file) genera(file);
        }}
      >
        Trascina qui il file .xml
        <br />
        oppure clicca per sceglierlo
      </div>
      <input
        ref={inputRef}
        type="file"
        accept=".xml,text/xml"
        hidden
        onChange={(e) => {
          const file = e.target.files?.[0];
          if (file) genera(file);
          e.target.value = '';
        }}
      />
      {generazione && <p className="stato">Generazione del PDF in corso…</p>}
      {errore && <p className="errore">{errore}</p>}
    </main>
  );
}
```

- [ ] **Step 3: Verifica manuale con il server di sviluppo**

```bash
npm run dev &
sleep 5
curl -s -D - -o /tmp/fattura-generata.pdf \
  -F "file=@template/IT01511230086_AAAAA.xml" \
  http://localhost:3000/api/genera-pdf | head -5
```

Expected: `HTTP/1.1 200`, `Content-Type: application/pdf`, `Content-Disposition: attachment; filename="Fattura_4_2026.pdf"`. Poi apri `http://localhost:3000` nel browser: trascina l'XML di esempio e verifica che parta il download del PDF; prova anche un file non XML e verifica il messaggio di errore.

- [ ] **Step 4: Esegui tutti i test**

Run: `npm test`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add app/page.tsx app/globals.css
git commit -m "feat: pagina di caricamento con drag & drop e download del PDF"
```

---

### Task 8: Verifica visiva di fedeltà al template

**Files:**
- Nessun file nuovo obbligatorio; eventuali ritocchi a `components/FatturaPDF.tsx`.

**Interfaces:**
- Consumes: endpoint del Task 6, `template/fattura.pdf` come riferimento.
- Produces: PDF generato visivamente sovrapponibile al template (a meno delle differenze di dati tra le due fatture).

- [ ] **Step 1: Genera il PDF dalla fattura di esempio**

```bash
npm run dev &
sleep 5
curl -s -o /tmp/fattura-generata.pdf \
  -F "file=@template/IT01511230086_AAAAA.xml" \
  http://localhost:3000/api/genera-pdf
```

Expected: file `/tmp/fattura-generata.pdf` creato.

- [ ] **Step 2: Verifica i font incorporati**

Run: `pdffonts /tmp/fattura-generata.pdf`
Expected: solo face `TrebuchetMS` (regular/bold/italic), tutte con `emb: yes`. Nessun font standard react-pdf (Helvetica).

- [ ] **Step 3: Confronto affiancato con il template**

```bash
python3 - <<'EOF'
import fitz
generata = fitz.open('/tmp/fattura-generata.pdf')
originale = fitz.open('template/fattura.pdf')
zoom = fitz.Matrix(2, 2)
pg = generata[0].get_pixmap(matrix=zoom)
po = originale[0].get_pixmap(matrix=zoom)
unione = fitz.Pixmap(fitz.csRGB, fitz.IRect(0, 0, pg.width + po.width, max(pg.height, po.height)))
unione.clear_with(255)
unione.copy(po, fitz.IRect(0, 0, po.width, po.height))
pg.set_origin(po.width, 0)
unione.copy(pg, fitz.IRect(po.width, 0, po.width + pg.width, pg.height))
unione.save('/tmp/confronto.png')
print('salvato /tmp/confronto.png')
EOF
```

Poi guarda `/tmp/confronto.png` (originale a sinistra, generata a destra) e verifica ogni sezione: header, box destinatario, tabella (bordi, sfondo alternato, allineamenti), NOTE, blocco basso (quadranti, riepilogo, totali, totale grande), piè di pagina. I dati differiscono (fattura 1/2026 vs 4/2026): conta la struttura, non i valori.

- [ ] **Step 4: Correggi le differenze e ripeti**

Ritocca spaziature/dimensioni in `components/FatturaPDF.tsx` e ripeti gli step 1-3 finché la struttura non coincide. Poi `npm test` per confermare che i test passino ancora.

- [ ] **Step 5: Commit (se ci sono stati ritocchi)**

```bash
git add components/FatturaPDF.tsx
git commit -m "fix: ritocchi di fedeltà al template originale"
```

Nota: la revisione finale paragrafo per paragrafo la farà Paolo con la moglie; questo task porta la fedeltà al massimo ottenibile a occhio.

---

### Task 9: Repo privata GitHub e deploy su Vercel

**ATTENZIONE: azione verso l'esterno. Chiedi conferma a Paolo prima di eseguire questo task** (nome della repo e account GitHub/Vercel da usare).

**Files:**
- Nessuno (solo operazioni git/Vercel).

**Interfaces:**
- Consumes: repo locale completa e testata.
- Produces: repo privata su GitHub e app raggiungibile su URL Vercel.

- [ ] **Step 1: Verifica lo stato del repo e degli strumenti**

```bash
git status --short
gh auth status
```

Expected: working tree pulito; `gh` autenticato. Se `gh` non è autenticato, chiedi a Paolo di eseguire `! gh auth login`.

- [ ] **Step 2: Crea la repo privata e push**

```bash
gh repo create xml-pdf --private --source . --push
```

Expected: repo creata e branch `main` pushato.

- [ ] **Step 3: Collega e deploya su Vercel**

```bash
npx vercel link
npx vercel --prod
```

Expected: build su Vercel senza errori, URL di produzione stampato. Se `vercel` non è autenticato, chiedi a Paolo di eseguire `! npx vercel login`.

- [ ] **Step 4: Verifica in produzione**

```bash
curl -s -D - -o /tmp/fattura-prod.pdf \
  -F "file=@template/IT01511230086_AAAAA.xml" \
  https://<url-produzione>/api/genera-pdf | head -5
pdffonts /tmp/fattura-prod.pdf
```

Expected: `HTTP/2 200`, `application/pdf`, font Trebuchet incorporati (conferma che `outputFileTracingIncludes` ha portato i font nel bundle serverless). Poi prova dal browser sull'URL di produzione.

---

## Verifica finale

Al termine di tutti i task: `npm test` (tutto verde), `npm run build` (senza errori), prova completa dal browser in locale e in produzione con l'XML di esempio.





