managarten/docs/plans/invoices-module.md
Till JS dc1a0a65fb docs(invoices): mark all milestones done, list Phase-2/3 open items
Quick status sync after M8. M1–M8 all landed; what's left are the
Phase-2/3 items (multi-device number collision, structured address
schema, finance cross-link, camt bank reconciliation) and the
Spaces-SSR-unblock-then-dogfood step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:23:02 +02:00

392 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Invoices — Module Plan
## Status (2026-04-20)
**Alle Milestones DONE** — Modul ist solo-produktiv.
| Milestone | Commit | Notes |
|---|---|---|
| M1 Skelett | `2cf89ce26` | Modul registriert, Dexie v27, Encryption-Registry, leere ListView |
| M2 CRUD | `8d00ee069` | Store, Totals, Status-Maschine, ListView mit Stats-Cards, Routes |
| M3 Settings + Logo | `76060b063` | SenderProfileForm, Logo-Upload via mana-media, im PDF-Header embedded |
| M4 PDF Basic | `2dc298a79` | pdf-lib, pagination, preview + download |
| M5 QR-Rechnung | `5af23d30b` | swissqrbill/svg + PNG-Overlay, SCOR auto, address-parser-Heuristik |
| M6 Versand | `08b7ac16b` | SendModal mit mailto + PDF-Download + Confirm → status sent |
| M7 Dashboard | `2ee3a1a93` | InvoicesOpenWidget mit Offen/Überfällig + Top-3 älteste überfällig |
| M8 AI-Tools | `a4bc7d2ee` | create_invoice, mark_paid, list_invoices, get_invoice_stats |
**Tests (59+ green):** totals math, SCOR + address-parser (swissqrbill `isSCORReferenceValid` roundtrip), mail template + mailto encoding, shared-ai function-schema round-trip, mana-ai planner drift-guard.
**Offene Phase-2/3-Punkte (nicht blockierend für Solo-Einsatz):**
- **Nummernkreis-Multi-Device-Kollision:** dokumentiert als gap (pragmatisch, solo-MVP-tauglich)
- **Strukturierte Adressen:** Heuristik-Parser deckt Standard-CH/DE ab; komplexere Adressen fehlen → User sieht in Warning, was fehlt
- **Bezahlte Rechnung → finance-Transaktion:** Cross-Modul-Hook, Phase 3
- **camt.053 Bankabgleich:** Phase 2
- **Browser-Smoke-Test:** SSR im Repo während Spaces-Refactor broken für alle Module — sobald das geheilt ist, erste echte Rechnung stellen.
---
## Ziel
Ein Modul, mit dem der Nutzer **Rechnungen an Kunden stellt**: Rechnung anlegen → PDF generieren (inkl. **Schweizer QR-Rechnung**) → per Mail versenden → Zahlungs-Status nachhalten. Kernfrage: *"Hat der Kunde bezahlt — und wenn nein, wann muss ich mahnen?"*
Solo-Zielgruppe: Freelancer/Selbstständige in CH (+DE). Später trivial auf Vereine erweiterbar (Empfänger = Mitgliedergruppe → Serienrechnungslauf für Mitgliederbeiträge).
## Abgrenzung
- **Kein Ersatz für `finance`** — das bleibt das private Einnahmen/Ausgaben-Tracker. `invoices` ist die **Outbound**-Seite: Rechnungen *stellen* statt Transaktionen *erfassen*. Ein bezahlter Rechnungseingang kann später automatisch eine `finance`-Einnahme erzeugen (Cross-Modul-Hook, Phase 3).
- **Keine vollständige Buchhaltung** — keine Kontenrahmen (SKR03/SKR42), keine doppelte Buchführung, keine MwSt-Voranmeldung. Das ist ClubDesk-Niveau Paket B-2 und explizit *nicht* MVP.
- **Kein Bankabgleich (camt.053)** — Phase 2/3, separater Plan.
- **Kein Payment-Provider** — keine Stripe/PayPal-Zahlungslinks im MVP. Zahlungseingang wird manuell markiert oder (später) per camt abgeglichen.
- **Kein CRM** — Kundendaten kommen aus dem bestehenden `contacts`-Modul, nicht aus einer eigenen Kunden-Tabelle.
## Entscheidung: eigenes Modul, nicht Finance-Erweiterung
Zwei Optionen wurden erwogen:
1. **In `finance` integrieren** — Rechnung wäre ein Transaktions-Typ.
2. **Eigenes Modul `invoices`** (gewählt).
Begründung für (2):
- Rechnungen haben komplexeres Schema (Positionen, PDF-Assets, QR-Zahldaten, Status-Maschine Entwurf/Versendet/Bezahlt/Überfällig) — das verunreinigt das schlanke Finance-Schema.
- Unterschiedliche Zielgruppe: `finance` ist Alltags-Tracking (jeder), `invoices` ist Selbstständigen-Workflow (Teilmenge).
- Separater Launcher-Eintrag, eigene Route, eigene AI-Tools.
- Finance bleibt encryption-minimal, invoices hat mehr sensitive Felder.
Cross-Link: Bezahlte Rechnung → optional auto-erzeugte `finance`-Einnahme (Phase 3).
## Modul-Struktur
```
apps/mana/apps/web/src/lib/modules/invoices/
├── types.ts # LocalInvoice, Invoice, InvoiceLine, InvoiceStatus, Client
├── collections.ts # invoices + invoiceClients tables + guest-seed
├── queries.ts # useAllInvoices, useInvoicesByStatus, useInvoice(id), useStats, useOverdue
├── stores/
│ ├── invoices.svelte.ts # create, update, markSent, markPaid, markOverdue, void, duplicate, delete
│ ├── clients.svelte.ts # createClient, updateClient, deleteClient (optional — kann auch in contacts)
│ └── settings.svelte.ts # Absender-Profil, Logo, Fußzeile, IBAN, MwSt-Nummer, Nummernkreis
├── components/
│ ├── InvoiceCard.svelte # Listeneintrag: Nummer + Kunde + Betrag + Status-Badge + Fälligkeit
│ ├── InvoiceForm.svelte # Create/Edit inkl. Positionen-Editor
│ ├── LinesEditor.svelte # Positionen: Titel, Menge, Einheit, Einzelpreis, MwSt-Satz
│ ├── StatusBadge.svelte # draft / sent / paid / overdue / void
│ ├── StatusFilter.svelte # Filter-Chips
│ ├── ClientPicker.svelte # Auswahl aus contacts ODER invoiceClients
│ ├── SenderProfileForm.svelte # einmalige Einrichtung (Name, IBAN, MwSt, Logo)
│ └── InvoicePreview.svelte # Live-Preview des PDFs (iframe oder react-pdf react-free Variante)
├── pdf/
│ ├── renderer.ts # erzeugt Blob via pdf-lib (Header, Tabelle, Footer, QR-Slip)
│ ├── qr-bill.ts # swissqrbill-Integration (SPC-kompatibel, CHF/EUR)
│ └── templates/
│ └── default.ts # Layout-Konstanten (Margins, Fonts, Farben)
├── views/
│ ├── ListView.svelte # komponiert Filter + Grid/Liste
│ ├── DetailView.svelte # Rechnungsdetail + PDF-Preview + Aktionen
│ └── SettingsView.svelte # Absender-Profil + Nummernkreis-Editor
├── tools.ts # AI-Tools — siehe §AI-Integration
├── constants.ts # STATUS_LABELS, VAT_RATES_CH, VAT_RATES_DE, DEFAULT_DUE_DAYS
├── module.config.ts # { appId: 'invoices', tables: [{ name: 'invoices' }, { name: 'invoiceClients' }, { name: 'invoiceSettings' }] }
└── index.ts # Re-Exports
```
## Daten-Schema
### `LocalInvoice` (Dexie)
```typescript
export type InvoiceStatus = 'draft' | 'sent' | 'paid' | 'overdue' | 'void';
export type Currency = 'CHF' | 'EUR' | 'USD';
export interface LocalInvoiceLine {
id: string; // client-generated, stabil für Drag-Reorder
title: string; // encrypted
description?: string | null; // encrypted
quantity: number; // plaintext
unit?: string | null; // plaintext — "Std", "Stk", "Tag"
unitPrice: number; // plaintext — in kleinster Währungseinheit (Rappen/Cent), Integer
vatRate: number; // plaintext — 0, 2.6, 3.8, 8.1 (CH) oder 0, 7, 19 (DE)
discount?: number | null; // plaintext — Prozent (0..100)
}
export interface LocalInvoice extends BaseRecord {
number: string; // plaintext — "2026-0042", aus Nummernkreis generiert
status: InvoiceStatus; // plaintext — filterbar
clientId: string | null; // plaintext — FK entweder contacts.id ODER invoiceClients.id
clientSource: 'contact' | 'invoice-client'; // plaintext — welche Tabelle
clientSnapshot: { // encrypted — Kunden-Daten zum Zeitpunkt des Versands eingefroren
name: string;
address?: string;
email?: string;
vatNumber?: string;
};
currency: Currency; // plaintext
issueDate: string; // plaintext — YYYY-MM-DD
dueDate: string; // plaintext — YYYY-MM-DD
sentAt?: string | null; // plaintext — ISO timestamp
paidAt?: string | null; // plaintext — ISO timestamp
lines: LocalInvoiceLine[]; // teils encrypted (Titel/Description), teils plaintext (Zahlen)
subject?: string | null; // encrypted — "Beratungsleistung April 2026"
notes?: string | null; // encrypted — Freitext unterhalb der Positionen
terms?: string | null; // encrypted — AGB/Zahlungsbedingungen
referenceNumber?: string | null; // plaintext — QR-Referenz (CH) oder Kundenreferenz
pdfBlobKey?: string | null; // plaintext — uload/media-ID des gerenderten PDFs (Cache)
// Berechnete Totals — denormalisiert, bei jedem update neu
totals: {
net: number; // plaintext
vat: number; // plaintext
gross: number; // plaintext
vatBreakdown: Array<{ rate: number; base: number; tax: number }>;
};
}
```
### `LocalInvoiceClient` (Dexie) — *optional, Phase 1.5*
Separate Tabelle für Rechnungs-spezifische Kundendaten, falls `contacts` nicht reicht. Gleiche Felder wie `clientSnapshot`, aber zentral editierbar. MVP startet ohne — bestehende `contacts` reichen für solo-User.
```typescript
export interface LocalInvoiceClient extends BaseRecord {
name: string; // encrypted
address?: string | null; // encrypted
email?: string | null; // encrypted
vatNumber?: string | null; // encrypted
iban?: string | null; // encrypted — für spätere SEPA-Lastschrift
defaultCurrency: Currency; // plaintext
defaultDueDays: number; // plaintext — 14, 30, 60
notes?: string | null; // encrypted
}
```
### `LocalInvoiceSettings` (Dexie, Singleton pro User)
```typescript
export interface LocalInvoiceSettings extends BaseRecord {
// Absender
senderName: string; // encrypted
senderAddress: string; // encrypted
senderEmail: string; // encrypted
senderVatNumber?: string | null; // encrypted — "CHE-123.456.789 MWST"
senderIban: string; // encrypted — CH-IBAN für QR-Rechnung
senderBic?: string | null; // encrypted — für SEPA
// Branding
logoMediaId?: string | null; // plaintext — uload-ID
accentColor: string; // plaintext — Hex
footer?: string | null; // encrypted — Fußzeile unter jedem PDF
// Nummernkreis
numberPrefix: string; // plaintext — "2026-"
numberPadding: number; // plaintext — 4 → "0042"
nextNumber: number; // plaintext — Zähler, wird atomisch inkrementiert
// Defaults
defaultCurrency: Currency;
defaultVatRate: number; // plaintext — 8.1 (CH) oder 19 (DE)
defaultDueDays: number;
defaultTerms?: string | null; // encrypted
}
```
### Encryption-Registry
`apps/mana/apps/web/src/lib/data/crypto/registry.ts`:
```typescript
invoices: {
fields: ['clientSnapshot', 'subject', 'notes', 'terms', 'lines'],
version: 1,
},
invoiceClients: {
fields: ['name', 'address', 'email', 'vatNumber', 'iban', 'notes'],
version: 1,
},
invoiceSettings: {
fields: ['senderName', 'senderAddress', 'senderEmail', 'senderVatNumber', 'senderIban', 'senderBic', 'footer', 'defaultTerms'],
version: 1,
},
```
**Wichtig zu `lines`:** Die komplette `lines`-Array wird encrypted serialisiert. Die Zahlenanteile wären zwar plaintext-fähig, aber die Trennung von encrypted Titel + plaintext Zahlen pro Line ist teuer in Dexie-Queries. Pragmatisch: ganze `lines`-Array encrypten, `totals` daneben plaintext halten für Filter/Summen.
## Routing
```
apps/mana/apps/web/src/routes/(app)/invoices/
├── +page.svelte # ListView mit Status-Filter
├── new/+page.svelte # InvoiceForm im Create-Mode
├── [id]/+page.svelte # DetailView
├── [id]/edit/+page.svelte # InvoiceForm im Edit-Mode
└── settings/+page.svelte # SettingsView (Absender, Nummernkreis)
```
## UI-Konzept
### Landing (`/invoices`)
- **Top-Leiste:** Status-Chips (Alle | Entwurf | Versendet | Bezahlt | **Überfällig** als rote Count-Badge), Zeitraum-Filter (Dieses Jahr | Letztes Jahr | Alle), Search
- **Kennzahlen-Karten:** Offene Summe, Überfällig, YTD fakturiert, YTD bezahlt
- **Liste:** `InvoiceCard` mit Nummer, Kunde, Fälligkeit, Bruttobetrag, Status-Badge; Click → DetailView
- **FAB:** "+" öffnet `new`-Route (oder Modal, konsistent mit anderen Modulen)
- **Scene-Scope**: Invoices ignorieren Scene-Scope per Default (kein Tag-System — Kunden/Nummer identifizieren, nicht Tags)
### InvoiceForm
Links Eingabe, rechts Live-Preview des PDFs (auf Desktop). Mobile: Tab-Switch.
Sections:
1. **Kunde** — ClientPicker (sucht in Contacts + InvoiceClients)
2. **Metadaten** — Nummer (auto-generiert, editierbar), Issue-Date, Due-Date, Währung, Betreff
3. **Positionen**`LinesEditor`: Tabelle mit Add/Remove/Reorder. Spalten: Titel, Menge, Einheit, Einzelpreis, MwSt%, Total (berechnet)
4. **Totals** — Auto-berechnet: Netto pro MwSt-Satz, MwSt, Brutto
5. **Notizen & AGB** — Markdown-Felder für zusätzlichen Text
Save-Actions: "Als Entwurf speichern" (status: draft) oder "Speichern & Versenden" (status: sent, öffnet Mail-Flow).
### DetailView
- Header: Nummer, Status-Badge, Aktionen-Menu (Bearbeiten, Duplizieren, Als bezahlt markieren, Stornieren, PDF herunterladen, Per Mail senden)
- Linke Spalte: Rechnungs-Metadaten + Positionen als Read-Only-Tabelle
- Rechte Spalte: PDF-Preview (iframe)
- Footer: Activity-Log (erstellt, versendet, bezahlt, gemahnt) — aus `_activity` gefiltert
### SettingsView
- Absender-Profil-Form (einmalig auszufüllen, Onboarding-Block wenn leer)
- Logo-Upload via bestehende `uload`-Infrastruktur
- Nummernkreis-Konfigurator mit Preview ("Nächste Rechnung: 2026-0042")
- Default-Werte (Währung, MwSt, Zahlungsfrist)
## PDF-Rendering
### Library-Wahl
- **`pdf-lib`** (npm, ~130 KB gzipped, WebAssembly-frei) — reicht für Tabellen + Text + Logo.
- **`swissqrbill`** (npm, CH-spezifisch, SPC-kompatibel) — generiert den Zahlteil als PDF-Seite oder PNG-Overlay.
Alternativen verworfen:
- `@react-pdf/renderer` — React-only, Mana ist Svelte.
- `jsPDF` — großer Footprint, weniger flexibel bei komplexen Layouts.
- Server-Side-Rendering via Puppeteer in `apps/api` — wäre besser aber *Phase 2*. MVP bleibt client-side (local-first-Prinzip + keine neuen Service-Dependencies).
### Render-Pipeline
```
invoice → renderer.ts
├── Layout-Engine: Header (Absender + Logo) → Empfänger-Block → Rechnungsmeta → Positionen-Tabelle → Totals → Notes → Footer
├── qr-bill.ts: swissqrbill.generate({ amount, iban, creditor, debtor, reference }) → PDF-Seite appendieren (nur wenn Currency=CHF)
└── → Blob (application/pdf)
```
Rendering dauert ~100-300ms für typische Rechnungen (akzeptabel für Live-Preview mit debounce 500ms).
### QR-Rechnung (CH) — Kern-USP
- Pflichtfelder: IBAN, Creditor (Name + Adresse), Amount, Currency=CHF, Reference (QRR oder SCOR)
- `referenceNumber` aus Invoice-Schema generiert via QRR-Algorithmus (27 Ziffern, Mod-10-Prüfziffer) ODER SCOR (ISO 11649 Creditor Reference)
- swissqrbill kümmert sich um Swiss Cross + Perforation + Zahlteil-Layout
## Registrierung (Checklist)
1. `apps/mana/apps/web/src/lib/modules/invoices/module.config.ts` anlegen
2. Config in `apps/mana/apps/web/src/lib/data/module-registry.ts` importieren + in `MODULE_CONFIGS` aufnehmen
3. Dexie-Schema-Migration: `db.version(N+1).stores({ invoices: 'id, number, status, userId, clientId, issueDate, dueDate', invoiceClients: 'id, userId', invoiceSettings: 'id, userId' })` hinzufügen — NICHT bestehende Versionen ändern
4. Encryption-Registry-Einträge (siehe oben)
5. Routes unter `(app)/invoices/` anlegen
6. App-Eintrag in `packages/shared-branding/src/mana-apps.ts`:
```typescript
{ id: 'invoices', name: 'Rechnungen', description: {...}, icon: APP_ICONS.invoices, color: '#059669', status: 'development', requiredTier: 'alpha' }
```
7. Icon in `packages/shared-branding/src/app-icons.ts`
8. `docs/MODULE_REGISTRY.md` unter "Arbeit & Finanzen" ergänzen
9. Guest-Seed in `collections.ts` (eine Demo-Rechnung im Status `draft` an einen seed-contact)
10. Vitest-Tests:
- Store-Mutationen (create, updateLines → totals neu berechnet, markPaid → paidAt gesetzt)
- Encryption-Roundtrip
- Nummer-Generator (atomisch, kein Duplikat bei gleichzeitigem Create)
- QRR-Prüfziffer
- PDF-Rendering smoke-test (Blob > 0 bytes)
## AI-Integration (Phase 2)
Nachdem MVP steht, Tools in `tools.ts` registrieren und in `@mana/shared-ai/src/tools/schemas.ts` + `AI_PROPOSABLE_TOOL_NAMES` aufnehmen:
| Tool | Policy | Beschreibung |
|------|--------|--------------|
| `create_invoice` | propose | Rechnung als Entwurf anlegen — Kunde, Positionen, Fälligkeit aus Chat extrahieren |
| `mark_invoice_paid` | propose | Rechnung als bezahlt markieren (mit Datum) |
| `list_invoices` | auto | Status-Filter + Zeitraum für Planner-Kontext |
| `get_invoice_stats` | auto | Offene Summe, YTD-Umsatz, überfällige Anzahl |
| `suggest_dunning` | propose | Mahntext für überfällige Rechnung vorschlagen (nutzt Kundensnapshot + Fälligkeit + Betrag) |
| `draft_invoice_from_timesheet` | propose | Phase 3: verbindet `times`-Modul — "letzte 4 Wochen für Kunde X" → Rechnung |
UX-Integration: `<AiProposalInbox module="invoices" />` auf ListView einbetten. Mission-Beispiel: *"Erinnere mich freitags an überfällige Rechnungen und schlag Mahntexte vor."*
## Kommunikation / Versand
### MVP (Phase 1)
- Button "Per Mail senden" → öffnet bestehende `mail`-Compose mit pre-filled Empfänger (aus clientSnapshot.email), Betreff (`Rechnung {number}`), Body (Vorlage mit Platzhaltern), Anhang = gerendertes PDF
- User sendet manuell ab → Store setzt `status='sent'` + `sentAt=now`
- Optional: nach Versand automatisches Dexie-`_activity`-Event "Rechnung versendet"
### Phase 2 — Automatisiert
- Eigener Endpoint in `apps/api`: `POST /api/v1/invoices/:id/send` → serverseitig Mail verschicken via Stalwart
- E-Mail-Tracking (Opens) — analog zu Newsletter-Modul später
- Zahlungs-Reminder (cron im `mana-ai`): "Rechnung seit 5 Tagen überfällig"
## Milestones
| Milestone | Umfang | Geschätzter Aufwand |
|-----------|--------|---------------------|
| **M1 Skelett** | Modul registriert, Dexie-Tables, Encryption-Registry, leere ListView, Guest-Seed, App-Entry + Icon | 0.5 Tag |
| **M2 CRUD** | InvoiceForm + LinesEditor + DetailView, Totals-Berechnung, Status-Maschine, Nummer-Generator | 3 Tage |
| **M3 Settings** | SettingsView + Singleton-Store, Logo-Upload via uload, Nummernkreis-Editor | 1 Tag |
| **M4 PDF Basic** | pdf-lib-Renderer mit Header/Tabelle/Totals/Footer, Preview-iframe in DetailView, Download-Button | 2 Tage |
| **M5 QR-Rechnung** | swissqrbill-Integration, QRR-Referenz-Generator, IBAN-Validation, CHF-only Gating | 1.5 Tage |
| **M6 Versand** | "Per Mail senden"-Flow über bestehendes mail-Modul, status-Transition sent+sentAt | 0.5 Tag |
| **M7 Dashboard & Stats** | Kennzahlen-Karten, Overdue-Highlighting, `useStats()` + Dashboard-Widget | 1 Tag |
| **M8 AI-Tools** | 4 Tools (create_invoice, mark_paid, list, stats) + Proposal-Inbox | 1.5 Tage |
**Summe MVP (M1M7):** ~9.5 Arbeitstage → realistisch **~2.5 Wochen** mit Testing/Polishing.
## Phase 2 (nach MVP)
- camt.053/054-Bankabgleich (separater Plan, eigenes Backend-Feature in mana-api)
- SEPA-Lastschrift (pain.008-XML-Export)
- Mahnwesen (3-Stufen-Mahnlauf)
- Wiederkehrende Rechnungen (via `rituals`)
- PDF-Rendering server-side via Puppeteer (bessere Typografie, custom fonts)
- Auto-Sync bezahlter Rechnungen → `finance`-Transaktion
## Phase 3 (Vereinsvariante)
- Empfänger = Gruppe (aus contacts-Gruppen) → Serienrechnungslauf
- Integration Mitglieder-Entität (aus Paket A)
- Beitragsmodelle (Jahresbeitrag, Halbjahresbeitrag, gestaffelt nach Kategorie)
- QR-Rechnung mit Vereins-Referenz pro Mitglied
## Offene Fragen
1. **Nummernkreis-Atomicität**: Bei schnellem doppeltem Create-Klick ohne Sync darf keine doppelte Nummer entstehen. Dexie-Transaktion um `nextNumber`-Read + `nextNumber++` wrappen. Bei Sync-Konflikt (Offline Create auf zwei Geräten): LWW löst durch `__fieldTimestamps`, aber Nummern wären doppelt. Lösung: Beim Sync-Merge Kollision erkennen → älterer Eintrag behält Nummer, neuerer bekommt `nextNumber` neu zugewiesen. **Design nötig vor M2.**
2. **Kundensnapshot-Strategie**: Beim Versenden wird der Kunden-Stand eingefroren (`clientSnapshot`). Was passiert bei Edit der Rechnung im Status `sent`? Option A: Edit nur im Status `draft` erlauben. Option B: Jede Edit-Operation neuen Snapshot nehmen. **Empfehlung A** — einfacher, entspricht Realität (versendete Rechnung ist rechtsverbindlich).
3. **Logo-Upload-Pfad**: `uload` liefert Media-ID, aber PDF-Renderer braucht Bytes. Zwei Optionen: (a) Logo beim Render aus uload ziehen (async), oder (b) Base64-inline in `invoiceSettings` speichern. Empfehlung (a) weil Logos typischerweise >20 KB und wir encryption sparen wollen.
4. **Currency-Scope**: MVP CHF + EUR reicht für 95% der Zielgruppe. USD/GBP später.
5. **Mehrsprachigkeit**: Sollen Rechnungen sprachlich anpassbar sein (DE/EN/FR auf dem PDF)? Phase 2 — MVP: Sprache = UI-Sprache.
6. **Zero-Knowledge-Kompatibilität**: Der `clientSnapshot` ist encrypted. Wenn ZK aktiv ist, kann der Server kein PDF renderen/versenden (Phase 2). Für MVP (client-side-Render) kein Problem.
## Abhängigkeiten
- `contacts`-Modul: vorhanden, wird für ClientPicker genutzt.
- `mail`-Modul: vorhanden (beta), wird für Versand genutzt.
- `uload`-Modul: vorhanden, wird für Logo + PDF-Cache genutzt.
- npm: `pdf-lib@^1`, `swissqrbill@^4` — beide MIT-lizenziert, aktiv gewartet.
- Keine neue Backend-Service nötig für MVP.
## Erfolgs-Kriterien
- **Tag 1 nach Launch:** Till kann für einen realen Kunden eine QR-Rechnung stellen, als PDF herunterladen und manuell versenden.
- **Woche 2:** Rechnung wird aus Mana heraus per Mail versendet, PDF landet als Anhang.
- **Monat 1:** 35 externe Nutzer (Freelancer) testen das Modul und haben bezahlte Rechnungen generiert.
- **Monat 3:** Mission "Erinnere mich an überfällige Rechnungen" läuft und schlägt Mahnungen vor.