611 lines
15 KiB
Markdown
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*
|