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); } }