Wallet-Topup ueber Stripe (Credit-Pakete mit Bonus-Staffel)

End-to-end-Aufladung der Credit-Wallet, spiegelt den Einzel-PM-Fluss:

- Paket-Staffel mit Bonus in config/credits.php (10->10, 25->27, 50->55,
  100->115 Credits; price_cents netto, Stripe Tax ergaenzt USt.)
- credit_topups (Pending->Paid) + CreditTopupService: startTopup legt den
  Pending-Kauf an, fulfill() schreibt die Wallet idempotent gut (credited_at)
- StripeCheckoutService::forCreditTopup via checkoutCharge + credit_topup_id
  Metadata; ProcessStripeWebhook schreibt bei checkout.session.completed gut
- Checkout-Route /admin/me/checkout/credits/{pack} mit Rechnungsadress-Gate

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-17 14:56:23 +00:00
parent c89589beb0
commit bd7022ffaf
10 changed files with 382 additions and 1 deletions

View file

@ -0,0 +1,44 @@
<?php
namespace App\Models;
use App\Enums\SinglePurchaseStatus;
use App\Services\Billing\CreditTopupService;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Eine Wallet-Aufladung über Stripe. Anlage/Erfüllung über
* {@see CreditTopupService}.
*/
class CreditTopup extends Model
{
protected $fillable = [
'user_id',
'pack_key',
'credits',
'price_cents',
'currency',
'status',
'stripe_checkout_session_id',
'stripe_payment_intent_id',
'paid_at',
'credited_at',
];
protected function casts(): array
{
return [
'credits' => 'integer',
'price_cents' => 'integer',
'status' => SinglePurchaseStatus::class,
'paid_at' => 'datetime',
'credited_at' => 'datetime',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View file

@ -316,6 +316,11 @@ class User extends Authenticatable implements MustVerifyEmail
return $this->hasMany(ReviewCheck::class);
}
public function creditTopups(): HasMany
{
return $this->hasMany(CreditTopup::class);
}
/**
* Aktuelles Credit-Guthaben (1 Credit = 1 €). 0, solange keine Wallet
* angelegt wurde.