8.7 KiB
AZA Stripe Live-Setup — Vollständige Anleitung
Stand: 14. März 2026
1. Ist-Analyse: Was existiert im Code
Zwei Stripe-Integrationen (parallel)
| Datei | Env-Prefix | Methode | Status |
|---|---|---|---|
stripe_routes.py |
STRIPE_* (kein Prefix) |
Lookup-Keys (aza_basic_monthly etc.) |
Primär / Aktiv |
license_server.py |
AZA_STRIPE_* |
Direkte Price-IDs | Legacy / Parallel |
Wichtig: stripe_routes.py ist der aktive Pfad. Die Landing-Page (web/index.html)
ruft /stripe/create_checkout_session auf und übergibt einen lookup_key.
Erwartete Lookup-Keys im Code
| Lookup-Key | Plan | Policy |
|---|---|---|
aza_basic_monthly |
Basic (1 User, 2 Geräte) | Checkout auf Landing-Page |
aza_basic_yearly |
Basic (1 User, 2 Geräte) | Noch nicht auf Landing-Page |
aza_team_monthly |
Team (3 User, 2 Geräte/User) | Checkout auf Landing-Page |
aza_team_yearly |
Team (3 User, 2 Geräte/User) | Noch nicht auf Landing-Page |
Aktuelle Preise auf der Landing-Page (web/index.html)
| Plan | Preis (monatlich) | Preis (jährlich) | Lookup-Key (monatlich) | Lookup-Key (jährlich) |
|---|---|---|---|---|
| AZA Basic (1 User) | CHF 59 / Monat | CHF 590 / Jahr | aza_basic_monthly |
aza_basic_yearly |
| AZA Team (2 User) | CHF 89 / Monat | CHF 890 / Jahr | aza_team_monthly |
aza_team_yearly |
Fehlend auf der Landing-Page
- Jahreslizenz (17 % günstiger) ist NICHT dargestellt
- Nur monatliche Preise sind sichtbar
- Kein Umschalter Monatlich/Jährlich
2. Env-Variablen-Diskrepanz (KRITISCH)
stripe_routes.py liest:
STRIPE_SECRET_KEY ← deploy/.env setzt diese ✓
STRIPE_WEBHOOK_SECRET ← deploy/.env setzt diese ✓
STRIPE_SUCCESS_URL ← deploy/.env setzt diese ✓
STRIPE_CANCEL_URL ← deploy/.env setzt diese ✓
STRIPE_PORTAL_RETURN_URL ← deploy/.env setzt diese ✓
license_server.py liest:
AZA_STRIPE_SECRET_KEY ← deploy/.env setzt diese NICHT ✗
AZA_STRIPE_WEBHOOK_SECRET ← deploy/.env setzt diese NICHT ✗
AZA_STRIPE_PRICE_BASIC ← deploy/.env setzt diese NICHT ✗
AZA_STRIPE_PRICE_TEAM ← deploy/.env setzt diese NICHT ✗
AZA_STRIPE_PRICE_BASIC_YEARLY ← deploy/.env setzt diese NICHT ✗
AZA_STRIPE_PRICE_TEAM_YEARLY ← deploy/.env setzt diese NICHT ✗
AZA_STRIPE_SUCCESS_URL ← deploy/.env setzt diese NICHT ✗
AZA_STRIPE_CANCEL_URL ← deploy/.env setzt diese NICHT ✗
Zusätzlich in license_server.py (Zeile 629, 867):
webhook_secret = os.getenv("STRIPE_WEBHOOK_SECRET") # ohne AZA_
secret_key = os.getenv("STRIPE_SECRET_KEY") or os.getenv("STRIPE_API_KEY") # ohne AZA_
→ Inkonsistenz innerhalb derselben Datei.
Konsequenz
stripe_routes.pyfunktioniert mitdeploy/.env→ OKlicense_server.pyCheckout-Endpoint bekommt leere Price-IDs → Broken- Aber: Landing-Page nutzt
/stripe/create_checkout_session(=stripe_routes.py) → OK - Der
license_server.pyCheckout ist ein Parallel-Pfad, der aktuell nicht aufgerufen wird
Empfehlung
Für Go-Live: stripe_routes.py ist der korrekte Pfad. Kein Umbau nötig.
Die license_server.py Env-Vars mit AZA_* Prefix können später bereinigt werden.
3. Was im Stripe-Dashboard angelegt werden muss
Produkte und Preise
Gehe zu: Stripe Dashboard → Produkte → + Produkt hinzufügen
Produkt 1: AZA Desktop Praxis
| Feld | Wert |
|---|---|
| Name | AZA Desktop – Praxis |
| Beschreibung | Medizinische KI-Software für Einzelpraxen. Alle 6 Module. |
Preise für dieses Produkt:
| Preis | Intervall | Lookup-Key (WICHTIG!) | Price-ID |
|---|---|---|---|
| CHF 59.00 | Monatlich | aza_basic_monthly |
price_1T53xHL5lREAW68VbuK43lmz |
| CHF 590.00 | Jährlich | aza_basic_yearly |
price_1T542BL5lREAW68VNLQGCKWZ |
Produkt 2: AZA Desktop Team (2 Benutzer)
| Feld | Wert |
|---|---|
| Name | AZA Desktop – Team |
| Beschreibung | Für Gemeinschaftspraxen. Bis zu 2 Benutzer. |
Preise für dieses Produkt:
| Preis | Intervall | Lookup-Key (WICHTIG!) | Price-ID |
|---|---|---|---|
| CHF 89.00 | Monatlich | aza_team_monthly |
price_1T544tL5lREAW68VkmnmZ21Q |
| CHF 890.00 | Jährlich | aza_team_yearly |
price_1T545RL5lREAW68VLbIh73AN |
KRITISCH: Beim Erstellen jedes Preises den Lookup-Key setzen! In Stripe: Preis erstellen → Erweiterte Optionen → Lookup Key → exakt den Key eingeben.
4. Webhook-Setup
Webhook-Endpunkt
| Feld | Wert |
|---|---|
| URL | https://app.aza-medwork.ch/stripe/webhook |
| Oder (falls auf Hostpoint) | https://aza-medwork.ch/stripe/webhook |
Hinweis: Der Webhook-Endpunkt muss auf dem Server laufen, auf dem stripe_routes.py
läuft. Aktuell ist das der Hetzner-Server (Docker/Caddy). Erst relevant, wenn
der Backend-Server auf Hetzner live ist.
Für den Start über WooCommerce/Hostpoint: WooCommerce Stripe Plugin hat seinen eigenen Webhook — der wird automatisch konfiguriert.
Benötigte Webhook-Events
| Event | Zweck |
|---|---|
checkout.session.completed |
Neue Subscription registrieren, Lizenz anlegen |
customer.subscription.updated |
Statusänderungen (Verlängerung, Pause, Planwechsel) |
customer.subscription.deleted |
Kündigung, Lizenz deaktivieren |
Webhook-Secret
Nach dem Erstellen des Webhooks zeigt Stripe ein whsec_... Secret an.
Dieses muss in deploy/.env als STRIPE_WEBHOOK_SECRET eingetragen werden.
5. Success/Cancel-Flow — Prüfung
Success-Flow ✓
- URL:
https://aza-medwork.ch/billing/success?session_id={CHECKOUT_SESSION_ID} - Endpunkt:
backend_main.py→@app.get("/billing/success") - Liest
session_idaus Query-Parameter - Ruft
stripe.checkout.Session.retrieve(session_id)auf - Zeigt Bestätigungsseite mit Download-Link und Anleitung
- Funktioniert korrekt (lokal verifiziert)
Cancel-Flow ✓
- URL:
https://aza-medwork.ch/billing/cancel - Endpunkt:
backend_main.py→@app.get("/billing/cancel") - Zeigt Info-Seite "Checkout abgebrochen" mit Link zurück
- Funktioniert korrekt (lokal verifiziert)
Portal-Return ✓
- URL:
https://aza-medwork.ch/ - Für Stripe Billing Portal (Abo verwalten, kündigen)
6. Benötigte Live-Secrets für deploy/.env
# 1. Stripe Secret Key (Dashboard → Developers → API keys → Secret key)
STRIPE_SECRET_KEY=sk_live_XXXXXXXXXXXXXXXXXXXXXX
# 2. Stripe Webhook Secret (Dashboard → Developers → Webhooks → Signing secret)
STRIPE_WEBHOOK_SECRET=whsec_XXXXXXXXXXXXXXXXXXXXXX
# 3. API-Token für Backend-Zugriff (selbst generieren, z.B. openssl rand -hex 32)
MEDWORK_API_TOKENS=<TOKEN_1>,<TOKEN_2>
Die übrigen Werte in deploy/.env sind bereits korrekt gesetzt:
STRIPE_SUCCESS_URL✓STRIPE_CANCEL_URL✓STRIPE_PORTAL_RETURN_URL✓AZA_DOMAIN✓ACME_EMAIL✓
7. Offene Punkte / Landing-Page-Update nötig
web/index.html muss aktualisiert werden:
- Basic (1 User): CHF 59/Monat →
aza_basic_monthly, CHF 590/Jahr →aza_basic_yearly - Team (2 User): CHF 89/Monat →
aza_team_monthly, CHF 890/Jahr →aza_team_yearly - Alle vier Preise sind in Stripe angelegt mit korrekten Lookup-Keys
8. Zwei Wege zum Verkauf (Entscheidung nötig)
Weg A: WooCommerce Subscriptions (Hostpoint)
- Alles auf der bestehenden WordPress-Seite
- WooCommerce Subscriptions Plugin nötig (kostenpflichtig, ca. $200/Jahr)
- WooCommerce Stripe Gateway macht Webhooks automatisch
- Kein eigener Backend-Server nötig für den Verkauf
- Download-Auslieferung über WooCommerce
- Vorteil: Kein Hetzner-Deploy nötig für den Start
Weg B: Eigener Stripe-Flow (Hetzner)
- Landing-Page +
stripe_routes.py+ Backend auf Hetzner - Voller Kontrolle über den Checkout-Flow
- Webhook an eigenen Server
- Nachteil: Hetzner muss erst deployed werden
Empfehlung für sofortigen Start
Weg A (WooCommerce) für den Verkauf. Weg B später für Browser-AZA.
Zusammenfassung
Vorhanden ✓
- Stripe-Integration im Code komplett (
stripe_routes.py) - Checkout-Session-Erstellung mit Lookup-Keys
- Webhook-Handler für 3 Events (checkout.completed, subscription.updated/deleted)
- Idempotente Event-Verarbeitung (Deduplizierung)
- Lizenz-Datenbank (SQLite) mit Upsert-Logik
- Success/Cancel-Seiten implementiert
- Billing-Portal-Integration
- Env-Variablen-Template (
deploy/.env.example)
Fehlt ✗
- Live Stripe-Produkte + Preise im Stripe-Dashboard (mit korrekten Lookup-Keys)
- Live Stripe Secret Key in
deploy/.env - Live Webhook Secret in
deploy/.env - Jahreslizenz auf Landing-Page (nur monatlich dargestellt)
- Entscheidung: WooCommerce vs. eigener Flow für den sofortigen Start
- Hetzner-Deploy (nur nötig für Weg B)