Changeset View
Changeset View
Standalone View
Standalone View
src/tests/Feature/Controller/PaymentsStripeTest.php
<?php | <?php | ||||
namespace Tests\Feature\Controller; | namespace Tests\Feature\Controller; | ||||
use App\Http\Controllers\API\V4\PaymentsController; | use App\Http\Controllers\API\V4\PaymentsController; | ||||
use App\Payment; | use App\Payment; | ||||
use App\Providers\PaymentProvider; | use App\Providers\PaymentProvider; | ||||
use App\Transaction; | use App\Transaction; | ||||
use App\Wallet; | use App\Wallet; | ||||
use App\WalletSetting; | use App\WalletSetting; | ||||
use App\VatRate; | |||||
use GuzzleHttp\Psr7\Response; | use GuzzleHttp\Psr7\Response; | ||||
use Illuminate\Support\Facades\Bus; | use Illuminate\Support\Facades\Bus; | ||||
use Tests\TestCase; | use Tests\TestCase; | ||||
use Tests\StripeMocksTrait; | use Tests\StripeMocksTrait; | ||||
class PaymentsStripeTest extends TestCase | class PaymentsStripeTest extends TestCase | ||||
{ | { | ||||
use StripeMocksTrait; | use StripeMocksTrait; | ||||
/** | /** | ||||
* {@inheritDoc} | * {@inheritDoc} | ||||
*/ | */ | ||||
public function setUp(): void | public function setUp(): void | ||||
{ | { | ||||
parent::setUp(); | parent::setUp(); | ||||
// All tests in this file use Stripe | // All tests in this file use Stripe | ||||
\config(['services.payment_provider' => 'stripe']); | \config(['services.payment_provider' => 'stripe']); | ||||
\config(['app.vat.mode' => 0]); | |||||
$this->deleteTestUser('payment-test@' . \config('app.domain')); | |||||
$john = $this->getTestUser('john@kolab.org'); | $john = $this->getTestUser('john@kolab.org'); | ||||
$wallet = $john->wallets()->first(); | $wallet = $john->wallets()->first(); | ||||
Payment::where('wallet_id', $wallet->id)->delete(); | |||||
Wallet::where('id', $wallet->id)->update(['balance' => 0]); | Wallet::where('id', $wallet->id)->update(['balance' => 0]); | ||||
WalletSetting::where('wallet_id', $wallet->id)->delete(); | WalletSetting::where('wallet_id', $wallet->id)->delete(); | ||||
Transaction::where('object_id', $wallet->id) | Transaction::where('object_id', $wallet->id) | ||||
->where('type', Transaction::WALLET_CREDIT)->delete(); | ->where('type', Transaction::WALLET_CREDIT)->delete(); | ||||
Payment::query()->delete(); | |||||
VatRate::query()->delete(); | |||||
} | } | ||||
/** | /** | ||||
* {@inheritDoc} | * {@inheritDoc} | ||||
*/ | */ | ||||
public function tearDown(): void | public function tearDown(): void | ||||
{ | { | ||||
$this->deleteTestUser('payment-test@' . \config('app.domain')); | |||||
$john = $this->getTestUser('john@kolab.org'); | $john = $this->getTestUser('john@kolab.org'); | ||||
$wallet = $john->wallets()->first(); | $wallet = $john->wallets()->first(); | ||||
Payment::where('wallet_id', $wallet->id)->delete(); | |||||
Wallet::where('id', $wallet->id)->update(['balance' => 0]); | Wallet::where('id', $wallet->id)->update(['balance' => 0]); | ||||
WalletSetting::where('wallet_id', $wallet->id)->delete(); | WalletSetting::where('wallet_id', $wallet->id)->delete(); | ||||
Transaction::where('object_id', $wallet->id) | Transaction::where('object_id', $wallet->id) | ||||
->where('type', Transaction::WALLET_CREDIT)->delete(); | ->where('type', Transaction::WALLET_CREDIT)->delete(); | ||||
Payment::query()->delete(); | |||||
VatRate::query()->delete(); | |||||
parent::tearDown(); | parent::tearDown(); | ||||
} | } | ||||
/** | /** | ||||
* Test creating/updating/deleting an outo-payment mandate | * Test creating/updating/deleting an outo-payment mandate | ||||
* | * | ||||
* @group stripe | * @group stripe | ||||
▲ Show 20 Lines • Show All 635 Lines • ▼ Show 20 Lines | public function testTopUpAndWebhook(): void | ||||
$this->assertEquals(2010, $wallet->fresh()->balance); | $this->assertEquals(2010, $wallet->fresh()->balance); | ||||
// Assert that email notification job wasn't dispatched, | // Assert that email notification job wasn't dispatched, | ||||
// it is expected only for recurring payments | // it is expected only for recurring payments | ||||
Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0); | Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0); | ||||
} | } | ||||
/** | /** | ||||
* Generate Stripe-Signature header for a webhook payload | * Test payment/top-up with VAT_MODE=1 | ||||
* | |||||
* @group stripe | |||||
*/ | */ | ||||
protected function webhookRequest($post) | public function testPaymentsWithVatModeOne(): void | ||||
{ | { | ||||
$secret = \config('services.stripe.webhook_secret'); | \config(['app.vat.mode' => 1]); | ||||
$ts = time(); | |||||
$payload = "$ts." . json_encode($post); | $user = $this->getTestUser('payment-test@' . \config('app.domain')); | ||||
$sig = sprintf('t=%d,v1=%s', $ts, \hash_hmac('sha256', $payload, $secret)); | $user->setSetting('country', 'US'); | ||||
$wallet = $user->wallets()->first(); | |||||
$vatRate = VatRate::create([ | |||||
'country' => 'US', | |||||
'rate' => 5.0, | |||||
'start' => now()->subDay(), | |||||
]); | |||||
return $this->withHeaders(['Stripe-Signature' => $sig]) | // Payment | ||||
->json('POST', "api/webhooks/payment/stripe", $post); | $post = ['amount' => '10', 'currency' => 'CHF', 'methodId' => 'creditcard']; | ||||
$response = $this->actingAs($user)->post("api/v4/payments", $post); | |||||
$response->assertStatus(200); | |||||
// Check that the payments table contains a new record with proper amount(s) | |||||
$payment = $wallet->payments()->first(); | |||||
$this->assertSame(1000 + intval(round(1000 * $vatRate->rate / 100)), $payment->amount); | |||||
$this->assertSame(1000, $payment->credit_amount); | |||||
$this->assertSame($payment->amount, $payment->currency_amount); | |||||
$this->assertSame('CHF', $payment->currency); | |||||
$this->assertSame($vatRate->id, $payment->vat_rate_id); | |||||
$this->assertSame('open', $payment->status); | |||||
$wallet->payments()->delete(); | |||||
$wallet->balance = -1000; | |||||
$wallet->save(); | |||||
// Top-up (mandate creation) | |||||
// Create a valid mandate first (expect an extra payment) | |||||
$post = ['amount' => 20.10, 'balance' => 0, 'methodId' => PaymentProvider::METHOD_CREDITCARD]; | |||||
$response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); | |||||
$response->assertStatus(200); | |||||
// Check that the payments table contains a new record with proper amount(s) | |||||
// Stripe mandates always use amount=0 | |||||
$payment = $wallet->payments()->first(); | |||||
$this->assertSame(0, $payment->amount); | |||||
$this->assertSame(0, $payment->credit_amount); | |||||
$this->assertSame(0, $payment->currency_amount); | |||||
$this->assertSame(null, $payment->vat_rate_id); | |||||
$wallet->payments()->delete(); | |||||
$wallet->balance = -1000; | |||||
$wallet->save(); | |||||
// Top-up (recurring payment) | |||||
// Expect a recurring payment as we have a valid mandate at this point | |||||
// and the balance is below the threshold | |||||
$wallet->setSettings(['stripe_mandate_id' => 'AAA']); | |||||
$setupIntent = json_encode([ | |||||
"id" => "AAA", | |||||
"object" => "setup_intent", | |||||
"created" => 123456789, | |||||
"payment_method" => "pm_YYY", | |||||
"status" => "succeeded", | |||||
"usage" => "off_session", | |||||
"customer" => null | |||||
]); | |||||
$paymentMethod = json_encode([ | |||||
"id" => "pm_YYY", | |||||
"object" => "payment_method", | |||||
"card" => [ | |||||
"brand" => "visa", | |||||
"country" => "US", | |||||
"last4" => "4242" | |||||
], | |||||
"created" => 123456789, | |||||
"type" => "card" | |||||
]); | |||||
$paymentIntent = json_encode([ | |||||
"id" => "pi_XX", | |||||
"object" => "payment_intent", | |||||
"created" => 123456789, | |||||
"amount" => 2010 + intval(round(2010 * $vatRate->rate / 100)), | |||||
"currency" => "chf", | |||||
"description" => "Recurring Payment" | |||||
]); | |||||
$client = $this->mockStripe(); | |||||
$client->addResponse($setupIntent); | |||||
$client->addResponse($paymentMethod); | |||||
$client->addResponse($setupIntent); | |||||
$client->addResponse($paymentIntent); | |||||
$result = PaymentsController::topUpWallet($wallet); | |||||
$this->assertTrue($result); | |||||
// Check that the payments table contains a new record with proper amount(s) | |||||
$payment = $wallet->payments()->first(); | |||||
$this->assertSame(2010 + intval(round(2010 * $vatRate->rate / 100)), $payment->amount); | |||||
$this->assertSame(2010, $payment->credit_amount); | |||||
$this->assertSame($payment->amount, $payment->currency_amount); | |||||
$this->assertSame($vatRate->id, $payment->vat_rate_id); | |||||
} | } | ||||
/** | /** | ||||
* Test listing payment methods | * Test listing payment methods | ||||
* | * | ||||
* @group stripe | * @group stripe | ||||
*/ | */ | ||||
public function testListingPaymentMethods(): void | public function testListingPaymentMethods(): void | ||||
Show All 15 Lines | public function testListingPaymentMethods(): void | ||||
$response = $this->actingAs($user)->get('api/v4/payments/methods?type=' . PaymentProvider::TYPE_RECURRING); | $response = $this->actingAs($user)->get('api/v4/payments/methods?type=' . PaymentProvider::TYPE_RECURRING); | ||||
$response->assertStatus(200); | $response->assertStatus(200); | ||||
$json = $response->json(); | $json = $response->json(); | ||||
$this->assertCount(1, $json); | $this->assertCount(1, $json); | ||||
$this->assertSame('creditcard', $json[0]['id']); | $this->assertSame('creditcard', $json[0]['id']); | ||||
} | } | ||||
/** | |||||
* Generate Stripe-Signature header for a webhook payload | |||||
*/ | |||||
protected function webhookRequest($post) | |||||
{ | |||||
$secret = \config('services.stripe.webhook_secret'); | |||||
$ts = time(); | |||||
$payload = "$ts." . json_encode($post); | |||||
$sig = sprintf('t=%d,v1=%s', $ts, \hash_hmac('sha256', $payload, $secret)); | |||||
return $this->withHeaders(['Stripe-Signature' => $sig]) | |||||
->json('POST', "api/webhooks/payment/stripe", $post); | |||||
} | |||||
} | } |