# 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](database/migrations/2025_10_20_181750_create_companies_table.php) ```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](app/Models/Company.php) ```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](database/migrations/2025_10_20_181753_create_transactions_table.php) **Gesamt**: 49 Spalten (ohne timestamps) ##### Core Felder (15 Spalten) ```php $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)** ```php $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)** ```php $table->jsonb('genesis_context')->nullable(); $table->dateTime('genesis_last_refreshed_at')->nullable(); ``` **3. GovData (2 Felder)** ```php $table->jsonb('govdata_data')->nullable(); $table->dateTime('govdata_last_refreshed_at')->nullable(); ``` **4. Bundesanzeiger (2 Felder)** ```php $table->jsonb('bundesanzeiger_data')->nullable(); $table->dateTime('bundesanzeiger_last_refreshed_at')->nullable(); ``` **5. Insolvency (2 Felder)** ```php $table->jsonb('insolvency_data')->nullable(); $table->dateTime('insolvency_last_refreshed_at')->nullable(); ``` **6. RSS Alerts (2 Felder)** ```php $table->jsonb('rss_alerts')->nullable(); $table->dateTime('rss_last_refreshed_at')->nullable(); ``` **7. Sanctions (2 Felder)** ```php $table->jsonb('sanctions_data')->nullable(); $table->dateTime('sanctions_last_refreshed_at')->nullable(); ``` **8. PEP (2 Felder)** ```php $table->jsonb('pep_data')->nullable(); $table->dateTime('pep_last_refreshed_at')->nullable(); ``` **9. GLEIF (3 Felder)** ```php $table->text('gleif_lei')->nullable(); $table->json('gleif_data')->nullable(); $table->dateTime('gleif_last_refreshed_at')->nullable(); ``` **10. EU Sanctions (2 Felder)** ```php $table->jsonb('eu_sanctions_data')->nullable(); $table->dateTime('eu_sanctions_last_refreshed_at')->nullable(); ``` **11. Handelsregister (4 Felder)** ```php $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](app/Models/Transaction.php) ```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 ```php 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) ```php protected $guarded = ['id']; ``` **Option 3**: Separate Accessor/Mutator für Enrichment-Felder ```php 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**: ```php 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 - [x] Migration vollständig - [x] Model synchron mit Migration - [x] Datenbank korrekt strukturiert - [x] Relationships definiert - [x] Casts korrekt ### Transactions - [x] Migration vollständig - [ ] Model synchron mit Migration ⚠️ - [x] Datenbank korrekt strukturiert - [x] 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*