Files
famous-ly4-ev/app-modules/payment/tests/Feature/InitiatePaymentApiTest.php
T
Nyan Lin Paing 8d74ac74cd
PHP Tests / php-tests (push) Failing after 9m1s
fix dashboard and apis
2026-08-16 23:50:39 +07:00

194 lines
7.7 KiB
PHP

<?php
use App\Models\User;
use Modules\Booking\Enums\BookingStatus;
use Modules\Booking\Models\Booking;
use Modules\Payment\Contracts\PaymentGatewayInterface;
use Modules\Payment\Data\PaymentRequestData;
use Modules\Payment\Data\PaymentResultData;
use Modules\Payment\Data\RefundResultData;
use Modules\Payment\Enums\PaymentMethod;
use Modules\Payment\Enums\PaymentStatus;
use Modules\Payment\Factories\PaymentGatewayFactory;
use Modules\Payment\Models\Payment;
use Spatie\Permission\Models\Permission;
/**
* A fake PaymentGatewayInterface swapped in via the factory — never call the
* real KBZ gateway in tests (T5.8).
*/
class FakeInitiatePaymentGateway implements PaymentGatewayInterface
{
public static ?PaymentRequestData $lastRequest = null;
public static int $initiateCalls = 0;
public static int $verifyCalls = 0;
public static PaymentStatus $verifyStatus = PaymentStatus::Pending;
public function initiate(PaymentRequestData $data): PaymentResultData
{
self::$lastRequest = $data;
self::$initiateCalls++;
return new PaymentResultData(
status: PaymentStatus::Pending,
gatewayTransactionId: $data->merchantOrderId,
gatewayPayload: ['prepay_id' => 'PREPAY123'],
);
}
public function verify(string $gatewayTransactionId): PaymentResultData
{
self::$verifyCalls++;
return new PaymentResultData(
status: self::$verifyStatus,
gatewayTransactionId: $gatewayTransactionId,
gatewayPayload: ['trade_status' => self::$verifyStatus->value],
);
}
public function refund(string $gatewayTransactionId, string $amount, string $reason): RefundResultData
{
throw new RuntimeException('not needed for this test');
}
public function handleWebhook(array $payload): PaymentResultData
{
throw new RuntimeException('not needed for this test');
}
}
beforeEach(function () {
Permission::findOrCreate('manage_bookings', 'web');
app(PaymentGatewayFactory::class)->register(PaymentMethod::KbzMiniApp, FakeInitiatePaymentGateway::class);
FakeInitiatePaymentGateway::$lastRequest = null;
FakeInitiatePaymentGateway::$initiateCalls = 0;
FakeInitiatePaymentGateway::$verifyCalls = 0;
FakeInitiatePaymentGateway::$verifyStatus = PaymentStatus::Pending;
$this->owner = User::factory()->create();
$this->token = $this->owner->createToken('test-token')->plainTextToken;
});
test('the owner can initiate payment for their own pending_payment booking', function () {
$booking = Booking::factory()->create([
'user_id' => $this->owner->id,
'status' => BookingStatus::PendingPayment,
'price' => 15000,
]);
$this->withHeader('Authorization', "Bearer {$this->token}")
->postJson("/api/v1/payments/{$booking->booking_ref}/initiate")
->assertCreated()
->assertJsonPath('data.status', PaymentStatus::Pending->value)
->assertJsonPath('data.gateway_payload.prepay_id', 'PREPAY123');
expect(Payment::where('booking_id', $booking->id)->count())->toBe(1);
$payment = Payment::where('booking_id', $booking->id)->first();
expect($payment->status)->toBe(PaymentStatus::Pending)
->and($payment->gateway)->toBe(PaymentMethod::KbzMiniApp)
->and((float) $payment->amount)->toBe(15000.0)
->and($payment->gateway_transaction_id)->toBe("{$booking->booking_ref}-1");
});
test('a repeat call while the previous attempt is still pending reuses it instead of precreating again', function () {
$booking = Booking::factory()->create(['user_id' => $this->owner->id, 'status' => BookingStatus::PendingPayment]);
FakeInitiatePaymentGateway::$verifyStatus = PaymentStatus::Pending;
$this->withHeader('Authorization', "Bearer {$this->token}")
->postJson("/api/v1/payments/{$booking->booking_ref}/initiate")
->assertCreated();
$firstPaymentId = Payment::where('booking_id', $booking->id)->sole()->id;
$this->withHeader('Authorization', "Bearer {$this->token}")
->postJson("/api/v1/payments/{$booking->booking_ref}/initiate")
->assertCreated()
->assertJsonPath('data.id', $firstPaymentId);
expect(Payment::where('booking_id', $booking->id)->count())->toBe(1)
->and(FakeInitiatePaymentGateway::$initiateCalls)->toBe(1)
->and(FakeInitiatePaymentGateway::$verifyCalls)->toBe(1);
});
test('a repeat call reused attempt found completed on re-verify is returned without precreating again', function () {
$booking = Booking::factory()->create(['user_id' => $this->owner->id, 'status' => BookingStatus::PendingPayment]);
$this->withHeader('Authorization', "Bearer {$this->token}")
->postJson("/api/v1/payments/{$booking->booking_ref}/initiate")
->assertCreated();
FakeInitiatePaymentGateway::$verifyStatus = PaymentStatus::Completed;
$this->withHeader('Authorization', "Bearer {$this->token}")
->postJson("/api/v1/payments/{$booking->booking_ref}/initiate")
->assertCreated()
->assertJsonPath('data.status', PaymentStatus::Completed->value);
expect(Payment::where('booking_id', $booking->id)->count())->toBe(1)
->and(FakeInitiatePaymentGateway::$initiateCalls)->toBe(1);
});
test('a retried payment attempt gets a unique merchant order id', function () {
$booking = Booking::factory()->create(['user_id' => $this->owner->id, 'status' => BookingStatus::PendingPayment]);
Payment::factory()->failed()->create(['booking_id' => $booking->id]);
$this->withHeader('Authorization', "Bearer {$this->token}")
->postJson("/api/v1/payments/{$booking->booking_ref}/initiate")
->assertCreated();
expect(FakeInitiatePaymentGateway::$lastRequest->merchantOrderId)->toBe("{$booking->booking_ref}-2");
});
test('initiating payment on a non-pending_payment booking surfaces as 422', function () {
$booking = Booking::factory()->create(['user_id' => $this->owner->id, 'status' => BookingStatus::Confirmed]);
$this->withHeader('Authorization', "Bearer {$this->token}")
->postJson("/api/v1/payments/{$booking->booking_ref}/initiate")
->assertStatus(422);
expect(Payment::where('booking_id', $booking->id)->count())->toBe(0);
});
test('a non-owner without manage_bookings cannot initiate payment for someone else\'s booking', function () {
$booking = Booking::factory()->create([
'user_id' => User::factory()->create()->id,
'status' => BookingStatus::PendingPayment,
]);
$this->withHeader('Authorization', "Bearer {$this->token}")
->postJson("/api/v1/payments/{$booking->booking_ref}/initiate")
->assertForbidden();
expect(Payment::where('booking_id', $booking->id)->count())->toBe(0);
});
test('staff with manage_bookings can initiate payment on behalf of a customer', function () {
$staff = User::factory()->create()->givePermissionTo('manage_bookings');
$staffToken = $staff->createToken('staff-token')->plainTextToken;
$booking = Booking::factory()->create(['user_id' => $this->owner->id, 'status' => BookingStatus::PendingPayment]);
$this->withHeader('Authorization', "Bearer {$staffToken}")
->postJson("/api/v1/payments/{$booking->booking_ref}/initiate")
->assertCreated();
});
test('unauthenticated requests are rejected', function () {
$booking = Booking::factory()->create(['status' => BookingStatus::PendingPayment]);
$this->postJson("/api/v1/payments/{$booking->booking_ref}/initiate")->assertUnauthorized();
});
test('404s for a booking that does not exist', function () {
$this->withHeader('Authorization', "Bearer {$this->token}")
->postJson('/api/v1/payments/EVB-DOES-NOT-EXIST/initiate')
->assertNotFound();
});