Files
AFC-Demo/misc/laravel-models-migrations-analysis.md
T

15 KiB

Analyse: Laravel Models vs Migrations vs Datenbank

Übersicht

Diese Analyse vergleicht die Laravel Eloquent Models mit den entsprechenden Migration-Dateien und der tatsächlichen Datenbankstruktur.


1. Company Model & Migration

PERFEKT SYNCHRON

Migration

Datei: database/migrations/2025_10_20_181750_create_companies_table.php

Schema::create('companies', function (Blueprint $table) {
    $table->id();
    $table->string('name')->unique();
    $table->string('legal_name')->nullable();
    $table->string('ticker')->nullable();
    $table->string('sector')->nullable();
    $table->string('country', 2)->default('DE');
    $table->string('headquarters')->nullable();
    $table->string('kyc_risk_level')->default('medium');
    $table->text('summary')->nullable();
    $table->timestamps();
});

Felder:

  • id (auto-increment)
  • name (string, unique)
  • legal_name (string, nullable)
  • ticker (string, nullable)
  • sector (string, nullable)
  • country (string(2), default: 'DE')
  • headquarters (string, nullable)
  • kyc_risk_level (string, default: 'medium')
  • summary (text, nullable)
  • timestamps (created_at, updated_at)

Model

Datei: app/Models/Company.php

protected $fillable = [
    'name',
    'legal_name',
    'ticker',
    'sector',
    'country',
    'headquarters',
    'kyc_risk_level',
    'summary',
];

public function transactions(): HasMany
{
    return $this->hasMany(Transaction::class);
}

Datenbank-Status

  • Alle Felder vorhanden
  • Datentypen stimmen überein
  • Defaults korrekt gesetzt
  • Unique Constraint auf name
  • Relationship hasMany(Transaction::class) definiert

2. Transaction Model & Migration

⚠️ TEILWEISE DISKREPANZEN

Migration

Datei: database/migrations/2025_10_20_181753_create_transactions_table.php

Gesamt: 49 Spalten (ohne timestamps)

Core Felder (15 Spalten)
$table->id();
$table->foreignId('company_id')->constrained()->cascadeOnDelete();
$table->string('reference')->unique();
$table->decimal('amount', 16, 2);
$table->string('currency', 3)->default('EUR');
$table->string('counterparty');
$table->string('counterparty_country', 2)->nullable();
$table->string('channel')->nullable();
$table->dateTime('executed_at');
$table->unsignedTinyInteger('risk_score')->default(0);
$table->string('status', 32)->index();
$table->boolean('requires_review')->default(true);
$table->string('flagged_by')->nullable();
$table->text('flagged_reason')->nullable();
$table->json('signals')->nullable();
Enrichment-Felder (11 Datenquellen, 34 Spalten)

1. Registry (5 Felder)

$table->text('registry_company_number')->nullable();
$table->text('registry_source')->nullable();
$table->double('registry_match_score')->nullable();
$table->jsonb('registry_data')->nullable();
$table->dateTime('registry_last_refreshed_at')->nullable();

2. Genesis (2 Felder)

$table->jsonb('genesis_context')->nullable();
$table->dateTime('genesis_last_refreshed_at')->nullable();

3. GovData (2 Felder)

$table->jsonb('govdata_data')->nullable();
$table->dateTime('govdata_last_refreshed_at')->nullable();

4. Bundesanzeiger (2 Felder)

$table->jsonb('bundesanzeiger_data')->nullable();
$table->dateTime('bundesanzeiger_last_refreshed_at')->nullable();

5. Insolvency (2 Felder)

$table->jsonb('insolvency_data')->nullable();
$table->dateTime('insolvency_last_refreshed_at')->nullable();

6. RSS Alerts (2 Felder)

$table->jsonb('rss_alerts')->nullable();
$table->dateTime('rss_last_refreshed_at')->nullable();

7. Sanctions (2 Felder)

$table->jsonb('sanctions_data')->nullable();
$table->dateTime('sanctions_last_refreshed_at')->nullable();

8. PEP (2 Felder)

$table->jsonb('pep_data')->nullable();
$table->dateTime('pep_last_refreshed_at')->nullable();

9. GLEIF (3 Felder)

$table->text('gleif_lei')->nullable();
$table->json('gleif_data')->nullable();
$table->dateTime('gleif_last_refreshed_at')->nullable();

10. EU Sanctions (2 Felder)

$table->jsonb('eu_sanctions_data')->nullable();
$table->dateTime('eu_sanctions_last_refreshed_at')->nullable();

11. Handelsregister (4 Felder)

$table->jsonb('handelsregister_data')->nullable();
$table->dateTime('handelsregister_last_refreshed_at')->nullable();
$table->text('handelsregister_status')->nullable();
$table->bigInteger('handelsregister_entity_id')->nullable();

Model

Datei: app/Models/Transaction.php

// Status-Konstanten
public const STATUS_TRUE_POSITIVE = 'true_positive';
public const STATUS_FALSE_POSITIVE = 'false_positive';
public const STATUS_CLEARED = 'cleared';

// Fillable (nur Core-Felder!)
protected $fillable = [
    'company_id',
    'reference',
    'amount',
    'currency',
    'counterparty',
    'counterparty_country',
    'channel',
    'executed_at',
    'risk_score',
    'status',
    'requires_review',
    'flagged_by',
    'flagged_reason',
    'signals',
];

// Casts
protected $casts = [
    'executed_at' => 'datetime',
    'requires_review' => 'boolean',
    'signals' => 'array',
];

// Relationship
public function company(): BelongsTo
{
    return $this->belongsTo(Company::class);
}

// Helper-Methode
public function statusLabel(): string
{
    return match ($this->status) {
        self::STATUS_TRUE_POSITIVE => __('Bestätigter Treffer'),
        self::STATUS_FALSE_POSITIVE => __('Fehlalarm'),
        default => __('Freigegeben'),
    };
}

⚠️ FEHLENDE FELDER IM MODEL

Das Transaction Model hat NUR 14 Core-Felder im $fillable Array, aber die Migration definiert 49 Felder (exkl. timestamps).

Fehlende Enrichment-Felder (34 Spalten):

Registry-Felder:

  • registry_company_number
  • registry_source
  • registry_match_score
  • registry_data
  • registry_last_refreshed_at

Genesis-Felder:

  • genesis_context
  • genesis_last_refreshed_at

GovData-Felder:

  • govdata_data
  • govdata_last_refreshed_at

Bundesanzeiger-Felder:

  • bundesanzeiger_data
  • bundesanzeiger_last_refreshed_at

Insolvency-Felder:

  • insolvency_data
  • insolvency_last_refreshed_at

RSS-Felder:

  • rss_alerts
  • rss_last_refreshed_at

Sanctions-Felder:

  • sanctions_data
  • sanctions_last_refreshed_at

PEP-Felder:

  • pep_data
  • pep_last_refreshed_at

GLEIF-Felder:

  • gleif_lei
  • gleif_data
  • gleif_last_refreshed_at

EU Sanctions-Felder:

  • eu_sanctions_data
  • eu_sanctions_last_refreshed_at

Handelsregister-Felder:

  • handelsregister_data
  • handelsregister_last_refreshed_at
  • handelsregister_status
  • handelsregister_entity_id

⚠️ FEHLENDE CASTS

Das Model sollte Casts für alle zeitbasierten und JSON-Felder haben:

Fehlende DateTime-Casts:

  • registry_last_refreshed_at
  • genesis_last_refreshed_at
  • govdata_last_refreshed_at
  • bundesanzeiger_last_refreshed_at
  • insolvency_last_refreshed_at
  • rss_last_refreshed_at
  • sanctions_last_refreshed_at
  • pep_last_refreshed_at
  • gleif_last_refreshed_at
  • eu_sanctions_last_refreshed_at
  • handelsregister_last_refreshed_at

Fehlende JSON/Array-Casts:

  • registry_data
  • genesis_context
  • govdata_data
  • bundesanzeiger_data
  • insolvency_data
  • rss_alerts
  • sanctions_data
  • pep_data
  • gleif_data
  • eu_sanctions_data
  • handelsregister_data

3. Vergleich: Datenbank vs Migration

public.companies

Status: 100% Übereinstimmung

Feature Migration Datenbank Status
Spalten 11 11
Unique Constraint name name
Defaults country='DE', kyc_risk_level='medium' Identisch

public.transactions

Status: Migration deckt alle DB-Felder ab

Constraints & Indizes

Constraint Migration Datenbank Status
Foreign Key company_id → companies.id Vorhanden
Cascade Delete cascadeOnDelete() Implementiert
Unique reference Vorhanden
Index status Vorhanden

Datentypen-Vergleich

Feld Migration Datenbank Status
id id() bigint
company_id foreignId() bigint
amount decimal(16,2) numeric
currency string(3) varchar
counterparty_country string(2) varchar
risk_score unsignedTinyInteger smallint ⚠️*
status string(32) varchar
requires_review boolean boolean
signals json json
*_data jsonb jsonb
gleif_data json json
executed_at dateTime timestamp
*_last_refreshed_at dateTime timestamp

*unsignedTinyInteger (0-255) vs smallint (-32768 bis 32767) sind funktional kompatibel


Zusammenfassung

Stärken

  1. Migration-Dateien sind vollständig

    • Alle Datenbank-Felder korrekt definiert
    • Foreign Key Constraints implementiert
    • Indizes sinnvoll gesetzt
  2. Core-Model-Felder stimmen überein

    • Basis-Transaktionsfelder vollständig
    • Relationships sauber definiert
  3. Datenbank-Konsistenz

    • Migrations wurden korrekt ausgeführt
    • Constraints sind aktiv

⚠️ Probleme & Empfehlungen

Problem 1: Transaction Model ist unvollständig

Problem:

  • Das Model definiert nur 14 von 49 Feldern im $fillable Array
  • Alle 34 Enrichment-Felder fehlen

Auswirkungen:

  • Enrichment-Felder können nicht via Mass Assignment gesetzt werden
  • Keine automatischen Type Casts für externe Datenfelder
  • Potenzielle Fehler beim Zugriff auf nicht-gecastete JSON-Daten
  • DateTime-Felder werden als Strings zurückgegeben

Lösungsvorschläge:

Option 1: Alle Felder zu $fillable hinzufügen

protected $fillable = [
    // Core fields
    'company_id', 'reference', 'amount', 'currency',
    'counterparty', 'counterparty_country', 'channel',
    'executed_at', 'risk_score', 'status',
    'requires_review', 'flagged_by', 'flagged_reason', 'signals',

    // Registry
    'registry_company_number', 'registry_source', 'registry_match_score',
    'registry_data', 'registry_last_refreshed_at',

    // Genesis
    'genesis_context', 'genesis_last_refreshed_at',

    // ... alle weiteren Felder
];

Option 2: $guarded verwenden (empfohlen für interne Anwendungen)

protected $guarded = ['id'];

Option 3: Separate Accessor/Mutator für Enrichment-Felder

public function registryData(): Attribute
{
    return Attribute::make(
        get: fn ($value) => json_decode($value, true),
        set: fn ($value) => json_encode($value),
    );
}

Problem 2: Fehlende Casts für Enrichment-Felder

Problem:

  • Keine Casts für *_last_refreshed_at Felder
  • Keine Casts für *_data JSON-Felder

Auswirkungen:

  • DateTime-Felder werden als Strings zurückgegeben (kein Carbon-Objekt)
  • JSON-Felder müssen manuell dekodiert werden

Lösung:

protected $casts = [
    // Existing
    'executed_at' => 'datetime',
    'requires_review' => 'boolean',
    'signals' => 'array',

    // DateTime casts for all refresh timestamps
    'registry_last_refreshed_at' => 'datetime',
    'genesis_last_refreshed_at' => 'datetime',
    'govdata_last_refreshed_at' => 'datetime',
    'bundesanzeiger_last_refreshed_at' => 'datetime',
    'insolvency_last_refreshed_at' => 'datetime',
    'rss_last_refreshed_at' => 'datetime',
    'sanctions_last_refreshed_at' => 'datetime',
    'pep_last_refreshed_at' => 'datetime',
    'gleif_last_refreshed_at' => 'datetime',
    'eu_sanctions_last_refreshed_at' => 'datetime',
    'handelsregister_last_refreshed_at' => 'datetime',

    // JSON/Array casts for all data fields
    'registry_data' => 'array',
    'genesis_context' => 'array',
    'govdata_data' => 'array',
    'bundesanzeiger_data' => 'array',
    'insolvency_data' => 'array',
    'rss_alerts' => 'array',
    'sanctions_data' => 'array',
    'pep_data' => 'array',
    'gleif_data' => 'array',
    'eu_sanctions_data' => 'array',
    'handelsregister_data' => 'array',
];

Problem 3: Datenbank-Schema-Anomalie

Problem:

  • Die public.transactions Tabelle zeigt 9x duplizierte id Spalten im describe_table Output

Mögliche Ursachen:

  • Korruptes Schema-Metadaten
  • Mehrfache Migration-Ausführungen ohne Rollback
  • PostgreSQL-Katalog-Problem

Lösung:

  1. Schema inspizieren: \d+ transactions in psql
  2. Bei Bedarf Migration neu ausführen
  3. Oder manuelles ALTER TABLE zur Bereinigung

Nächste Schritte

Empfohlene Reihenfolge:

  1. Transaction Model aktualisieren

    • Alle fehlenden Felder zu $fillable hinzufügen
    • Alle fehlenden Casts definieren
  2. Tests schreiben

    • Unit-Tests für Model-Casts
    • Feature-Tests für Enrichment-Datenfluss
  3. ⚠️ Datenbank-Anomalie untersuchen

    • PostgreSQL-Schema inspizieren
    • Ggf. Migration neu ausführen
  4. 📝 Dokumentation erweitern

    • Enrichment-Pipeline dokumentieren
    • API für externe Datenquellen dokumentieren

Checkliste

Companies

  • Migration vollständig
  • Model synchron mit Migration
  • Datenbank korrekt strukturiert
  • Relationships definiert
  • Casts korrekt

Transactions

  • Migration vollständig
  • Model synchron mit Migration ⚠️
  • Datenbank korrekt strukturiert
  • Relationships definiert
  • Casts vollständig ⚠️
  • Schema-Anomalie behoben ⚠️

Anhang: Vollständige Feldliste Transaction Model

Core Felder (14)

Im Model vorhanden

  1. company_id
  2. reference
  3. amount
  4. currency
  5. counterparty
  6. counterparty_country
  7. channel
  8. executed_at
  9. risk_score
  10. status
  11. requires_review
  12. flagged_by
  13. flagged_reason
  14. signals

Enrichment Felder (34)

Im Model fehlend

Registry (5) 15. registry_company_number 16. registry_source 17. registry_match_score 18. registry_data 19. registry_last_refreshed_at

Genesis (2) 20. genesis_context 21. genesis_last_refreshed_at

GovData (2) 22. govdata_data 23. govdata_last_refreshed_at

Bundesanzeiger (2) 24. bundesanzeiger_data 25. bundesanzeiger_last_refreshed_at

Insolvency (2) 26. insolvency_data 27. insolvency_last_refreshed_at

RSS (2) 28. rss_alerts 29. rss_last_refreshed_at

Sanctions (2) 30. sanctions_data 31. sanctions_last_refreshed_at

PEP (2) 32. pep_data 33. pep_last_refreshed_at

GLEIF (3) 34. gleif_lei 35. gleif_data 36. gleif_last_refreshed_at

EU Sanctions (2) 37. eu_sanctions_data 38. eu_sanctions_last_refreshed_at

Handelsregister (4) 39. handelsregister_data 40. handelsregister_last_refreshed_at 41. handelsregister_status 42. handelsregister_entity_id


Analysiert am: 2025-11-12