8.6 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:
- MWST 8.1 % (CH) muss separat ausgewiesen werden -- "obendrauf", nicht inkludiert.
- Vollstaendige Empfaenger-Adresse (Firma, Strasse, PLZ/Ort, Land, optional UID-Nr.) muss auf der Rechnung erscheinen.
- 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):
- Stripe Dashboard -> Tax -> Origin address: PowerOn-Hauptsitz (Schweiz) eintragen.
- Tax registrations -> Switzerland hinzufuegen, Steuernummer (UID) hinterlegen.
- Default tax behavior auf
exclusive(= "MWST kommt obendrauf"). - Default tax category fuer unser Produkt:
Software as a Service (SaaS)-- damit greift automatisch CH-MWST 8.1 % (Standard-Rate). - APP_CONFIG /
.envsetzen:STRIPE_AUTOMATIC_TAX_ENABLED=true.
Variante B -- Manueller Tax-Rate (Fallback ohne Stripe Tax):
- 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%
- Display name:
- Tax-Rate-ID kopieren (Format
txr_...). - APP_CONFIG /
.envsetzen:STRIPE_AUTOMATIC_TAX_ENABLED=falseSTRIPE_TAX_RATE_ID_CH_VAT=txr_xxxxxxxxxxxxxx
2b. Rechnungs-Template
- Stripe Dashboard -> Settings -> Invoice template.
- Public business name:
PowerOn(oder offizieller Firmenname inkl. AG/GmbH). - Business address: vollstaendige Schweizer Geschaeftsadresse.
- Tax IDs to display: PowerOn-UID-Nr (z. B.
CHE-123.456.789 MWST) anhaken -- erscheint dann automatisch im Header jeder Rechnung. - 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).
- Default payment terms:
Due upon receipt(sollte fuer Top-Ups eh irrelevant sein, da Status sofortpaid).
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:
invoiceLine1undinvoiceCity. Fehlt eines davon, faellt_buildStripeAddressaufNonezurueck 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_dataist nur beim Customer-Create wirksam. Aenderst duinvoiceVatNumberan einem Mandanten, dessen Stripe-Customer bereits existiert, musst du die UID einmalig in Stripe haendisch setzen (Customers -> Tax IDs) -- die App rufttax_ids.createaktuell nicht auf einem bestehenden Customer auf, weil dascustomer.tax_idszur 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
- Stripe-Account in Test-Modus schalten.
- Mandant
Demo Treuhandanlegen und alleinvoice*-Felder befuellen (mindestensinvoiceLine1,invoiceCity,invoiceCountry). - Top-Up 25 CHF ausfuehren (Test-Karte 4242 4242 4242 4242).
- Stripe Dashboard -> Customers ->
Demo Treuhand:name=invoiceCompanyNameemail=invoiceEmailaddress= strukturiert mit line1/postal_code/city/countrytax_idsenthaelt die UID-Nr (Typch_vat)metadata.mandateId/metadata.mandateLabelgesetzt
- Stripe Dashboard -> Invoices -> letzte Rechnung oeffnen:
- Status
Paid - Empfaenger-Block oben links zeigt strukturierte Adresse + UID
Custom fieldszeigenUID-Nr. Empfaengerund ggf.z. H.- Zeile
MWST 8.1%separat ausgewiesen, Total = Netto + MWST - Footer
Diese Rechnung wurde bereits via Kreditkarte bezahlt - Header zeigt PowerOn-UID
- Status
- PDF herunterladen und mit Treuhand abgleichen.
Wenn alle 6 Punkte stimmen: Live-Modus aktivieren und Roll-out.