Files
Nyan Lin Paing 4737838021 Add Payment module: contracts, payments/refunds tables, KBZ gateway, factory, orchestrator (T5.1-T5.7)
- 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
2026-08-08 22:42:16 +07:00

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