feat: add reputation system with deals, ratings, level badges, and chat-widget deal confirmation
This commit is contained in:
380
docs/REPUTATION.md
Normal file
380
docs/REPUTATION.md
Normal 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 | 1–5 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 (1–5 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"
|
||||
```
|
||||
Reference in New Issue
Block a user