381 lines
12 KiB
Markdown
381 lines
12 KiB
Markdown
# 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"
|
||
```
|