feat: add reputation system with deals, ratings, level badges, and chat-widget deal confirmation

This commit is contained in:
2026-02-09 17:46:49 +01:00
parent 2db3e56f00
commit 83f1303d13
11 changed files with 1072 additions and 0 deletions

380
docs/REPUTATION.md Normal file
View File

@@ -0,0 +1,380 @@
# Reputation-System — dgray.io
## Ziel
Vertrauen zwischen anonymen Nutzern aufbauen, ohne KYC, ohne Escrow.
Trust durch nachweisbares Verhalten statt durch Identität.
---
## Stufensystem
| Level | Bedingung | Badge | Effekt |
|-------|-----------|-------|--------|
| **Neu** | 0 bestätigte Deals | ⚪ | Hinweis "Neuer Account" auf Anzeigen |
| **Aktiv** | 3+ Deals, Account 30+ Tage | 🔵 | Kein Hinweis |
| **Vertrauenswürdig** | 10+ Deals, ⌀ 4.0+ Sterne | 🟢 | Badge auf Anzeigen |
| **Power Seller** | 30+ Deals, ⌀ 4.5+ Sterne | 🟣 | Badge + bevorzugte Platzierung |
---
## Datenmodell
### Directus: Collection `deals`
Bestätigte Transaktionen zwischen zwei Nutzern.
| Feld | Typ | Beschreibung |
|------|-----|--------------|
| `id` | UUID | Primary Key |
| `listing_id` | UUID, FK → listings | Bezug zur Anzeige |
| `conversation_id` | UUID, FK → conversations | Bezug zur Konversation |
| `seller_hash` | string(64) | SHA-256 Hash des Verkäufers |
| `buyer_hash` | string(64) | SHA-256 Hash des Käufers |
| `seller_confirmed` | boolean, default false | Verkäufer hat Deal bestätigt |
| `buyer_confirmed` | boolean, default false | Käufer hat Deal bestätigt |
| `status` | string | `pending`, `confirmed`, `disputed` |
| `date_created` | datetime | Erstellungsdatum |
| `date_confirmed` | datetime, nullable | Zeitpunkt der beidseitigen Bestätigung |
Ein Deal gilt als `confirmed`, wenn **beide Seiten** bestätigt haben.
### Directus: Collection `ratings`
Bewertungen nach bestätigtem Deal.
| Feld | Typ | Beschreibung |
|------|-----|--------------|
| `id` | UUID | Primary Key |
| `deal_id` | UUID, FK → deals | Bezug zum Deal |
| `rater_hash` | string(64) | Wer bewertet |
| `rated_hash` | string(64) | Wer bewertet wird |
| `score` | integer | 15 Sterne |
| `date_created` | datetime | Zeitpunkt |
Pro Deal kann jeder Teilnehmer **einmal** bewerten (nach Bestätigung).
Kein Freitext-Kommentar — nur Sterne (Privacy).
### Directus: Permissions
| Collection | Public Read | Public Create | Public Update |
|------------|:-----------:|:------------:|:------------:|
| `deals` | Ja (Filter: eigener Hash) | Ja | Ja (nur eigene `*_confirmed`) |
| `ratings` | Ja (Filter: `rated_hash`) | Ja (nur nach bestätigtem Deal) | Nein |
### Computed Fields (Frontend)
Diese Werte werden im Frontend aus `deals` + `ratings` berechnet:
```
reputation = {
deals_completed: count(deals WHERE status = 'confirmed' AND user = seller OR buyer),
avg_rating: avg(ratings WHERE rated_hash = user),
account_age_days: now() - user.date_created,
level: computed from above
}
```
---
## User Flow
### Deal bestätigen (im Chat)
```
1. Käufer und Verkäufer handeln im Chat
2. Eine Seite klickt "Deal abschliessen" → Deal wird erstellt (status: pending)
3. Andere Seite sieht Bestätigungsanfrage im Chat
4. Andere Seite klickt "Deal bestätigen" → status: confirmed
5. Beide Seiten können danach bewerten (15 Sterne)
```
### UI im Chat-Widget
```
┌─────────────────────────────────────┐
│ 💬 Chat mit Verkäufer │
│ │
│ ... Nachrichten ... │
│ │
│ ┌─────────────────────────────┐ │
│ │ 🤝 Deal abschliessen │ │
│ └─────────────────────────────┘ │
│ │
│ Nach beidseitiger Bestätigung: │
│ ┌─────────────────────────────┐ │
│ │ ⭐⭐⭐⭐☆ Bewertung abgeben │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
```
### Badge auf Listing-Card
```
┌──────────────────────┐
│ 📷 Bild │
│ Titel der Anzeige │
│ 50.00 EUR │
│ 📍 Zürich 🟢 10+ │ ← Badge + Deal-Count
└──────────────────────┘
```
### Seller-Card auf Listing-Seite
```
┌──────────────────────────┐
│ ? Anonymer Anbieter │
│ Mitglied seit 2025 │
│ 🟢 Vertrauenswürdig │
│ 12 Deals · ⌀ 4.3 ⭐ │
└──────────────────────────┘
```
---
## Scam-Prävention
| Massnahme | Beschreibung |
|-----------|--------------|
| **Neuer-Account-Warnung** | "Neuer Account — starte mit kleinen Beträgen" |
| **Preis-Limit** | Max. Anzeigenpreis für Level "Neu": 100 EUR/USD/CHF |
| **Cooldown** | Neue Accounts: max. 3 Anzeigen pro Tag |
| **Report-Konsequenz** | 3+ Reports → automatische Prüfung, Anzeigen ausgeblendet |
| **Self-Rating-Schutz** | Nur nach beidseitig bestätigtem Deal bewertbar |
---
## Implementierung
### Phase 1: Backend (Directus)
- [ ] Collection `deals` anlegen (siehe Anleitung unten)
- [ ] Collection `ratings` anlegen (siehe Anleitung unten)
- [ ] Permissions setzen (Public-Rolle, siehe unten)
- [ ] Flow: Notification bei Deal-Bestätigung (optional)
---
## Directus: Collection `deals` anlegen
```
Settings > Data Model > + Create Collection
Name: deals
Primary Key: UUID (auto-generated)
```
| Feld | Typ | Interface | Einstellungen |
|------|-----|-----------|---------------|
| `id` | UUID | | Primary Key, auto-generated |
| `listing` | UUID (String) | Input | Listing-ID (kein FK, da Public-Rolle keinen Zugriff auf M2O hat) |
| `conversation` | UUID (String) | Input | Conversation-ID |
| `seller_hash` | String | Input | Required, max 64 chars |
| `buyer_hash` | String | Input | Required, max 64 chars |
| `seller_confirmed` | Boolean | Toggle | Default: `false` |
| `buyer_confirmed` | Boolean | Toggle | Default: `false` |
| `status` | String | Dropdown | `pending`, `confirmed`, `disputed` (Default: `pending`) |
| `date_created` | DateTime | DateTime | Auto, Read-only (Directus: "On Create" → Save Current Date/Time) |
| `date_confirmed` | DateTime | DateTime | Nullable, kein Default |
### Schritt-für-Schritt
1. **Collection erstellen:**
- Settings → Data Model → `+ Create Collection`
- Name: `deals`
- Primary Key Field: Type `UUID`, Auto-generated: ✓
- Optional Fields: ✓ `date_created`
2. **Felder anlegen (in dieser Reihenfolge):**
**listing** (String):
- Interface: Input
- Schema: String, max length 36
- Nicht nullable
**conversation** (String):
- Interface: Input
- Schema: String, max length 36
- Nicht nullable
**seller_hash** (String):
- Interface: Input
- Schema: String, max length 64
- Nicht nullable
**buyer_hash** (String):
- Interface: Input
- Schema: String, max length 64
- Nicht nullable
**seller_confirmed** (Boolean):
- Interface: Toggle
- Schema: Boolean, Default: `false`
**buyer_confirmed** (Boolean):
- Interface: Toggle
- Schema: Boolean, Default: `false`
**status** (String):
- Interface: Dropdown
- Options: `pending`, `confirmed`, `disputed`
- Schema: String, Default: `pending`
**date_confirmed** (DateTime):
- Interface: DateTime
- Schema: Timestamp, Nullable: ✓
---
## Directus: Collection `ratings` anlegen
```
Settings > Data Model > + Create Collection
Name: ratings
Primary Key: UUID (auto-generated)
```
| Feld | Typ | Interface | Einstellungen |
|------|-----|-----------|---------------|
| `id` | UUID | | Primary Key, auto-generated |
| `deal` | UUID (String) | Input | Deal-ID |
| `rater_hash` | String | Input | Required, max 64 chars, wer bewertet |
| `rated_hash` | String | Input | Required, max 64 chars, wer bewertet wird |
| `score` | Integer | Slider | Min: 1, Max: 5 |
| `date_created` | DateTime | DateTime | Auto, Read-only |
### Schritt-für-Schritt
1. **Collection erstellen:**
- Settings → Data Model → `+ Create Collection`
- Name: `ratings`
- Primary Key Field: Type `UUID`, Auto-generated: ✓
- Optional Fields: ✓ `date_created`
2. **Felder anlegen:**
**deal** (String):
- Interface: Input
- Schema: String, max length 36
- Nicht nullable
**rater_hash** (String):
- Interface: Input
- Schema: String, max length 64
- Nicht nullable
**rated_hash** (String):
- Interface: Input
- Schema: String, max length 64
- Nicht nullable
**score** (Integer):
- Interface: Slider
- Options: Min `1`, Max `5`, Step `1`
- Schema: Integer, nicht nullable
---
## Directus: Permissions (Public-Rolle)
```
Settings > Access Control > Public
```
### Collection `deals`
**Read:**
- ✓ Erlaubt
- Felder: Alle
- Filter: Eigene Deals sehen
```json
{
"_or": [
{ "seller_hash": { "_eq": "$CURRENT_USER" } },
{ "buyer_hash": { "_eq": "$CURRENT_USER" } }
]
}
```
**Hinweis:** Da wir SHA-256 Hashes nutzen (nicht Directus-User-IDs), funktioniert `$CURRENT_USER` hier NICHT.
Stattdessen: **Keine Read-Filter setzen** (alle Deals lesbar). Die Filterung passiert im Frontend via API-Query.
Alternativ: Kein Filter, aber nur bestimmte Felder freigeben (z.B. `seller_hash`, `buyer_hash`, `status`, `date_confirmed` — ohne `conversation`, `listing`).
**Empfohlene Read-Permissions (ohne Filter, mit Feld-Einschränkung):**
- Felder: `id`, `listing`, `conversation`, `seller_hash`, `buyer_hash`, `seller_confirmed`, `buyer_confirmed`, `status`, `date_created`, `date_confirmed`
**Create:**
- ✓ Erlaubt
- Felder: `listing`, `conversation`, `seller_hash`, `buyer_hash`, `seller_confirmed`, `buyer_confirmed`, `status`
- Kein Validation-Filter nötig (Frontend validiert)
**Update:**
- ✓ Erlaubt
- Felder: **Nur** `seller_confirmed`, `buyer_confirmed`, `status`, `date_confirmed`
- **Kein Custom-Filter** (da `$CURRENT_USER` nicht mit Hashes funktioniert)
- Schutz: Im Frontend wird geprüft ob der User Teilnehmer ist
**Delete:**
- ✗ Nicht erlaubt
### Collection `ratings`
**Read:**
- ✓ Erlaubt
- Felder: `id`, `deal`, `rater_hash`, `rated_hash`, `score`, `date_created`
- Kein Filter (Ratings sind öffentlich sichtbar für Reputation)
**Create:**
- ✓ Erlaubt
- Felder: `deal`, `rater_hash`, `rated_hash`, `score`
**Update:**
- ✗ Nicht erlaubt (Bewertungen sind final)
**Delete:**
- ✗ Nicht erlaubt
---
## Directus: DIRECTUS-SCHEMA.md aktualisieren
Nach dem Anlegen diese Tabellen in `docs/DIRECTUS-SCHEMA.md` ergänzen und die Permissions-Tabelle erweitern:
```
| `deals` | ✓ | ✓ | ✓ | - | Update nur confirmed/status Felder |
| `ratings` | ✓ | ✓ | - | - | Bewertungen sind final |
```
### Phase 2: Service (Frontend)
- [x] `js/services/reputation.js` — Deals/Ratings CRUD, Level-Berechnung, Caching
- [x] i18n-Keys in allen 7 Sprachen
- [ ] Integration in `conversations.js` — Deal-Bestätigung im Chat
### Phase 3: UI
- [x] Chat-Widget: "Deal abschliessen" Button + Bestätigungsanfrage
- [x] Chat-Widget: Sterne-Bewertung nach bestätigtem Deal
- [ ] Listing-Card: Badge + Deal-Count
- [x] Listing-Page Seller-Card: Level, Deals, Rating
- [x] Neuer-Account-Warnung auf Listing-Page
### Phase 4: Scam-Prävention
- [ ] Preis-Limit für neue Accounts
- [ ] Anzeigen-Cooldown für neue Accounts
- [ ] Report-Integration mit Konsequenzen
---
## i18n Keys (benötigt)
```
reputation.level.new = "Neuer Account"
reputation.level.active = "Aktiv"
reputation.level.trusted = "Vertrauenswürdig"
reputation.level.power = "Power Seller"
reputation.deals = "{{count}} Deals"
reputation.dealsSingular = "1 Deal"
reputation.avgRating = "⌀ {{rating}} ⭐"
reputation.newWarning = "Neuer Account — starte mit kleinen Beträgen"
reputation.confirmDeal = "Deal abschliessen"
reputation.dealPending = "Warte auf Bestätigung"
reputation.dealConfirmed = "Deal bestätigt"
reputation.rate = "Bewertung abgeben"
reputation.rated = "Bewertet"
```