Admin-Zahlungsmodul: Zahlungs-Übersicht + Tarif-Verwaltung mit Stripe-Sync
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8f3261d0b4
commit
bda755fcf8
9 changed files with 1109 additions and 23 deletions
121
app/Services/Billing/StripePlanSyncService.php
Normal file
121
app/Services/Billing/StripePlanSyncService.php
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Billing;
|
||||
|
||||
use App\Models\Plan;
|
||||
use Laravel\Cashier\Cashier;
|
||||
use Stripe\StripeClient;
|
||||
|
||||
/**
|
||||
* Spiegelt Tarif-Änderungen aus der Admin-Verwaltung sofort nach Stripe.
|
||||
*
|
||||
* Stripe-Preise sind unveränderlich: Eine Preisänderung legt deshalb einen
|
||||
* neuen Price an (netto, `tax_behavior: exclusive` wie beim Sync-Command),
|
||||
* deaktiviert den alten für neue Buchungen und schreibt die neue ID in
|
||||
* `plans` zurück. Bestandsabos behalten bewusst ihren bisherigen Preis —
|
||||
* Stripe rechnet laufende Subscriptions auf dem alten Price-Objekt weiter.
|
||||
*/
|
||||
class StripePlanSyncService
|
||||
{
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return (bool) config('cashier.secret');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gleicht einen lokal gespeicherten Tarif mit Stripe ab. `$changes`
|
||||
* sind die tatsächlich geänderten Attribute (`Model::getChanges()`),
|
||||
* damit nur die nötigen Stripe-Aufrufe passieren.
|
||||
*
|
||||
* @param array<string, mixed> $changes
|
||||
*/
|
||||
public function syncAfterUpdate(Plan $plan, array $changes): void
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stripe = Cashier::stripe();
|
||||
|
||||
if (! $plan->stripe_product_id) {
|
||||
$this->createProductWithPrices($stripe, $plan);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (array_key_exists('name', $changes)) {
|
||||
$stripe->products->update($plan->stripe_product_id, ['name' => $plan->name]);
|
||||
}
|
||||
|
||||
if (array_key_exists('monthly_price_cents', $changes)) {
|
||||
$plan->forceFill([
|
||||
'stripe_price_id_monthly' => $this->rotatePrice(
|
||||
$stripe,
|
||||
$plan,
|
||||
$plan->stripe_price_id_monthly,
|
||||
$plan->monthly_price_cents,
|
||||
'month',
|
||||
),
|
||||
])->save();
|
||||
}
|
||||
|
||||
if (array_key_exists('yearly_price_cents', $changes)) {
|
||||
$plan->forceFill([
|
||||
'stripe_price_id_yearly' => $this->rotatePrice(
|
||||
$stripe,
|
||||
$plan,
|
||||
$plan->stripe_price_id_yearly,
|
||||
$plan->yearly_price_cents,
|
||||
'year',
|
||||
),
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstanlage für Tarife ohne Stripe-Verknüpfung — gleiche Struktur wie
|
||||
* `billing:sync-stripe-plans`, nur direkt aus der Admin-Oberfläche.
|
||||
*/
|
||||
private function createProductWithPrices(StripeClient $stripe, Plan $plan): void
|
||||
{
|
||||
$product = $stripe->products->create([
|
||||
'name' => $plan->name,
|
||||
'metadata' => ['plan_slug' => $plan->slug],
|
||||
]);
|
||||
|
||||
$plan->forceFill([
|
||||
'stripe_product_id' => $product->id,
|
||||
'stripe_price_id_monthly' => $this->createPrice($stripe, $product->id, $plan, $plan->monthly_price_cents, 'month'),
|
||||
'stripe_price_id_yearly' => $this->createPrice($stripe, $product->id, $plan, $plan->yearly_price_cents, 'year'),
|
||||
])->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Neuen Preis anlegen und den bisherigen (falls vorhanden) für neue
|
||||
* Buchungen deaktivieren. Gibt die neue Price-ID zurück.
|
||||
*/
|
||||
private function rotatePrice(StripeClient $stripe, Plan $plan, ?string $oldPriceId, int $unitAmount, string $interval): string
|
||||
{
|
||||
$newPriceId = $this->createPrice($stripe, (string) $plan->stripe_product_id, $plan, $unitAmount, $interval);
|
||||
|
||||
if ($oldPriceId) {
|
||||
$stripe->prices->update($oldPriceId, ['active' => false]);
|
||||
}
|
||||
|
||||
return $newPriceId;
|
||||
}
|
||||
|
||||
private function createPrice(StripeClient $stripe, string $productId, Plan $plan, int $unitAmount, string $interval): string
|
||||
{
|
||||
$price = $stripe->prices->create([
|
||||
'product' => $productId,
|
||||
'currency' => strtolower($plan->currency),
|
||||
'unit_amount' => $unitAmount,
|
||||
'tax_behavior' => 'exclusive',
|
||||
'recurring' => ['interval' => $interval],
|
||||
'metadata' => ['plan_slug' => $plan->slug],
|
||||
]);
|
||||
|
||||
return $price->id;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue