managarten/docs/SYNC_BILLING_PLAN.md
Till JS 5c2ea614cd feat(credits): add sync billing — monthly credit subscription for cloud sync
Cloud Sync is now a paid feature: 30 credits/month (90/quarter, 360/year).
Users start in local-only mode and opt-in via Settings > Cloud Sync.
1 Credit = 1 Cent, so sync costs ~0.30€/month.

When credits run out, sync is paused (not deleted) and an in-app banner
prompts the user to top up. Local data is always preserved.

Backend (mana-credits):
- New sync_subscriptions table in credits schema
- SyncBillingService with activate/deactivate/chargeRecurring
- User-facing routes: GET/POST /api/v1/sync/{status,activate,deactivate,change-interval}
- Internal routes for server-side checks and cron triggers

Frontend (mana web):
- Sync API client + reactive sync-billing store
- syncEnabled parameter gates createUnifiedSync() — sync only starts when active
- Settings sync page with interval selection and activate/deactivate
- Pause banner in app layout when credits insufficient

Also: removed CALDAV_SYNC/GOOGLE_SYNC operations (not needed),
updated CLOUD_SYNC cost from 5 to 30 credits/month.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 22:21:58 +02:00

232 lines
9.4 KiB
Markdown

# Sync Billing Plan
## Kontext
Sync (mana-sync, Go, Port 3050) ist das Rückgrat der Multi-Device-Erfahrung. Aktuell läuft Sync für jeden authentifizierten User kostenlos und automatisch — es gibt keine Abrechnungslogik. Die drei Sync-Operationen in `@mana/credits` (`CALDAV_SYNC`, `GOOGLE_SYNC`, `CLOUD_SYNC`) sind definiert aber nirgends enforced.
Sync verursacht **laufende Server-Kosten** (PostgreSQL, Go-Server, Bandwidth). Ein per-Operation-Pricing passt nicht — stattdessen eine **monatliche Flat-Fee in Credits**.
**1 Credit = 1 Cent.**
## Modell
### Sync als monatliches Credit-Abo
| Intervall | Preis | Pro Tag |
|-----------|-------|---------|
| **Monatlich** | 30 Credits (0.30€) | ~1 Credit/Tag |
| **Quartalsweise** | 90 Credits (0.90€) | ~1 Credit/Tag |
| **Jährlich** | 360 Credits (3.60€) | ~1 Credit/Tag |
Kein Rabatt bei längeren Intervallen — der Preis ist schon sehr niedrig. Der Vorteil längerer Intervalle ist Bequemlichkeit (seltener nachdenken).
### Zwei Zustände
| Zustand | Was passiert |
|---------|-------------|
| **Local-Only** (Default) | App funktioniert vollständig in IndexedDB. Kein Push/Pull. |
| **Cloud Sync** (Aktiv) | Multi-Device-Sync, Backup, Conflict Resolution via mana-sync. |
- Neue User starten immer in **Local-Only**
- Keine Welcome Credits — Credits kommen von Käufen, Geschenken oder Gutscheinen
- CalDAV/Google Sync: eventuell später, nicht Teil dieses Plans
### Abrechnungs-Mechanik
1. User öffnet Settings → Sync → wählt Intervall (monatlich/quartalsweise/jährlich)
2. System prüft Balance >= Preis für gewähltes Intervall
3. Credits werden sofort abgebucht, `nextChargeAt` wird gesetzt
4. Am Abrechnungstag: automatische Abbuchung via Cron
5. Wenn Balance < Kosten: Sync wird **pausiert**, nicht gelöscht
- In-App-Banner: "Sync pausiert Credits aufladen um fortzufahren"
- E-Mail-Notification via mana-notify
6. Lokale Daten bleiben immer erhalten. Sobald Credits da reaktivieren alles synct nach
7. User kann jederzeit deaktivieren keine weitere Abbuchung, Sync stoppt sofort
## Ist-Zustand
| Komponente | Status | Datei |
|------------|--------|-------|
| Sync-Engine (Client) | Startet automatisch nach Auth, kein Enable/Disable | `apps/mana/apps/web/src/lib/data/sync.ts` |
| Sync-Server | Akzeptiert alle authentifizierten Requests | `services/mana-sync/` |
| Credit-Ops | `CALDAV_SYNC`, `GOOGLE_SYNC`, `CLOUD_SYNC` definiert, nie enforced | `packages/credits/src/operations.ts` |
| Settings UI | Kein Sync-Toggle | `routes/(app)/settings/+page.svelte` |
## Änderungen
### 1. Credit-Operationen bereinigen
**Datei**: `packages/credits/src/operations.ts`
`CALDAV_SYNC` und `GOOGLE_SYNC` entfernen (nicht Teil dieses Plans). `CLOUD_SYNC` behalten und Kosten anpassen:
```typescript
CLOUD_SYNC = 'cloud_sync' // 30 Credits/Monat (oder 90/Quartal, 360/Jahr)
```
Metadata updaten: `description: 'Cloud-Synchronisation über alle Geräte'`
### 2. Sync-Subscriptions-Tabelle
**Neue Datei**: `services/mana-credits/src/db/schema/sync.ts`
```sql
-- In credits schema
CREATE TABLE credits.sync_subscriptions (
user_id TEXT PRIMARY KEY,
active BOOLEAN NOT NULL DEFAULT false,
billing_interval TEXT NOT NULL DEFAULT 'monthly', -- 'monthly' | 'quarterly' | 'yearly'
amount_charged INTEGER NOT NULL, -- 30, 90, oder 360
activated_at TIMESTAMPTZ,
next_charge_at TIMESTAMPTZ,
paused_at TIMESTAMPTZ, -- Gesetzt wenn Balance zu niedrig
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
```
Export in `schema/index.ts` hinzufügen.
### 3. Sync-Billing-Service
**Neue Datei**: `services/mana-credits/src/services/sync-billing.ts`
Methoden:
- `activateSync(userId, interval)` Balance prüfen, abbuchen, Subscription anlegen
- `deactivateSync(userId)` Subscription deaktivieren, keine Rückerstattung
- `getSyncStatus(userId)` `{ active, interval, nextChargeAt, pausedAt }`
- `chargeRecurring()` Cron: alle fälligen Subscriptions abrechnen, bei Insufficient-Balance pausieren
- `reactivateSync(userId)` Nach Pause: Balance prüfen, abbuchen, reaktivieren
Alle Credit-Bewegungen über den bestehenden `CreditsService.useCredits()` immutable Ledger bleibt konsistent.
### 4. API-Endpoints
**User-facing** (JWT auth) neue Route-Datei `services/mana-credits/src/routes/sync.ts`:
| Method | Path | Beschreibung |
|--------|------|--------------|
| GET | `/api/v1/sync/status` | Sync-Status des Users |
| POST | `/api/v1/sync/activate` | Sync aktivieren (Body: `{ interval: 'monthly' | 'quarterly' | 'yearly' }`) |
| POST | `/api/v1/sync/deactivate` | Sync deaktivieren |
| POST | `/api/v1/sync/change-interval` | Intervall wechseln (ab nächster Abrechnung) |
**Internal** (X-Service-Key) in bestehende `routes/internal.ts` einfügen:
| Method | Path | Beschreibung |
|--------|------|--------------|
| GET | `/api/v1/internal/sync/status/:userId` | Sync-Status für mana-sync Server-Check |
| POST | `/api/v1/internal/sync/charge-recurring` | Cron-Trigger für monatliche Abbuchung |
### 5. Cron-Job für monatliche Abbuchung
**Neue Datei**: `services/mana-credits/src/jobs/sync-charge.ts`
- Läuft täglich (via mana-events Scheduler oder eigener Bun.cron)
- Query: `WHERE active = true AND next_charge_at <= NOW()`
- Für jeden: `useCredits()` aufrufen
- Erfolg: `next_charge_at += interval`
- Insufficient Credits: `active = false`, `paused_at = NOW()`
- Notification senden via mana-notify (In-App-Banner + E-Mail)
### 6. Client-seitiges Sync-Gating
**Datei**: `apps/mana/apps/web/src/lib/data/sync.ts`
`createUnifiedSync()` bekommt einen `syncEnabled`-Parameter:
```typescript
export function createUnifiedSync(serverUrl: string, getToken: () => Promise<string>, syncEnabled: boolean) {
// Wenn !syncEnabled: startAll() wird zum No-Op, ensureAppSynced() gibt sofort zurück
}
```
**Datei**: `apps/mana/apps/web/src/routes/(app)/+layout.svelte`
Beim Auth-Ready:
1. `GET /api/v1/sync/status` aufrufen
2. Ergebnis in einen reaktiven Store (`syncStatusStore`) schreiben
3. `createUnifiedSync(..., syncStatus.active)` aufrufen
4. Wenn `syncStatus.paused`: Banner anzeigen
**Neuer Store**: `apps/mana/apps/web/src/lib/stores/sync-status.svelte.ts`
```typescript
export const syncStatus = $state({
active: false,
interval: 'monthly' as 'monthly' | 'quarterly' | 'yearly',
nextChargeAt: null as string | null,
paused: false,
});
```
### 7. In-App-Banner bei pausiertem Sync
**Datei**: `apps/mana/apps/web/src/routes/(app)/+layout.svelte` (oder eigene Komponente)
Wenn `syncStatus.paused`:
```
⚠️ Cloud Sync pausiert — deine Credits reichen nicht aus.
[Credits aufladen] [Sync-Einstellungen]
```
Persistent am oberen Rand, dismissable aber kommt nach 24h wieder.
### 8. Settings UI
**Neue Sektion** in Settings oder eigene Route `routes/(app)/settings/sync/+page.svelte`:
- **Status**: Aktiv/Inaktiv/Pausiert
- **Intervall-Auswahl**: Monatlich (30 Credits) / Quartal (90) / Jahr (360)
- **Info**: Nächste Abbuchung am X, aktueller Kontostand: Y Credits
- **Aktivieren/Deaktivieren**-Button
- **Hinweis bei Deaktivierung**: "Deine Daten bleiben lokal erhalten. Du kannst jederzeit reaktivieren."
### 9. Server-seitiges Gating (mana-sync)
**Datei**: `services/mana-sync/` (Go)
Middleware auf Push/Pull-Endpoints:
- Vor jedem Request: `GET /api/v1/internal/sync/status/:userId` (cached 5 Minuten)
- Bei `active = false`: HTTP 402 `{ error: "sync_inactive", message: "Cloud Sync ist nicht aktiv" }`
- Client fängt 402 ab setzt `syncStatus.paused = true` zeigt Banner
### 10. E-Mail-Notification bei Pause
Über mana-notify (bestehender Service):
- Template: "Dein Cloud Sync wurde pausiert"
- Inhalt: Aktueller Kontostand, Link zu Credits kaufen, Link zu Settings
- Trigger: Wenn `chargeRecurring()` eine Subscription pausiert
## Dateien-Übersicht
| Aktion | Datei |
|--------|-------|
| **Neu** | `services/mana-credits/src/db/schema/sync.ts` |
| **Neu** | `services/mana-credits/src/services/sync-billing.ts` |
| **Neu** | `services/mana-credits/src/routes/sync.ts` |
| **Neu** | `services/mana-credits/src/jobs/sync-charge.ts` |
| **Neu** | `apps/mana/apps/web/src/lib/stores/sync-status.svelte.ts` |
| **Neu** | `apps/mana/apps/web/src/routes/(app)/settings/sync/+page.svelte` |
| **Editieren** | `packages/credits/src/operations.ts` CALDAV/GOOGLE entfernen, CLOUD_SYNC auf 30 |
| **Editieren** | `services/mana-credits/src/db/schema/index.ts` Sync-Schema exportieren |
| **Editieren** | `services/mana-credits/src/index.ts` SyncBillingService + Sync-Routes registrieren |
| **Editieren** | `services/mana-credits/src/routes/internal.ts` Sync-Status + Charge-Endpoint |
| **Editieren** | `services/mana-credits/src/lib/validation.ts` Sync-Schemas |
| **Editieren** | `apps/mana/apps/web/src/lib/data/sync.ts` Gating-Parameter |
| **Editieren** | `apps/mana/apps/web/src/routes/(app)/+layout.svelte` Sync-Status laden |
| **Editieren** | `apps/mana/apps/web/src/routes/(app)/settings/+page.svelte` Sync-Link |
| **Editieren** | `services/mana-sync/` (Go) Middleware für 402 |
## Phasen
### Phase 1: Backend + Client-Gating
- Schema, Service, API-Endpoints in mana-credits
- Sync-Status-Store + Gating in sync.ts
- Settings UI mit Activate/Deactivate
- In-App-Banner bei Pause
### Phase 2: Server-Gating + Cron + Notifications
- mana-sync Go-Middleware (402 bei inaktivem Sync)
- Cron-Job für tägliche Abrechnung
- E-Mail-Notification via mana-notify bei Pause