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:
@@ -220,6 +220,72 @@ function stripe_verify_webhook_signature(string $payload, string $signatureHeade
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a subscription including its first item's price, so the caller
|
||||
* can see the currently billed price and swap it out in place.
|
||||
*
|
||||
* @return array{ok: bool, id: ?string, item_id: ?string, price_lookup_key: ?string, status: ?string, cancel_at_period_end: bool, error: ?string}
|
||||
*/
|
||||
function stripe_get_subscription(string $subscriptionId): array
|
||||
{
|
||||
$result = stripe_request('GET', "subscriptions/{$subscriptionId}");
|
||||
if (!$result['ok']) {
|
||||
return ['ok' => false, 'id' => null, 'item_id' => null, 'price_lookup_key' => null, 'status' => null, 'cancel_at_period_end' => false, 'error' => $result['error']];
|
||||
}
|
||||
|
||||
$item = $result['data']['items']['data'][0] ?? null;
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'id' => (string)($result['data']['id'] ?? ''),
|
||||
'item_id' => $item !== null ? (string)($item['id'] ?? '') : null,
|
||||
'price_lookup_key' => $item !== null ? (string)($item['price']['lookup_key'] ?? '') : null,
|
||||
'status' => (string)($result['data']['status'] ?? ''),
|
||||
'cancel_at_period_end' => (bool)($result['data']['cancel_at_period_end'] ?? false),
|
||||
'error' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches an existing active subscription to a different Price in place
|
||||
* (upgrade or downgrade between paid tiers), instead of creating a brand
|
||||
* new Checkout Session. Stripe prorates the difference automatically.
|
||||
* Also clears any pending cancel_at_period_end, since choosing a plan is
|
||||
* an explicit signal the tenant wants to keep the subscription running.
|
||||
*
|
||||
* @return array{ok: bool, error: ?string}
|
||||
*/
|
||||
function stripe_update_subscription_price(string $subscriptionId, string $subscriptionItemId, string $newPriceId): array
|
||||
{
|
||||
$result = stripe_request('POST', "subscriptions/{$subscriptionId}", [
|
||||
'items' => [['id' => $subscriptionItemId, 'price' => $newPriceId]],
|
||||
'proration_behavior' => 'create_prorations',
|
||||
'cancel_at_period_end' => 'false',
|
||||
]);
|
||||
|
||||
if (!$result['ok']) {
|
||||
return ['ok' => false, 'error' => $result['error']];
|
||||
}
|
||||
|
||||
return ['ok' => true, 'error' => null];
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a subscription immediately (not at period end) - used when a
|
||||
* tenant downgrades all the way to the free plan from the admin UI.
|
||||
*
|
||||
* @return array{ok: bool, error: ?string}
|
||||
*/
|
||||
function stripe_cancel_subscription(string $subscriptionId): array
|
||||
{
|
||||
$result = stripe_request('DELETE', "subscriptions/{$subscriptionId}");
|
||||
if (!$result['ok']) {
|
||||
return ['ok' => false, 'error' => $result['error']];
|
||||
}
|
||||
|
||||
return ['ok' => true, 'error' => null];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{id: string, type: string, data: array}|null
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user