# Entwicklungsplan: B2in / Local for Local Marktplatz-Ökosystem

**Erstellt:** 12.02.2026
**Letzte Aktualisierung:** 12.02.2026
**Basis:** konzeption.md (Version 1.1)
**Status:** Phase 1 ✅, Phase 2 ✅, Phase 2.5 Produkt-Bearbeitung ✅, Phase 2.6 Refactoring & UX ✅, Phase 2.7 Admin-Produktverwaltung & Freigabe ✅, Phase 3 Kern ✅, Phase 2.8 Partner-Präsentation ✅
**Docker** Projekt läuft in Docker, root /var/www/html nutze php artisan ... nicht vendor/bin/sail artisan ...
---

## 0. Umsetzungsprotokoll

### Phase 2.7: Admin-Produktverwaltung, Freigabe-Workflow & Statusaktionen – ABGESCHLOSSEN (13.02.2026)

Vollstaendige Admin-Produktverwaltung mit tabellarischer Uebersicht, Filtern, Freigabe-Workflow mit Ablehnungsgrund, sowie Archivieren/Verkauft-Aktionen fuer Haendler und Admin.

**Produkt archivieren / als Verkauft markieren**
- Neue `archiveProduct()` und `markAsSold()` Methoden in `form-standard.blade.php`, `form-teaser.blade.php` und `products/index.blade.php`
- Edit-Formulare: Buttons "Als verkauft markieren" und "Archivieren" links neben Speichern-Button (nur im Edit-Modus)
- Produktliste: Dropdown-Menue (3-Punkte-Menue) je Produkt mit "Als verkauft" und "Archivieren" Optionen
- `wire:confirm` Dialoge fuer Sicherheitsabfrage
- Activity-Log-Eintraege (action: `archived` / `sold`) werden automatisch erstellt
- Produkte im Status Archived/Sold zeigen kein Dropdown mehr

**Erstellungsdatum in Produktliste**
- Neue Spalte "Erstellt" in `products/index.blade.php` (Format: dd.mm.YYYY)
- Sowohl fuer Haendler als auch Admin sichtbar

**Admin-Produktuebersicht (komplett neu)**
- `admin/products/index.blade.php` komplett umgebaut: von Card-/Tab-Layout zu tabellarischer Uebersicht
- Statistik-Karten: Gesamt, Zur Freigabe, In Korrektur, Freigegeben (klickbar als Schnellfilter)
- Filter: Suche (Name, Artikelnummer), Status (alle ProductStatus-Werte), Produkttyp, Haendler, Kategorie
- Tabelle: Produkt (mit Bild), Haendler, Kategorie, Status, Kuration (Freigabe-Buttons), Erstellt, Aktionen
- Admin kann alle Produkte bearbeiten – Edit-Link fuehrt zum gleichen Formular wie fuer Haendler
- Suche durchsucht auch `b2in_article_number` und `partner_product_number`

**Freigabe-Workflow mit Ablehnungsgrund**
- **Freigeben:** Direkt-Button in Kuration-Spalte (Pending → Active + is_curated)
- **Korrektur:** Inline-Formular (orange) mit Pflicht-Textfeld → Status `Correction`, `curation_notes` gespeichert
- **Ablehnung (NEU):** Inline-Formular (rot) mit Pflicht-Textfeld → Status `Archived`, Ablehnungsgrund in `curation_notes` gespeichert
- Activity-Log-Eintrag bei allen drei Aktionen (mit `note` bei Korrektur/Ablehnung)
- `Flux::toast()` Benachrichtigungen statt Flash-Messages

**Kuration-Hinweis beim Haendler**
- Standard- und Teaser-Edit-Formulare zeigen prominenten Callout wenn `curation_notes` vorhanden:
  - Korrektur (Status `correction`): Gelbes Warning-Callout "Korrektur erforderlich"
  - Ablehnung (Status `archived`): Rotes Danger-Callout "Produkt abgelehnt"
- Angezeigt oberhalb des Formulars, immer sichtbar

**Admin Archiv/Verkauft in Admin-Uebersicht**
- Dropdown-Menue analog zur Haendler-Produktliste
- Admin kann alle Produkte archivieren oder als verkauft markieren

**Erstellte/geaenderte Dateien (5):**

| Kategorie | Dateien | Details |
|-----------|---------|---------|
| **Volt** (3) | `form-standard.blade.php`, `form-teaser.blade.php`, `products/index.blade.php` | Archive/Sold-Methoden, Kuration-Callout, Erstellungsdatum, Dropdown-Aktionen |
| **Admin-Volt** (1) | `admin/products/index.blade.php` | Komplett umgebaut: Tabelle, Filter, Statistiken, Rejection mit Textfeld |
| **Tests** (1) | `ProductCurationTest.php`, `ProductEditTest.php` | Aktualisiert + erweitert |

**Neue/aktualisierte Tests (25):**
- ProductCurationTest: 23 Tests (komplett ueberarbeitet fuer neues Tabellen-Layout, Rejection mit Pflicht-Textfeld, Archive/Sold, Filter, Kuration-Notes-Anzeige)
- ProductEditTest: +2 Tests (archiveProduct, markAsSold aus Edit)

**Tests gesamt: 194 Produkt-Tests, alle bestanden ✅ (533 Assertions)**

---

### Phase 2.6: Refactoring, UX-Verbesserungen & Teaser-Erweiterung – ABGESCHLOSSEN (13.02.2026)

Umfassende Qualitaetsverbesserung der Produkt-Formulare: Code-Redundanzen eliminiert, UX-Workflow optimiert, Bildsortierung implementiert, Teaser-Produkte mit Produktnummern und korrektem Freigabe-Workflow ausgestattet.

**Refactoring: Create + Edit zu einem Formular zusammengefuehrt**
- `create.blade.php` + `edit.blade.php` → `form-standard.blade.php` (Standard-Produkte)
- `create-teaser.blade.php` + `edit-teaser.blade.php` → `form-teaser.blade.php` (Teaser-Produkte)
- Steuerung ueber `$isEditing`-Flag in `mount()`: `?Product $product = null`
- Separate `saveNew()` und `saveExisting()` Methoden
- Alte Einzeldateien geloescht

**UX: Save-Verhalten bei Bearbeitung**
- Bei Edit: Seite bleibt offen statt Redirect zur Produktliste
- Flux Toast-Notification ("Produkt wurde gespeichert" / "zur Freigabe eingereicht")
- Bei Standard-Produkten: aktiver Tab bleibt erhalten (kein Page-Reload)
- `<flux:toast />` global im Sidebar-Layout ergaenzt
- Bei Create: weiterhin Redirect zur Produktliste mit Flash-Message

**Bildsortierung per Drag & Drop**
- Vorhandene Bilder per HTML5 Drag & Drop umsortierbar (Alpine.js, keine externe Dependency)
- Erstes Bild = Standardbild, visuell markiert (blaues "Standard"-Badge + blauer Ring)
- `updateMediaOrder(array $orderedIds)` Methode speichert Reihenfolge in `order_column`
- `existingMedia` wird immer nach `order_column` sortiert geladen
- Drag-Feedback: halbtransparent, blauer Ring am Ziel, Grab-Cursor, Sortier-Icon bei Hover

**Vorschaubild in Produktliste**
- `products/index.blade.php`: Erstes Bild (nach `order_column`) als 40x40px Thumbnail vor dem Produktnamen
- Platzhalter-Icon (Foto-Symbol) wenn kein Bild vorhanden

**Teaser-Produkte: Produktnummern & Status-Fix**
- `partnerProductNumber` hinzugefuegt: wird bei Create automatisch generiert (z.B. P003-0001), bei Edit pre-filled
- `b2inArticleNumber` wird beim Speichern automatisch erzeugt (z.B. B2IN-000001)
- Eigene "Produktnummern"-Karte im Formular (B2in-Badge + Partner-Nummer-Feld)
- **Bug-Fix:** Status "aktiv" im UI setzte Status direkt auf Active statt Pending. Jetzt korrekt: UI "aktiv" → DB `pending` (zur Freigabe)
- UI-Text vereinheitlicht: immer "Zur Freigabe einreichen" (nicht mehr "Aktiv – direkt veröffentlichen")
- Freigabe-Workflow-Hinweis erscheint bei Create und Edit

**Routing-Aenderung:**
```
products/create/standard  → products.form-standard (products.create.standard)
products/create/teaser    → products.form-teaser   (products.create.teaser)
products/{product}/edit-standard → products.form-standard (products.edit.standard)
products/{product}/edit-teaser   → products.form-teaser   (products.edit.teaser)
```

**Erstellte/geaenderte Dateien (8):**

| Kategorie | Dateien | Details |
|-----------|---------|---------|
| **Volt** (2) | `form-standard.blade.php`, `form-teaser.blade.php` | Merged Create+Edit, Toast, Bildsortierung, Produktnummern |
| **Views** (2) | `index.blade.php`, `sidebar.blade.php` | Vorschaubild, `<flux:toast />` |
| **Routes** (1) | `routes/admin.php` | Neue Routennamen (.standard/.teaser) |
| **Tests** (4) | `StandardProductCreateTest`, `ProductEditTest`, `TeaserProductCreateTest`, `TeaserProductEditTest` | Komponentennamen aktualisiert, 13 neue Tests |

**Geloeschte Dateien:** `create.blade.php`, `edit.blade.php`, `create-teaser.blade.php`, `edit-teaser.blade.php`

**Neue Tests (13):** Bildsortierung (5), Teaser-Status pending/draft (2), Teaser B2in-Artikelnummer (1), Teaser Partnernummer create+edit (3), Teaser Partnernummer pre-fill (2)

**Tests gesamt: 109 Produkt-Tests, alle bestanden ✅**

---

### Phase 2.5: Produkt-Bearbeitung (Standard + Teaser) – ABGESCHLOSSEN (13.02.2026)

Beide Produkttypen koennen jetzt vollstaendig bearbeitet werden. Standard-Produkte (SmartOrder) nutzen das 8-Tab-Formular, Teaser-Produkte (LocalStock) ein vereinfachtes Einseiten-Formular. Die Produkt-Liste leitet automatisch zur richtigen Edit-Seite weiter.

Erstellte/geaenderte Dateien (4):
- Volt: resources/views/livewire/products/edit-teaser.blade.php (neu) - Teaser-Edit mit Pre-Fill, Status-Handling, Media-Verwaltung, Activity-Log
- Routes: routes/admin.php - products.edit.teaser Route hinzugefuegt
- Views: resources/views/livewire/products/index.blade.php - Edit-Link basierend auf product_type
- Tests: tests/Feature/TeaserProductEditTest.php (neu) - 24 Tests

Infrastruktur-Fix: Route-Cache und Test-DB-Migration bereinigt.

Tests gesamt: 24 Tests (TeaserProductEditTest) + 22 Tests (ProductEditTest), alle bestanden.
Produkt-bezogene Tests gesamt: 133 Tests, alle bestanden.

---

### Phase 2.3b: CSV-Felder Erweiterung (Moebeldatenliste) – ✅ ABGESCHLOSSEN (13.02.2026)

**Änderung:** Alle fehlenden Felder aus `Moebeldatenliste Stand 4.11.2025.csv` (70 Felder, 13 Sektionen) wurden als DB-Spalten und Formularfelder ergänzt. Von ~30 existierenden Feldern auf ~70 erweitert. Neue Tabelle `product_wood_origins` für EUDR-Compliance. Formular von 5 auf 8 Tabs umgebaut.

**Erstellte/geänderte Dateien (13):**

| Kategorie | Dateien | Details |
|-----------|---------|---------|
| **Migrationen** (4) | `add_csv_fields_to_products_table`, `add_csv_fields_to_product_logistics_table`, `add_currency_to_product_variants_table`, `create_product_wood_origins_table` | +21 Spalten products, +4 Spalten product_logistics, +1 Spalte product_variants, neue Tabelle product_wood_origins |
| **Models** (4) | `Product.php`, `ProductLogistics.php`, `ProductVariant.php`, `ProductWoodOrigin.php` (neu) | Fillable, Casts, woodOrigins() Relationship |
| **Factories** (1) | `ProductWoodOriginFactory.php` (neu) | Factory mit Holzarten, Ländern, EUDR-Daten |
| **Volt-Komponenten** (1) | `products/create.blade.php` | 8-Tab-Layout, ~25 neue Properties, Wood-Origins-Repeater, erweiterte Validierung + Save-Logik |
| **Tests** (2) | `StandardProductCreateTest.php` (+12 Tests), `Models/ProductWoodOriginTest.php` (4 Tests, neu) | Material, Logistik, Services, Nachhaltigkeit, EUDR, Scoring, Währung, Validierung |

**Neue DB-Spalten (26):** products: country_of_origin, main_material, surface_material, cover_material, color_finish, certificates(JSON), assembly_time_min, load_capacity_kg, delivery_type, assembly_service, service_radius_km, warranty_months, production_time_days, visible_from, visible_until, co2_footprint_kg, recycling_percentage, is_regional_production, storage_volume_liters, assembly_effort_score, design_score. product_logistics: packaging_type, packaging_recyclable_percent, is_palletizable, hs_code. product_variants: currency.

**Neue Tabelle:** `product_wood_origins` (1:n von products) – EUDR-Compliance mit Holzart, Herkunftsland, Region, Erntejahr, Forstbetrieb, Zertifikat, EUDR-ID

**Pint-Formatierung:** ✅ (keine Korrekturen nötig)

**Tests gesamt: 33 Tests (StandardProductCreateTest) + 4 Tests (ProductWoodOriginTest), alle bestanden ✅**

---

### Phase 2.3: Standard-Produkt Erstellung (Maske 2) – ✅ ABGESCHLOSSEN (13.02.2026)

**Änderung:** Das Standard-Produkt-Formular (`create.blade.php`) wurde komplett neu geschrieben – von einer nicht-funktionalen Dummy-Vorlage zu einem voll funktionsfähigen class-based Volt Component mit 5 Tabs.

**Erstellte/geänderte Dateien (3):**

| Kategorie | Dateien | Details |
|-----------|---------|---------|
| **Volt-Komponenten** (1) | `resources/views/livewire/products/create.blade.php` | Komplett neugeschrieben: 5-Tab-Layout (Basis, Bilder, Physisch, Kommerziell, Zuordnung), erstellt Product + ProductVariant + ProductLogistics + Media, inline Validierung, Preise EUR→Cents |
| **Migrationen** (1) | `database/migrations/2026_02_13_..._make_tax_rate_id_nullable_on_product_variants_table.php` | `tax_rate_id` nullable gemacht (war NOT NULL ohne Default, tax_rates Tabelle leer) |
| **Tests** (1) | `tests/Feature/StandardProductCreateTest.php` | 17 neue Tests: Zugriff (3), Happy Path (2), Physisch+Logistik (1), Kommerziell (1), SEO (1), Validierung (8), Preistypen (1) |

**Pint-Formatierung:** ✅ (1 Fix: unused import in StandardProductCreateTest)

**Tests gesamt: 17 Tests (StandardProductCreateTest), alle bestanden ✅**

---

### Phase 2 Ergänzung: Beide Rollen → Beide Produkttypen (13.02.2026)

**Änderung:** Sowohl Händler (Retailer) als auch Hersteller (Manufacturer) können nun BEIDE Produkttypen anlegen (Teaser + Standard). Vorher war die Erstellmaske rollenbasiert auf einen Typ beschränkt.

**Erstellte/geänderte Dateien (3):**

| Kategorie | Dateien | Details |
|-----------|---------|---------|
| **Volt-Komponenten** (1) | `resources/views/livewire/products/index.blade.php` | Zwei Buttons ("Neues Teaser-Produkt" + "Neues Standard-Produkt"), neuer `productTypeFilter` State + Query-Filter, Produkttyp-Dropdown im Filterbereich |
| **Tests** (2) | `tests/Feature/LocalFeedTest.php`, `tests/Feature/TeaserProductCreateTest.php` | +6 neue Tests: Produkttyp-Filter (3), Zwei-Button-Anzeige (3), Manufacturer Teaser-Zugriff (1), Manufacturer Teaser-Erstellung (1). Fix: `mainImage` → `mainImages` in bestehenden Tests |

**Pint-Formatierung:** ✅ (keine Korrekturen nötig)

**Tests gesamt: 21 Tests (LocalFeedTest + TeaserProductCreateTest), alle bestanden ✅**

---

### Phase 3: Kunden-Frontend & Local Feed – ✅ KERN ABGESCHLOSSEN (12.02.2026)

**Erstellte/geänderte Dateien (8):**

| Kategorie | Dateien | Details |
|-----------|---------|---------|
| **Actions** (1) | `app/Actions/Fortify/CreateNewUser.php` | `origin` aus `config('app.theme')` via `UserOrigin::tryFrom()`, `hub_id` aus Input |
| **Models** (1) | `app/Models/Partner.php` | `PartnerType` Cast hinzugefügt (`type` Feld) |
| **Volt-Komponenten** (2) | `resources/views/livewire/products/index.blade.php`, `resources/views/livewire/partner/profile.blade.php` | products/index: echte DB-Queries, Rollen-basierte Filterung (Admin/Customer/Partner). partner/profile: neue öffentliche Profilseite |
| **Routes** (1) | `routes/admin.php` | `partner.profile` Route hinzugefügt |
| **Tests** (3) | `CreateNewUserOriginTest`, `LocalFeedTest`, `PartnerProfilePageTest` | **17 Tests – alle bestanden** |

**Wichtige Korrekturen (12.02.2026):**
- `products` Tabelle hat keine `sku` Spalte – aus Suche und Tabellenansicht entfernt
- Partner `type` war nicht auf `PartnerType` Enum gecastet – `PartnerType::class` Cast zu `Partner.php` hinzugefügt
- `Volt::test()` wirft `ModelNotFoundException` direkt (kein HTTP 404) – 404-Tests nutzen `toThrow()`

**Pint-Formatierung:** ✅ (5 Dateien: Partner.php, CreateNewUser.php, 3 Test-Dateien)

**Tests Phase 3 gesamt: 17 Tests, alle bestanden ✅**

---

### Phase 2: Händler-Profil & Produkt-Management – ✅ KERN ABGESCHLOSSEN (12.02.2026)

**Erstellte/geänderte Dateien (11):**

| Kategorie | Dateien | Details |
|-----------|---------|---------|
| **Policies** (2) | `app/Policies/PartnerPolicy.php`, `ProductPolicy.php` | PartnerPolicy: viewAny/view/create/update/delete/curateProducts. ProductPolicy: viewAny/view/create/update/delete/curate |
| **Volt-Komponenten** (4) | `admin/partners/index.blade.php`, `admin/partners/edit.blade.php`, `products/create-teaser.blade.php`, `admin/products/index.blade.php` | Partner-Übersicht, Profil-Edit (Story+Öffnungszeiten), Teaser-Erstellen (Typ A, Preislogik via Enum), Kuration-Queue |
| **Routes** (1) | `routes/admin.php` | 4 neue Routen: `admin.partners.index`, `admin.partners.edit`, `products.create.teaser`, `admin.products.index` |
| **Seeders** (1) | `database/seeders/RoleSeeder.php` | `curate products` Permission hinzugefügt |
| **Tests** (5) | `PartnerPolicyTest`, `ProductPolicyTest`, `PartnerProfileUpdateTest`, `TeaserProductCreateTest`, `ProductCurationTest` | **48 Tests – alle bestanden** |

**Enums erweitert (12.02.2026):**
- `app/Enums/ProductType.php`: `requiresTicket(): bool`, `allowedPriceTypes(): array`
- `tests/Unit/Enums/ProductTypeTest.php`: 4 neue Tests für Typ A/B Geschäftsregeln

**Pint-Formatierung:** ✅ (keine Style-Korrekturen nötig)

**Wichtige Erkenntnisse:**
- Volt `actingAs()` ist `void` – für Tests immer `$this->actingAs($user)` VOR `Volt::test()` aufrufen
- Für HTTP-Tests mit `partner.setup` Middleware: `Partner::factory()->setupCompleted()->create()` verwenden
- `Storage::fake('public')` + `UploadedFile::fake()->image()` für File-Upload-Tests

---

### Phase 1: DB & Core-Vervollständigung – ✅ ABGESCHLOSSEN (12.02.2026)

**Erstellte Dateien (29 Dateien):**

| Kategorie | Dateien | Details |
|-----------|---------|---------|
| **Enums** (6) | `app/Enums/ProductType.php`, `ProductStatus.php`, `PriceType.php`, `PartnerType.php`, `UserOrigin.php`, `CurationStatus.php` | Alle mit `label()`, Status-Enums zusätzlich mit `color()`, UserOrigin mit `tonality()` |
| **Neue Models** (4) | `app/Models/Product.php`, `Media.php`, `Setting.php`, `ProductLogistics.php` | Product: 6 Relationships, 6 Scopes. Media: Polymorphe Beziehung. Setting: Key-Value mit getValue/setValue Helpers |
| **Aktualisierte Models** (11) | `Attribute`, `AttributeValue`, `Category`, `Collection`, `Tag`, `ProductVariant`, `ShippingClass`, `TaxRate`, `Hub`, `Partner`, `User` | Alle Sparse Models mit Fillable, Casts, Relationships ergänzt. Partner: +products(), +media(). User: +hub(), +origin Cast |
| **Migrationen** (4) | `2026_02_12_000001` bis `000004` | Users (+origin, +hub_id), Products (+product_type, +is_curated, +hub_id, +price_type, +is_available, +curated_at, +curated_by), Partners (+story_text, +opening_hours, +specialties, +founded_year), Settings-Tabelle |
| **Factories** (5) | `ProductFactory`, `MediaFactory`, `PartnerFactory`, `HubFactory`, `BrandFactory` | Alle mit sinnvollen States (localStock, smartOrder, active, retailer, manufacturer, etc.) |
| **Seeders** (1) | `SettingsSeeder` | 6 Default-Settings: Ticket-Gültigkeit, Beleg-Deadline, Ticket-Limits, Provisions-Defaults |
| **Config** (1) | `config/domains.php` | +local4local Domain (local4local.test / local4local.online) |
| **Tests** (7 Dateien) | 3× Unit (Enums), 4× Feature (Product, Setting, User, Partner) | **47 Tests, 84 Assertions – alle bestanden** |

**Migrationen auf Produktions-DB ausgeführt:** ✅
**Settings-Seeder ausgeführt:** ✅ (6 Settings angelegt)
**Pint-Formatierung:** ✅ (7 Style-Issues automatisch korrigiert)

**Vorbestehende Test-Failures – BEHOBEN (12.02.2026):**
Alle 16 vorbestehenden Fehler plus 15 weitere Fehler (31 insgesamt) wurden systematisch behoben. Die gesamte Test-Suite besteht jetzt mit **139 Tests, 269 Assertions – alle bestanden** ✅

Behobene Probleme:
- `phpunit.xml`: `BASIC_AUTH_ENABLED=false` hinzugefügt (BasicAuth-Middleware blockierte alle HTTP-Tests)
- `admin/partners/index.blade.php`: Flux UI v2 Shorthand `<flux:columns>` → `<flux:table.columns>` korrigiert
- `config/livewire.php`: `component_layout` Key hinzugefügt (Livewire 4 Layout-Resolution)
- Auth-Tests: Portal-Domain (`portal.b2in.test`) für alle HTTP-Requests
- `RegistrationTest`: Komplett neu geschrieben für code-basierte Registrierung über `/reg/{role}`
- `PasswordResetTest`: `CustomResetPasswordNotification` statt Standard-`ResetPassword`
- `EmailVerificationTest`: Redirect zu `partner.setup.wizard` statt `dashboard`
- `ProfileUpdateTest`: SoftDeletes-Assertion (`->trashed()`) und Layout-Redirect
- `DashboardTest`: `RefreshDatabase` Trait hinzugefügt
- `ProductCurationTest`, `PartnerProfileUpdateTest`, `TeaserProductCreateTest`: `actingAs()` Chaining korrigiert (Volt `actingAs()` ist `void`)
- `TeaserProductCreateTest`: `mainImage` Upload, `setupCompleted()` Factory-State, `CategoryFactory` erstellt
- `ProductCurationTest`: Authorization-Test angepasst (Component authorize in `with()`)
- `Category` Model: `HasFactory` Trait hinzugefügt

---

## 1. Validierung: IST-Zustand vs. Konzeption

### ✅ Bereits implementiert und konzeptkonform

| Bereich | Status | Details |
|---------|--------|---------|
| **Hub-System** | ✅ Fertig | `hubs` + `hub_locations` Tabellen, Hub Model mit Relationships |
| **Partner-System** | ✅ Basis fertig | `partners` Tabelle mit `hub_id`, `type`, Provisionsfelder, Parent/Child-Beziehungen |
| **User-System** | ✅ Basis fertig | Users mit `partner_id`, SoftDeletes, Spatie Roles |
| **Rollen & Permissions** | ✅ Fertig | Customer, Estate-Agent, Retailer, Manufacturer, Admin, Super-Admin (via Spatie) |
| **Registrierungs-Codes** | ✅ Fertig | `registration_codes` mit `broker_partner_id` für Makler→Kunden Attribution |
| **Partner-Invitations** | ✅ Fertig | `partner_invitations` mit Token, Expiry, Status |
| **Multi-Domain-Routing** | ✅ Fertig | ThemeMiddleware, ThemeServiceProvider, `config/domains.php` (5 Domains) |
| **Auth-System** | ✅ Fertig | Fortify + Sanctum, Login, Register, Passwort-Reset, Email-Verifizierung, 2FA |
| **Admin-Portal** | ✅ Basis fertig | Dashboard, User-Management, Partner-Management, Hub-Management, CMS |
| **Partner-Setup-Wizard** | ✅ Fertig | Setup-Workflow für neue Partner nach Registrierung |
| **Produkt-DB-Struktur** | ✅ Tabellen vorhanden | `products`, `product_variants`, `categories`, `tags`, `brands`, `collections`, `attributes`, `media` |

### ⚠️ Teilweise implementiert – Erweiterung nötig

| Bereich | Was fehlt | Konzept-Referenz |
|---------|-----------|-----------------|
| **Product Model** | Migration existiert, aber **kein `App\Models\Product`** – Model muss erstellt werden | Abschnitt 2: Produkt-Modul |
| **Media Model** | Migration existiert, aber **kein `App\Models\Media`** – Model muss erstellt werden | Für Produkt-Bilder, Partner-Galerie |
| **Partner-Profil** | Basis-Felder vorhanden, aber es fehlen: Team-Fotos, Showroom-Galerie, Story-Text, Öffnungszeiten | Abschnitt 3: "Faces" Profile |
| **Produkt-Upload UI** | `livewire/products/create` existiert als Blade, aber unvollständig | Abschnitt 2: Händler-Upload |
| **Produkt-Feed** | `livewire/products/index` existiert, aber keine Hub-basierte Filterung | Abschnitt 5: Local Feed |
| **Sparse Models** | `Attribute`, `AttributeValue`, `Category`, `Collection`, `Tag`, `ProductVariant`, `ShippingClass`, `TaxRate` haben keine Relationships definiert | Allgemein |

### ❌ Nicht implementiert – Muss gebaut werden

| Bereich | Beschreibung | Konzept-Referenz |
|---------|-------------|-----------------|
| **User `origin` Feld** | Herkunft des Kunden (`style2own` / `stileigentum`) für Theme-Steuerung | Abschnitt 1: Core-Modul |
| **User `hub_id` Feld** | Direkte Hub-Zuordnung für Kunden (nicht nur indirekt über Partner) | Abschnitt 1: Hub-Logik |
| **`product_type` Feld** | Unterscheidung `local_stock` (Säule A) vs. `smart_order` (Säule B) | Abschnitt 2: Produkt-Modul |
| **`is_curated` Feld** | Admin-Freigabe-Flag für Produkt-Sichtbarkeit | Abschnitt 2: Produkt-Modul |
| **Ticket-System** | Komplett fehlend: Tickets, QR-Codes, Voucher-Generierung | Abschnitt 3A: Ticket-System |
| **Transaction/Receipt** | Beleg-Upload, State Machine (pending→confirmed→paid→distributed) | Abschnitt 3B: Clearing-System |
| **Wallet/Ledger** | Cashback-Wallets, Provisions-Split, Kassenbuch | Abschnitt 3C: Wallet-Logik |
| **Kunden-Dashboard** | "Mein Zuhause" – emotionaler Feed mit Produkten aus dem eigenen Hub | Abschnitt 4: User Journey |
| **Setup-Buchung** | Service-Store für Händler (z.B. Setup-Paket 399€) | Abschnitt 4: Partner-Modul |
| **Enums** | Keine PHP Enums vorhanden (ProductType, TransactionStatus, TicketStatus, etc.) | Best Practice |
| **Form Requests** | Keine Form Request Klassen vorhanden | Best Practice |
| **Policies** | Keine Authorization Policies vorhanden | Best Practice |

---

## 2. Entwicklungsphasen

### Phase 1: Datenbank & Core-Vervollständigung ✅ ABGESCHLOSSEN
**Geschätzter Aufwand:** 2-3 Tage | **Tatsächlich:** 1 Tag (12.02.2026)
**Priorität:** HÖCHSTE – Basis für alle weiteren Phasen

#### 1.1 Fehlende Models erstellen
- [ ] `App\Models\Product` erstellen mit allen Relationships:
  - `belongsTo(Partner)`, `belongsTo(Brand)`, `belongsTo(Collection)`
  - `belongsToMany(Category)`, `belongsToMany(Tag)`
  - `hasMany(ProductVariant)`, `morphMany(Media)`
  - `hasMany(RelatedProduct)`
  - Scopes: `active()`, `curated()`, `localStock()`, `smartOrder()`, `inHub($hubId)`
- [ ] `App\Models\Media` erstellen (polymorphe Beziehung)
- [ ] Fehlende Relationships in Sparse Models ergänzen:
  - `Attribute` → `hasMany(AttributeValue)`
  - `AttributeValue` → `belongsTo(Attribute)`
  - `Category` → self-referencing parent/children, `belongsToMany(Product)`
  - `Tag` → `belongsToMany(Product)`
  - `ProductVariant` → `belongsTo(Product)`, `belongsToMany(AttributeValue)`
  - etc.
- [ ] `Partner::products()` Relationship aktivieren (aktuell auskommentiert)

#### 1.2 Migrationen: User-Erweiterung
```
Migration: add_origin_and_hub_id_to_users_table
```
- [ ] `origin` (nullable string) – `style2own` | `stileigentum` | null
- [ ] `hub_id` (nullable FK → hubs) – direkte Hub-Zuordnung für Kunden
- [ ] `broker_partner_id` (nullable FK → partners) – direkte Makler-Attribution (Alternative zu indirekter Verknüpfung über Partner-Hierarchie; **Entscheidung nötig**: reicht `partner.parent_partner_id` oder braucht User direkt ein Feld?)

> **Offene Frage 1:** Soll der Kunde einen eigenen Partner-Eintrag bekommen (so wie jetzt über `partner_id` → Partner mit `parent_partner_id`) oder reicht eine direkte `hub_id` + `broker_partner_id` auf dem User? Die aktuelle Architektur erstellt für jeden Kunden einen Partner-Eintrag. Das könnte für das Wallet/Ledger System nützlich sein (jeder Partner hat eigene Provision-Settings). **Empfehlung:** Beibehalten, aber `hub_id` und `origin` direkt auf User setzen für schnelle Queries.

#### 1.3 Migrationen: Produkt-Erweiterung
```
Migration: add_marketplace_fields_to_products_table
```
- [ ] `product_type` (string, default: `local_stock`) – `local_stock` | `smart_order`
- [ ] `is_curated` (boolean, default: false) – Admin-Freigabe
- [ ] `curated_at` (nullable datetime) – Zeitpunkt der Freigabe
- [ ] `curated_by` (nullable FK → users) – Wer hat freigegeben
- [ ] `hub_id` (nullable FK → hubs) – Direkte Hub-Zuordnung (ergänzend zu Partner→Hub)
- [ ] `price_type` (string) – `fixed` | `from_price` | `on_request` (Preisanzeige-Logik)
- [ ] `price_display_text` (nullable string) – z.B. "Ab 2.500 €" Freitext
- [ ] `is_available` (boolean, default: true) – Verfügbar/Verkauft Toggle für Händler

#### 1.4 Migrationen: Partner-Profil Erweiterung
```
Migration: add_profile_fields_to_partners_table
```
- [ ] `story_text` (nullable text) – "Seit 1950 in Herford..."
- [ ] `opening_hours` (nullable JSON) – Öffnungszeiten strukturiert
- [ ] `specialties` (nullable JSON) – Fachgebiete/Spezialisierungen
- [ ] `founded_year` (nullable integer) – Gründungsjahr

> **Hinweis:** Team-Fotos und Showroom-Galerie werden über die polymorphe `media` Tabelle abgebildet (type: `team_photo`, `showroom`, `gallery`).

#### 1.5 Migration: Settings-Tabelle
```
Migration: create_settings_table
```
- [ ] `id`
- [ ] `group` (string) – z.B. `tickets`, `marketplace`, `commissions`
- [ ] `key` (string, unique innerhalb group)
- [ ] `value` (text, nullable)
- [ ] `type` (string) – `string` | `integer` | `boolean` | `json`
- [ ] `description` (nullable text) – Beschreibung für Admin-UI
- [ ] `timestamps`

Default-Werte per Seeder:
- `tickets.validity_days` = 30
- `tickets.receipt_upload_deadline_days` = 30
- `tickets.max_per_merchant_per_customer` = 3
- `tickets.max_merchants_per_customer` = 4
- `commissions.default_broker_rate` = 0 (individuell)
- `commissions.default_cashback_rate` = 0 (individuell)

#### 1.6 Domain-Ergänzung: local4local
- [ ] `config/domains.php` um `local4local` Domain erweitern:
  - Produktion: `local4local.online`
  - Entwicklung: `local4local.test`
  - Theme, Farben, Fonts definieren
- [ ] `.env` Variablen: `DOMAIN_LOCAL4LOCAL=local4local.test`
- [ ] Routing in `routes/domains.php` einbinden

#### 1.7 Enums erstellen
- [ ] `App\Enums\ProductType` – `LocalStock`, `SmartOrder`
- [ ] `App\Enums\ProductStatus` – `Draft`, `Active`, `Archived`, `Sold`
- [ ] `App\Enums\PriceType` – `Fixed`, `FromPrice`, `OnRequest`
- [ ] `App\Enums\PartnerType` – `Retailer`, `Manufacturer`, `EstateAgent`
- [ ] `App\Enums\UserOrigin` – `Style2Own`, `StilEigentum`
- [ ] `App\Enums\CurationStatus` – `Pending`, `Approved`, `Rejected`

#### 1.8 Factories & Seeders
- [ ] `ProductFactory` erstellen
- [ ] `MediaFactory` erstellen
- [ ] `SettingFactory` erstellen
- [ ] `PartnerFactory` erweitern (fehlende Felder)
- [ ] `ProductSeeder` für Testdaten
- [ ] `SettingsSeeder` für Default-Werte
- [ ] `HubSeeder` erweitern (wenn nötig)

#### 1.9 Tests Phase 1
- [ ] Unit-Tests für alle neuen Models und Relationships
- [ ] Unit-Tests für Enums
- [ ] Feature-Tests für Migrations (Datenintegrität)

---

### Phase 2: Händler-Profil & Produkt-Management – ✅ KERN ABGESCHLOSSEN (12.02.2026)
**Geschätzter Aufwand:** 5-6 Tage | **Tatsächlich:** 1 Tag (12.02.2026)
**Priorität:** HOCH – Content-Erstellung ermöglichen

#### 2.0 Architektur-Entscheidung: EINE Tabelle, ZWEI Masken

> **Entscheidung:** Beide Produkttypen nutzen dieselbe `products` Tabelle. Das Feld `product_type` (`local_stock` | `smart_order`) bestimmt, welche Eingabemaske und Validierung angewendet wird.

**Begründung:**
- Beides sind Produkte mit identischem Kern (Name, Beschreibung, Preis, Bilder, Partner, Hub, Kategorie)
- Der Local Feed zeigt beide Typen gemischt an – eine Tabelle ermöglicht einfache Queries ohne UNION
- Das Teaser-Produkt ist ein "leichtes" Produkt: die Felder, die es nicht braucht, bleiben `null`
- Ein Model, ein Satz Relationships, ein Satz Scopes – kein doppelter Code
- Filterung per Scope: `Product::localStock()`, `Product::smartOrder()`

---

#### Kundenfeedback: Typ A vs. Typ B (12.02.2026)

> **Implementiert in `app/Enums/ProductType.php`:**
> - `requiresTicket(): bool` → `LocalStock = true`, `SmartOrder = false`
> - `allowedPriceTypes(): array` → `LocalStock = [FromPrice, OnRequest]`, `SmartOrder = [Fixed, FromPrice, OnRequest]`

**Typ A – Teaser-Produkte** (`product_type = 'local_stock'`)

Komplex → Beratung → Laden → **Ticket zwingend erforderlich**

- Aufwendige Konfiguration (Maße, Module, Materialien) – Abschluss nur im Laden
- Online nur Beispiele, Referenzen, grobe Preisindikationen
- Typisch: Küchenstudios, maßgefertigte Möbel (Cabinet), Einbausysteme
- **Preistyp:** Nur `from_price` oder `on_request` – Festpreis online nicht erlaubt
- **Ticket:** zwingend (`ProductType::LocalStock->requiresTicket() === true`)

**Typ B – Standard-Produkte** (`product_type = 'smart_order'`)

Einfach → verständlich → skalierbar → **Ticket optional / Direktkauf möglich**

- Klare Varianten, vollständig online konfigurierbar, ca. 99 % des Sortiments
- **Preistyp:** Alle erlaubt (`fixed`, `from_price`, `on_request`)
- **Ticket:** optional – Direktkauf online möglich oder Ticket + Ladenbesuch

---

**Maske 1: Typ A – Teaser-Produkt / Local Stock (Händler)**
Ziel: Extrem vereinfacht, handy-optimiert. Kein komplexes Warenwirtschafts-Monster.

| Feld | DB-Spalte | Pflicht | Beschreibung |
|------|-----------|---------|-------------|
| Fotos | → `media` (morph) | Ja (min. 1) | 1 Hauptbild + optional 2 Galerie-Bilder |
| Titel | `name` | Ja | Produktname |
| Kurzbeschreibung | `description_short` | Ja | Max. 180 Zeichen |
| Preistyp | `price_type` | Ja | Nur `from_price` oder `on_request` (via `allowedPriceTypes()`) |
| Preisangabe | `price_display_text` | Cond. | Pflicht wenn `from_price` (z.B. "Ab 2.500 €") |
| Kategorie | → `category_product` (Pivot) | Ja | Dropdown-Auswahl |
| Status | `status` | Ja | Verfügbar / Verkauft |

Automatisch gesetzt: `product_type = 'local_stock'`, `hub_id` = Hub des Händlers, `partner_id` = Partner des Users.
`PriceType::Fixed` für Maske 1 **nicht erlaubt** – Validierung via `ProductType::LocalStock->allowedPriceTypes()`.

**Maske 2: Typ B – Konfigurations-Produkt / Smart Order (Hersteller)**
Das bestehende 6-Tab-Formular (`livewire/products/create.blade.php`) mit 13 Sektionen:
Basis → Bilder → Physisch → Material & Herkunft → Kommerziell → Zuordnung & Verwaltung

Automatisch gesetzt: `product_type = 'smart_order'`, `partner_id` = Partner des Users.
Hub-Zuordnung: Manuell wählbar (Hersteller können in mehreren Hubs aktiv sein).
Alle Preistypen erlaubt; Standard-Varianten mit Festpreis möglich.

**Routing-Logik:** ✅ AKTUALISIERT (13.02.2026)
```
// Beide Rollen können BEIDE Produkttypen anlegen.
// Die Produktliste zeigt zwei Buttons:
//   "Neues Teaser-Produkt"    → Maske 1 (Typ A: Teaser, vereinfacht, Ticket zwingend)
//   "Neues Standard-Produkt"  → Maske 2 (Typ B: Konfiguration, komplex, Ticket optional)
// Sichtbar für: Retailer, Manufacturer, Admin, Super-Admin
// Zusätzlich: Produkttyp-Filter in der Produktliste (Teaser / Standard / Alle)
```

#### 2.1 Partner-Profil Erweiterung (Admin-Portal) – ✅ Kern implementiert
- [x] Partner-Profil Formular erweitern:
  - [x] Story-Text Editor (`<flux:textarea>`)
  - [x] Öffnungszeiten-Eingabe (strukturiertes JSON-Formular, 7 Wochentage)
  - [ ] Team-Fotos Upload (mehrere Bilder via `<flux:file-upload>`) – ausstehend
  - [ ] Showroom-Galerie Upload – ausstehend
- [ ] Form Request: `UpdatePartnerProfileRequest` – inline Validierung verwendet
- [x] Policy: `PartnerPolicy` (viewAny, view, update, delete, curateProducts)
- [x] Admin Partner-Übersicht: `admin/partners/index.blade.php` (Suche, Filter, Tabelle)

#### 2.2 Produkt-CRUD: Maske 1 – Teaser / Local Stock (Händler) – ✅ Implementiert
- [x] Volt-Komponente: `livewire/products/form-teaser.blade.php` (merged Create + Edit)
  - [x] Einzelseiten-Formular (KEIN Tab-Layout)
  - [x] Bild-Upload via `WithFileUploads` + Drag & Drop Bildsortierung
  - [x] Felder: Foto, Titel, Kurzbeschreibung, Preistyp (nur FromPrice/OnRequest), Preistext, Kategorie, Status, Produktnummer
  - [x] Produktnummern: Partner-Produktnummer (auto-generiert) + B2in-Artikelnummer (auto bei Save)
  - [x] Automatisch: `product_type = 'local_stock'`, `hub_id` vom Händler-Hub, `partner_id`
  - [x] Preistyp-Validierung via `ProductType::LocalStock->allowedPriceTypes()` (kein Festpreis)
  - [x] Freigabe-Workflow: UI "aktiv" → DB `pending` (korrekt zur Freigabe, nicht direkt Active)
- [x] Produkt-Liste: Zwei Buttons ("Neues Teaser-Produkt" + "Neues Standard-Produkt") für alle Partner-Rollen
- [x] Produkt-Liste: Produkttyp-Filter (Teaser / Standard / Alle)
- [x] Produkt-Liste: Vorschaubild (erstes Bild nach order_column) vor Produktname
- [x] Produkt bearbeiten: Pre-Fill, Status-Handling, Media-Verwaltung, Activity-Log, Toast-Notification
- [x] Bildsortierung: Drag & Drop, erstes Bild = Standardbild, order_column in DB
- [x] Produkt archivieren / als "Verkauft" markieren – in Edit-Formular und Produktliste (Dropdown-Menue)
- [ ] Form Request: `StoreTeaserProductRequest` – inline Validierung verwendet
- [x] Policy: `ProductPolicy` (viewAny, view, create, update, delete, curate)

#### 2.3 Produkt-CRUD: Maske 2 – Standard / Smart Order – ✅ Implementiert (13.02.2026)
- [x] Volt-Komponente: `livewire/products/form-standard.blade.php` (merged Create + Edit)
  - [x] 8-Tab-Layout: Basis, Bilder, Maße & Material, Verpackung & Versand, Kommerziell, Service & Garantie, Nachhaltigkeit, Zuordnung
  - [x] Erstellt Product + Master-Variante (ProductVariant) + ProductLogistics + Media
  - [x] Preis-Typ Logik: Festpreis / Ab-Preis / Preis auf Anfrage (alle 3 für SmartOrder erlaubt)
  - [x] Automatisch: `product_type = 'smart_order'`, Hub-Fallback auf Partner-Hub
  - [x] File-Uploads: Mehrere Bilder (max 10, JPG/PNG, max 10 MB) + Drag & Drop Bildsortierung
  - [x] Preise in EUR eingeben → automatische Umrechnung in Cents bei Speicherung
  - [x] Alle optionalen Felder: MPN, EAN/GTIN, UVP, EK, Lagerstatus, Lieferzeit, SEO-Metadaten, Maße, Verpackung
  - [x] Validierung inline (kein separater Form Request – konsistent mit Teaser-Formular)
  - [x] Authorization: ProductPolicy + Rollenprüfung (Retailer, Manufacturer, Admin, Super-Admin)
  - [x] Bei Edit: Toast-Notification, aktiver Tab bleibt erhalten, kein Redirect
- [x] Migration: `tax_rate_id` auf `product_variants` nullable gemacht (war NOT NULL ohne Default)

#### 2.4 Admin: Produkt-Kuration (Approval Queue) – ✅ Vollstaendig implementiert
- [x] Admin-View: `admin/products/index.blade.php` – Tabelle mit Filtern (Status, Typ, Haendler, Kategorie, Suche)
- [x] Statistik-Karten: Gesamt, Zur Freigabe, In Korrektur, Freigegeben (klickbar als Schnellfilter)
- [x] Approve Action: `is_curated=true`, `curated_at`, `curated_by` + Flux::toast()
- [x] Correction Action: Inline-Formular mit Pflicht-Textfeld → `curation_notes`
- [x] Reject Action: Inline-Formular mit Pflicht-Ablehnungsgrund → `curation_notes` + `status=Archived`
- [x] Autorisierung via `ProductPolicy::curate` (`curate products` Permission)
- [x] Admin kann alle Produkte bearbeiten (gleiche Edit-Formulare wie Haendler)
- [x] Admin kann Produkte archivieren / als verkauft markieren (Dropdown-Menue)
- [x] Erstellungsdatum in Tabelle sichtbar
- [x] Kuration-Hinweis beim Haendler: Callout im Edit-Formular (Korrektur gelb, Ablehnung rot)
- [ ] Notification an Händler/Hersteller bei Freigabe/Ablehnung – ausstehend
- [ ] Admin kann `product_type` bei Bedarf nachträglich ändern – ausstehend

#### 2.4a Nachbesserungen Phase 2 – ✅ Abgeschlossen (27.02.2026)

**Fix: `curate products` Permission fehlte in der Datenbank**
- Die Permission war im `RoleSeeder` definiert, aber nie in die Produktions-DB eingespielt
- `Attempt to read property "id" on null` beim Öffnen von `/admin/products`
- Lösung: Migration `2026_02_27_154145_add_curate_products_permission.php`
  - `Permission::firstOrCreate(['name' => 'curate products', 'guard_name' => 'web'])`
  - Admin-Rolle bekommt Permission via `Role::where('name', 'Admin')->first()` (kein `findByName` = würde Exception werfen wenn Rolle fehlt)
- Tests (`ProductCurationTest`, `ProductPolicyTest`, `PartnerPolicyTest`): `Permission::create()` → `Permission::firstOrCreate()` (verhindert Fehler wenn Migration die Permission bereits angelegt hat)

**Fix: Admin kann Produkte anlegen (Partner-Selector)**
- Admin hat keine eigene `partner_id` → `$user->partner` war `null` → Crash beim Speichern
- Lösung in `form-teaser.blade.php` und `form-standard.blade.php`:
  - Neues Property `$selectedPartnerId` + `resolvePartner()` Methode
  - Für Admins: Partner-Auswahl-Karte erscheint vor dem Formular (nur bei Neuanlage)
  - `updatedSelectedPartnerId()` aktualisiert Produktnummer automatisch nach Partnerwahl
  - Validierung: `selectedPartnerId` Pflichtfeld für Admins bei Neuanlage
- Alle bestehenden 158 Produkt-Tests weiterhin grün ✅

**Fix: Portal Textarea Dark-Mode Kontrast**
- Korrektur-Textfeld in der Kuration zeigte weißen Text auf weißem Hintergrund
- Ursache: `@apply dark:text-zinc-500` innerhalb von `::placeholder` Pseudo-Elementen kompiliert in Tailwind v4 zu ungültigem `:is():where(.dark,.dark *)` Selektor
- Lösung in `resources/css/portal.css`: Explizites CSS mit `:where(.dark,.dark *)` als Vorfahren-Selektor statt `@apply dark:`
- Portal-Build (`npm run build:portal`) aktualisiert

#### 2.5 Tests Phase 2 – ✅ Kern abgeschlossen
- [x] Feature-Tests: `PartnerPolicyTest` (10 Tests) – viewAny, view, update, curateProducts
- [x] Feature-Tests: `ProductPolicyTest` (14 Tests) – alle Policy-Methoden
- [x] Feature-Tests: `PartnerProfileUpdateTest` (8 Tests) – Profil-Update, Öffnungszeiten, Validierung
- [x] Feature-Tests: `TeaserProductCreateTest` (18 Tests) – Happy Path, Preistyp-Validierung, Autorisierung, Status-Logik, Produktnummern
- [x] Feature-Tests: `ProductCurationTest` (23 Tests) – Approve, Reject mit Pflicht-Textfeld, Correction, Archive/Sold, Filter, Kuration-Notes-Anzeige im Edit
- [x] Feature-Tests: `StandardProductCreateTest` (39 Tests) – Zugriff, Happy Path, alle Tabs, Validierung, Preistypen, Produktnummern, Marken
- [x] Feature-Tests: `ProductEditTest` (27 Tests) – Pre-Fill, Save, Status, Validierung, Media-Sortierung, Wood Origins, Activity, Archive/Sold
- [x] Feature-Tests: `TeaserProductEditTest` (30 Tests) – Pre-Fill, Save, Status, Media-Sortierung, Produktnummern
- [x] Tests: Beide Rollen (Händler + Hersteller) können BEIDE Produkttypen anlegen (Teaser + Standard)

**Tests Phase 2 gesamt: 109 Produkt-Tests + 48 Policy/Profil/Kuration-Tests, alle bestanden ✅**

---

### Phase 2.8: Partner-Präsentation & Selbst-Service-Profil – ✅ ABGESCHLOSSEN (27.02.2026)
**Geschätzter Aufwand:** 2-3 Tage | **Tatsächlich:** 1 Sitzung

#### 2.8.1 Händler-Profil (Retailer) – Präsentation
- [x] `my-data.blade.php` erweitert um:
  - [x] **Story-Text** – Freitext max. 2000 Zeichen mit Live-Zeichenzähler, Feld: `story_text`
  - [x] **Öffnungszeiten** – 7-Tage-Eingabe (Mo–So), `open`/`close`-Felder + "Geschlossen"-Checkbox, Feld: `opening_hours` (JSON)
  - [x] **Spezialisierungen** – kommagetrennte Eingabe → gespeichert als JSON-Array, Feld: `specialties`
  - [x] **Gründungsjahr** – Zahlfeld mit Validierung (1800–heute), Feld: `founded_year`
  - [x] **Team-Fotos** – Mehrfach-Upload via `WithFileUploads`, gespeichert als `Media.type = 'team_photo'`
  - [x] **Showroom-Galerie** – Mehrfach-Upload, `Media.type = 'showroom'`
  - [x] Foto-Löschen per Klick (mit `wire:confirm`)
- [x] Formular in 4 separate `flux:card`-Abschnitte gegliedert: Stammdaten, Präsentation & Story, Öffnungszeiten, Fotos

#### 2.8.2 Hersteller-Profil (Manufacturer) – Firma & Marke
- [x] `my-data.blade.php` erweitert um:
  - [x] **Story-Text** – Unternehmensgeschichte, Feld: `story_text`
  - [x] **Gründungsjahr** – Feld: `founded_year`
  - [x] **Spezialisierungen** – Feld: `specialties`
  - [x] **Marken-Bilder** – Mehrfach-Upload, `Media.type = 'brand_image'`
- [x] Abschnitte: Stammdaten, Über das Unternehmen, Marke (mit Markenname, Beschreibung, Bilder)

#### 2.8.3 Öffentliches Partnerprofil (`livewire/partner/profile.blade.php`)
- [x] Story-Text anzeigen wenn vorhanden (bereits vorhanden)
- [x] Öffnungszeiten anzeigen (bereits vorhanden)
- [x] Spezialisierungen + Gründungsjahr anzeigen (bereits vorhanden)
- [x] **Showroom-Galerie** – 3-spaltige Bild-Galerie unterhalb der Story
- [x] **Marken-Bilder** (Hersteller) – 3-spaltige Galerie unter der Story
- [x] **Team-Fotos** – 2-spaltige Galerie in der rechten Sidebar

#### 2.8.4 Datenbank
- [x] Alle Felder vorhanden: `story_text`, `opening_hours`, `specialties`, `founded_year` (Migration `2026_02_12_000003_add_profile_fields_to_partners_table`)
- [x] Media-System: Custom `Media` Model mit `type`-Feld (kein Spatie) – `Partner::media()` morph-Relation bereits vorhanden

#### 2.8.5 Tests Phase 2.8
- [x] `PartnerSelfServiceProfileTest` (15 Tests) – alle bestanden ✅
  - Händler kann Story, Öffnungszeiten, Spezialisierungen, Gründungsjahr speichern
  - Händler kann Team-Fotos und Showroom-Fotos hochladen
  - Händler kann Foto löschen
  - Hersteller kann Story, Markendaten, Gründungsjahr speichern
  - Hersteller kann Marken-Bilder hochladen
  - Öffentliches Profil zeigt Story und Spezialisierungen an
  - Validierung: Story-Text max. 2000 Zeichen, Gründungsjahr-Grenzen

**Erstellte/geänderte Dateien (2):**

| Datei | Änderungen |
|-------|-----------|
| `resources/views/livewire/partner/my-data.blade.php` | Komplett überarbeitet: 4 Karten-Abschnitte, neue Profil-Felder, Foto-Upload mit `WithFileUploads` |
| `resources/views/livewire/partner/profile.blade.php` | Showroom-Galerie, Marken-Bilder, Team-Fotos hinzugefügt; `media` eager-loaded |

---

### Phase 3: Kunden-Frontend & Local Feed – ✅ KERN ABGESCHLOSSEN (12.02.2026)
**Geschätzter Aufwand:** 4-5 Tage | **Tatsächlich:** 1 Tag (12.02.2026)
**Priorität:** HOCH – Kunden-Facing Funktionalität

#### 3.1 User-Origin Tracking – ✅ Implementiert
- [x] Bei Registrierung: `origin` automatisch aus Domain setzen
  - Request von `style2own.test` → `origin = 'style2own'`
  - Request von `stileigentum.test` → `origin = 'stileigentum'`
- [x] `CreateNewUser` Action (Fortify) erweitern um `origin` + `hub_id`
- [ ] Hub-Auswahl bei Registrierung (oder automatisch via Invite-Code des Maklers) – ausstehend

#### 3.2 Kunden-Dashboard ("Mein Zuhause") – ✅ Kern implementiert
- [x] `topOffers` im Dashboard: echte Produkte aus dem Hub des Kunden laden
  - `Product::query()->where('status', Active)->where('is_curated', true)->...->take(3)`
  - Fallback auf Dummy-Daten wenn noch keine Produkte vorhanden
- [ ] Eigene Dashboard-Seite für Customer-Rolle – ausstehend
- [ ] Theme-Steuerung basierend auf `origin` (`style2own` / `stileigentum`) – ausstehend

#### 3.3 Local Feed (Marktplatz-View) – ✅ Implementiert
- [x] `products/index` mit echter DB-Abfrage und Rollen-Filterung:
  - **Admin**: sieht alle Produkte
  - **Customer**: nur `active + is_curated + is_available` aus eigenem Hub
  - **Retailer/Partner**: nur eigene Produkte
- [x] Suchfilter nach Name, Kategorie-Filter, Paginierung (20 Einträge)
- [ ] Produkt-Detailseite – ausstehend
- [ ] Filteroptionen: Preisbereich, Verfügbarkeit – ausstehend

#### 3.4 Partner-Profilseite (öffentlich) – ✅ Implementiert
- [x] `partner/profile.blade.php`: Story, Öffnungszeiten, Produkte (max. 6), Spezialisierungen, Adresse
- [x] Route: `partner/{partnerId}/profile` → `partner.profile`
- [ ] Team-Fotos / Showroom-Galerie – ausstehend (abhängig von Media-Upload)

#### 3.5 Tests Phase 3 – ✅ Abgeschlossen
- [x] `CreateNewUserOriginTest` (6 Tests) – Origin-Tracking, hub_id
- [x] `LocalFeedTest` (5 Tests) – Hub-Filterung, Kuration, Rollen, Suche, Kategorie
- [x] `PartnerProfilePageTest` (6 Tests) – Profilseite, Story, Produkte, Auth

**Tests Phase 3 gesamt: 17 Tests, alle bestanden ✅**

---

### Phase 4: Ticket-System & QR-Generierung
**Geschätzter Aufwand:** 3-4 Tage
**Priorität:** HOCH – Kern der Monetarisierung

#### 4.1 Datenbank: Tickets
```
Migration: create_tickets_table
```
- [ ] `id`, `uuid` (public identifier)
- [ ] `user_id` (FK → users, Kunde)
- [ ] `merchant_partner_id` (FK → partners, Händler)
- [ ] `product_id` (nullable FK → products)
- [ ] `hub_id` (FK → hubs)
- [ ] `code` (unique string, z.B. "TK-2026-XXXXX")
- [ ] `qr_code_path` (nullable string, Pfad zur generierten QR-Datei)
- [ ] `status` (string: `active` | `redeemed` | `expired`)
- [ ] `discount_type` (string: `percentage` | `fixed`)
- [ ] `discount_value` (decimal)
- [ ] `valid_until` (datetime)
- [ ] `redeemed_at` (nullable datetime)
- [ ] `timestamps`

#### 4.2 Ticket Model & Enum
- [ ] `App\Models\Ticket` mit Relationships
- [ ] `App\Enums\TicketStatus` – `Active`, `Redeemed`, `Expired`
- [ ] `TicketFactory` erstellen
- [ ] `TicketPolicy` erstellen

#### 4.3 Ticket-Generierung
- [ ] Composer-Paket für QR-Code Generierung (`simplesoftwareio/simple-qrcode` oder ähnlich) – **Genehmigung einholen**
- [ ] Service: `App\Services\TicketService`
  - `generateTicket(User $user, Partner $merchant, ?Product $product): Ticket`
  - `generateQrCode(Ticket $ticket): string` (gibt Pfad zurück)
  - `validateTicket(string $code): ?Ticket`
  - `redeemTicket(Ticket $ticket): void`
- [ ] Livewire-Komponente: "Ticket ziehen" Button auf Produktdetailseite
- [ ] Mail/PDF-Generierung mit QR-Code + Händler-Info

#### 4.4 Händler: Ticket-Einlösung
- [ ] Händler-Dashboard: "Eingelöste Tickets" Übersicht
- [ ] QR-Code Scanner (Optional, Phase 5)
- [ ] Manuelle Code-Eingabe zum Einlösen

#### 4.5 Tests Phase 4
- [ ] Unit-Tests für TicketService
- [ ] Feature-Tests für Ticket-Generierung
- [ ] Feature-Tests für Ticket-Einlösung
- [ ] Tests für QR-Code Generierung

---

### Phase 5: Transaction-Engine & Clearing
**Geschätzter Aufwand:** 5-6 Tage
**Priorität:** MITTEL – Aufbauend auf Ticket-System

#### 5.1 Datenbank: Transaktionen
```
Migration: create_transactions_table
```
- [ ] `id`, `uuid`
- [ ] `ticket_id` (FK → tickets)
- [ ] `user_id` (FK → users, Kunde)
- [ ] `merchant_partner_id` (FK → partners, Händler)
- [ ] `broker_partner_id` (nullable FK → partners, Makler)
- [ ] `amount` (integer, in Cents)
- [ ] `receipt_image_path` (nullable string, hochgeladener Beleg)
- [ ] `receipt_amount` (nullable integer, vom Kunden angegebener Betrag in Cents)
- [ ] `status` (string: State Machine)
- [ ] `merchant_confirmed_at` (nullable datetime)
- [ ] `merchant_confirmed_amount` (nullable integer, in Cents)
- [ ] `invoice_generated_at` (nullable datetime)
- [ ] `paid_at` (nullable datetime)
- [ ] `distributed_at` (nullable datetime)
- [ ] `notes` (nullable text)
- [ ] `timestamps`

#### 5.2 Transaction State Machine
```
Enum: App\Enums\TransactionStatus
```
- [ ] `PendingReceipt` – Ticket eingelöst, Kunde muss Beleg hochladen
- [ ] `PendingMerchant` – Beleg hochgeladen, Händler muss bestätigen
- [ ] `Confirmed` – Händler hat bestätigt → Rechnung an Händler generieren
- [ ] `Invoiced` – Rechnung erstellt → Zahlung ausstehend
- [ ] `Paid` – Händler hat Provision an B2in überwiesen
- [ ] `Distributed` – Provisionen an Makler/Kunde ausgeschüttet
- [ ] `Rejected` – Händler hat abgelehnt
- [ ] `Disputed` – Streitfall

#### 5.3 Beleg-Upload (Kunden-View)
- [ ] Livewire-Komponente: Beleg-Upload Formular
  - Foto-Upload des Kaufbelegs
  - Betrag-Eingabe
  - Verknüpfung mit Ticket
- [ ] Validierung: Ticket muss `redeemed` sein
- [ ] Mail-Notification an Händler: "Neuer Beleg zur Bestätigung"

#### 5.4 Händler-Bestätigung
- [ ] Händler-Dashboard: "Offene Umsatz-Bestätigungen"
- [ ] Beleg anzeigen mit Bestätigungs-/Ablehnungs-Buttons
- [ ] Händler kann Betrag korrigieren (falls Kunde falschen Betrag eingegeben hat)
- [ ] Bei Bestätigung: Event `TransactionConfirmed` feuern

#### 5.5 Tests Phase 5
- [ ] Unit-Tests für State Machine Übergänge
- [ ] Feature-Tests für Beleg-Upload
- [ ] Feature-Tests für Händler-Bestätigung
- [ ] Tests für Events und Notifications

---

### Phase 6: Wallet, Cashback & Provisions-System
**Geschätzter Aufwand:** 4-5 Tage
**Priorität:** MITTEL – Monetarisierung

#### 6.1 Datenbank: Wallets & Ledger
```
Migration: create_wallets_table
```
- [ ] `id`
- [ ] `partner_id` (nullable FK → partners)
- [ ] `user_id` (nullable FK → users)
- [ ] `balance` (integer, in Cents)
- [ ] `type` (string: `cashback` | `commission` | `platform`)
- [ ] `timestamps`

```
Migration: create_ledger_entries_table
```
- [ ] `id`, `uuid`
- [ ] `wallet_id` (FK → wallets)
- [ ] `transaction_id` (nullable FK → transactions)
- [ ] `type` (string: `credit` | `debit`)
- [ ] `amount` (integer, in Cents, immer positiv)
- [ ] `balance_after` (integer, in Cents)
- [ ] `description` (string)
- [ ] `timestamps`

#### 6.2 Provisions-Berechnung
- [ ] Service: `App\Services\CommissionService`
  - `calculateSplit(Transaction $transaction): CommissionSplit`
  - Berechnet: Makler-Anteil, Kunden-Cashback, B2in-Marge
  - Nutzt `partner.provision_rate_percentage` und `partner.provision_fixed_amount`
- [ ] Event Listener für `TransactionPaid`:
  - Erstellt Ledger-Einträge
  - Aktualisiert Wallet-Balances
  - Setzt Transaction-Status auf `distributed`

#### 6.3 Wallet-Views
- [ ] Kunden-Dashboard: "Mein Cashback" (Guthaben, Historie)
- [ ] Makler-Dashboard: "Meine Provisionen" (Guthaben, Historie)
- [ ] Admin: Wallet-Übersicht aller Partner und Kunden

#### 6.4 Tests Phase 6
- [ ] Unit-Tests für CommissionService (Splits korrekt berechnet)
- [ ] Feature-Tests für Wallet-Operationen
- [ ] Feature-Tests für Event-basierte Distribution
- [ ] Edge Cases: Rundung, Null-Beträge, fehlende Provisionsregeln

---

### Phase 7: Frontend-Polish & Domains
**Geschätzter Aufwand:** 3-4 Tage
**Priorität:** MITTEL

#### 7.1 Style2Own vs. StilEigentum Feinschliff
- [ ] CSS-Variablen-Sets für beide Brands finalisieren
- [ ] Wording-Datenbank: "Du" vs. "Sie" Ansprache in allen Texten
- [ ] Landingpage-Anpassungen je Brand

#### 7.2 "Local for Local" Branding
- [ ] Hub-spezifisches Branding (z.B. "Local for Local OWL")
- [ ] Hub-Landingpages mit regionalen Inhalten
- [ ] Domain-Routing für `localforlocal.de` (wenn verfügbar)

#### 7.3 Responsive & Mobile
- [ ] Händler-Upload Formular mobil-optimiert ("Handy-optimiert" laut Konzept)
- [ ] Ticket-Anzeige mobil-optimiert (QR-Code gut scanbar)
- [ ] Kunden-Dashboard responsive

#### 7.4 Tests Phase 7
- [ ] Browser-Tests (Laravel Dusk) für Multi-Domain
- [ ] Responsive Tests

---

## 3. Beantwortete Fragen & Entscheidungen

### Architektur-Entscheidungen

**Frage 1: Kunden-Partner-Beziehung** ✅
> **Entscheidung:** Beibehalten. Jeder Kunde behält seinen Partner-Eintrag. Zusätzlich `hub_id` und `origin` direkt auf User für schnelle Queries.

**Frage 2: Hub-Zuordnung bei Produkten** ✅
> **Entscheidung:** Direktes `hub_id` Feld auf Products. Hersteller können Produkte in mehreren Hubs anbieten.

**Frage 3: QR-Code Paket** ✅
> **Entscheidung:** `chillerlan/php-qrcode`

**Frage 4: PDF-Generierung für Tickets** ✅
> **Entscheidung:** `spatie/laravel-pdf`

**Frage 5: Makler-Invite und Hub-Zuweisung** ✅
> **Entscheidung:** Automatisch vom Makler übernehmen. Admin kann manuell ändern.

**Frage 6: Domain Local for Local** ✅
> **Entscheidung:** `local4local.online` (Produktion) / `local4local.test` (Entwicklung). Muss in `config/domains.php` ergänzt werden.

**Frage 11: Produkttypen – Eine oder zwei Tabellen?** ✅
> **Entscheidung:** EINE `products` Tabelle, ZWEI Eingabemasken. Das Feld `product_type` (`local_stock` | `smart_order`) steuert welche Maske und Validierung greift. Siehe Phase 2.0 für Details.
> - Maske 1 (Teaser/Local Stock): Vereinfacht, handy-optimiert, für Händler
> - Maske 2 (Konfiguration/Smart Order): Komplex mit 6 Tabs, für Hersteller

### Business-Logic Entscheidungen

**Frage 7: Provisions-Split** ✅
> **Entscheidung:** Individuell pro Partner. Admin-Settings-Seite mit Feldern für:
> - Makler-Provision (%)
> - Kunden-Cashback (%)
> - B2in-Marge (Rest)
> Felder werden pro Partner im Admin-Backend konfiguriert.

**Frage 8: Ticket-Gültigkeit** ✅
> **Entscheidung:** Default 30 Tage. Konfigurierbar über Admin-Settings-Seite.

**Frage 9: Beleg-Upload Deadline** ✅
> **Entscheidung:** Default 30 Tage. Konfigurierbar über Admin-Settings-Seite.

**Frage 10: Ticket-Begrenzung pro Kunde** ✅
> **Entscheidung:** Produkte sind immer an einen Händler gebunden. Ein Kunde kann Tickets bei verschiedenen Händlern für ähnliche Produkte ziehen. Begrenzung über Admin-Settings:
> - Max. Tickets pro Händler (z.B. 3)
> - Max. Händler pro Zeitraum (z.B. 4)
> - Konfigurierbar durch Admin

### Benötigte Admin-Settings-Seite (Neue Anforderung)
Aus den Antworten ergibt sich die Notwendigkeit einer zentralen **Admin-Settings-Seite** für:
- Ticket-Gültigkeit (Tage)
- Beleg-Upload Deadline (Tage)
- Max. Tickets pro Händler pro Kunde
- Max. Händler pro Kunde pro Zeitraum
- Standard-Provisions-Split (Default-Werte für neue Partner)

> **Umsetzung:** `settings` Tabelle (key-value) + Admin-Settings-View in Phase 4/5. Alternativ: `config/marketplace.php` mit `.env`-Variablen für nicht-dynamische Werte.
---

## 4. Technische Richtlinien für die Entwicklung

### Architektur-Prinzipien
- **Eloquent-First:** Keine `DB::` Facades, immer `Model::query()`
- **Form Requests:** Jede Validierung in eigene Request-Klassen
- **Policies:** Jede Autorisierung über Laravel Policies
- **Events/Listeners:** Für alle Seiteneffekte (Mail, Wallet-Buchung, Notifications)
- **Services:** Business-Logik in Service-Klassen (`TicketService`, `CommissionService`)
- **Enums:** Alle Status-Werte als PHP 8.1+ Enums

### Namenskonventionen
- Models: Singular, PascalCase (`Ticket`, `LedgerEntry`)
- Tabellen: Plural, snake_case (`tickets`, `ledger_entries`)
- Enums: PascalCase Keys (`LocalStock`, `SmartOrder`)
- Services: `{Domain}Service` (`TicketService`, `CommissionService`)
- Policies: `{Model}Policy` (`TicketPolicy`, `TransactionPolicy`)
- Form Requests: `{Action}{Model}Request` (`StoreProductRequest`, `UpdatePartnerProfileRequest`)
- Events: Past Tense (`TransactionConfirmed`, `TicketRedeemed`)

### Testing-Strategie
- Jede Phase muss vollständig getestet sein bevor die nächste beginnt
- Feature-Tests für alle Endpoints und Livewire-Komponenten
- Unit-Tests für Services und komplexe Business-Logik
- Pest als Test-Framework, Factories für Testdaten
- Mindestens: Happy Path + Validation + Authorization pro Feature

### Reihenfolge der Entwicklung (innerhalb jeder Phase)
1. Migration(s) erstellen und ausführen
2. Model(s) mit Relationships, Casts, Scopes
3. Enum(s) falls nötig
4. Factory und Seeder
5. Form Request(s)
6. Policy
7. Service-Klasse (falls komplexe Logik)
8. Livewire/Volt Komponente
9. Tests schreiben und ausführen
10. `pint --dirty` für Code-Formatierung

---

## 5. Abhängigkeiten zwischen Phasen

```
Phase 1 (Core) ──────────┬──────────> Phase 2 (Produkte)
                         │                    │
                         │                    ▼
                         └──────────> Phase 3 (Frontend) ──> Phase 7 (Polish)
                                              │
                                              ▼
                                     Phase 4 (Tickets)
                                              │
                                              ▼
                                     Phase 5 (Transactions)
                                              │
                                              ▼
                                     Phase 6 (Wallet/Cashback)
```

- **Phase 1** ist Voraussetzung für alles
- **Phase 2 und 3** können teilweise parallel laufen
- **Phase 4** setzt Phase 3 voraus (Produktdetailseite muss existieren)
- **Phase 5** setzt Phase 4 voraus (Tickets müssen existieren)
- **Phase 6** setzt Phase 5 voraus (Transaktionen müssen existieren)
- **Phase 7** kann teilweise parallel zu Phase 4-6 laufen

---

## 6. Neue Permissions (zu ergänzen)

Folgende Permissions müssen im RoleSeeder ergänzt werden:

| Permission | Beschreibung | Rollen |
|-----------|-------------|--------|
| `curate products` | Produkte freigeben/ablehnen | Admin, Super-Admin |
| `view tickets` | Eigene Tickets sehen | Customer |
| `create tickets` | Ticket generieren | Customer |
| `redeem tickets` | Ticket einlösen | Retailer |
| `view transactions` | Transaktionen sehen | Customer, Retailer, Estate-Agent, Admin |
| `confirm transactions` | Umsatz bestätigen | Retailer |
| `upload receipts` | Beleg hochladen | Customer |
| `view wallet` | Wallet-Guthaben sehen | Customer, Estate-Agent, Retailer |
| `manage wallets` | Wallets verwalten | Admin |
| `view commissions` | Provisionen einsehen | Estate-Agent, Admin |
| `manage setup packages` | Setup-Pakete verwalten | Admin |
| `book setup package` | Setup-Paket buchen | Retailer |

---

*Dieses Dokument dient als lebende Referenz für die Entwicklung. Bei Änderungen am Konzept muss dieser Plan entsprechend aktualisiert werden.*
ochladen | Customer |
| `view wallet` | Wallet-Guthaben sehen | Customer, Estate-Agent, Retailer |
| `manage wallets` | Wallets verwalten | Admin |
| `view commissions` | Provisionen einsehen | Estate-Agent, Admin |
| `manage setup packages` | Setup-Pakete verwalten | Admin |
| `book setup package` | Setup-Paket buchen | Retailer |

---

*Dieses Dokument dient als lebende Referenz für die Entwicklung. Bei Änderungen am Konzept muss dieser Plan entsprechend aktualisiert werden.*
