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
+52 -14
View File
@@ -192,34 +192,72 @@ include 'nav.php';
Aktive Teilnehmer: <?php echo (int)$billingCheck['active_participants']; ?>
</p>
<?php if (isset($_GET['upgrade']) && $_GET['upgrade'] === 'success'): ?>
<div class="hint-box success"><p>Danke! Die Zahlung wird verarbeitet, der Tarif ist gleich aktuell.</p></div>
<div class="hint-box success"><p>Danke! Die Änderung wird verarbeitet, der Tarif ist gleich aktuell.</p></div>
<?php elseif (isset($_GET['upgrade']) && $_GET['upgrade'] === 'cancelled'): ?>
<div class="hint-box error"><p>Der Bezahlvorgang wurde abgebrochen, es wurde nichts geändert.</p></div>
<?php endif; ?>
<?php if ($billingCheck['needs_upgrade']): ?>
<div class="hint-box error">
<p>Mit <?php echo (int)$billingCheck['active_participants']; ?> aktiven Teilnehmern benötigt ihr den
Tarif „<?php echo saas_html($billingPlans[$billingCheck['required_plan']]['label'] ?? $billingCheck['required_plan']); ?>".
Details zu den Stufen stehen unter <a href="preise.php">Preise</a>.</p>
<?php if (($billingPlans[$billingCheck['required_plan']]['stripe_lookup_key'] ?? null) !== null): ?>
<form method="post" action="abo-upgrade.php">
<?php echo app_csrf_field(); ?>
<input type="hidden" name="plan_code" value="<?php echo saas_html($billingCheck['required_plan']); ?>">
<button type="submit">Jetzt auf „<?php echo saas_html($billingPlans[$billingCheck['required_plan']]['label']); ?>" upgraden</button>
</form>
<?php else: ?>
<p>Für diese Teilnehmerzahl <a href="mailto:info@ctb-it.de">meldet euch bitte bei uns</a>.</p>
<?php endif; ?>
<p>Mit <?php echo (int)$billingCheck['active_participants']; ?> aktiven Teilnehmern benötigt ihr mindestens den
Tarif „<?php echo saas_html($billingPlans[$billingCheck['required_plan']]['label'] ?? $billingCheck['required_plan']); ?>".</p>
</div>
<?php else: ?>
<div class="hint-box success"><p>Euer aktueller Tarif deckt eure Teilnehmerzahl ab.</p></div>
<?php endif; ?>
<table>
<tr>
<th>Tarif</th>
<th>Max. aktive Teilnehmer</th>
<th>Preis/Monat</th>
<th>Status</th>
<th></th>
</tr>
<?php foreach ($billingPlans as $planCode => $plan): ?>
<?php
$isCurrent = $planCode === $billingCheck['current_plan'];
$selectable = billing_plan_selectable_for($pdo, (int)$user['tenant_id'], $planCode);
$isSelfService = $plan['stripe_lookup_key'] !== null || $planCode === 'free';
?>
<tr<?php echo $isCurrent ? ' class="hint-box success"' : ''; ?>>
<td><?php echo saas_html($plan['label']); ?></td>
<td><?php echo $plan['max_participants'] !== null ? (int)$plan['max_participants'] : 'unbegrenzt'; ?></td>
<td><?php echo $plan['price_cents'] > 0 ? saas_html(saas_format_money_cents($plan['price_cents'])) . ' €' : 'kostenlos'; ?></td>
<td>
<?php if ($isCurrent): ?>
aktuell gebucht
<?php elseif (!$selectable): ?>
deckt eure <?php echo (int)$billingCheck['active_participants']; ?> Teilnehmer nicht ab
<?php endif; ?>
</td>
<td>
<?php if ($isCurrent): ?>
<?php elseif (!$isSelfService): ?>
<a href="mailto:info@ctb-it.de" class="button small">Anfragen</a>
<?php elseif (!$selectable): ?>
<button type="button" class="button small" disabled>Nicht wählbar</button>
<?php else: ?>
<form method="post" action="abo-upgrade.php" onsubmit="return confirm('Tarif wirklich auf ' + <?php echo json_encode($plan['label']); ?> + ' ändern?');">
<?php echo app_csrf_field(); ?>
<input type="hidden" name="plan_code" value="<?php echo saas_html($planCode); ?>">
<button type="submit" class="button small primary">
<?php echo $planCode === 'free' ? 'Kündigen / auf Kostenlos wechseln' : 'Buchen'; ?>
</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</table>
<p><small>Ein Wechsel zwischen bezahlten Tarifen wird bei Stripe anteilig verrechnet (Proration) und
gilt sofort. Details zu den Tarifgrenzen stehen unter <a href="preise.php">Preise</a>.</small></p>
<?php if (($billing['stripe_customer_id'] ?? null) !== null): ?>
<form method="post" action="abo-portal.php">
<?php echo app_csrf_field(); ?>
<button type="submit">Zahlungsmethode verwalten / Abo kündigen</button>
<button type="submit">Zahlungsmethode verwalten</button>
</form>
<?php endif; ?>