fix: added upload formular, reorganized some files and put them into a new folder misc
This commit is contained in:
1 parent
100fe2de18
commit
0d534f5013
22 files changed
+5700
-3
No files matched your search
@@ -0,0 +1,610 @@
|
||||
# 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*
|
||||
Reference in new issue
Block a user