From 9718fb8c7ceafe1290d49d66892264657e0d7e15 Mon Sep 17 00:00:00 2001 From: mehrab esmailnia Date: Mon, 21 Sep 2026 05:03:39 +0330 Subject: [PATCH] feat: add comprehensive tests for ZibalPaymentService functionality --- docs/TESTING.md | 2 +- .../Payment/ZibalPaymentServiceTest.php | 199 ++++++++++++++++++ 2 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/Services/Payment/ZibalPaymentServiceTest.php diff --git a/docs/TESTING.md b/docs/TESTING.md index 566c528..57b08a0 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -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` | ☐ | diff --git a/tests/Feature/Services/Payment/ZibalPaymentServiceTest.php b/tests/Feature/Services/Payment/ZibalPaymentServiceTest.php new file mode 100644 index 0000000..588048a --- /dev/null +++ b/tests/Feature/Services/Payment/ZibalPaymentServiceTest.php @@ -0,0 +1,199 @@ +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); + } +}