Selbstbedienungs-Tarifverwaltung: Admin kann Abo im Backend waehlen und bestellen

- mandant-einstellungen.php: vollstaendige Tarif-Vergleichstabelle statt nur
  erzwungenem Upgrade-Hinweis. Jeder Tarif zeigt Limit, Preis und einen
  Aktions-Button (Buchen/Kuendigen/Anfragen); aktueller Tarif markiert,
  zu kleine Tarife fuer die aktuelle Teilnehmerzahl deaktiviert.
- abo-upgrade.php: unterscheidet jetzt Upgrade, Downgrade zwischen bezahlten
  Stufen (Preis wird in-place gewechselt statt neuer Checkout-Session,
  Stripe prorated automatisch) und Wechsel auf 'free' (echte Kuendigung der
  bestehenden Subscription), statt nur eine Checkout-Session zu erzeugen.
- app/stripe.php: neue Helper stripe_get_subscription(),
  stripe_update_subscription_price(), stripe_cancel_subscription().
- app/billing.php: billing_plan_selectable_for() prueft serverseitig, ob
  ein Zieltarif die aktuelle Teilnehmerzahl noch abdeckt (Downgrade-Schutz).
- docs/billing.md: Phase 4 dokumentiert.

Getestet: php -l fuer alle geaenderten Dateien und das gesamte Repo (keine
Syntaxfehler), reine Funktionslogik-Tests ohne DB (billing_plans(),
billing_required_plan_code(), stripe_flatten_params()) gruen. Kein Live-Test
gegen echte Stripe-Testobjekte moeglich (keine PHP/DB-Laufzeitumgebung
verfuegbar) - vor Go-Live im Stripe-Testmodus nachholen.
This commit is contained in:
2026-07-20 20:19:05 +00:00
parent ef5d0e1822
commit dd2277c803
5 changed files with 283 additions and 17 deletions
+22
View File
@@ -221,3 +221,25 @@ function billing_check_tenant(PDO $pdo, int $tenantId): array
'needs_upgrade' => $requiredRank !== false && $currentRank !== false && $requiredRank > $currentRank,
];
}
/**
* Whether a tenant could self-service switch to $planCode given its current
* active participant count - i.e. the plan's limit (if any) still covers
* them. Used to grey out/reject plan selections that would immediately put
* the tenant over their own member count (a downgrade or lateral move that
* doesn't fit). Unknown plan codes are treated as not selectable.
*/
function billing_plan_selectable_for(PDO $pdo, int $tenantId, string $planCode): bool
{
$plans = billing_plans();
if (!isset($plans[$planCode])) {
return false;
}
$max = $plans[$planCode]['max_participants'];
if ($max === null) {
return true;
}
return billing_count_active_participants($pdo, $tenantId) <= $max;
}