feat: add comprehensive tests for ZibalPaymentService functionality
This commit is contained in:
+1
-1
@@ -104,7 +104,7 @@ Legend: `☐` not written · `☑` written · `—` intentionally not tested
|
||||
| P1 | `app/Services/Wallet/WalletService.php` | `tests/Feature/Services/Wallet/WalletServiceTest.php` | ☑ |
|
||||
| P1 | `app/Services/Wallet/WithdrawRequestService.php` | `tests/Feature/Services/Wallet/WithdrawRequestServiceTest.php` | ☑ |
|
||||
| P1 | `app/Services/Invoice/InvoiceService.php` | `tests/Feature/Services/Invoice/InvoiceServiceTest.php` | ☑ |
|
||||
| P1 | `app/Services/Payment/ZibalPaymentService.php` | `tests/Feature/Services/Payment/ZibalPaymentServiceTest.php` | ☐ |
|
||||
| P1 | `app/Services/Payment/ZibalPaymentService.php` | `tests/Feature/Services/Payment/ZibalPaymentServiceTest.php` | ☑ |
|
||||
| P1 | `app/Services/Auth/OtpService.php` | `tests/Feature/Services/Auth/OtpServiceTest.php` | ☐ |
|
||||
| P1 | `app/Services/Auth/AuthService.php` | `tests/Feature/Services/Auth/AuthServiceTest.php` | ☐ |
|
||||
| P1 | `app/Services/Auth/RegistrationService.php` | `tests/Feature/Services/Auth/RegistrationServiceTest.php` | ☐ |
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Services\Payment;
|
||||
|
||||
use App\Enums\PaymentPurposeEnum;
|
||||
use App\Enums\PaymentStatusEnum;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Models\User;
|
||||
use App\Services\Payment\ZibalPaymentService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Client\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use RuntimeException;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ZibalPaymentServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected ZibalPaymentService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->service = app(ZibalPaymentService::class);
|
||||
}
|
||||
|
||||
public function test_request_creates_pending_payment_from_gateway_track_id(): void
|
||||
{
|
||||
Http::fake(['gateway.zibal.ir/v1/request' => Http::response(['result' => 100, 'trackId' => 123456789])]);
|
||||
$payer = User::factory()->create();
|
||||
|
||||
$payment = $this->service->request($payer, 500000, 'order-1');
|
||||
|
||||
$this->assertSame('123456789', $payment->track_id);
|
||||
$this->assertSame(PaymentStatusEnum::Pending, $payment->status);
|
||||
$this->assertDatabaseHas('payments', [
|
||||
'id' => $payment->id,
|
||||
'track_id' => '123456789',
|
||||
'amount' => 500000,
|
||||
'payer_id' => $payer->id,
|
||||
'purpose' => PaymentPurposeEnum::Invoice->value,
|
||||
'invoice_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_request_sends_amount_in_rials_with_merchant_order_and_callback(): void
|
||||
{
|
||||
config(['services.zibal.sandbox' => false, 'services.zibal.merchant' => 'real-merchant', 'services.zibal.callback_url' => 'https://example.test/callback']);
|
||||
Http::fake(['gateway.zibal.ir/v1/request' => Http::response(['result' => 100, 'trackId' => 1])]);
|
||||
|
||||
$this->service->request(User::factory()->create(), 500000, 'order-9');
|
||||
|
||||
Http::assertSent(function (Request $request) {
|
||||
return $request->url() === 'https://gateway.zibal.ir/v1/request'
|
||||
&& $request['merchant'] === 'real-merchant'
|
||||
&& $request['amount'] === 5000000
|
||||
&& $request['orderId'] === 'order-9'
|
||||
&& $request['callbackUrl'] === 'https://example.test/callback';
|
||||
});
|
||||
}
|
||||
|
||||
public function test_request_uses_sandbox_merchant_and_route_callback_by_default(): void
|
||||
{
|
||||
config(['services.zibal.sandbox' => true, 'services.zibal.merchant' => 'real-merchant', 'services.zibal.callback_url' => null]);
|
||||
Http::fake(['gateway.zibal.ir/v1/request' => Http::response(['result' => 100, 'trackId' => 1])]);
|
||||
|
||||
$this->service->request(User::factory()->create(), 1000, 'order-1');
|
||||
|
||||
Http::assertSent(function (Request $request) {
|
||||
return $request['merchant'] === 'zibal'
|
||||
&& $request['callbackUrl'] === route('dashboard.payments.callback');
|
||||
});
|
||||
}
|
||||
|
||||
public function test_request_merges_extra_attributes_into_payment(): void
|
||||
{
|
||||
Http::fake(['gateway.zibal.ir/v1/request' => Http::response(['result' => 100, 'trackId' => 55])]);
|
||||
$invoice = Invoice::factory()->create();
|
||||
|
||||
$payment = $this->service->request($invoice->user, $invoice->amount, $invoice->number, [
|
||||
'invoice_id' => $invoice->id,
|
||||
'purpose' => PaymentPurposeEnum::WalletTopUp->value,
|
||||
]);
|
||||
|
||||
$this->assertSame($invoice->id, $payment->invoice_id);
|
||||
$this->assertSame(PaymentPurposeEnum::WalletTopUp, $payment->purpose);
|
||||
}
|
||||
|
||||
public function test_request_throws_with_gateway_message_when_result_is_not_100(): void
|
||||
{
|
||||
Http::fake(['gateway.zibal.ir/v1/request' => Http::response(['result' => 102, 'message' => 'merchant یافت نشد'])]);
|
||||
|
||||
try {
|
||||
$this->service->request(User::factory()->create(), 1000, 'order-1');
|
||||
$this->fail('RuntimeException was not thrown');
|
||||
} catch (RuntimeException $e) {
|
||||
$this->assertStringContainsString('merchant یافت نشد', $e->getMessage());
|
||||
}
|
||||
|
||||
$this->assertDatabaseCount('payments', 0);
|
||||
}
|
||||
|
||||
public function test_request_throws_when_gateway_response_is_invalid(): void
|
||||
{
|
||||
Http::fake(['gateway.zibal.ir/v1/request' => Http::response('not json', 500)]);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
|
||||
$this->service->request(User::factory()->create(), 1000, 'order-1');
|
||||
}
|
||||
|
||||
public function test_payment_url_points_to_gateway_start_with_track_id(): void
|
||||
{
|
||||
$payment = Payment::factory()->create(['track_id' => '987']);
|
||||
|
||||
$this->assertSame('https://gateway.zibal.ir/start/987', $this->service->paymentUrl($payment));
|
||||
}
|
||||
|
||||
public function test_verify_marks_payment_paid_when_result_is_100(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-09-21 10:00:00');
|
||||
Http::fake(['gateway.zibal.ir/v1/verify' => Http::response(['result' => 100, 'refNumber' => 'REF-1'])]);
|
||||
$payment = Payment::factory()->create(['track_id' => '321']);
|
||||
|
||||
$verified = $this->service->verify($payment);
|
||||
|
||||
$this->assertSame(PaymentStatusEnum::Paid, $verified->status);
|
||||
$this->assertSame('REF-1', $verified->ref_number);
|
||||
$this->assertTrue($verified->verified_at->eq(Carbon::parse('2026-09-21 10:00:00')));
|
||||
Http::assertSent(fn (Request $request) => $request->url() === 'https://gateway.zibal.ir/v1/verify' && $request['trackId'] === '321');
|
||||
}
|
||||
|
||||
public function test_verify_marks_payment_paid_when_already_verified_result_is_201(): void
|
||||
{
|
||||
Http::fake(['gateway.zibal.ir/v1/verify' => Http::response(['result' => 201])]);
|
||||
$payment = Payment::factory()->create();
|
||||
|
||||
$verified = $this->service->verify($payment);
|
||||
|
||||
$this->assertSame(PaymentStatusEnum::Paid, $verified->status);
|
||||
$this->assertNull($verified->ref_number);
|
||||
$this->assertNotNull($verified->verified_at);
|
||||
}
|
||||
|
||||
public function test_verify_marks_payment_failed_when_result_is_not_successful(): void
|
||||
{
|
||||
Http::fake(['gateway.zibal.ir/v1/verify' => Http::response(['result' => 202, 'message' => 'پرداخت نشده'])]);
|
||||
$payment = Payment::factory()->create();
|
||||
|
||||
$verified = $this->service->verify($payment);
|
||||
|
||||
$this->assertSame(PaymentStatusEnum::Failed, $verified->status);
|
||||
$this->assertNull($verified->verified_at);
|
||||
}
|
||||
|
||||
public function test_verify_marks_payment_failed_when_response_is_invalid(): void
|
||||
{
|
||||
Http::fake(['gateway.zibal.ir/v1/verify' => Http::response('', 500)]);
|
||||
$payment = Payment::factory()->create();
|
||||
|
||||
$verified = $this->service->verify($payment);
|
||||
|
||||
$this->assertSame(PaymentStatusEnum::Failed, $verified->status);
|
||||
}
|
||||
|
||||
public function test_verify_does_not_call_gateway_when_payment_is_not_pending(): void
|
||||
{
|
||||
Http::fake();
|
||||
$payment = Payment::factory()->paid()->create();
|
||||
|
||||
$verified = $this->service->verify($payment);
|
||||
|
||||
$this->assertSame(PaymentStatusEnum::Paid, $verified->status);
|
||||
Http::assertNothingSent();
|
||||
}
|
||||
|
||||
public function test_mark_failed_sets_pending_payment_to_failed(): void
|
||||
{
|
||||
$payment = Payment::factory()->create();
|
||||
|
||||
$failed = $this->service->markFailed($payment);
|
||||
|
||||
$this->assertSame(PaymentStatusEnum::Failed, $failed->status);
|
||||
$this->assertDatabaseHas('payments', ['id' => $payment->id, 'status' => PaymentStatusEnum::Failed->value]);
|
||||
}
|
||||
|
||||
public function test_mark_failed_leaves_paid_payment_untouched(): void
|
||||
{
|
||||
$payment = Payment::factory()->paid()->create();
|
||||
|
||||
$failed = $this->service->markFailed($payment);
|
||||
|
||||
$this->assertSame(PaymentStatusEnum::Paid, $failed->status);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user