vcard4reseller/docs/KONZEPT.md
Thomas Peterson 408b37a5ea Druckdaten: druckfertige Visitenkarten-PDF (CMYK, Beschnitt, V/R)
- CardTemplate-Entität (Layout pro Firma; company=null = globale Vorlage)
- CardTemplateFactory: Standardlayout, greift Firmen-Branding + QR ab
- CardPdfRenderer (TCPDF): 85x55mm + 2mm Beschnitt, Schnittmarken, CMYK,
  Vorder-/Rückseite, mm-genaue Element-Platzierung, eingebetteter QR
- GET /api/employees/{id}/card.pdf (Auth + Mandantenprüfung)
- Konzept §13 (Druckdaten) ergänzt

Verifiziert: 2 Seiten, CMYK-Farbraum, Schnittmarken, Branding durchgängig.
Offen: PDF/X-1a-Finishing (Ghostscript), Font-Embedding, visueller Editor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 11:54:02 +02:00

21 KiB
Raw Blame History

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 (apple|google), serial, authToken (Apple Web-Service-Auth), passUrl, employee_id, lastGeneratedAt. Siehe §12.
  • WalletDevice — registriertes Apple-Gerät für Push-Updates (Apple-spezifisch). Felder: deviceLibraryId, pushToken, serial (→ WalletPass), registeredAt. Google braucht das nicht (Server-Push über die API).
  • 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 / PricePlanzurü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

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
    WalletPass ||--o{ WalletDevice : "Apple-Push"
    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

  1. Wallet-Pässe: Apple Developer Account + Google Wallet API vorhanden? (Zertifikate erforderlich). White-Label-Frage: läuft der Apple-Pass unter einem zentralen Pass Type ID (Plattform) oder pro Reseller? Konzept dazu in §12.
  2. Druckdaten: Welches Format erwarten die Druckereien (PDF/X, bestimmte Maße, Beschnitt)? Gibt es Vorlagen?
  3. 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.


12. Wallet-Pässe & Kontakt-Synchronisation

Das Grundproblem

Eine heruntergeladene vCard (.vcf) ist ein Schnappschuss: Einmal in der Kontakte-App gespeichert, aktualisiert sie sich nicht mehr. Ändert ein Mitarbeiter seine Nummer, haben alle Empfänger veraltete Daten. Das ist eine OS-Einschränkung. Es gibt drei Wege zu „aktuellen Daten":

Weg Echter Auto-Sync? Wo landet es? Aufwand
Wallet-Pass (Apple/Google) Ja, over-the-air Wallet-App (nicht Kontakte) hoch (Zertifikate)
Link behalten (QR/NFC/Kurz-URL) Daten aktuell beim Öffnen, vCard wird live generiert Browser → Kontakt bei Bedarf neu umgesetzt (Phase 2)
CardDAV Ja, auf Kontakt-Ebene native Kontakte-App mittel, für Einzel-Empfänger unpraktisch (account-basiert)

Fazit: Den einzigen echten „Push in eine bereits gespeicherte Karte" liefert der Wallet-Pass. Der Link (unsere Profilseite) ist der pragmatische Standard: nicht die Daten, sondern der Link wird gespeichert; die vCard erzeugen wir bei jedem Abruf frisch.

Apple Wallet (PassKit)

  • .pkpass = ZIP aus pass.json + Bildern + manifest.json + PKCS#7-Signatur (Pass Type ID-Zertifikat + Apple-WWDR-Cert).
  • Pass-Stil generic (Visitenkarte): Name / Position / Firma / Telefon / E-Mail / Link.
  • PHP: z. B. includable/php-pkpass, gekapselt in einem WalletService.
  • Sync (Push): Pass enthält webServiceURL + authenticationToken.
    1. Nutzer fügt Pass hinzu → Gerät registriert sich (→ WalletDevice).
    2. Profil ändert sich → leerer APNs-Push an registrierte Geräte.
    3. Gerät holt aktualisierten Pass von unserer PassKit-Web-Service-API ab.
  • Nötige Endpunkte (PassKit Web Service): register, unregister, list serials, latest pass.

Google Wallet

  • REST-API + Service-Account (kostenlos). Generic-Pass-Klasse + pro Mitarbeiter ein Objekt.
  • Hinzufügen via „Add to Google Wallet"-Link (signiertes JWT).
  • Sync: Objekt server-seitig per API patchen → Google pusht selbst. Kein APNs, keine Geräte-Registrierung. → einfacher als Apple, daher als erster Wallet-Kanal empfohlen.

Einbettung in die Architektur

Employee geändert ──► ProfileUpdated-Event ──► Messenger (async)
   ├─ Apple:  Pass neu signieren  +  APNs-Push an WalletDevices
   └─ Google: Wallet-Objekt per API patchen (Google pusht selbst)

Neue Bausteine: WalletPass/WalletDevice (Datenmodell §4), WalletService (Pass bauen/signieren/patchen), ApplePassController (Web-Service-Endpunkte), Messenger-Handler am ProfileUpdated-Event (gleicher Trigger wie Employee::touch()).

White-Label-Überlegung

Apple Pass Type ID ist an einen Apple-Account gebunden. Optionen: (a) ein zentraler Pass Type ID der Plattform für alle Reseller (einfacher, Branding über Pass-Felder/Logo) oder (b) pro Reseller ein eigener (aufwändig, jeder Reseller braucht Apple-Account). Empfehlung: (a) zentral, Reseller-/Firmen-Branding über Logo & Farben im Pass. → zu klären (offene Frage #4).

Voraussetzungen (offen)

  • Apple Developer Account + Pass Type ID + Zertifikat (.p12) — für Apple Wallet.
  • Google-Cloud-Projekt + Wallet-API + Service-Account — für Google Wallet.

13. Druckdaten: Visitenkarten-PDF (Kerngeschäft)

Reseller drucken für ihre Firmenkunden Visitenkarten und brauchen druckfertige PDFs. Das Layout variiert pro Firma. Später kommt Briefpapier (gleiches System, anderes Format) dazu.

Festgelegte Entscheidungen (2026-05-31)

  • Farbraum/Qualität: CMYK, Ziel PDF/X-1a, eingebettete Fonts.
  • Format: 85×55 mm, 2 mm Beschnitt (→ 89×59 mm Dokument), Schnittmarken, Sicherheitsabstand ~4 mm.
  • Seiten: Vorder- und Rückseite (2 PDF-Seiten).
  • Ausgabe: ein PDF pro Mitarbeiter (Druckbogen/Ausschießen später).
  • Layout-Definition: visueller Editor im Dashboard → strukturierte Element-Definition (JSON), nicht HTML.

Datenmodell

CardTemplate — gehört zu einer Company (company = null ⇒ globale Standardvorlage). Felder: name, widthMm (85), heightMm (55), bleedMm (2), safeMm (4), front (Element[]), back (Element[]), type (card | später letterhead), company_id.

Element (JSON): type (field | text | image | qr | rect | line), xMm, yMm, wMm, hMm, rotation, Typografie (fontFamily, fontSizePt, bold, italic, align, lineHeight), color (CMYK {c,m,y,k} 0100), fill (CMYK), binding (bei field: Mitarbeiter-Feld), text (bei text: statisch), src (bei image).

Bindings (Datenquelle = Mitarbeiterprofil): firstName, lastName, fullName, position, department, email, phone, mobile, company.name, profileUrl, shortUrl; Spezial: qr (Kurz-URL), logo (Firmen-Logo aus brandingConfig).

Rendering-Pipeline

CardTemplate (JSON) + Employee + Branding
        │
        ▼  TCPDF (Koordinaten in mm, CMYK, Fonts eingebettet)
   89×59 mm Seite (Trim 85×55 + 2 mm Bleed), Schnittmarken, V/R = 2 Seiten
        │
        ▼  (Finishing) Ghostscript → PDF/X-1a:2001 mit CMYK-Output-Intent (ICC, z. B. ISO Coated v2)
   druckfertiges PDF
  • TCPDF: exakte mm-Platzierung pro Element, setColor/setTextColor mit CMYK, cropMark() an den Trim-Ecken, Bilder/QR als eingebettete Grafik, Fonts eingebettet.
  • PDF/X-1a-Finishing (Ghostscript) als nachgelagerter Schritt — braucht ghostscript
    • ICC-Profil im Container. Bis dahin: valides CMYK-PDF mit Bleed + Schnittmarken.

Endpunkte

  • GET /api/employees/{id}/card.pdf — Einzelkarte (auth, Reseller/Firma; mandantengeprüft).
  • später: Sammel-PDF / ausgeschossener Bogen je Firma; Anbindung an PrintOrder.

Visueller Editor (SPA)

  • Canvas im mm-Maßstab mit sichtbarem Beschnitt-, Trim- und Sicherheitsrahmen.
  • Elemente per Drag&Drop; Eigenschaften-Panel (Position/Größe/Font/Farbe/Datenbindung).
  • Tabs Vorder-/Rückseite; Live-Vorschau mit echten oder Beispiel-Daten.
  • Speichern als CardTemplate (pro Firma); Default-Vorlage als Startpunkt.

Briefpapier (später)

Gleiches CardTemplate-Modell mit type = letterhead und Format A4; der Renderer ist format-agnostisch (Maße + Elementliste). Bindings identisch + ggf. Adressfeld/Faltmarken.