Projekt Skizze
This commit is contained in:
commit
03407b76e3
247
docs/KONZEPT.md
Normal file
247
docs/KONZEPT.md
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
# 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`.
|
||||||
Loading…
Reference in New Issue
Block a user