presseportale/dev/migration 2026/04-DATA-MODEL.md
Kevin Adametz 5b8bdf4182
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
12-05-2026 Frontend dev
2026-05-12 18:32:33 +02:00

17 KiB
Raw Blame History

04 Datenmodell (Mapping Doctrine → Eloquent)

Dieses Dokument beschreibt das neue DB-Schema und wie die Legacy-Tabellen dorthin gemappt werden. Ziel: Saubere Laravel-Konventionen, portal-Scoping und Legacy-IDs für Imports.

Gemeinsame Konventionen

  • Jede Tabelle hat: id (BIGINT UNSIGNED AI), created_at, updated_at.
  • Fachliche Entitäten haben: deleted_at (Soft-Delete).
  • Multi-Portal-Entitäten haben zusätzlich: portal (ENUM presseecho, businessportal24, both both nur für Stammdaten).
  • Importierte Datensätze haben: legacy_portal (ENUM), legacy_id (BIGINT), UNIQUE (legacy_portal, legacy_id).
  • FK-Spalten: *_id BIGINT UNSIGNED, constrainted, i.d.R. ON DELETE wie in Legacy.
  • UUIDs nur dort, wo externe Referenzen nötig (z.B. öffentliche PM-Links): Spalte uuid, indiziert.

1. Users & Profil

users (erweitert das bestehende Schema)

Spalte Typ Legacy-Feld Bemerkung
id BIGINT PK Neu
name VARCHAR(120) Display-Name
email VARCHAR(190) UNIQUE sfGuardUserProfile.email
email_verified_at TIMESTAMP
password VARCHAR(255) NULL bcrypt; null erlaubt (Magic-Link-Only-User wie viele Companies)
remember_token VARCHAR(100) sfGuardRememberKey
two_factor_* (bestehend) Fortify
registration_type ENUM('agency','company','apiuser','existing_*') sfGuardUserProfile.registration_type
language CHAR(2) DEFAULT 'de' sfGuardUserProfile.language
portal ENUM('presseecho','businessportal24','both') Standard Portal
is_active BOOL DEFAULT 1 sfGuardUser.is_active
is_super_admin BOOL DEFAULT 0 sfGuardUser.is_super_admin nur Migrationshilfe; später via Spatie-Rolle
last_login_at TIMESTAMP sfGuardUser.last_login
last_login_ip VARCHAR(45) sfGuardUser.ip_address
gdpr_consent_at TIMESTAMP NULL Neu, für Consent-Tracking
legacy_portal ENUM Import-Spur
legacy_id BIGINT sfGuardUser.id
deleted_at TIMESTAMP

Nicht übernommen (D-09): Legacy-Password-Hash (sha1+salt) und Legacy-api_key wandern nicht in die neue users-Tabelle. Bei Go-Live bekommen alle User eine Reset-Mail; API-Clients müssen sich neuen Sanctum-Token generieren.

profiles (1:1 zu User, entspricht Rest von sfGuardUserProfile)

Spalte Typ Legacy-Feld
id, user_id (UK) user_id
salutation_key VARCHAR(20) salutation_id → Config-Key (z.B. mr,mrs)
title VARCHAR(80) title
first_name, last_name VARCHAR(80)
phone VARCHAR(40)
address VARCHAR(1000)
country_code CHAR(2) country_id → ISO Code aus Config
birthdate DATE
backlink_url VARCHAR(255)
show_stats BOOL
validation_date TIMESTAMP
contract_date TIMESTAMP
validate_token VARCHAR(32) validate
tax_id_number VARCHAR(255)
tax_exempt BOOL
tax_exempt_reason VARCHAR(1000)
disable_footer_code BOOL
timestamps

Rollen/Rechte (Spatie)

  • Tabellen: roles, permissions, model_has_roles, model_has_permissions, role_has_permissions (von Spatie).
  • Initial-Rollen: admin, editor, customer, api-only.
  • Migration: sfGuardGroup → Mapping auf neue Rollen (nicht 1:1!), sfGuardPermission → Permissions.
Spalte Typ Bemerkung
id BIGINT PK
user_id FK users.id
token_hash CHAR(64) UNIQUE sha256 des ausgegebenen Tokens
purpose ENUM('login','press_release_access','invoice_access','initial_setup')
payload JSON NULL z. B. {"press_release_id": 42}
expires_at TIMESTAMP TTL typisch 15 min
consumed_at TIMESTAMP NULL Single-Use
ip_requested VARCHAR(45)
ip_consumed VARCHAR(45) NULL
timestamps

Scheduler-Job PurgeExpiredMagicLinks räumt alte/konsumierte Einträge > 30 Tage auf.


2. Companies & Contacts

companies

Spalte Typ Legacy (Company)
id BIGINT PK
portal ENUM
owner_user_id BIGINT NULL user_id
type ENUM('agency','company') DEFAULT 'company' ctype
name VARCHAR(255)
slug VARCHAR(255) UNIQUE WITHIN portal (Sluggable)
address VARCHAR(1000)
country_code CHAR(2) country_id
phone VARCHAR(40)
fax VARCHAR(40)
email VARCHAR(190)
website VARCHAR(190)
logo_path VARCHAR(255) logo (Legacy, minimal)
logo_variants JSON NULL (neu, D-05: {sq,wide,dark})
is_active BOOL DEFAULT 1
disable_footer_code BOOL
legacy_portal, legacy_id
timestamps, deleted_at

company_user (Pivot)

| user_id (PK) | company_id (PK) | role ENUM('member','responsible','owner') |

→ Vereinigt CompanyUser + ResponsibleCompanyUser in eine Pivot-Tabelle mit role.

contact_user (Pivot)

| user_id (PK) | contact_id (PK) |

→ Direkte User-Kontakt-Zuordnung für den Admin- und Customer-Kontext. Im Legacy-System wurde diese Zugehörigkeit indirekt aus company_user/responsible_company_user plus contacts.company_id abgeleitet. Der Import befüllt die Tabelle über legacy:import --step=link-associations idempotent aus bestehenden Firmenzuordnungen.

contacts

Spalte Typ Legacy (Contact)
id BIGINT
company_id FK companies.id
salutation_key VARCHAR(20) salutation_id
title VARCHAR(80)
first_name, last_name VARCHAR(80)
responsibility VARCHAR(255)
phone, fax VARCHAR(40)
email VARCHAR(190)
legacy_portal, legacy_id
timestamps, deleted_at

3. Kategorien

categories

| id | parent_id NULL | portal | is_active | legacy_*, timestamps |

category_translations

| id | category_id FK | locale CHAR(2) | name VARCHAR(255) | slug VARCHAR(255) | description TEXT | | UNIQUE (category_id, locale) |


4. Pressemitteilungen

press_releases

Spalte Typ Legacy (PressRelease)
id BIGINT
uuid CHAR(36) UNIQUE neu für öffentliche Links
portal ENUM
user_id FK
company_id FK NULL
category_id FK
language CHAR(2)
title VARCHAR(255)
slug VARCHAR(255) (unique: portal+language+slug)
text MEDIUMTEXT
backlink_url VARCHAR(255)
keywords VARCHAR(255)
status ENUM('draft','review','published','rejected','archived') status string
hits INT UNSIGNED DEFAULT 0
teaser_begin INT UNSIGNED
teaser_end INT UNSIGNED
no_export BOOL
published_at TIMESTAMP NULL neu
legacy_portal, legacy_id
timestamps, deleted_at

press_release_images (erweitert, D-05)

Spalten: id, press_release_id FK, disk VARCHAR(30) DEFAULT 'public', path VARCHAR(512), variants JSON (thumb/medium/large), title VARCHAR(120), description VARCHAR(500), copyright VARCHAR(255), is_preview BOOL, sort_order INT UNSIGNED, width INT, height INT, mime VARCHAR(60), legacy_*, timestamps, deleted_at.

Legacy war minimalistisch künftig dürfen PMs mehrere, größere Bilder enthalten (kein hartes Limit, UI-Limit konfigurierbar).

press_release_contact (Pivot, wie Legacy PressReleaseContact)

| press_release_id | contact_id |


5. Newsletter

newsletter_subscriptions

Spalte Typ Legacy (NewsletterSubscription)
id
portal ENUM
user_id FK NULL user_id
salutation_key VARCHAR(20)
first_name, last_name VARCHAR(80)
email VARCHAR(190) (unique per portal)
ip_address VARCHAR(45)
is_confirmed BOOL is_active
confirmation_token VARCHAR(32) validate
subscribed_at TIMESTAMP subscribe_date
unsubscribed_at TIMESTAMP unsubscribe_date
legacy_portal, legacy_id
timestamps

6. Blacklist / FooterCode

blacklists

| id | portal | title VARCHAR(255) | content MEDIUMTEXT | timestamps |

| id | portal | language CHAR(2) NULL | title VARCHAR(255) | content TEXT | is_global BOOL | timestamps |

| footer_code_id | category_id |

Entfällt (D-14): promotion_links, promotion_link_category werden nicht migriert und nicht neu angelegt.


7. Billing & Payment (komplett neu, D-03/D-12/D-13)

Wichtig: Legacy-PaymentOptions, -UserPaymentOptions, -UserPayments und -Coupons werden nicht 1:1 in die neue Billing-Logik migriert. Nur aktive Subscriptions werden als "grandfathered" übernommen. Legacy-Rechnungen werden dagegen vollständig als read-only Archivdaten importiert (legacy_invoices) und nicht in den neuen Rechnungskreis (invoices) umgebucht.

billing_addresses (entspricht UserBillingAddress)

| id | user_id FK UNIQUE | salutation_key | title | name | address1 | address2 | postal_code | city | country_code | timestamps |

invoice_billing_addresses

Wie billing_addresses, aber ohne user_id-FK; dafür Snapshot zum Zeitpunkt der Rechnung.

payment_options (neu definiert)

Spalte Typ Bemerkung
id BIGINT PK
article_number VARCHAR(60) UNIQUE Interne Artikelnummer
type ENUM('recurring','onetime') Stripe-Subscription oder One-Off
price_cents INT UNSIGNED
currency CHAR(3) DEFAULT 'EUR'
interval ENUM('monthly','yearly','once') NULL Für Recurring
is_hidden BOOL
stripe_product_id VARCHAR(60)
stripe_price_id VARCHAR(60)
timestamps, deleted_at

payment_option_translations (DE/EN)

| id | payment_option_id FK | locale | name | description |

user_payment_options (aktive Abos)

Spalte Typ Bemerkung
id BIGINT PK
user_id FK
payment_option_id FK
status ENUM('active','past_due','cancelled','grandfathered')
grandfathered_until DATE NULL D-13: Alt-Konditionen bis zu diesem Datum
legacy_conditions JSON NULL Snapshot alter Konditionen für Grandfathering
current_period_start DATE
current_period_end DATE
stripe_subscription_id VARCHAR(60) NULL
cancelled_at TIMESTAMP NULL
timestamps

user_payment_option_company (Pivot)

| user_payment_option_id | company_id | is_active | timestamps |

user_payments (nur noch Stripe-Charges)

| id | user_payment_option_id FK NULL | amount_cents | currency | status ENUM('pending','succeeded','failed','refunded') | stripe_charge_id | stripe_invoice_id NULL | timestamps |

invoices (neuer Rechnungskreis)

Spalte Bemerkung
id
user_id FK
user_payment_id FK NULL
invoice_billing_address_id FK Snapshot
number VARCHAR(40) UNIQUE neue Nummernkreis, z. B. {YYYY}{MM}-{seq}
status ENUM('open','paid','void','uncollectible') (Stripe-nah)
amount_cents / tax_cents / total_cents
currency CHAR(3)
is_netto BOOL
invoice_date / due_date / paid_at
stripe_invoice_id VARCHAR(60) NULL
pdf_path
timestamps, deleted_at

Entfällt: payment_method-Enum es gibt nur noch Stripe. Keine Rechnungszahlungs-Variante mehr.

legacy_invoices ARCHIV (D-12)

Separate, read-only Archivtabelle mit allen Rechnungen aus beiden Legacy-Portalen. Ziel ist der vollständige Import der vorhandenen Rechnungsdaten inkl. Status und User-Zuordnung; die PDF-Erzeugung für Legacy-Rechnungen bleibt DB-basiert und erfolgt bei Abruf/on demand.

Spalte Typ Legacy-Quelle
id BIGINT PK (neu)
legacy_portal ENUM
legacy_id BIGINT invoice.id
user_id FK users.id NULL Via Import-Map auflösbar
legacy_user_id BIGINT Original-User-ID im Legacy
number VARCHAR(40) Original-Rechnungsnummer
amount_cents / tax_cents / total_cents
status VARCHAR(40) Original-Status-String
invoice_date / due_date / paid_at
payment_method VARCHAR(40) Nur als Info (SPK, PayPal, Invoice …)
pdf_path VARCHAR(512) NULL Optionaler Cachepfad, falls ein generiertes PDF abgelegt wird; nicht primäre Quelle
raw_snapshot JSON Vollständige Original-Zeile bzw. alle für die Legacy-PDF-Erzeugung nötigen Daten für Audit/Rendering
pdf_payload JSON NULL Snapshot für PDF-Erzeugung (invoice, invoice_billing_address, user_payment)
pdf_generated_at TIMESTAMP NULL Zeitpunkt der letzten on-demand PDF-Erzeugung
imported_at TIMESTAMP

UNIQUE (legacy_portal, legacy_id).

DB-Vorbereitung vor Go-Live: pdf_payload enthält die für die on-demand PDF-Erzeugung nötigen Legacy-Snapshots. Der neue invoices-Kreislauf bleibt davon getrennt.

Coupons vertagt (D-16): Keine Tabelle in Phase 1. Wird später, ggf. direkt als Stripe-Coupons, nachgezogen.


8. Import-Tracking

legacy_import_map

Spalte Typ Zweck
id BIGINT
legacy_portal ENUM('presseecho','businessportal24')
legacy_table VARCHAR(80) z. B. sf_guard_user, press_release
legacy_id BIGINT
target_table VARCHAR(80)
target_id BIGINT
imported_at TIMESTAMP
UNIQUE (legacy_portal, legacy_table, legacy_id)

Diese Tabelle erlaubt idempotente Imports und Rückverfolgung.


9. Tabellen-Übersicht (Total)

# Tabelle Quelle / Herkunft
1 users sfGuardUser + sfGuardUserProfile (teilw.)
2 profiles sfGuardUserProfile
3 magic_links NEU (D-10)
4 roles / permissions / … Spatie
5 companies Company + Agency
6 company_user CompanyUser + ResponsibleCompanyUser
7 contacts Contact
8 categories Category
9 category_translations Category_Translation
10 press_releases PressRelease
11 press_release_images PressReleaseImage (erweitert)
12 press_release_contact PressReleaseContact
13 newsletter_subscriptions NewsletterSubscription (nur Daten, Workflow neu)
14 blacklists Blacklist
15 footer_codes, category_footer_code FooterCode (bleibt)
16 billing_addresses UserBillingAddress
17 invoice_billing_addresses InvoiceBillingAddress
18 payment_options NEU (nicht aus Legacy)
19 payment_option_translations NEU
20 user_payment_options Teil-Übernahme aktiver Abos (Grandfathering)
21 user_payment_option_company s.o.
22 user_payments NEU (Stripe-Charges)
23 invoices NEU (Stripe-getriebener Rechnungskreis)
24 legacy_invoices ARCHIV aller alten Rechnungen
25 legacy_import_map NEU (technisch)

Raus aus der DB, rein in Config/Code:

Legacy-Tabelle Ziel
Country config/countries.php
Salutation + Salutation_Translation config/salutations.php

Ganz raus / entfallen:

Legacy-Tabelle Grund
ApiUser Duplikat zu users.registration_type = apiuser / Rolle api-only
sfGuardRememberKey Laravel remember_token-Spalte reicht
sfGuardForgotPassword Fortify löst Reset selbst
PromotionLink / PromotionLinkCategory Feature entfällt (D-14)
Coupon Vertagt (D-16)
PaymentOption_ExcludeGroup / _AccessGroup Ersetzt durch Spatie-Permissions + direkte Product-Whitelist
Invoice (als aktive Tabelle) Ersetzt durch neuen Rechnungskreis + legacy_invoices-Archiv

10. Indizes & Performance

  • press_releases: Index (portal, status, published_at DESC) für Public-Listing; Unique (portal, language, slug).
  • press_releases: Index uuid (öffentliche Share-Links).
  • newsletter_subscriptions: Unique (portal, email).
  • magic_links: Index (user_id, purpose, consumed_at); Unique token_hash.
  • invoices: Index (user_id, invoice_date), stripe_invoice_id.
  • legacy_invoices: Unique (legacy_portal, legacy_id); Index (user_id, invoice_date DESC).
  • legacy_import_map: Unique-Index siehe oben.

11. Migrationen (Reihenfolge)

  1. users (Alterung der bestehenden Tabelle durch Zusatz-Spalten, password NULLABLE)
  2. profiles, billing_addresses
  3. magic_links
  4. Spatie-Migrationen (+ Seeder für Rollen admin, editor, customer, api-only)
  5. companies, company_user, contacts
  6. categories, category_translations (inkl. EN-Seed)
  7. blacklists, footer_codes, category_footer_code
  8. press_releases, press_release_images, press_release_contact
  9. newsletter_subscriptions
  10. payment_options, payment_option_translations
  11. user_payment_options, user_payment_option_company, user_payments
  12. invoice_billing_addresses, invoices
  13. legacy_invoices (Archiv)
  14. legacy_import_map

Separate Phase (Phase 6): Import-Commands füllen diese Tabellen aus den Legacy-DBs.