# 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): ```python 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.py` funktioniert mit `deploy/.env` → **OK** - `license_server.py` Checkout-Endpoint bekommt leere Price-IDs → **Broken** - Aber: Landing-Page nutzt `/stripe/create_checkout_session` (= `stripe_routes.py`) → **OK** - Der `license_server.py` Checkout 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 (LIVE) | `https://api.aza-medwork.ch/stripe/webhook` | | Hinweis | Webhook laeuft auf Hetzner (api.aza-medwork.ch), NICHT auf Hostpoint | **Hinweis:** Der Webhook-Endpunkt laeuft auf dem Hetzner-Server (Docker/Caddy) unter `https://api.aza-medwork.ch/stripe/webhook`. Dieser Pfad ist LIVE und funktional (2026-03-28). 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_id` aus 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 ```bash # 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=, ``` 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: 1. **Basic (1 User)**: CHF 59/Monat → `aza_basic_monthly`, CHF 590/Jahr → `aza_basic_yearly` 2. **Team (2 User)**: CHF 89/Monat → `aza_team_monthly`, CHF 890/Jahr → `aza_team_yearly` 3. 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`) ### Erledigt seit 2026-03-28 ✓ - ~~Live Stripe-Produkte + Preise~~ → Produkt AzA Office mit 4 Preisen angelegt (aza_basic_monthly, aza_basic_yearly, aza_team_monthly, aza_team_yearly) - ~~Live Stripe Secret Key~~ → STRIPE_SECRET_KEY in deploy/.env gesetzt - ~~Live Webhook Secret~~ → STRIPE_WEBHOOK_SECRET in deploy/.env gesetzt - ~~Hetzner-Deploy~~ → api.aza-medwork.ch LIVE, Webhook functional (200 OK) - Sandbox-Kauf erfolgreich verarbeitet (admin@aza-medwork.ch, aza_basic_monthly, active) - lookup_key-Fallback aktiv via _lookup_key_from_price() fuer Sandbox ohne price.lookup_key ### Noch offen ✗ - **Jahreslizenz auf Landing-Page** (nur monatlich dargestellt) - **Desktop-Lizenzpfad** gegen /stripe/license_debug validieren - **WooCommerce-Grundkonfiguration** (getrennter Admin-Block)