vcard4reseller/docs/KONZEPT.md
2026-05-30 20:46:05 +02:00

248 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# vcard4reseller — Konzept & Datenmodell
> White-Label-Plattform für digitale Visitenkarten (PrintShopCreator).
> Reseller (Druckereien/Agenturen) bieten ihren Firmenkunden ein zentrales
> Identity-Management an: ein Mitarbeiterprofil als *Single Source of Truth*,
> das in alle digitalen und gedruckten Ausgabekanäle synchronisiert wird.
**Status:** Konzeptphase — noch kein Code.
**Stack-Entscheidung:** Symfony (API Platform, JSON-API) + eigenständige Vue 3 SPA (Vite), MariaDB. Architektur **Variante A** (SPA + getrennte API).
---
## 1. Ziele & Leitprinzipien
1. **Single Source of Truth:** Eine Änderung am Mitarbeiterprofil propagiert in *alle* Kanäle (vCard, Wallet, Landingpage, NFC, QR, Druckdaten, E-Mail-Signatur).
2. **Mandantenfähigkeit (Multi-Tenancy):** Strikte Datentrennung pro Reseller und pro Firmenkunde.
3. **White-Label:** Jeder Reseller / Firmenkunde tritt unter eigener Domain & eigenem Branding auf.
4. **Wiederkehrender Umsatz:** Reseller zahlt Plattformgebühr (Paket), Firmenkunde zahlt Reseller (eigene Preisgestaltung).
5. **Öffentliche Profilseiten** müssen schnell & SEO-fähig sein → serverseitig gerendert (Twig), getrennt von den Dashboards (Vue-SPA).
---
## 2. Rollen & Berechtigungen
| Rolle | Symfony-Role | Scope | Kann |
|-------|--------------|-------|------|
| Plattform-Admin | `ROLE_PLATFORM_ADMIN` | global | Reseller anlegen/verwalten, Plattform-Pakete, Abrechnung gegenüber Resellern, Demo-Leads |
| Reseller-Admin | `ROLE_RESELLER_ADMIN` | 1 Reseller | Firmenkunden + Domains + Branding anlegen, Preise definieren, Druckaufträge, Reseller-Mitarbeiter |
| Firmen-Admin | `ROLE_COMPANY_ADMIN` | 1 Company | Standorte, Mitarbeiter/Profile, Templates der Firma, NFC/QR ausrollen |
| Mitarbeiter | `ROLE_EMPLOYEE` | 1 Profil | eigenes Profil pflegen (sofern Firma das erlaubt) |
| Öffentlicher Besucher | — | — | Profilseite ansehen, vCard/Wallet herunterladen, NFC/QR scannen |
**Hierarchie der Mandanten:** `Plattform → Reseller → Company → Location → Employee/Profil`
Durchsetzung über:
- **Doctrine-Filter** (automatisches Scoping nach `reseller_id` / `company_id` je nach eingeloggtem Kontext)
- **Security Voters** (z. B. `ProfileVoter`, `CompanyVoter`) für feingranulare Aktionen
- API-Platform `security`-Attribute pro Resource & Operation
---
## 3. Architektur
```
┌──────────────────────────┐ ┌──────────────────────────────────┐
│ Vue 3 SPA (Vite) │ │ Öffentliche Profilseiten │
│ - Reseller-Dashboard │ │ (Symfony + Twig, SSR, SEO) │
│ - Firmen-Dashboard │ │ /{slug}, /c/{company}/{slug} │
│ - Mitarbeiter-Self-Svc │ │ vCard-Download, Wallet, QR-Ziel │
└───────────┬──────────────┘ └─────────────────┬────────────────┘
│ JSON (JWT/Session) │
▼ ▼
┌──────────────────────────────────────────────────────────────────────┐
│ Symfony Backend │
│ ├─ API Platform (REST/JSON, OpenAPI) │
│ ├─ Security (JWT/LexikJWT od. Session), Voters, Doctrine-Filter │
│ ├─ Domain Services (ProfileSync, ChannelGenerator) │
│ ├─ Messenger (async): Wallet-Pass, Druckdaten-PDF, QR/NFC, E-Mail │
│ └─ Doctrine ORM │
└───────────┬───────────────────────┬───────────────────┬──────────────┘
▼ ▼ ▼
MariaDB Object-Storage externe Dienste
(Mandantendaten) (Assets, PDFs, Pässe) (Apple/Google Wallet,
Payment/Stripe, Mail)
```
**Generierungs-Pipeline (ChannelGenerator):** Bei jeder Profiländerung wird ein
`ProfileUpdated`-Event ausgelöst → Messenger-Handler regenerieren die abgeleiteten
Artefakte (vCard-String, Wallet-Pass, QR/Kurz-URL, Druck-PDF, E-Mail-Signatur-HTML)
und legen sie versioniert ab. Öffentliche Endpunkte liefern immer den aktuellen Stand.
---
## 4. Datenmodell (Entitäten)
### Kernentitäten
- **User** — Auth-Identität. Felder: `email`, `password`, `roles[]`, `status`, `lastLogin`. Verknüpft optional mit `reseller_id`, `company_id`, `employee_id` (je nach Rolle).
- **Reseller** — oberster Mandant. Felder: `name`, `slug`, `primaryDomain`, Branding-Defaults, `status`, `platformPlan_id`. Hat Limits aus dem Plattform-Paket.
- **PlatformPlan** — Reseller-Pakete (Starter/Professional/Business). Felder: `name`, `pricePerMonth`, `maxProfiles`, `maxCompanies`, `features[]`.
- **ResellerSubscription** — Abo des Resellers bei der Plattform. Felder: `plan_id`, `status`, `startedAt`, `renewsAt`, `paymentRef` (Stripe).
- **Company** (Firmenkunde) — gehört zu Reseller. Felder: `name`, `slug`, Branding (Logo, Farben, Fonts), `defaultLocation_id`, `status`, `selfEditEnabled` (erlaubt Mitarbeiter-Self-Service generell). Domains ausgelagert in `Domain`.
- **Domain** — gehört zu Company. Felder: `hostname`, `type` (`subdomain`|`custom`), `status` (`pending`|`verified`|`failed`), `verificationCheckedAt`, `tlsStatus`. Custom-Domains zeigen per A-Record auf unsere IP (siehe §11).
- **Location** (Standort) — gehört zu Company. Felder: Adresse, Geo, Telefon, Öffnungszeiten, Standort-spezifisches Branding-Override.
- **Employee / Profile** — gehört zu Company, optional Location. **Single Source of Truth.** Felder: Name, Titel, Position, Abteilung, Telefon(e), E-Mail, Foto, Bio, `slug` (öffentlich), `status`, `user_id` (optional, für Self-Service), `selfEditAllowed` (bool — Firmen-Admin gibt diesen Mitarbeiter frei), `editableFields` (optional: welche Felder der Mitarbeiter ändern darf).
- **ContactLink** — Social-/Web-Links eines Profils (Typ + URL + Reihenfolge). 1:n zu Employee.
### Branding & Vorlagen
- **Template** — Vorlage, scoped auf Reseller *oder* Company. Felder: `type` (`print_card` | `email_signature` | `landing_page` | `wallet_pass`), `name`, `config` (JSON), `assets`.
- **Asset** — hochgeladene Datei (Logo, Foto, Hintergrund). Felder: `path`, `mimeType`, `owner` (polymorph: reseller/company/employee).
### Ausgabekanäle (generierte Artefakte)
- **NfcTag** — physischer Tag. Felder: `uid`, `shortUrl`, `employee_id` (nullable bis Zuweisung), `status` (`unassigned`/`active`/`disabled`), `lastScanAt`.
- **QrCode** — generierter QR (statisch oder dynamische Redirect-URL). Felder: `target`, `imageAsset`, `employee_id`.
- **WalletPass** — Referenz auf Apple/Google-Pass. Felder: `provider`, `serial`, `passUrl`, `employee_id`, `lastGeneratedAt`.
- **GeneratedArtifact** (optional, generisch) — Cache abgeleiteter Outputs: `type` (vcard/print_pdf/signature_html), `employee_id`, `payload`/`fileRef`, `generatedAt`, `version`.
### Vertrieb & Abrechnung
- **Invoice** — Rechnung **Plattform→Reseller**. Felder: `reseller_id`, `amount`, `period`, `status`, `pdfRef`. (Reseller→Company-Rechnungen vorerst nicht Teil der Plattform, siehe §8.)
- ~~**CompanySubscription / PricePlan**~~ — *zurückgestellt:* Reseller↔Company-Abrechnung läuft außerhalb der Plattform.
- **PrintOrder** — Druckauftrag. Felder: `company_id`, `status` (`new`/`in_production`/`shipped`), `items[]` (je Employee), `printDataRef`.
### Marketing / Sonstiges
- **DemoRequest** — Lead vom Marketing-Formular (`/demo`). Felder: Name, Firma, E-Mail, Telefon, Nachricht, `status`.
- **AuditLog** — Nachvollziehbarkeit kritischer Aktionen (wer/was/wann).
---
## 5. ER-Diagramm
```mermaid
erDiagram
PlatformPlan ||--o{ Reseller : "definiert Limits"
Reseller ||--|| ResellerSubscription : hat
Reseller ||--o{ Company : verwaltet
Reseller ||--o{ User : "Reseller-Staff"
Reseller ||--o{ Template : "Defaults"
Company ||--o{ Location : hat
Company ||--o{ Domain : "Sub-/Custom-Domain"
Company ||--o{ Employee : beschäftigt
Company ||--o{ User : "Firmen-Admins"
Company ||--o{ Template : "eigene"
Location ||--o{ Employee : "zugeordnet"
Employee ||--o{ ContactLink : hat
Employee ||--o| NfcTag : verknüpft
Employee ||--o{ QrCode : hat
Employee ||--o| WalletPass : hat
Employee ||--o{ GeneratedArtifact : erzeugt
Employee ||--o| User : "Self-Service"
Company ||--o{ PrintOrder : bestellt
Reseller ||--o{ Invoice : "Plattform→Reseller"
```
---
## 6. API-Struktur (API Platform, grob)
Alle unter `/api`, JWT-geschützt, mandantengescoped. Beispiele:
| Resource | Operationen | Zugriff |
|----------|-------------|---------|
| `/api/resellers` | CRUD | Platform-Admin |
| `/api/companies` | CRUD | Reseller-Admin (eigene), Platform-Admin |
| `/api/locations` | CRUD | Company-/Reseller-Admin |
| `/api/employees` | CRUD | Company-Admin; GET self für Employee |
| `/api/employees/{id}/regenerate` | POST | löst Kanal-Neugenerierung aus |
| `/api/templates` | CRUD | Reseller-/Company-Admin |
| `/api/nfc-tags` | CRUD + assign | Company-Admin |
| `/api/print-orders` | CRUD | Company-Admin / Reseller |
| `/api/demo-requests` | POST (öffentlich), GET (Admin) | Marketing |
**Öffentliche, nicht-API Endpunkte (Symfony/Twig, SSR):**
- `GET /{slug}` oder `https://{companyDomain}/{employeeSlug}` → Landingpage
- `GET /{slug}/vcard.vcf` → vCard-Download
- `GET /{slug}/wallet` → Wallet-Pass
- `GET /t/{nfcShortCode}` → NFC/QR-Redirect auf aktuelles Profil
---
## 7. Sicherheit & Mandantentrennung
- **Auth:** LexikJWTAuthenticationBundle (Stateless API) oder Session für SPA-Komfort. Refresh-Tokens.
- **Scoping:** Doctrine-`Filter`, der je nach eingeloggtem User automatisch `reseller_id`/`company_id` in alle Queries injiziert. Kein Datenleck zwischen Mandanten.
- **Voters:** `CompanyVoter`, `EmployeeVoter`, `TemplateVoter` für Aktion×Objekt-Prüfung.
- **Domain-Auflösung:** Middleware ermittelt aus Host-Header (Custom-Domain/Subdomain) den Mandantenkontext für öffentliche Seiten.
- **Datenschutz:** DSGVO — Mitarbeiterdaten, Lösch-/Export-Funktion, AuditLog. Standort EU.
---
## 8. Billing-Modell
**Entscheidung:** Vorerst rechnet **nur die Plattform gegenüber dem Reseller** ab. Wie der Reseller seinerseits mit den Firmenkunden abrechnet, regelt er selbst — das ist *nicht* Teil der Plattform (Stand jetzt). Eine spätere optionale Reseller→Company-Fakturierung bleibt als Erweiterung vorgemerkt.
1. **Plattform → Reseller (umgesetzt):** Reseller wählt `PlatformPlan` (Starter 99 € / Professional 249 € / Business 599 €). Stripe-Abo, Limits (`maxProfiles`, `maxCompanies`) werden durchgesetzt.
2. **Reseller → Company (außerhalb der Plattform):** Reseller vereinbart Preise selbst. Plattform bietet höchstens einen **Umsatzrechner/Reporting** zur Orientierung, aber keine Rechnungsstellung. → Entität `CompanySubscription`/`Invoice` (Reseller→Company) wird **zurückgestellt** auf Phase „später, falls gewünscht".
3. **Umsatzrechner** (aus Marketing): Profile × Preis Plattformgebühr = Marge (rein informativ).
---
## 9. Roadmap / Phasen
**Phase 0 — Setup** (nach Konzept-Freigabe)
- Symfony-Projekt + API Platform + Doctrine + MariaDB
- Vue 3 SPA (Vite) Grundgerüst, Auth-Flow
- CI, Docker-Dev-Umgebung
**Phase 1 — Kern-Domäne & Auth**
- Entitäten: User, Reseller, Company, Location, Employee, ContactLink
- Rollen, Voters, Doctrine-Mandantenfilter
- Reseller-/Firmen-Dashboards (CRUD)
**Phase 2 — Öffentliche Profile**
- SSR-Landingpage, vCard-Download, QR-Code, Kurz-URL-Redirect
- Branding/Template-Engine (landing_page)
**Phase 3 — Ausgabekanäle**
- Wallet-Pässe (Apple/Google), E-Mail-Signaturen, Druckdaten-PDF
- NFC-Tag-Verwaltung & Zuweisung
- Messenger-Pipeline für async Generierung
**Phase 4 — Billing & Vertrieb**
- Plattform-Pakete + Stripe, Limit-Durchsetzung
- Reseller-Preisgestaltung, Umsatzrechner, PrintOrders, Invoices
**Phase 5 — Marketing-Site & Politur**
- Öffentliche Landingpage (vcard4reseller.de-Nachbau), Demo-Formular → DemoRequest
- Reporting, AuditLog, DSGVO-Tools
---
## 10. Geklärte Entscheidungen & offene Fragen
### Geklärt (2026-05-30)
1. **Domains:** Standard = **Subdomains** (`firma.reseller.de`). Firmenkunden können zusätzlich eine **eigene Domain** hinterlegen; sie müssen dann selbst per **A-Record auf unsere IP** zeigen. Plattform übernimmt Domain-Verifikation + TLS-Provisionierung (Let's Encrypt). Siehe §11.
2. **Self-Service:** **Möglich, aber muss vom Firmen-Admin freigegeben werden** — zweistufig: `Company.selfEditEnabled` (Firma erlaubt es generell) + `Employee.selfEditAllowed` (pro Mitarbeiter). Mitarbeiter bearbeitet nur freigegebene Felder.
3. **Billing:** Nur **Plattform → Reseller** (siehe §8). Reseller↔Company außerhalb der Plattform; spätere Erweiterung möglich.
### Noch offen
4. **Wallet-Pässe:** Apple Developer Account + Google Wallet API vorhanden? (Zertifikate erforderlich)
5. **Druckdaten:** Welches Format erwarten die Druckereien (PDF/X, bestimmte Maße, Beschnitt)? Gibt es Vorlagen?
6. **Bestehende Daten/Branding:** Existieren Design-Assets/CI zur bestehenden vcard4reseller.de, die wir übernehmen?
---
## 11. Domain-Handling
**Subdomain (Default):** Jede Company bekommt `{company.slug}.{reseller.primaryDomain}`. Wildcard-DNS + Wildcard-TLS auf Reseller-Ebene → keine Aktion durch den Kunden nötig.
**Eigene Domain (optional):**
1. Firmen-Admin trägt seine Domain (`visitenkarte.firma.de`) im Dashboard ein → Status `pending`.
2. Plattform zeigt die einzutragende **IP (A-Record)** (+ optional CNAME) an.
3. Kunde setzt den DNS-Eintrag bei seinem Provider.
4. Plattform prüft periodisch (DNS-Lookup) → bei Treffer Status `verified`, dann **automatisches TLS-Zertifikat** (Let's Encrypt / ACME).
5. Reverse-Proxy/Router löst den `Host`-Header → Mandantenkontext (Company) auf.
**Datenmodell-Ergänzung** an `Company` bzw. neue Entität **`Domain`**:
`hostname`, `type` (`subdomain`|`custom`), `status` (`pending`|`verified`|`failed`), `verificationCheckedAt`, `tlsStatus`, `company_id`.