802 lines
22 KiB
Markdown
802 lines
22 KiB
Markdown
# Migrationsplan: Schema-Restrukturierung
|
|||
|
|
|
||
|
|
## Zielsetzung
|
||
|
|
|
||
|
|
### Aktueller Zustand
|
||
|
|
```
|
||
|
|
public.companies (11 Spalten)
|
||
|
|
↓ 1:N
|
||
|
|
public.transactions (49 Spalten) - Angereicherte Produktionsdaten
|
||
|
|
|
||
|
|
backend.transactions (14 Spalten) - Rohdaten
|
||
|
|
↓ N:M
|
||
|
|
backend.transaction_outputs (5 Spalten) - AI-generierte Outputs
|
||
|
|
```
|
||
|
|
|
||
|
|
### Ziel-Zustand
|
||
|
|
```
|
||
|
|
backend.transactions (14 Spalten) - Ersetzt public.companies
|
||
|
|
↓ 1:N
|
||
|
|
public.transactions (5 Spalten, ähnlich backend.transaction_outputs) - Vereinfacht
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ⚠️ Klärungsfragen (KRITISCH)
|
||
|
|
|
||
|
|
Bevor wir fortfahren, müssen folgende Fragen geklärt werden:
|
||
|
|
|
||
|
|
### 1. Companies → backend.transactions Mapping
|
||
|
|
**Problem**: Die Strukturen sind sehr unterschiedlich
|
||
|
|
|
||
|
|
| public.companies | backend.transactions | Kompatibilität |
|
||
|
|
|------------------|---------------------|----------------|
|
||
|
|
| id (bigint) | id (integer) | ⚠️ Typ-Unterschied |
|
||
|
|
| name | corporate_entity | ✅ Ähnlich |
|
||
|
|
| legal_name | - | ❌ Fehlt |
|
||
|
|
| ticker | - | ❌ Fehlt |
|
||
|
|
| sector | - | ❌ Fehlt |
|
||
|
|
| country | tx_country_outgoing? | ⚠️ Unklar |
|
||
|
|
| headquarters | - | ❌ Fehlt |
|
||
|
|
| kyc_risk_level | - | ❌ Fehlt |
|
||
|
|
| summary | - | ❌ Fehlt |
|
||
|
|
| - | corporate_counterparty | ❌ Neu |
|
||
|
|
| - | tx_date, tx_amount, tx_currency | ❌ Neu |
|
||
|
|
| - | tx_purpose | ❌ Neu |
|
||
|
|
| - | source_file, raw_payload | ❌ Neu |
|
||
|
|
| - | status | ❌ Neu |
|
||
|
|
|
||
|
|
**Frage**:
|
||
|
|
- Soll `backend.transactions` erweitert werden, um Company-Felder aufzunehmen?
|
||
|
|
- Oder sollen Companies als einzelne Zeilen ohne Transaktionsdaten gespeichert werden?
|
||
|
|
- Wie wird `corporate_entity` zu `companies.name` gemappt?
|
||
|
|
|
||
|
|
### 2. public.transactions → transaction_outputs Mapping
|
||
|
|
**Problem**: Drastischer Datenverlust bei Vereinfachung
|
||
|
|
|
||
|
|
| public.transactions (49 Felder) | backend.transaction_outputs (5 Felder) |
|
||
|
|
|---------------------------------|----------------------------------------|
|
||
|
|
| Alle Core-Felder (14) | ❌ Verloren |
|
||
|
|
| Alle Enrichment-Felder (34) | ❌ Verloren |
|
||
|
|
| - | transaction_id (Foreign Key) |
|
||
|
|
| - | prompt_id (Welcher?) |
|
||
|
|
| - | output_key (Welcher Typ?) |
|
||
|
|
| - | content (Wie strukturiert?) |
|
||
|
|
| - | run_id (Optional) |
|
||
|
|
|
||
|
|
**Frage**:
|
||
|
|
- Welche Daten aus den 49 Feldern sollen in `content` serialisiert werden?
|
||
|
|
- Welchen `output_key` verwenden wir? (z.B. "transaction_data", "risk_assessment"?)
|
||
|
|
- Welchen `prompt_id` verwenden wir? (Muss in `backend.prompt_templates` existieren)
|
||
|
|
- Was passiert mit den 11 Enrichment-Datenquellen?
|
||
|
|
|
||
|
|
### 3. Relationship & Foreign Keys
|
||
|
|
**Problem**: Beziehungen ändern sich fundamental
|
||
|
|
|
||
|
|
**Aktuell**:
|
||
|
|
```
|
||
|
|
companies.id → transactions.company_id (1:N)
|
||
|
|
```
|
||
|
|
|
||
|
|
**Ziel** (unklar):
|
||
|
|
```
|
||
|
|
backend.transactions.id → public.transactions.transaction_id (1:N)?
|
||
|
|
```
|
||
|
|
|
||
|
|
**Frage**:
|
||
|
|
- Bleibt die 1:N Beziehung erhalten?
|
||
|
|
- Wie wird `transaction_id` in der neuen `public.transactions` gemappt?
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Vorgeschlagene Alternative: Erweiterte Migration
|
||
|
|
|
||
|
|
Ich schlage eine modifizierte Zielstruktur vor, die Datenverlust minimiert:
|
||
|
|
|
||
|
|
### Option A: Erweitere backend.transactions (Empfohlen)
|
||
|
|
|
||
|
|
```sql
|
||
|
|
backend.companies (neue Tabelle)
|
||
|
|
- id (integer)
|
||
|
|
- name (text)
|
||
|
|
- legal_name (text, nullable)
|
||
|
|
- country (text)
|
||
|
|
- kyc_risk_level (text)
|
||
|
|
- ... weitere Company-Felder
|
||
|
|
|
||
|
|
backend.transactions (erweitert, bleibt)
|
||
|
|
- id (integer)
|
||
|
|
- company_id (integer FK → backend.companies)
|
||
|
|
- corporate_counterparty (text)
|
||
|
|
- tx_date (text → sollte timestamp werden)
|
||
|
|
- tx_amount (double precision)
|
||
|
|
- tx_currency (text)
|
||
|
|
- ... bestehende Felder
|
||
|
|
|
||
|
|
public.transaction_enrichments (neue Tabelle)
|
||
|
|
- id (bigint)
|
||
|
|
- transaction_id (integer FK → backend.transactions)
|
||
|
|
- enrichment_type (varchar) -- 'registry', 'sanctions', etc.
|
||
|
|
- data (jsonb)
|
||
|
|
- last_refreshed_at (timestamp)
|
||
|
|
- created_at, updated_at
|
||
|
|
```
|
||
|
|
|
||
|
|
**Vorteile**:
|
||
|
|
- ✅ Kein Datenverlust
|
||
|
|
- ✅ Klare Trennung: Companies, Transactions, Enrichments
|
||
|
|
- ✅ Backend-Schema behält Rohdaten
|
||
|
|
- ✅ Public-Schema behält angereicherte Daten
|
||
|
|
- ✅ Laravel-Logik kann schrittweise migriert werden
|
||
|
|
|
||
|
|
### Option B: Vollständiger Umzug zu backend Schema
|
||
|
|
|
||
|
|
```sql
|
||
|
|
backend.companies (neu)
|
||
|
|
- Alle Felder von public.companies
|
||
|
|
|
||
|
|
backend.transactions (bleibt)
|
||
|
|
- Bestehende Struktur
|
||
|
|
|
||
|
|
backend.transaction_enrichments (neu)
|
||
|
|
- Alle Enrichment-Daten aus public.transactions
|
||
|
|
|
||
|
|
public.* (deprecated, später löschen)
|
||
|
|
```
|
||
|
|
|
||
|
|
**Vorteile**:
|
||
|
|
- ✅ Alles im backend Schema
|
||
|
|
- ✅ Klare Schema-Trennung
|
||
|
|
- ❌ Laravel-App muss komplett umgeschrieben werden
|
||
|
|
- ❌ Größere Breaking Changes
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Migrationsplan (nach Klärung)
|
||
|
|
|
||
|
|
### Phase 1: Vorbereitung (1-2 Tage)
|
||
|
|
|
||
|
|
#### 1.1 Backup erstellen
|
||
|
|
```bash
|
||
|
|
# Vollständiges Backup
|
||
|
|
pg_dump -h localhost -U username -d database_name > backup_$(date +%Y%m%d_%H%M%S).sql
|
||
|
|
|
||
|
|
# Schema-spezifische Backups
|
||
|
|
pg_dump -h localhost -U username -d database_name -n public > backup_public_$(date +%Y%m%d).sql
|
||
|
|
pg_dump -h localhost -U username -d database_name -n backend > backup_backend_$(date +%Y%m%d).sql
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 1.2 Datenanalyse
|
||
|
|
```sql
|
||
|
|
-- Anzahl Companies
|
||
|
|
SELECT COUNT(*) FROM public.companies;
|
||
|
|
|
||
|
|
-- Anzahl Transactions
|
||
|
|
SELECT COUNT(*) FROM public.transactions;
|
||
|
|
|
||
|
|
-- Datenintegrität prüfen
|
||
|
|
SELECT
|
||
|
|
COUNT(*) as total_transactions,
|
||
|
|
COUNT(DISTINCT company_id) as unique_companies,
|
||
|
|
COUNT(*) FILTER (WHERE company_id NOT IN (SELECT id FROM public.companies)) as orphaned_transactions
|
||
|
|
FROM public.transactions;
|
||
|
|
|
||
|
|
-- Enrichment-Daten Analyse
|
||
|
|
SELECT
|
||
|
|
COUNT(*) FILTER (WHERE registry_data IS NOT NULL) as has_registry,
|
||
|
|
COUNT(*) FILTER (WHERE sanctions_data IS NOT NULL) as has_sanctions,
|
||
|
|
COUNT(*) FILTER (WHERE pep_data IS NOT NULL) as has_pep
|
||
|
|
FROM public.transactions;
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 1.3 Test-Umgebung aufsetzen
|
||
|
|
```bash
|
||
|
|
# Kopie der Datenbank für Tests
|
||
|
|
createdb -T production_db test_migration_db
|
||
|
|
```
|
||
|
|
|
||
|
|
### Phase 2: Schema-Erweiterung (2-3 Tage)
|
||
|
|
|
||
|
|
#### 2.1 backend.companies erstellen
|
||
|
|
|
||
|
|
**Laravel Migration**:
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
// database/migrations/2025_11_12_create_backend_companies_table.php
|
||
|
|
|
||
|
|
use Illuminate\Database\Migrations\Migration;
|
||
|
|
use Illuminate\Database\Schema\Blueprint;
|
||
|
|
use Illuminate\Support\Facades\Schema;
|
||
|
|
|
||
|
|
return new class extends Migration
|
||
|
|
{
|
||
|
|
public function up(): void
|
||
|
|
{
|
||
|
|
Schema::connection('backend')->create('companies', function (Blueprint $table) {
|
||
|
|
$table->id();
|
||
|
|
$table->string('name');
|
||
|
|
$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();
|
||
|
|
|
||
|
|
$table->index('name');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
public function down(): void
|
||
|
|
{
|
||
|
|
Schema::connection('backend')->dropIfExists('companies');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.2 backend.transactions erweitern
|
||
|
|
|
||
|
|
**SQL Migration** (wenn Laravel Multi-Schema-Support limitiert ist):
|
||
|
|
```sql
|
||
|
|
-- Füge company_id zu backend.transactions hinzu
|
||
|
|
ALTER TABLE backend.transactions
|
||
|
|
ADD COLUMN company_id INTEGER REFERENCES backend.companies(id) ON DELETE CASCADE;
|
||
|
|
|
||
|
|
-- Index für Performance
|
||
|
|
CREATE INDEX idx_backend_transactions_company_id ON backend.transactions(company_id);
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.3 public.transaction_enrichments erstellen
|
||
|
|
|
||
|
|
**Laravel Migration**:
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
// database/migrations/2025_11_12_create_transaction_enrichments_table.php
|
||
|
|
|
||
|
|
use Illuminate\Database\Migrations\Migration;
|
||
|
|
use Illuminate\Database\Schema\Blueprint;
|
||
|
|
use Illuminate\Support\Facades\Schema;
|
||
|
|
|
||
|
|
return new class extends Migration
|
||
|
|
{
|
||
|
|
public function up(): void
|
||
|
|
{
|
||
|
|
Schema::create('transaction_enrichments', function (Blueprint $table) {
|
||
|
|
$table->id();
|
||
|
|
$table->integer('backend_transaction_id'); // FK zu backend.transactions
|
||
|
|
$table->string('enrichment_type', 50); // 'registry', 'sanctions', etc.
|
||
|
|
$table->jsonb('data');
|
||
|
|
$table->timestamp('last_refreshed_at')->nullable();
|
||
|
|
$table->timestamps();
|
||
|
|
|
||
|
|
$table->index(['backend_transaction_id', 'enrichment_type']);
|
||
|
|
$table->unique(['backend_transaction_id', 'enrichment_type']);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
public function down(): void
|
||
|
|
{
|
||
|
|
Schema::dropIfExists('transaction_enrichments');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### Phase 3: Datenmigration (3-5 Tage)
|
||
|
|
|
||
|
|
#### 3.1 Companies migrieren
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
// database/migrations/2025_11_12_migrate_companies_data.php
|
||
|
|
|
||
|
|
use Illuminate\Database\Migrations\Migration;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
|
||
|
|
return new class extends Migration
|
||
|
|
{
|
||
|
|
public function up(): void
|
||
|
|
{
|
||
|
|
// Kopiere Companies von public → backend
|
||
|
|
DB::statement("
|
||
|
|
INSERT INTO backend.companies (
|
||
|
|
id, name, legal_name, ticker, sector,
|
||
|
|
country, headquarters, kyc_risk_level, summary,
|
||
|
|
created_at, updated_at
|
||
|
|
)
|
||
|
|
SELECT
|
||
|
|
id, name, legal_name, ticker, sector,
|
||
|
|
country, headquarters, kyc_risk_level, summary,
|
||
|
|
created_at, updated_at
|
||
|
|
FROM public.companies
|
||
|
|
ON CONFLICT (id) DO NOTHING
|
||
|
|
");
|
||
|
|
|
||
|
|
// Aktualisiere Sequence
|
||
|
|
DB::statement("
|
||
|
|
SELECT setval('backend.companies_id_seq',
|
||
|
|
(SELECT MAX(id) FROM backend.companies)
|
||
|
|
)
|
||
|
|
");
|
||
|
|
}
|
||
|
|
|
||
|
|
public function down(): void
|
||
|
|
{
|
||
|
|
DB::statement("TRUNCATE backend.companies CASCADE");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 3.2 Transaction Core-Daten migrieren
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
// database/migrations/2025_11_12_migrate_transaction_core_data.php
|
||
|
|
|
||
|
|
use Illuminate\Database\Migrations\Migration;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
|
||
|
|
return new class extends Migration
|
||
|
|
{
|
||
|
|
public function up(): void
|
||
|
|
{
|
||
|
|
// Füge company_id zu bestehenden backend.transactions hinzu
|
||
|
|
// (falls diese Daten schon existieren und verknüpft werden müssen)
|
||
|
|
|
||
|
|
// ODER: Migriere von public.transactions → backend.transactions
|
||
|
|
DB::statement("
|
||
|
|
INSERT INTO backend.transactions (
|
||
|
|
corporate_entity,
|
||
|
|
corporate_counterparty,
|
||
|
|
tx_date,
|
||
|
|
tx_amount,
|
||
|
|
tx_currency,
|
||
|
|
tx_purpose,
|
||
|
|
tx_country_incoming,
|
||
|
|
company_id,
|
||
|
|
status,
|
||
|
|
created_at,
|
||
|
|
last_modified_at
|
||
|
|
)
|
||
|
|
SELECT
|
||
|
|
c.name as corporate_entity,
|
||
|
|
t.counterparty as corporate_counterparty,
|
||
|
|
t.executed_at::text as tx_date,
|
||
|
|
t.amount as tx_amount,
|
||
|
|
t.currency as tx_currency,
|
||
|
|
t.flagged_reason as tx_purpose,
|
||
|
|
t.counterparty_country as tx_country_incoming,
|
||
|
|
t.company_id,
|
||
|
|
t.status,
|
||
|
|
t.created_at::text,
|
||
|
|
t.updated_at::text
|
||
|
|
FROM public.transactions t
|
||
|
|
JOIN public.companies c ON t.company_id = c.id
|
||
|
|
WHERE NOT EXISTS (
|
||
|
|
SELECT 1 FROM backend.transactions bt
|
||
|
|
WHERE bt.corporate_entity = c.name
|
||
|
|
AND bt.tx_date = t.executed_at::text
|
||
|
|
)
|
||
|
|
");
|
||
|
|
}
|
||
|
|
|
||
|
|
public function down(): void
|
||
|
|
{
|
||
|
|
// Rollback nur für migrierte Daten
|
||
|
|
DB::statement("
|
||
|
|
DELETE FROM backend.transactions
|
||
|
|
WHERE company_id IS NOT NULL
|
||
|
|
");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 3.3 Enrichment-Daten migrieren
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
// database/migrations/2025_11_12_migrate_enrichment_data.php
|
||
|
|
|
||
|
|
use Illuminate\Database\Migrations\Migration;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
|
||
|
|
return new class extends Migration
|
||
|
|
{
|
||
|
|
private array $enrichmentSources = [
|
||
|
|
'registry' => ['registry_data', 'registry_last_refreshed_at'],
|
||
|
|
'genesis' => ['genesis_context', 'genesis_last_refreshed_at'],
|
||
|
|
'govdata' => ['govdata_data', 'govdata_last_refreshed_at'],
|
||
|
|
'bundesanzeiger' => ['bundesanzeiger_data', 'bundesanzeiger_last_refreshed_at'],
|
||
|
|
'insolvency' => ['insolvency_data', 'insolvency_last_refreshed_at'],
|
||
|
|
'rss' => ['rss_alerts', 'rss_last_refreshed_at'],
|
||
|
|
'sanctions' => ['sanctions_data', 'sanctions_last_refreshed_at'],
|
||
|
|
'pep' => ['pep_data', 'pep_last_refreshed_at'],
|
||
|
|
'gleif' => ['gleif_data', 'gleif_last_refreshed_at'],
|
||
|
|
'eu_sanctions' => ['eu_sanctions_data', 'eu_sanctions_last_refreshed_at'],
|
||
|
|
'handelsregister' => ['handelsregister_data', 'handelsregister_last_refreshed_at'],
|
||
|
|
];
|
||
|
|
|
||
|
|
public function up(): void
|
||
|
|
{
|
||
|
|
foreach ($this->enrichmentSources as $type => [$dataField, $refreshField]) {
|
||
|
|
DB::statement("
|
||
|
|
INSERT INTO public.transaction_enrichments (
|
||
|
|
backend_transaction_id,
|
||
|
|
enrichment_type,
|
||
|
|
data,
|
||
|
|
last_refreshed_at,
|
||
|
|
created_at,
|
||
|
|
updated_at
|
||
|
|
)
|
||
|
|
SELECT
|
||
|
|
bt.id as backend_transaction_id,
|
||
|
|
'{$type}' as enrichment_type,
|
||
|
|
t.{$dataField} as data,
|
||
|
|
t.{$refreshField} as last_refreshed_at,
|
||
|
|
NOW(),
|
||
|
|
NOW()
|
||
|
|
FROM public.transactions t
|
||
|
|
JOIN backend.transactions bt ON (
|
||
|
|
bt.tx_date = t.executed_at::text
|
||
|
|
AND bt.tx_amount = t.amount
|
||
|
|
)
|
||
|
|
WHERE t.{$dataField} IS NOT NULL
|
||
|
|
ON CONFLICT (backend_transaction_id, enrichment_type) DO NOTHING
|
||
|
|
");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function down(): void
|
||
|
|
{
|
||
|
|
DB::statement("TRUNCATE public.transaction_enrichments");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### Phase 4: Model-Anpassung (2-3 Tage)
|
||
|
|
|
||
|
|
#### 4.1 Neue Models erstellen
|
||
|
|
|
||
|
|
**Backend\Company Model**:
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
// app/Models/Backend/Company.php
|
||
|
|
|
||
|
|
namespace App\Models\Backend;
|
||
|
|
|
||
|
|
use Illuminate\Database\Eloquent\Model;
|
||
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||
|
|
|
||
|
|
class Company extends Model
|
||
|
|
{
|
||
|
|
protected $connection = 'backend';
|
||
|
|
protected $table = 'companies';
|
||
|
|
|
||
|
|
protected $fillable = [
|
||
|
|
'name', 'legal_name', 'ticker', 'sector',
|
||
|
|
'country', 'headquarters', 'kyc_risk_level', 'summary'
|
||
|
|
];
|
||
|
|
|
||
|
|
public function transactions(): HasMany
|
||
|
|
{
|
||
|
|
return $this->hasMany(Transaction::class);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Backend\Transaction Model**:
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
// app/Models/Backend/Transaction.php
|
||
|
|
|
||
|
|
namespace App\Models\Backend;
|
||
|
|
|
||
|
|
use App\Models\TransactionEnrichment;
|
||
|
|
use Illuminate\Database\Eloquent\Model;
|
||
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||
|
|
|
||
|
|
class Transaction extends Model
|
||
|
|
{
|
||
|
|
protected $connection = 'backend';
|
||
|
|
protected $table = 'transactions';
|
||
|
|
|
||
|
|
protected $fillable = [
|
||
|
|
'company_id', 'corporate_entity', 'corporate_counterparty',
|
||
|
|
'tx_date', 'tx_amount', 'tx_currency', 'tx_purpose',
|
||
|
|
'tx_country_outgoing', 'tx_country_incoming',
|
||
|
|
'source_file', 'raw_payload', 'status'
|
||
|
|
];
|
||
|
|
|
||
|
|
protected $casts = [
|
||
|
|
'tx_amount' => 'decimal:2',
|
||
|
|
];
|
||
|
|
|
||
|
|
public function company(): BelongsTo
|
||
|
|
{
|
||
|
|
return $this->belongsTo(Company::class);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function enrichments(): HasMany
|
||
|
|
{
|
||
|
|
return $this->hasMany(TransactionEnrichment::class, 'backend_transaction_id');
|
||
|
|
}
|
||
|
|
|
||
|
|
public function getEnrichment(string $type): ?array
|
||
|
|
{
|
||
|
|
return $this->enrichments()
|
||
|
|
->where('enrichment_type', $type)
|
||
|
|
->first()
|
||
|
|
?->data;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**TransactionEnrichment Model**:
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
// app/Models/TransactionEnrichment.php
|
||
|
|
|
||
|
|
namespace App\Models;
|
||
|
|
|
||
|
|
use App\Models\Backend\Transaction;
|
||
|
|
use Illuminate\Database\Eloquent\Model;
|
||
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||
|
|
|
||
|
|
class TransactionEnrichment extends Model
|
||
|
|
{
|
||
|
|
protected $fillable = [
|
||
|
|
'backend_transaction_id',
|
||
|
|
'enrichment_type',
|
||
|
|
'data',
|
||
|
|
'last_refreshed_at'
|
||
|
|
];
|
||
|
|
|
||
|
|
protected $casts = [
|
||
|
|
'data' => 'array',
|
||
|
|
'last_refreshed_at' => 'datetime'
|
||
|
|
];
|
||
|
|
|
||
|
|
public function transaction(): BelongsTo
|
||
|
|
{
|
||
|
|
return $this->belongsTo(Transaction::class, 'backend_transaction_id');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Phase 5: Code-Refactoring (5-7 Tage)
|
||
|
|
|
||
|
|
#### 5.1 Controller anpassen
|
||
|
|
|
||
|
|
**Vorher**:
|
||
|
|
```php
|
||
|
|
use App\Models\Transaction;
|
||
|
|
|
||
|
|
$transactions = Transaction::with('company')
|
||
|
|
->where('risk_score', '>', 50)
|
||
|
|
->get();
|
||
|
|
```
|
||
|
|
|
||
|
|
**Nachher**:
|
||
|
|
```php
|
||
|
|
use App\Models\Backend\Transaction;
|
||
|
|
|
||
|
|
$transactions = Transaction::with(['company', 'enrichments'])
|
||
|
|
->where('risk_score', '>', 50)
|
||
|
|
->get();
|
||
|
|
|
||
|
|
// Enrichment-Daten abrufen
|
||
|
|
foreach ($transactions as $transaction) {
|
||
|
|
$sanctionsData = $transaction->getEnrichment('sanctions');
|
||
|
|
$pepData = $transaction->getEnrichment('pep');
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 5.2 Compatibility Layer (Optional)
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
// app/Models/Transaction.php (Legacy-Support)
|
||
|
|
|
||
|
|
namespace App\Models;
|
||
|
|
|
||
|
|
use App\Models\Backend\Transaction as BackendTransaction;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @deprecated Use App\Models\Backend\Transaction instead
|
||
|
|
*/
|
||
|
|
class Transaction extends BackendTransaction
|
||
|
|
{
|
||
|
|
// Proxy zu neuem Model für Backward-Compatibility
|
||
|
|
|
||
|
|
public function __get($key)
|
||
|
|
{
|
||
|
|
// Prüfe ob Enrichment-Feld
|
||
|
|
if (str_ends_with($key, '_data')) {
|
||
|
|
$type = str_replace('_data', '', $key);
|
||
|
|
return $this->getEnrichment($type);
|
||
|
|
}
|
||
|
|
|
||
|
|
return parent::__get($key);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Phase 6: Testing (3-5 Tage)
|
||
|
|
|
||
|
|
#### 6.1 Unit Tests
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
// tests/Unit/Models/Backend/TransactionTest.php
|
||
|
|
|
||
|
|
use App\Models\Backend\Transaction;
|
||
|
|
use App\Models\TransactionEnrichment;
|
||
|
|
|
||
|
|
test('transaction has enrichments relationship', function () {
|
||
|
|
$transaction = Transaction::factory()->create();
|
||
|
|
|
||
|
|
TransactionEnrichment::factory()->create([
|
||
|
|
'backend_transaction_id' => $transaction->id,
|
||
|
|
'enrichment_type' => 'sanctions',
|
||
|
|
'data' => ['status' => 'clear']
|
||
|
|
]);
|
||
|
|
|
||
|
|
expect($transaction->enrichments)->toHaveCount(1);
|
||
|
|
expect($transaction->getEnrichment('sanctions'))->toBe(['status' => 'clear']);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 6.2 Integration Tests
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
// tests/Feature/TransactionMigrationTest.php
|
||
|
|
|
||
|
|
test('data integrity after migration', function () {
|
||
|
|
// Vergleiche Anzahl
|
||
|
|
$oldCount = DB::table('public.transactions')->count();
|
||
|
|
$newCount = DB::table('backend.transactions')->count();
|
||
|
|
|
||
|
|
expect($newCount)->toBeGreaterThanOrEqual($oldCount);
|
||
|
|
|
||
|
|
// Vergleiche Enrichment-Daten
|
||
|
|
$oldEnrichments = DB::table('public.transactions')
|
||
|
|
->whereNotNull('sanctions_data')
|
||
|
|
->count();
|
||
|
|
|
||
|
|
$newEnrichments = DB::table('public.transaction_enrichments')
|
||
|
|
->where('enrichment_type', 'sanctions')
|
||
|
|
->count();
|
||
|
|
|
||
|
|
expect($newEnrichments)->toBe($oldEnrichments);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Phase 7: Deployment (1-2 Tage)
|
||
|
|
|
||
|
|
#### 7.1 Deployment-Schritte
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# 1. Backup
|
||
|
|
php artisan backup:database
|
||
|
|
|
||
|
|
# 2. Migrations ausführen (in Reihenfolge!)
|
||
|
|
php artisan migrate --path=database/migrations/2025_11_12_create_backend_companies_table.php
|
||
|
|
php artisan migrate --path=database/migrations/2025_11_12_create_transaction_enrichments_table.php
|
||
|
|
php artisan migrate --path=database/migrations/2025_11_12_migrate_companies_data.php
|
||
|
|
php artisan migrate --path=database/migrations/2025_11_12_migrate_transaction_core_data.php
|
||
|
|
php artisan migrate --path=database/migrations/2025_11_12_migrate_enrichment_data.php
|
||
|
|
|
||
|
|
# 3. Verification
|
||
|
|
php artisan tinker
|
||
|
|
>>> DB::table('backend.companies')->count()
|
||
|
|
>>> DB::table('backend.transactions')->count()
|
||
|
|
>>> DB::table('public.transaction_enrichments')->count()
|
||
|
|
|
||
|
|
# 4. Clear caches
|
||
|
|
php artisan cache:clear
|
||
|
|
php artisan config:clear
|
||
|
|
php artisan route:clear
|
||
|
|
php artisan view:clear
|
||
|
|
|
||
|
|
# 5. Run tests
|
||
|
|
php artisan test --filter=TransactionMigration
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 7.2 Rollback-Plan
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Falls etwas schief geht
|
||
|
|
php artisan migrate:rollback --step=5
|
||
|
|
|
||
|
|
# Restore from backup
|
||
|
|
psql -U username -d database_name < backup_20251112_120000.sql
|
||
|
|
```
|
||
|
|
|
||
|
|
### Phase 8: Cleanup (nach 2-4 Wochen Monitoring)
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- Wenn alles stabil läuft, alte Tabellen entfernen
|
||
|
|
DROP TABLE public.transactions CASCADE;
|
||
|
|
DROP TABLE public.companies CASCADE;
|
||
|
|
|
||
|
|
-- Views für Backward-Compatibility (optional)
|
||
|
|
CREATE VIEW public.companies AS
|
||
|
|
SELECT * FROM backend.companies;
|
||
|
|
|
||
|
|
CREATE VIEW public.transactions AS
|
||
|
|
SELECT
|
||
|
|
bt.id,
|
||
|
|
bt.company_id,
|
||
|
|
bt.corporate_counterparty as counterparty,
|
||
|
|
bt.tx_amount as amount,
|
||
|
|
bt.tx_currency as currency,
|
||
|
|
bt.tx_date::timestamp as executed_at,
|
||
|
|
bt.status,
|
||
|
|
bt.created_at::timestamp,
|
||
|
|
bt.last_modified_at::timestamp as updated_at
|
||
|
|
FROM backend.transactions bt;
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Zeitplan
|
||
|
|
|
||
|
|
| Phase | Dauer | Abhängigkeiten |
|
||
|
|
|-------|-------|----------------|
|
||
|
|
| 1. Vorbereitung | 1-2 Tage | - |
|
||
|
|
| 2. Schema-Erweiterung | 2-3 Tage | Phase 1 |
|
||
|
|
| 3. Datenmigration | 3-5 Tage | Phase 2 |
|
||
|
|
| 4. Model-Anpassung | 2-3 Tage | Phase 3 |
|
||
|
|
| 5. Code-Refactoring | 5-7 Tage | Phase 4 |
|
||
|
|
| 6. Testing | 3-5 Tage | Phase 5 |
|
||
|
|
| 7. Deployment | 1-2 Tage | Phase 6 |
|
||
|
|
| 8. Cleanup | Nach 2-4 Wochen | Phase 7 |
|
||
|
|
|
||
|
|
**Gesamtdauer**: 17-27 Arbeitstage (3-5 Wochen)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Risiken & Mitigation
|
||
|
|
|
||
|
|
### Risiko 1: Datenverlust
|
||
|
|
**Mitigation**:
|
||
|
|
- Vollständige Backups vor jedem Schritt
|
||
|
|
- Test-Migration in Staging-Umgebung
|
||
|
|
- Datenvalidierung nach jeder Phase
|
||
|
|
|
||
|
|
### Risiko 2: Downtime
|
||
|
|
**Mitigation**:
|
||
|
|
- Migrations während Wartungsfenster
|
||
|
|
- Blue-Green Deployment
|
||
|
|
- Read-Replica für Zero-Downtime
|
||
|
|
|
||
|
|
### Risiko 3: Performance-Probleme
|
||
|
|
**Mitigation**:
|
||
|
|
- Indizes auf Foreign Keys
|
||
|
|
- Batch-Processing für große Datasets
|
||
|
|
- Query-Optimierung mit EXPLAIN ANALYZE
|
||
|
|
|
||
|
|
### Risiko 4: Code-Inkompatibilität
|
||
|
|
**Mitigation**:
|
||
|
|
- Compatibility Layer
|
||
|
|
- Schrittweises Refactoring
|
||
|
|
- Feature-Flags für graduelle Umstellung
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Nächste Schritte
|
||
|
|
|
||
|
|
1. ✅ **Klärung der Fragen oben**
|
||
|
|
2. ⬜ Detaillierte Datenanalyse durchführen
|
||
|
|
3. ⬜ Test-Umgebung aufsetzen
|
||
|
|
4. ⬜ Erste Migration in Staging testen
|
||
|
|
5. ⬜ Review & Approval vom Team
|
||
|
|
6. ⬜ Production-Migration planen
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
*Erstellt am: 2025-11-12*
|
||
|
|
*Status: ENTWURF - Wartet auf Klärung der kritischen Fragen*
|