feat: add comprehensive tests for ZibalPaymentService functionality

This commit is contained in:
2026-09-21 05:03:39 +03:30
parent 792387804c
commit 9718fb8c7c
2 changed files with 200 additions and 1 deletions
+1 -1
View File
@@ -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/WalletService.php` | `tests/Feature/Services/Wallet/WalletServiceTest.php` | ☑ |
| P1 | `app/Services/Wallet/WithdrawRequestService.php` | `tests/Feature/Services/Wallet/WithdrawRequestServiceTest.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/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/OtpService.php` | `tests/Feature/Services/Auth/OtpServiceTest.php` | ☐ |
| P1 | `app/Services/Auth/AuthService.php` | `tests/Feature/Services/Auth/AuthServiceTest.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` | ☐ | | 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);
}
}