4737838021
- T5.1 PaymentGatewayInterface, DTOs, PaymentMethod/PaymentStatus/RefundStatus enums - T5.2 payments/refunds tables, models, factories - T5.3-T5.5 KbzMiniAppGateway: initiate()/verify()/refund(), ported KBZ signing scheme, wired refund_amount through for partial refunds, mTLS options for refund - T5.6 PaymentGatewayFactory resolving gateways by PaymentMethod - T5.7 PaymentService orchestrator delegating to the resolved gateway
89 lines
3.2 KiB
PHP
89 lines
3.2 KiB
PHP
<?php
|
|
|
|
use Illuminate\Database\QueryException;
|
|
use Modules\Booking\Models\Booking;
|
|
use Modules\Payment\Enums\PaymentMethod;
|
|
use Modules\Payment\Enums\PaymentStatus;
|
|
use Modules\Payment\Enums\RefundStatus;
|
|
use Modules\Payment\Models\Payment;
|
|
use Modules\Payment\Models\Refund;
|
|
|
|
test('a payment belongs to a booking', function () {
|
|
$booking = Booking::factory()->create();
|
|
|
|
$payment = Payment::factory()->create(['booking_id' => $booking->id]);
|
|
|
|
expect($payment->booking)->toBeInstanceOf(Booking::class)
|
|
->and($payment->booking->is($booking))->toBeTrue()
|
|
->and($booking->payments->first()->is($payment))->toBeTrue();
|
|
});
|
|
|
|
test('a booking can have more than one payment attempt', function () {
|
|
$booking = Booking::factory()->create();
|
|
|
|
Payment::factory()->failed()->create(['booking_id' => $booking->id]);
|
|
Payment::factory()->completed()->create(['booking_id' => $booking->id]);
|
|
|
|
expect($booking->payments)->toHaveCount(2);
|
|
});
|
|
|
|
test('gateway and status cast to their enums', function () {
|
|
$payment = Payment::factory()->create([
|
|
'gateway' => PaymentMethod::KbzMiniApp,
|
|
'status' => PaymentStatus::Completed,
|
|
]);
|
|
|
|
expect($payment->gateway)->toBe(PaymentMethod::KbzMiniApp)
|
|
->and($payment->status)->toBe(PaymentStatus::Completed);
|
|
});
|
|
|
|
test('a payment defaults to pending', function () {
|
|
$payment = Payment::factory()->create();
|
|
|
|
expect($payment->status)->toBe(PaymentStatus::Pending);
|
|
});
|
|
|
|
test('a refund belongs to a payment, not the booking directly', function () {
|
|
$payment = Payment::factory()->completed()->create();
|
|
|
|
$refund = Refund::factory()->create(['payment_id' => $payment->id]);
|
|
|
|
expect($refund->payment)->toBeInstanceOf(Payment::class)
|
|
->and($refund->payment->is($payment))->toBeTrue()
|
|
->and($payment->refunds->first()->is($refund))->toBeTrue();
|
|
});
|
|
|
|
test('a payment can have more than one refund for partial refunds', function () {
|
|
$payment = Payment::factory()->completed()->create(['amount' => 20000]);
|
|
|
|
Refund::factory()->completed()->create(['payment_id' => $payment->id, 'amount' => 8000]);
|
|
Refund::factory()->create(['payment_id' => $payment->id, 'amount' => 12000]);
|
|
|
|
expect($payment->refunds)->toHaveCount(2);
|
|
});
|
|
|
|
test('refund status casts to its enum and defaults to pending', function () {
|
|
$refund = Refund::factory()->create();
|
|
|
|
expect($refund->status)->toBe(RefundStatus::Pending);
|
|
});
|
|
|
|
test('deleting a payment cascades to its refunds', function () {
|
|
$payment = Payment::factory()->completed()->create();
|
|
$refund = Refund::factory()->create(['payment_id' => $payment->id]);
|
|
|
|
$payment->delete();
|
|
|
|
expect(Refund::find($refund->id))->toBeNull();
|
|
});
|
|
|
|
test('gateway_transaction_id cannot be shared across unrelated retried attempts without being unique-constrained', function () {
|
|
// gateway_transaction_id is not unique-constrained since a failed attempt
|
|
// may legitimately be retried under a fresh Payment row with its own id;
|
|
// this just documents the column accepts duplicates without throwing.
|
|
Payment::factory()->create(['gateway_transaction_id' => 'kbz-txn-1']);
|
|
|
|
expect(fn () => Payment::factory()->create(['gateway_transaction_id' => 'kbz-txn-1']))
|
|
->not->toThrow(QueryException::class);
|
|
});
|