Kurs-Marktplatz (Course-Marketplace)
Zweck
Der Kurs-Marktplatz (/course-marketplace) verbindet alle SpeamCore-Mandanten: Jeder Mandant kann eigene Kurse anbieten und sie anderen Mandanten zur Verfügung stellen — und umgekehrt fertige Kurse anderer Mandanten beziehen, statt sie selbst zu produzieren. Die Abrechnung der Nutzung läuft zentral; Anbieter erhalten ihren Anteil, der bereitstellende Primärmandant seine Marge.
Drei Rollen
| Rolle | Wer | Bedeutung |
|---|---|---|
| Anbieter (provider) | hat eigene Kurse mit marketplaceEnabled = true | bietet Kurse an, erhält den Netto-Anteil. |
| Bezieher (consumer) | hat fremde Kurse bezogen (masterId ≠ null) | nutzt fremde Kurse, zahlt den Bruttopreis. |
| Primärmandant (primary) | stellt für Bezieher bereit | erhält die Marge zwischen Netto und Brutto. |
Über GET /course-marketplace/participation ermittelt SpeamCore die aktuelle(n) Rolle(n) des Mandanten.
Die Marktplatz-Seite
/course-marketplace zeigt den Katalog der von anderen Mandanten angebotenen Kurse (Karten mit Vorschaubild, Anbieter, Beschreibung, Bruttopreis; Suche nach Kurs/Anbieter). Pro Karte:
| Aktion (UI) | Wirkung | Endpoint |
|---|---|---|
| Beziehen | Kurs lokal übernehmen (kompletter Kurs-Baum wird heruntergeladen, masterId gesetzt) | POST /course-marketplace/courses/:id/claim |
| Nicht mehr beziehen | „Soft-Unclaim": bestehende Anmeldungen laufen weiter, neue sind gesperrt | DELETE /course-marketplace/courses/:id/unclaim |
| Wieder beziehen | nach Soft-Unclaim erneut aktivieren | POST …/claim |
Badges: Eingestellt (Anbieter hat den Kurs entfernt) und Bezug gelöst (nach Unclaim).
<KIHinweis titel="Bezogene Kurse sind „read-only" — bis auf vier Felder">
Ein bezogener Kurs (masterId ≠ null) wird vom Anbieter gepflegt — Inhalt (Titel, Module, Inhalte, SCORM) kommt automatisch per Sync und ist lokal gesperrt (Fehlermeldung beim Speichern: „mirrored from the marketplace — drop the marketplace claim instead"). Frei bleiben nur die tenant-lokalen Felder productId, marketplaceEnabled, marketplacePrice und marketplaceUnclaimedAt — genau diese überschreibt der Anbieter-Sync nicht (siehe Weiterverkaufen als Re-Provider). Beim Soft-Unclaim bleibt der Kurs lokal erhalten, damit laufende Anmeldungen nicht abreißen; nur neue Anmeldungen werden blockiert.
Anbieten und Preis
- Anbieten: Am eigenen Kurs
marketplaceEnabled = truesetzen und einen Marktplatz-Preis (marketplacePrice) hinterlegen. Der komplette Kurs-Baum (Module, Inhalte, Prüfungen, Zertifikate, SCORM, Bilder) wird in den Marktplatz synchronisiert. - Preis ändern: Über Preisänderung an Konsumenten schicken (
POST …/announce-price-change) wird je beziehendem Mandanten eineCoursePriceChangeRequesterzeugt (pending). Der Bezieher akzeptiert (neuer Preis gilt) oder lehnt ab (der Kurs wird für ihn auf „eingestellt" gesetzt). So ändert sich kein Preis ohne Zustimmung.
Weiterverkaufen als Re-Provider
Ein bezogener Kurs (Mirror, masterId ≠ null) kann selbst wieder im Marktplatz angeboten werden — der Bezieher wird damit zum Re-Provider (Bezieher und Anbieter zugleich). Das ist die Grundlage der mehrstufigen Verteilungskette (Anbieter → Plattform → Mandant → Sub-Mandant → Endkunde).
So funktioniert es:
- Am Mirror-Kurs setzt der Re-Provider
marketplaceEnabled = trueund einen eigenenmarketplacePrice. Diese vier tenant-lokalen Felder (productId,marketplaceEnabled,marketplacePrice,marketplaceUnclaimedAt) sind die einzigen, die er an einem Mirror-Kurs ändern darf. - Beim Anbieter-Sync (Pull vom Master) werden genau diese Felder ausgespart (
marketplacePrice,marketplaceEnabled) — der Anbieter überschreibt die Re-Provider-Position also nicht. - Der Re-Provider-Push enthält nur die Marktplatz-Position (Enabled, Preis, Produktzuordnung) — kein Kurs-Inhalt. Module, Inhalte und SCORM bleiben am ursprünglichen Master gepflegt und für alle Ebenen identisch.
- Flippt die übergeordnete Ebene den Kurs auf „nicht mehr angeboten", erhält der Mirror automatisch einen Soft-Unclaim (
marketplaceUnclaimedAt): bestehende Anmeldungen laufen weiter, neue werden gesperrt. Ein nächtlicher Aufräum-Worker (täglich 00:30 Uhr) löscht endgültig erst, wenn keine aktive Anmeldung mehr hängt.
Produktdaten-Synchronisation
Jeder Marktplatz-Kurs ist mit einem Produkt verknüpft (Course.productId → Product inkl. ProductComponent/ProductSupplier). Über den Button Produktdaten aktualisieren zieht der Bezieher die verknüpften Produktdaten frisch vom Anbieter:
- Endpoint:
POST /course-marketplace/courses/:id/sync-product(CASLupdate:Course), Antwort{ pulled: <Anzahl> }. - Optionen:
skipFields(einzelne Produktfelder nicht überschreiben) undskipChildTypes(z. B.ProductSupplier, um eigene Lieferantenkonditionen zu schützen). - Wechselt der Anbieter die Produktzuordnung (
productId), räumt einafterUpdate-Hook das alte Produkt samt Komponenten/Lieferanten/Attributen auf, sofern es kein anderer bezogener Kurs mehr nutzt.
Umsatz-Übersicht (/course-marketplace/revenue)
Die Revenue-Seite zeigt je nach Rolle:
| Tab | Rolle | Kennzahl |
|---|---|---|
| Meine Einnahmen | Anbieter / Primärmandant | Netto (eigene Kurse) + Marge (als Primärmandant) |
| Meine Ausgaben | Bezieher | Brutto je Anmeldung |
Pro Zeile: Kurs · Partner · Typ (Kursanmeldung oder SCORM-Start) · Anzahl · Betrag. Filter nach Jahr/Monat.
Abgerechnet wird pro Kursanmeldung und pro SCORM-Start (Letzterer einmal je Monat/Nutzer/Kurs). Anbieter sehen ihren Netto-Anteil, Primärmandanten ihre Marge, Bezieher den Brutto-Betrag — die genaue Margen-/Provisionsberechnung erfolgt zentral (Master), der Mandant sieht die fertigen Beträge.
Mandantenübergreifend (masterId)
Jeder geteilte Kurs hat im Master-System eine ID; beim Beziehen erhält die lokale Kopie (und alle Kind-Objekte: Module, Inhalte, Prüfungen, Zertifikate, SCORM) diese als masterId. Daran erkennt SpeamCore, dass ein Kurs fremd-gepflegt ist (read-only) und über welchen Master-Kurs Updates/Abrechnung laufen. Anbieter-eigene Kurse haben masterId = null.
Freischaltung pro Mandant
Der Marktplatz wird über das Tenant-Feature hasCourseMarketplace freigeschaltet (Master-Status, abgefragt via GET /tenant-features). Ist es aus, blendet die Navigation die Kacheln „Kurs-Marktplatz" und „Marktplatz-Umsätze" aus und die Seiten leiten auf die Startseite um.
Berechtigungen (CASL)
Der Marktplatz nutzt kein eigenes CASL-Subject — er läuft über das bestehende Course-Subject:
| Action | Subject | Wirkung |
|---|---|---|
view | FE_Course, Course | Marktplatz + Umsätze aufrufbar |
create | Course | Kurs beziehen (claim) |
delete | Course | Bezug lösen (unclaim) |
update | Course | Preisänderung ankündigen |
API/Schnittstellen
| Methode | Endpoint | Zweck | CASL |
|---|---|---|---|
GET | /api/course-marketplace/courses | Katalog angebotener Kurse | view Course |
POST | /api/course-marketplace/courses/:id/claim | Kurs beziehen | create Course |
DELETE | /api/course-marketplace/courses/:id/unclaim | Bezug lösen (soft) | delete Course |
POST | /api/course-marketplace/courses/:id/sync-product | Produktdaten frisch vom Anbieter ziehen | update Course |
POST | /api/course-marketplace/courses/:id/announce-price-change | Preisänderung ankündigen | update Course |
GET | /api/course-marketplace/usage-report | Umsatz-Report (Rolle/Jahr/Monat) | view Course |
GET | /api/course-marketplace/participation | Rolle(n) des Mandanten | view Course |
Verknüpfungen zu anderen Modulen
- Kurse — die Kurse, die angeboten/bezogen werden.
- Kurs-Anmeldungen — abrechnungsrelevante Anmeldungen; bei Soft-Unclaim laufen bestehende weiter.
- SCORM-Kurse — SCORM-Starts sind ebenfalls abrechnungsrelevant.
Versionshinweise
- 2026-06-11: Re-Provider-Mechanik (bezogene Mirror-Kurse selbst wieder anbieten) und Produktdaten-Synchronisation (
POST …/sync-product) dokumentiert; tenant-lokale Felder (productId,marketplaceEnabled,marketplacePrice,marketplaceUnclaimedAt) und der Anbieter-Sync-Schutz (SELECTIVE_UPDATE_SKIP_FIELDS) präzisiert; Auto-Soft-Unclaim + nächtlicher Cleanup-Worker ergänzt. Verifiziert ancourse.model.ts(TENANT_LOCAL_COURSE_FIELDS, after-Hooks),adminSync.service.ts,courseMarketplaceSync.service.ts,courseMarketplace.controller.ts(syncProduct). - 2026-06-09: Initiale Veröffentlichung. Mandantenübergreifender Kurs-Marktplatz: Anbieten (
marketplaceEnabled/marketplacePrice), Beziehen/Soft-Unclaim/Wieder-Beziehen, Preisänderung mit Zustimmung (CoursePriceChangeRequest), Umsatz-Übersicht (Netto/Marge/Brutto, Kursanmeldung + SCORM-Start),masterId-Sync, Freischaltung perhasCourseMarketplace. Verifiziert ancourseMarketplace.router.ts,CourseMarketplacePage.tsx,CourseMarketplaceRevenuePage.tsx,course.model.ts,coursePriceChangeRequest.model.ts,routes.tsx.