Zum Hauptinhalt springen

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.

- Das Tenant-Feature **`hasCourseMarketplace`** ist für den Mandanten aktiv (sonst sind die Marktplatz-Kacheln ausgeblendet und die Seite leitet auf die Startseite um). - Berechtigung `view:FE_Course` und `view:Course`; zum Beziehen `create:Course`, zum Lösen `delete:Course`, für Preisänderungen `update:Course`.

Drei Rollen

RolleWerBedeutung
Anbieter (provider)hat eigene Kurse mit marketplaceEnabled = truebietet 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 bereiterhä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)WirkungEndpoint
BeziehenKurs 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 gesperrtDELETE /course-marketplace/courses/:id/unclaim
Wieder beziehennach Soft-Unclaim erneut aktivierenPOST …/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 = true setzen 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 eine CoursePriceChangeRequest erzeugt (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 = true und einen eigenen marketplacePrice. 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.
Jeder Mandant sieht im Marktplatz **nur** den Katalog seines direkten Anbieters — nicht den globalen Speam-Katalog. Ein Sub-Mandant unter einem Primärmandanten sieht also ausschließlich dessen (re-angebotene) Kurse.

Produktdaten-Synchronisation

Jeder Marktplatz-Kurs ist mit einem Produkt verknüpft (Course.productIdProduct 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 (CASL update:Course), Antwort { pulled: <Anzahl> }.
  • Optionen: skipFields (einzelne Produktfelder nicht überschreiben) und skipChildTypes (z. B. ProductSupplier, um eigene Lieferantenkonditionen zu schützen).
  • Wechselt der Anbieter die Produktzuordnung (productId), räumt ein afterUpdate-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:

TabRolleKennzahl
Meine EinnahmenAnbieter / PrimärmandantNetto (eigene Kurse) + Marge (als Primärmandant)
Meine AusgabenBezieherBrutto 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:

ActionSubjectWirkung
viewFE_Course, CourseMarktplatz + Umsätze aufrufbar
createCourseKurs beziehen (claim)
deleteCourseBezug lösen (unclaim)
updateCoursePreisänderung ankündigen

API/Schnittstellen

MethodeEndpointZweckCASL
GET/api/course-marketplace/coursesKatalog angebotener Kurseview Course
POST/api/course-marketplace/courses/:id/claimKurs beziehencreate Course
DELETE/api/course-marketplace/courses/:id/unclaimBezug lösen (soft)delete Course
POST/api/course-marketplace/courses/:id/sync-productProduktdaten frisch vom Anbieter ziehenupdate Course
POST/api/course-marketplace/courses/:id/announce-price-changePreisänderung ankündigenupdate Course
GET/api/course-marketplace/usage-reportUmsatz-Report (Rolle/Jahr/Monat)view Course
GET/api/course-marketplace/participationRolle(n) des Mandantenview 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 an course.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 per hasCourseMarketplace. Verifiziert an courseMarketplace.router.ts, CourseMarketplacePage.tsx, CourseMarketplaceRevenuePage.tsx, course.model.ts, coursePriceChangeRequest.model.ts, routes.tsx.