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