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:
- Direkt:
billing_service_provider_interested_personPivot-Tabelle (schnelle Queries) - 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)
- DNS: CNAME-Eintrag
portal.mein-bsp.ch→ App-Server - Server: Domain in Nginx/Forge konfigurieren
- SSL: Let's Encrypt Zertifikat
- Datenbank:
custom_domainauf 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:
- Custom Domain Match (
billing_service_providers.custom_domain) - Fallback: BSP des eingeloggten
user_bspUsers
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 |