presseportale/app/Services/Billing/StripeCheckoutService.php
2026-06-12 14:36:18 +00:00

120 lines
4.1 KiB
PHP

<?php
namespace App\Services\Billing;
use App\Models\Plan;
use App\Models\SinglePurchase;
use App\Models\User;
use Illuminate\Support\Facades\Log;
use Laravel\Cashier\Checkout;
/**
* Dünner Wrapper um die Cashier-Checkout-Erzeugung (Phase 9E).
*
* Hier passiert ausschließlich der Stripe-Aufruf — alle Guards (Tarif
* synchronisiert, kein Doppel-Abo, Preis konfiguriert) liegen im
* CheckoutController. So bleibt der Controller ohne Stripe-Anbindung
* testbar, indem dieser Service im Container gemockt wird.
*/
class StripeCheckoutService
{
/**
* Stripe-Checkout für ein Tarif-Abo (monatlich/jährlich). Die Steuer
* ergänzt Stripe Tax automatisch (Cashier::calculateTaxes, Netto-Preise).
*/
public function forSubscription(User $user, Plan $plan, string $interval): Checkout
{
$priceId = $interval === 'yearly'
? $plan->stripe_price_id_yearly
: $plan->stripe_price_id_monthly;
$this->syncTaxIdFromBillingAddress($user);
return $user
->newSubscription('default', $priceId)
->checkout($this->sessionOptions());
}
/**
* Lokal gepflegte USt-ID vor dem Checkout an den Stripe-Customer
* übergeben (User-Panel-Restarbeiten, 12.06.2026) — Stripe Tax
* berücksichtigt sie dann ohne erneute Eingabe im Checkout. Fehler
* (z. B. von Stripe abgelehnte ID) blockieren den Checkout nicht:
* Stripe validiert die im Checkout erfasste ID ohnehin selbst.
*/
private function syncTaxIdFromBillingAddress(User $user): void
{
$vatId = strtoupper((string) preg_replace('/\s+/', '', (string) $user->billingAddress?->vat_id));
if ($vatId === '') {
return;
}
$prefixCountry = substr($vatId, 0, 2) === 'EL' ? 'GR' : substr($vatId, 0, 2);
if ($prefixCountry !== 'DE' && ! in_array($prefixCountry, (array) config('billing.eu_country_codes', []), true)) {
return;
}
try {
$user->createOrGetStripeCustomer();
$alreadySet = collect($user->taxIds())
->contains(fn (object $taxId): bool => strtoupper((string) $taxId->value) === $vatId);
if (! $alreadySet) {
$user->createTaxId('eu_vat', $vatId);
}
} catch (\Throwable $exception) {
Log::warning('USt-ID konnte nicht an Stripe übergeben werden.', [
'user_id' => $user->id,
'error' => $exception->getMessage(),
]);
}
}
/**
* Gemeinsame Session-Optionen: Stripe Tax braucht eine gültige
* Kundenadresse — die im Checkout erfasste Rechnungsadresse (und der
* Name, Pflicht bei USt-ID-Abfrage) wird darum am Stripe-Customer
* gespeichert (`customer_update: auto`).
*
* @return array<string, mixed>
*/
private function sessionOptions(): array
{
return [
'success_url' => route('me.bookings.index', ['checkout' => 'erfolg']),
'cancel_url' => route('me.bookings.index', ['checkout' => 'abbruch']),
'billing_address_collection' => 'required',
'customer_update' => [
'address' => 'auto',
'name' => 'auto',
],
];
}
/**
* URL zum Stripe Billing Portal (Zahlungsmethode, Rechnungen, Kündigung).
* Rücksprung auf die Buchungs-Seite.
*/
public function billingPortalUrl(User $user): string
{
return $user->billingPortalUrl(route('me.bookings.index'));
}
/**
* Stripe-Checkout für eine Einzel-PM. Die `single_purchase_id` in den
* Session-Metadaten schließt den Kreis: `checkout.session.completed`
* markiert den Kauf über ProcessStripeWebhook als bezahlt.
*/
public function forSinglePurchase(User $user, SinglePurchase $purchase): Checkout
{
$this->syncTaxIdFromBillingAddress($user);
return $user->checkout([config('billing.single_pm_stripe_price_id') => 1], [
...$this->sessionOptions(),
'metadata' => ['single_purchase_id' => (string) $purchase->id],
]);
}
}