wiki/d-guides/stripe-ch-vat.md
2026-04-21 00:50:21 +02:00

155 lines
8.6 KiB
Markdown

<!-- status: canonical -->
<!-- lastReviewed: 2026-04-20 (rev 2: structured invoice fields) -->
# Stripe-Rechnungen CH-Treuhand-konform aufsetzen
Schweizer Treuhand-Praxis verlangt fuer jede PowerOn-Rechnung (Top-Up &
Subscription) drei Dinge, die Stripe nicht von sich aus liefert:
1. **MWST 8.1 % (CH)** muss separat ausgewiesen werden -- "obendrauf", nicht
inkludiert.
2. **Vollstaendige Empfaenger-Adresse** (Firma, Strasse, PLZ/Ort, Land,
optional UID-Nr.) muss auf der Rechnung erscheinen.
3. **Zahlungsstatus** ("bereits bezahlt via Kreditkarte") muss vermerkt
sein -- die reine Stripe-Quittung reicht nicht aus.
Der Code-Pfad in PowerOn wurde so vorbereitet, dass nur noch die
Stripe-Konfiguration gemacht werden muss; danach werden alle neuen
Top-Up-Rechnungen automatisch konform erstellt.
---
## 1. App-seitig: was ist bereits implementiert (Stand 2026-04-20)
| Bereich | Datei | Was passiert |
|---------|-------|--------------|
| Mandant-Modell | `gateway/modules/datamodels/datamodelUam.py` -- `Mandate.invoice*` (10 strukturierte Felder) | Rechnungsadresse als einzelne Felder: `invoiceCompanyName`, `invoiceContactName`, `invoiceEmail`, `invoiceLine1`, `invoiceLine2`, `invoicePostalCode`, `invoiceCity`, `invoiceState`, `invoiceCountry` (default `CH`), `invoiceVatNumber`. Der FormGenerator rendert die Felder ueber `order: 200..209` automatisch gruppiert am Ende des Edit-Formulars. |
| Stripe-Customer | `serviceCenter/services/serviceBilling/stripeCheckout.py` -- `_ensureStripeCustomer` | Bei jedem Checkout wird ein Stripe-Customer angelegt/aktualisiert: `name` = `invoiceCompanyName` (Fallback Mandant-Label), `email` = `invoiceEmail`, `address` = strukturiertes `{ line1, line2, postal_code, city, state, country }`, `shipping` = z. H. + Adresse, beim _Create_ ausserdem `tax_id_data` aus `invoiceVatNumber` (CHE -> `ch_vat`, LI -> `li_uid`, EU -> `eu_vat`). Die `stripeCustomerId` wird im `BillingSettings` gecacht. |
| Rechnungserzeugung | `create_checkout_session` setzt `invoice_creation: { enabled: true, invoice_data: {...} }` | Stripe erzeugt automatisch eine **Rechnung** (statt nur einer Quittung) mit Status `paid`, der vollstaendigen Customer-Adresse, unserem Footer-Hinweis "bereits via Kreditkarte bezahlt" und (bei vorhandener UID/z.H.) `invoice_data.custom_fields` mit `UID-Nr. Empfaenger` / `z. H.`. |
| MWST | `create_checkout_session` -- `automatic_tax` ODER `tax_rates` | Wenn `STRIPE_AUTOMATIC_TAX_ENABLED=true`, wird Stripe Tax verwendet. Andernfalls wird der Tax-Rate aus `STRIPE_TAX_RATE_ID_CH_VAT` an jede Line-Item gehaengt. |
Das alles passiert pro Top-Up automatisch -- der App-Code ist fertig. **Was
zu tun bleibt, ist die Stripe-Dashboard-Konfiguration und das Hinterlegen der
Rechnungsadresse pro Mandant.**
---
## 2. Stripe-Dashboard: einmalige Einrichtung
### 2a. MWST 8.1 % aktivieren (Empfehlung: Stripe Tax)
**Variante A -- Stripe Tax (empfohlen, automatisch):**
1. Stripe Dashboard -> **Tax** -> Origin address: PowerOn-Hauptsitz
(Schweiz) eintragen.
2. Tax registrations -> **Switzerland** hinzufuegen, Steuernummer (UID)
hinterlegen.
3. **Default tax behavior** auf `exclusive` (= "MWST kommt obendrauf").
4. **Default tax category** fuer unser Produkt: `Software as a Service
(SaaS)` -- damit greift automatisch CH-MWST 8.1 % (Standard-Rate).
5. APP_CONFIG / `.env` setzen: `STRIPE_AUTOMATIC_TAX_ENABLED=true`.
**Variante B -- Manueller Tax-Rate (Fallback ohne Stripe Tax):**
1. Stripe Dashboard -> **Products** -> **Tax rates** -> **Add tax rate**.
- Display name: `MWST CH`
- Region: `Switzerland (CH)`
- Tax rate: `8.1` %
- Inclusive: **NEIN** (= "MWST kommt obendrauf")
- Description: `Schweizer Mehrwertsteuer 8.1%`
2. Tax-Rate-ID kopieren (Format `txr_...`).
3. APP_CONFIG / `.env` setzen:
- `STRIPE_AUTOMATIC_TAX_ENABLED=false`
- `STRIPE_TAX_RATE_ID_CH_VAT=txr_xxxxxxxxxxxxxx`
### 2b. Rechnungs-Template
1. Stripe Dashboard -> **Settings** -> **Invoice template**.
2. **Public business name**: `PowerOn` (oder offizieller Firmenname inkl. AG/GmbH).
3. **Business address**: vollstaendige Schweizer Geschaeftsadresse.
4. **Tax IDs to display**: PowerOn-UID-Nr (z. B. `CHE-123.456.789 MWST`)
anhaken -- erscheint dann automatisch im Header jeder Rechnung.
5. **Footer / Memo**: ggf. zusaetzlicher Hinweis "Bezahlt via Kreditkarte
am Rechnungsdatum" (wir setzen ihn auch im Code als invoice-spezifischen
Footer, das hier ist nur Fallback).
6. **Default payment terms**: `Due upon receipt` (sollte fuer Top-Ups eh
irrelevant sein, da Status sofort `paid`).
### 2c. Webhook fuer Invoice-Status (optional)
Wenn Sie wollen, dass die App-DB protokolliert, dass die Rechnung erfolgreich
ausgestellt wurde, koennen Sie das Event `invoice.finalized` und
`invoice.paid` zusaetzlich abonnieren -- die Webhook-Route ist bereits
vorbereitet (`/api/billing/webhook/stripe`). Aktuell genuegt fuer die reine
CH-Konformitaet aber `checkout.session.completed`.
---
## 3. Pro Mandant: Rechnungsadresse erfassen
Damit die Stripe-Rechnung die korrekte Empfaengeranschrift traegt,
muessen die strukturierten Adressfelder am Mandanten befuellt sein.
Seit 2026-04-20 (rev 2) sind das **einzelne Felder** -- der frueher
verwendete mehrzeilige Freitext wurde wieder gesplittet, damit Stripe
sie 1:1 in `customer.address` ablegen kann (Stripe verlangt strukturierte
Adressen, kein Freitext).
| Feld am Mandanten | Pflicht | Stripe-Mapping | Beispiel |
|-------------------|---------|----------------|----------|
| `invoiceCompanyName` | empfohlen | `customer.name` (Fallback: Mandant-Label) | `Muster Treuhand AG` |
| `invoiceContactName` | optional | `customer.shipping.name` (`"<z.H.> (<Firma>)"`) + `invoice_data.custom_fields[z. H.]` + `metadata.contactName` | `Buchhaltung` |
| `invoiceEmail` | empfohlen | `customer.email` (Stripe verschickt darauf die Rechnung) | `rechnungen@muster-treuhand.ch` |
| `invoiceLine1` | **ja** | `customer.address.line1` | `Bahnhofstrasse 1` |
| `invoiceLine2` | optional | `customer.address.line2` | `c/o Buchhaltung` |
| `invoicePostalCode` | empfohlen | `customer.address.postal_code` | `8000` |
| `invoiceCity` | **ja** | `customer.address.city` | `Zuerich` |
| `invoiceState` | optional | `customer.address.state` | `ZH` |
| `invoiceCountry` | ja (default `CH`) | `customer.address.country` (ISO-3166 Alpha-2) | `CH` |
| `invoiceVatNumber` | bei B2B empfohlen | `customer.tax_id_data` (CHE -> `ch_vat`, LI -> `li_uid`, andere -> `eu_vat`) + `invoice_data.custom_fields[UID-Nr. Empfaenger]` + `metadata.vatNumber` | `CHE-123.456.789 MWST` |
> **Pflichtminimum fuer eine "echte" Stripe-Customer-Adresse:**
> `invoiceLine1` **und** `invoiceCity`. Fehlt eines davon, faellt
> `_buildStripeAddress` auf `None` zurueck und Stripe erfragt die Adresse
> beim Checkout selbst (`billing_address_collection: required`); die
> Rechnung enthaelt dann die vom Endkunden eingegebene Adresse statt der
> hinterlegten.
> **`tax_id_data` ist nur beim Customer-_Create_ wirksam.** Aenderst du
> `invoiceVatNumber` an einem Mandanten, dessen Stripe-Customer bereits
> existiert, musst du die UID einmalig in Stripe haendisch setzen
> (Customers -> Tax IDs) -- die App ruft `tax_ids.create` aktuell nicht
> auf einem bestehenden Customer auf, weil das `customer.tax_ids` zur
> Vermeidung von Duplikaten erfordern wuerde.
> **Hinweis Bestandsdaten (vor 2026-04-20):** Die alte JSONB-Spalte
> `invoiceAddress` (Freitext oder strukturiertes Dict) wird vom
> Schema-Reconciler **nicht** automatisch in die neuen Felder
> umgeschrieben. Sie bleibt in der DB liegen, wird aber nicht mehr
> gelesen oder geschrieben. Bei Bedarf manuell ein einmaliges
> SQL-Update fahren oder die Adresse pro Mandant neu im Form erfassen
> (Empfehlung fuer Dev-Umgebungen).
---
## 4. Test-Checkliste vor Go-Live
1. Stripe-Account in **Test-Modus** schalten.
2. Mandant `Demo Treuhand` anlegen und alle `invoice*`-Felder befuellen
(mindestens `invoiceLine1`, `invoiceCity`, `invoiceCountry`).
3. Top-Up 25 CHF ausfuehren (Test-Karte 4242 4242 4242 4242).
4. Stripe Dashboard -> Customers -> `Demo Treuhand`:
- `name` = `invoiceCompanyName`
- `email` = `invoiceEmail`
- `address` = strukturiert mit line1/postal_code/city/country
- `tax_ids` enthaelt die UID-Nr (Typ `ch_vat`)
- `metadata.mandateId` / `metadata.mandateLabel` gesetzt
5. Stripe Dashboard -> Invoices -> letzte Rechnung oeffnen:
- Status `Paid`
- Empfaenger-Block oben links zeigt strukturierte Adresse + UID
- `Custom fields` zeigen `UID-Nr. Empfaenger` und ggf. `z. H.`
- Zeile `MWST 8.1%` separat ausgewiesen, Total = Netto + MWST
- Footer `Diese Rechnung wurde bereits via Kreditkarte bezahlt`
- Header zeigt PowerOn-UID
6. PDF herunterladen und mit Treuhand abgleichen.
Wenn alle 6 Punkte stimmen: Live-Modus aktivieren und Roll-out.