presseportale/tests/Feature/CreditTopupTest.php
Kevin Adametz 344aac0740 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>
2026-06-17 14:56:23 +00:00

105 lines
4 KiB
PHP

<?php
use App\Enums\SinglePurchaseStatus;
use App\Listeners\ProcessStripeWebhook;
use App\Models\BillingAddress;
use App\Models\User;
use App\Services\Billing\CreditTopupService;
use App\Services\Billing\CreditWalletService;
use App\Services\Billing\StripeCheckoutService;
use Database\Seeders\RolesAndPermissionsSeeder;
use Illuminate\Http\RedirectResponse;
use Laravel\Cashier\Checkout;
use Laravel\Cashier\Events\WebhookReceived;
beforeEach(function (): void {
$this->seed(RolesAndPermissionsSeeder::class);
$this->topups = app(CreditTopupService::class);
$this->wallet = app(CreditWalletService::class);
});
test('the bonus pack staffel is configured as agreed', function () {
expect($this->topups->packs())->toBe([
['key' => 'p10', 'price_cents' => 1000, 'credits' => 10],
['key' => 'p25', 'price_cents' => 2500, 'credits' => 27],
['key' => 'p50', 'price_cents' => 5000, 'credits' => 55],
['key' => 'p100', 'price_cents' => 10000, 'credits' => 115],
]);
});
test('starting a topup creates a pending record without crediting yet', function () {
$user = User::factory()->create();
$topup = $this->topups->startTopup($user, 'p25');
expect($topup->status)->toBe(SinglePurchaseStatus::Pending);
expect($topup->credits)->toBe(27);
expect($topup->price_cents)->toBe(2500);
expect($this->wallet->balance($user))->toBe(0);
});
test('an unknown pack is rejected', function () {
$user = User::factory()->create();
expect(fn () => $this->topups->startTopup($user, 'nope'))
->toThrow(InvalidArgumentException::class);
});
test('fulfilling a topup credits the wallet exactly once', function () {
$user = User::factory()->create();
$topup = $this->topups->startTopup($user, 'p50');
$this->topups->fulfill($topup, 'cs_test_1', 'pi_test_1');
$this->topups->fulfill($topup->fresh(), 'cs_test_1', 'pi_test_1'); // Replay
expect($this->wallet->balance($user))->toBe(55);
expect($topup->fresh()->status)->toBe(SinglePurchaseStatus::Paid);
expect($topup->fresh()->credited_at)->not->toBeNull();
expect($user->creditTransactions()->count())->toBe(1);
});
test('the checkout webhook credits the wallet from session metadata', function () {
$user = User::factory()->create();
$topup = $this->topups->startTopup($user, 'p100');
$event = new WebhookReceived([
'type' => 'checkout.session.completed',
'data' => ['object' => [
'id' => 'cs_test_hook',
'payment_intent' => 'pi_test_hook',
'metadata' => ['credit_topup_id' => (string) $topup->id],
]],
]);
app(ProcessStripeWebhook::class)->handle($event);
expect($this->wallet->balance($user))->toBe(115);
expect($topup->fresh()->credited_at)->not->toBeNull();
});
test('the topup checkout route redirects to the profile without a billing address', function () {
$user = User::factory()->create(['is_active' => true, 'email_verified_at' => now()]);
$user->assignRole('customer');
$this->actingAs($user)
->get(route('me.checkout.credit-topup', ['pack' => 'p25']))
->assertRedirect(route('me.profile'));
});
test('the topup checkout route starts a stripe checkout with a complete billing address', function () {
$user = User::factory()->create(['is_active' => true, 'email_verified_at' => now()]);
$user->assignRole('customer');
BillingAddress::factory()->create(['user_id' => $user->id]);
$checkout = Mockery::mock(Checkout::class);
$checkout->shouldReceive('toResponse')->andReturn(new RedirectResponse('https://checkout.stripe.com/c/pay/test'));
$mock = Mockery::mock(StripeCheckoutService::class);
$mock->shouldReceive('forCreditTopup')->once()->andReturn($checkout);
app()->instance(StripeCheckoutService::class, $mock);
$this->actingAs($user)
->get(route('me.checkout.credit-topup', ['pack' => 'p25']))
->assertRedirect('https://checkout.stripe.com/c/pay/test');
expect($user->creditTopups()->where('pack_key', 'p25')->exists())->toBeTrue();
});