2026-03-25 22:03:39 +01:00
|
|
|
|
# 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`)
|
|
|
|
|
|
|
2026-03-30 07:59:11 +02:00
|
|
|
|
| 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` |
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
### 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:
|
|
|
|
|
|
|
2026-03-30 07:59:11 +02:00
|
|
|
|
| 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` |
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
2026-03-30 07:59:11 +02:00
|
|
|
|
#### Produkt 2: AZA Desktop Team (2 Benutzer)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
| Feld | Wert |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| Name | AZA Desktop – Team |
|
2026-03-30 07:59:11 +02:00
|
|
|
|
| Beschreibung | Für Gemeinschaftspraxen. Bis zu 2 Benutzer. |
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
Preise für dieses Produkt:
|
|
|
|
|
|
|
2026-03-30 07:59:11 +02:00
|
|
|
|
| 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` |
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
**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_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=<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:
|
|
|
|
|
|
|
2026-03-30 07:59:11 +02:00
|
|
|
|
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
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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)
|