- 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>
64 lines
2 KiB
PHP
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;
|
|
});
|
|
}
|
|
}
|