presseportale/app/Services/Billing/ExtraPmPurchaseService.php
Kevin Adametz 4466858aac Optimierung 3.5: Kleinere Zentralisierungen (VAT, findByEmail, Waehrung, Provisioning)
- VatResolver::normalizeCountryCode()/isEuCountry() als Single Source of Truth;
  resolve(), VatIdValidationService und StripeCheckoutService nutzen jetzt die
  zentrale EU-Pruefung statt eigener config-Lookups.
- User::findByEmail() buendelt die LOWER(email)-Lookups (ContactAccessService,
  SocialAuthService). Bleibt bewusst case-insensitiv, da E-Mails nicht
  normalisiert gespeichert werden; der indexfreundliche Direktvergleich
  braucht erst eine Schreib-Normalisierung (offen, dokumentiert).
- config('billing.default_currency') ersetzt 8 verstreute 'EUR'/'eur'-Literale
  (Stripe-Calls via strtolower). Vereinheitlicht nebenbei die zuvor uneinheitlich
  gespeicherte SinglePurchase.currency.
- Neuer CustomerAccountProvisioner::provisionVerifiedCustomer() ist die
  gemeinsame Provisioning-Methode (verifiziert + aktiv + customer-Rolle) fuer
  ContactAccessService und SocialAuthService.
- Tests: UserFindByEmailTest, erweiterte VatResolverTest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:14:05 +00:00

64 lines
2 KiB
PHP

<?php
namespace App\Services\Billing;
use App\Enums\SinglePurchaseStatus;
use App\Enums\SinglePurchaseType;
use App\Models\SinglePurchase;
use App\Models\User;
use Illuminate\Support\Facades\DB;
/**
* Extra-PM-Nachkauf laut Decision-Update §2.1: die Brücke zwischen
* Tarif-Kontingent und Credit-Wallet. Ist das Monatskontingent voll, kann eine
* einzelne weitere PM über die Wallet nachgekauft werden — Preis tier-gestaffelt
* und zur Kaufzeit aus dem aktiven Abo abgeleitet (nicht statisch gespeichert).
*
* Der Kauf wird als bezahlter SinglePurchase (Typ ExtraPm) verbucht und greift
* damit in die bestehende Kontingent-/Slot-Mechanik
* ({@see User::pressReleaseQuotaRemaining()},
* PressReleaseService::consumePublishSlot()).
*/
class ExtraPmPurchaseService
{
public function __construct(
private readonly CreditPricingService $pricing,
private readonly CreditWalletService $wallet,
) {}
/**
* Tier-abgeleiteter Extra-PM-Preis für den User (Credits).
*/
public function priceCredits(User $user): int
{
return $this->pricing->extraPmCreditsFor($user);
}
/**
* Bucht eine Extra-PM aus der Wallet. Wirft InsufficientCreditsException,
* wenn das Guthaben nicht reicht (Signal für den Mini-Checkout).
*/
public function purchase(User $user): SinglePurchase
{
$credits = $this->priceCredits($user);
return DB::transaction(function () use ($user, $credits): SinglePurchase {
$purchase = $user->singlePurchases()->create([
'type' => SinglePurchaseType::ExtraPm,
'status' => SinglePurchaseStatus::Paid,
'price_cents' => $credits * 100,
'currency' => config('billing.default_currency'),
'paid_at' => now(),
]);
$this->wallet->debit(
$user,
$credits,
'Extra-PM ('.$user->currentTier()->label().')',
$purchase,
);
return $purchase;
});
}
}