Add docs
This commit is contained in:
268
docs/DEVELOPER-GUIDE.md
Normal file
268
docs/DEVELOPER-GUIDE.md
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
# Swiss QR-Bill - Entwickler-Anleitung
|
||||||
|
|
||||||
|
## Offizielle Spezifikation
|
||||||
|
|
||||||
|
Die **einzige verbindliche Quelle** ist die SIX Group Spezifikation:
|
||||||
|
|
||||||
|
**Swiss Implementation Guidelines QR-Rechnung v2.3**
|
||||||
|
https://www.six-group.com/dam/download/banking-services/standardization/qr-bill/ig-qr-bill-v2.3-en.pdf
|
||||||
|
|
||||||
|
Dieses Dokument ist Pflichtlektüre - es definiert alle Regeln.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wichtigste Regeln
|
||||||
|
|
||||||
|
### 1. IBAN-Typen und Referenzen
|
||||||
|
|
||||||
|
| Referenztyp | IBAN-Typ | IID (Stelle 5-9) | Prüfziffer |
|
||||||
|
|-------------|----------|------------------|------------|
|
||||||
|
| **QRR** | QR-IBAN | 30000-31999 | Modulo 10 rekursiv |
|
||||||
|
| **SCOR** | Normale IBAN | < 30000 oder > 31999 | Modulo 97 (ISO 11649) |
|
||||||
|
| **NON** | Normale IBAN | < 30000 oder > 31999 | Keine |
|
||||||
|
|
||||||
|
**Wichtig:** QRR-Referenz funktioniert **nur** mit QR-IBAN. SCOR/NON **nur** mit normaler IBAN.
|
||||||
|
|
||||||
|
### 2. QRR-Referenz (27 Stellen)
|
||||||
|
|
||||||
|
```
|
||||||
|
26 Ziffern + 1 Prüfziffer = 27 Stellen
|
||||||
|
|
||||||
|
Prüfziffer-Berechnung (Modulo 10 rekursiv):
|
||||||
|
- Tabelle: [0, 9, 4, 6, 8, 2, 7, 1, 3, 5]
|
||||||
|
- Für jede Ziffer: carry = tabelle[(carry + ziffer) % 10]
|
||||||
|
- Prüfziffer = (10 - carry) % 10
|
||||||
|
```
|
||||||
|
|
||||||
|
**JavaScript-Beispiel:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function calculateMod10Recursive(ref) {
|
||||||
|
const table = [0, 9, 4, 6, 8, 2, 7, 1, 3, 5];
|
||||||
|
let carry = 0;
|
||||||
|
|
||||||
|
for (const char of ref) {
|
||||||
|
const digit = parseInt(char, 10);
|
||||||
|
if (!isNaN(digit)) {
|
||||||
|
carry = table[(carry + digit) % 10];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (10 - carry) % 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verwendung:
|
||||||
|
const reference26 = '00000000000020240115123456'; // 26 Stellen
|
||||||
|
const checkDigit = calculateMod10Recursive(reference26);
|
||||||
|
const fullReference = reference26 + checkDigit; // 27 Stellen
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Strukturierte Adresse (seit Nov. 2025 Pflicht)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Max 70 Zeichen",
|
||||||
|
"street": "Strassenname (max 70)",
|
||||||
|
"buildingNumber": "Hausnummer (max 16)",
|
||||||
|
"postalCode": "PLZ (max 16)",
|
||||||
|
"city": "Ort (max 35)",
|
||||||
|
"country": "CH"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Feld | Max. Länge | Pflicht |
|
||||||
|
|------|------------|---------|
|
||||||
|
| name | 70 | Ja |
|
||||||
|
| street | 70 | Nein |
|
||||||
|
| buildingNumber | 16 | Nein |
|
||||||
|
| postalCode | 16 | Ja |
|
||||||
|
| city | 35 | Ja |
|
||||||
|
| country | 2 | Ja (ISO 3166-1 alpha-2) |
|
||||||
|
|
||||||
|
### 4. Betrag
|
||||||
|
|
||||||
|
- Minimum: 0.01
|
||||||
|
- Maximum: 999'999'999.99
|
||||||
|
- Maximal 2 Dezimalstellen
|
||||||
|
- Optional (kann leer sein für Spenden etc.)
|
||||||
|
|
||||||
|
### 5. Währung
|
||||||
|
|
||||||
|
Nur **CHF** oder **EUR** erlaubt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Empfohlene Bibliothek
|
||||||
|
|
||||||
|
**swissqrbill** (Node.js/TypeScript)
|
||||||
|
https://github.com/schoero/swissqrbill
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install swissqrbill pdfkit
|
||||||
|
```
|
||||||
|
|
||||||
|
Diese Bibliothek:
|
||||||
|
- Generiert korrektes PDF-Layout nach SIX-Vorgaben
|
||||||
|
- Erstellt den QR-Code
|
||||||
|
- Unterstützt alle Sprachen (DE, FR, IT, EN)
|
||||||
|
- Bietet Optionen für Perforierung/Schere
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Beispiel-Payload
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"creditor": {
|
||||||
|
"iban": "CH4431999123000889012",
|
||||||
|
"address": {
|
||||||
|
"name": "Meine Firma AG",
|
||||||
|
"street": "Hauptstrasse",
|
||||||
|
"buildingNumber": "1",
|
||||||
|
"postalCode": "8000",
|
||||||
|
"city": "Zürich",
|
||||||
|
"country": "CH"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"amount": 1500.00,
|
||||||
|
"currency": "CHF",
|
||||||
|
"reference": {
|
||||||
|
"type": "QRR",
|
||||||
|
"value": "000000000000000000000000000"
|
||||||
|
},
|
||||||
|
"debtor": {
|
||||||
|
"address": {
|
||||||
|
"name": "Kunde GmbH",
|
||||||
|
"street": "Kundenweg",
|
||||||
|
"buildingNumber": "42",
|
||||||
|
"postalCode": "3000",
|
||||||
|
"city": "Bern",
|
||||||
|
"country": "CH"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalInformation": {
|
||||||
|
"message": "Rechnung 2024-001"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"language": "de",
|
||||||
|
"separate": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validierung implementieren
|
||||||
|
|
||||||
|
### Checkliste
|
||||||
|
|
||||||
|
1. **IBAN-Format prüfen**
|
||||||
|
- CH/LI + 19 alphanumerische Zeichen
|
||||||
|
- Leerzeichen entfernen, Grossbuchstaben
|
||||||
|
|
||||||
|
2. **IBAN-Typ erkennen**
|
||||||
|
- IID extrahieren (Stelle 5-9)
|
||||||
|
- 30000-31999 = QR-IBAN
|
||||||
|
- Andere = normale IBAN
|
||||||
|
|
||||||
|
3. **Referenz-IBAN-Kompatibilität prüfen**
|
||||||
|
- QRR erfordert QR-IBAN
|
||||||
|
- SCOR/NON erfordert normale IBAN
|
||||||
|
|
||||||
|
4. **QRR-Prüfziffer validieren**
|
||||||
|
- Modulo 10 rekursiv (siehe oben)
|
||||||
|
|
||||||
|
5. **SCOR-Prüfziffer validieren**
|
||||||
|
- Format: RF + 2 Prüfziffern + max. 21 alphanumerisch
|
||||||
|
- Modulo 97 nach ISO 11649
|
||||||
|
|
||||||
|
6. **Adressfelder auf Länge prüfen**
|
||||||
|
- Alle Maximal-Längen einhalten
|
||||||
|
|
||||||
|
### IBAN-Validierung (JavaScript)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function isQrIban(iban) {
|
||||||
|
const cleanIban = iban.replace(/\s/g, '').toUpperCase();
|
||||||
|
const iid = parseInt(cleanIban.substring(4, 9), 10);
|
||||||
|
return iid >= 30000 && iid <= 31999;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateIbanReferenceCombo(iban, referenceType) {
|
||||||
|
const isQr = isQrIban(iban);
|
||||||
|
|
||||||
|
if (referenceType === 'QRR' && !isQr) {
|
||||||
|
return 'QRR erfordert eine QR-IBAN (IID 30000-31999)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((referenceType === 'SCOR' || referenceType === 'NON') && isQr) {
|
||||||
|
return 'SCOR/NON erfordert eine normale IBAN';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // OK
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sprachen
|
||||||
|
|
||||||
|
| Code | Sprache |
|
||||||
|
|------|---------|
|
||||||
|
| de | Deutsch |
|
||||||
|
| fr | Französisch |
|
||||||
|
| it | Italienisch |
|
||||||
|
| en | Englisch |
|
||||||
|
|
||||||
|
Die Sprache beeinflusst:
|
||||||
|
- Überschriften ("Zahlteil" / "Récépissé" / etc.)
|
||||||
|
- Feldbezeichnungen
|
||||||
|
- Zusatztexte
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PDF-Layout
|
||||||
|
|
||||||
|
Der QR-Zahlteil besteht aus zwei Teilen:
|
||||||
|
1. **Empfangsschein** (links, 62mm breit)
|
||||||
|
2. **Zahlteil** (rechts, 148mm breit)
|
||||||
|
|
||||||
|
Gesamtgrösse: 210mm x 105mm (A6 quer, unten auf A4)
|
||||||
|
|
||||||
|
Die `swissqrbill` Bibliothek generiert das Layout automatisch korrekt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testressourcen
|
||||||
|
|
||||||
|
- **SIX Testdaten:** Im Anhang der offiziellen Spezifikation
|
||||||
|
- **Online-Validator:** https://www.swiss-qr-invoice.org/validator/
|
||||||
|
- **Style Guide:** https://www.six-group.com/dam/download/banking-services/standardization/qr-bill/style-guide-qr-bill-en.pdf
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Häufige Fehler
|
||||||
|
|
||||||
|
| Fehler | Ursache | Lösung |
|
||||||
|
|--------|---------|--------|
|
||||||
|
| Ungültige Referenz | Falsche Prüfziffer | Modulo 10 rekursiv korrekt berechnen |
|
||||||
|
| IBAN/Referenz Mismatch | QRR mit normaler IBAN | QR-IBAN verwenden oder auf SCOR/NON wechseln |
|
||||||
|
| Adresse zu lang | Feldlänge überschritten | Auf max. Länge kürzen |
|
||||||
|
| Ungültige Währung | USD oder andere | Nur CHF oder EUR verwenden |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API-Endpunkt (dieses Projekt)
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v1/invoice/qr-bill
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
Response: application/pdf (Binary)
|
||||||
|
```
|
||||||
|
|
||||||
|
Health Check:
|
||||||
|
```
|
||||||
|
GET /health
|
||||||
|
GET /health/ready
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user