wiki/d-guides/stripe-ch-vat.md
2026-06-02 09:42:12 +02:00

8.7 KiB

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 platform-core/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.