LEG App Logo
LEG App

BSP White-Label Portal

Das BSP (Billing Service Provider) Portal ermoeglicht es Abrechnungsdienstleistern wie TE Consulting, die Plattform unter eigenem Branding zu betreiben. Jeder BSP erhaelt ein eigenes Portal mit Dashboard, Community-Verwaltung, Interessenten-Uebersicht und Vertrags-Management.

Architektur

User (Login) └── InterestedPerson (Vertragspartner) ├── GroupMembership → CommunityGroup → BillingServiceProvider ├── Contract → CommunityGroup → BillingServiceProvider └── billing_service_provider_interested_person (Pivot, M:N)

Kernprinzip: Der InterestedPerson ist der Community-Teilnehmer und Vertragspartner. Der User ist lediglich das Login-Konto. Ein InterestedPerson kann mehreren BSPs zugeordnet sein (Multi-BSP-Faehigkeit).

Tenant-Scoping

Das BSP-Scoping laeuft ueber zwei Wege:

  1. Direkt: billing_service_provider_interested_person Pivot-Tabelle (schnelle Queries)
  2. Indirekt: CommunityGroup → billing_service_provider_id (Quelle der Wahrheit)

Der BspTenantSyncService synchronisiert die Pivot-Tabelle automatisch bei Aenderungen an GroupMemberships und Contracts (via Observer).

Einrichtung

1. Testdaten erstellen

php artisan db:seed --class=BspTestDataSeeder

Dies erstellt:

  • BSP "TE Consulting" mit Branding
  • BSP-Staff User bsp@leg-app.ch (Rolle: user_bsp)
  • Test-Community mit 4 Mitgliedern (3 Personen, 1 Firma)
  • Tenant-Sync

2. BSP manuell erstellen

use App\Models\BillingServiceProvider; use App\Models\User; // BSP erstellen $bsp = BillingServiceProvider::create([ 'name' => 'Mein BSP', 'slug' => 'mein-bsp', // Eindeutig, URL-freundlich 'app_name' => 'Energie Community', // Angezeigter Name im Portal 'custom_domain' => 'portal.mein-bsp.ch', // Optional: eigene Domain 'email' => 'info@mein-bsp.ch', 'is_active' => true, 'primary_color' => '#1a365d', 'secondary_color' => '#2d3748', 'accent_color' => '#38a169', ]); // BSP-Staff User erstellen $user = User::create([ 'name' => 'BSP Admin', 'email' => 'admin@mein-bsp.ch', 'password' => Hash::make('sicheres-passwort'), 'role' => 'user_bsp', 'billing_service_provider_id' => $bsp->id, 'email_verified_at' => now(), ]);

3. Community zuordnen

use App\Models\CommunityGroup; use App\Services\BspTenantSyncService; // Community dem BSP zuordnen CommunityGroup::where('id', $communityId) ->update(['billing_service_provider_id' => $bsp->id]); // Tenant-Sync: Interessenten dem BSP zuordnen app(BspTenantSyncService::class)->syncForCommunityGroup($communityId); // Oder: Alle InterestedPersons synchronisieren app(BspTenantSyncService::class)->syncAll();

4. Custom Domain einrichten (Produktion)

  1. DNS: CNAME-Eintrag portal.mein-bsp.ch → App-Server
  2. Server: Domain in Nginx/Forge konfigurieren
  3. SSL: Let's Encrypt Zertifikat
  4. Datenbank: custom_domain auf dem BSP setzen:
$bsp->update(['custom_domain' => 'portal.mein-bsp.ch']);

Portal-Zugang

| Zugang | URL | |--------|-----| | Ueber Haupt-App | /bsp/ (als user_bsp eingeloggt) | | Ueber Custom Domain | https://portal.mein-bsp.ch/bsp/ |

Portal-Seiten

| Route | Komponente | Beschreibung | |-------|-----------|--------------| | /bsp/ | Bsp\Dashboard | KPIs: Communities, Interessenten, Vertraege, PV-Kapazitaet | | /bsp/communities | Bsp\CommunitiesOverview | Communities des BSPs mit Suche und Statistiken | | /bsp/interested-people | Bsp\InterestedPeopleOverview | Interessenten mit Filter, Suche, CSV-Export | | /bsp/contracts | Bsp\ContractsOverview | Vertraege mit Status-Filter und Suche | | /bsp/theme | Bsp\ThemeEditor | Logo, Farben, Schriften, App-Name anpassen |

Technische Details

Rollen

| Rolle | Beschreibung | |-------|-------------| | user_bsp | BSP-Staff User mit Zugang zum BSP Portal | | admin | Hat ebenfalls Zugang zu allen BSP Portalen |

Middleware-Stack

auth → verified → role:user_bsp,admin → resolve-bsp

Die ResolveBspFromDomain Middleware loest den BSP auf:

  1. Custom Domain Match (billing_service_providers.custom_domain)
  2. Fallback: BSP des eingeloggten user_bsp Users

BspContextService

use App\Services\BspContextService; $ctx = app(BspContextService::class); // Aktueller BSP $bsp = $ctx->current(); // Scoped Queries $communities = CommunityGroup::query()->tap(fn ($q) => $ctx->scopeCommunities($q))->get(); $people = InterestedPerson::query()->tap(fn ($q) => $ctx->scopeInterestedPeople($q))->get(); $contracts = Contract::query()->tap(fn ($q) => $ctx->scopeContracts($q))->get();

Query Scopes

Auf den Models stehen scopeForBsp() Scopes zur Verfuegung:

CommunityGroup::forBsp($bsp)->get(); InterestedPerson::forBsp($bsp)->get(); Contract::forBsp($bsp)->get();

Branding

Das BSP-Branding wird ueber CSS Custom Properties im Layout angewendet:

:root { --bsp-primary: #1a365d; --bsp-secondary: #2d3748; --bsp-accent: #38a169; }

Fuer PDFs steht $bsp->branding als Array zur Verfuegung:

$branding = $bsp->branding; // ['name', 'primary_color', 'secondary_color', 'accent_color', 'font_heading', 'font_body', 'logo_base64', 'website']

Fuer E-Mails gibt es ein eigenes Layout:

// In einem Mailable: return $this->view('emails.bsp-layout', [ 'branding' => $bsp->branding, 'footerText' => $bsp->footer_text, ]);

Dateien

| Datei | Beschreibung | |-------|-------------| | app/Http/Middleware/ResolveBspFromDomain.php | Domain/User-basierte BSP-Aufloesung | | app/Services/BspContextService.php | Query-Scoping und BSP-Kontext | | app/Services/BspTenantSyncService.php | Pivot-Synchronisierung | | app/Livewire/Bsp/Dashboard.php | Dashboard mit KPIs | | app/Livewire/Bsp/CommunitiesOverview.php | Community-Uebersicht | | app/Livewire/Bsp/InterestedPeopleOverview.php | Interessenten-Verwaltung | | app/Livewire/Bsp/ContractsOverview.php | Vertrags-Uebersicht | | app/Livewire/Bsp/ThemeEditor.php | Theme-Anpassung | | resources/views/components/layouts/bsp/app.blade.php | Gebrandetes Portal-Layout | | resources/views/emails/bsp-layout.blade.php | Gebrandetes E-Mail-Layout | | database/seeders/BspTestDataSeeder.php | Testdaten-Seeder |