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

611 lines
15 KiB
Markdown

# 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*