feat: implement wallet and invoice factories, enhance wallet service methods, and add comprehensive tests
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Services\Wallet;
|
||||
|
||||
use App\Enums\InvoicePaymentMethodEnum;
|
||||
use App\Enums\InvoiceStatusEnum;
|
||||
use App\Enums\WalletTransactionReasonEnum;
|
||||
use App\Enums\WalletTransactionTypeEnum;
|
||||
use App\Jobs\SendPatternSms;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Models\User;
|
||||
use App\Models\Wallet;
|
||||
use App\Models\WalletTransaction;
|
||||
use App\Services\Wallet\WalletService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Tests\TestCase;
|
||||
|
||||
class WalletServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected WalletService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Queue::fake();
|
||||
$this->service = app(WalletService::class);
|
||||
}
|
||||
|
||||
public function test_for_user_creates_wallet_when_user_has_none(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$wallet = $this->service->forUser($user);
|
||||
|
||||
$this->assertSame($user->id, $wallet->user_id);
|
||||
$this->assertSame(0, $wallet->balance);
|
||||
$this->assertDatabaseCount('wallets', 1);
|
||||
}
|
||||
|
||||
public function test_for_user_returns_existing_wallet(): void
|
||||
{
|
||||
$wallet = Wallet::factory()->create(['balance' => 5000]);
|
||||
|
||||
$found = $this->service->forUser($wallet->user);
|
||||
|
||||
$this->assertTrue($found->is($wallet));
|
||||
$this->assertSame(5000, $found->balance);
|
||||
$this->assertDatabaseCount('wallets', 1);
|
||||
}
|
||||
|
||||
public function test_system_returns_single_wallet_without_user(): void
|
||||
{
|
||||
$first = $this->service->system();
|
||||
$second = $this->service->system();
|
||||
|
||||
$this->assertNull($first->user_id);
|
||||
$this->assertTrue($first->is($second));
|
||||
$this->assertDatabaseCount('wallets', 1);
|
||||
}
|
||||
|
||||
public function test_deposit_increases_balance_and_records_transaction(): void
|
||||
{
|
||||
$wallet = Wallet::factory()->create(['balance' => 1000]);
|
||||
$payment = Payment::factory()->paid()->walletTopUp()->create(['payer_id' => $wallet->user_id, 'amount' => 2500]);
|
||||
|
||||
$transaction = $this->service->deposit($wallet->user, 2500, WalletTransactionReasonEnum::TopUp, $payment);
|
||||
|
||||
$this->assertSame(3500, $wallet->fresh()->balance);
|
||||
$this->assertDatabaseHas('wallet_transactions', [
|
||||
'id' => $transaction->id,
|
||||
'wallet_id' => $wallet->id,
|
||||
'type' => WalletTransactionTypeEnum::Deposit->value,
|
||||
'reason' => WalletTransactionReasonEnum::TopUp->value,
|
||||
'amount' => 2500,
|
||||
'balance_after' => 3500,
|
||||
'payment_id' => $payment->id,
|
||||
'invoice_id' => null,
|
||||
'withdraw_request_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_deposit_creates_wallet_when_user_has_none(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->service->deposit($user, 700, WalletTransactionReasonEnum::Commission);
|
||||
|
||||
$this->assertDatabaseHas('wallets', ['user_id' => $user->id, 'balance' => 700]);
|
||||
}
|
||||
|
||||
public function test_deposit_to_wallet_works_on_system_wallet(): void
|
||||
{
|
||||
$wallet = Wallet::factory()->system()->create(['balance' => 100]);
|
||||
|
||||
$transaction = $this->service->depositToWallet($wallet, 400, WalletTransactionReasonEnum::Commission);
|
||||
|
||||
$this->assertSame(500, $wallet->fresh()->balance);
|
||||
$this->assertSame($wallet->id, $transaction->wallet_id);
|
||||
$this->assertSame(500, $transaction->balance_after);
|
||||
}
|
||||
|
||||
public function test_withdraw_decreases_balance_and_records_transaction(): void
|
||||
{
|
||||
$wallet = Wallet::factory()->create(['balance' => 5000]);
|
||||
$invoice = Invoice::factory()->create(['user_id' => $wallet->user_id, 'amount' => 3000]);
|
||||
|
||||
$transaction = $this->service->withdraw($wallet->user, 3000, WalletTransactionReasonEnum::InvoicePayment, $invoice);
|
||||
|
||||
$this->assertSame(2000, $wallet->fresh()->balance);
|
||||
$this->assertDatabaseHas('wallet_transactions', [
|
||||
'id' => $transaction->id,
|
||||
'wallet_id' => $wallet->id,
|
||||
'type' => WalletTransactionTypeEnum::Withdrawal->value,
|
||||
'reason' => WalletTransactionReasonEnum::InvoicePayment->value,
|
||||
'amount' => 3000,
|
||||
'balance_after' => 2000,
|
||||
'invoice_id' => $invoice->id,
|
||||
'payment_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_withdraw_allows_spending_entire_balance(): void
|
||||
{
|
||||
$wallet = Wallet::factory()->create(['balance' => 1000]);
|
||||
|
||||
$this->service->withdraw($wallet->user, 1000, WalletTransactionReasonEnum::Withdraw);
|
||||
|
||||
$this->assertSame(0, $wallet->fresh()->balance);
|
||||
}
|
||||
|
||||
public function test_withdraw_throws_when_balance_is_insufficient(): void
|
||||
{
|
||||
$wallet = Wallet::factory()->create(['balance' => 1000]);
|
||||
|
||||
try {
|
||||
$this->service->withdraw($wallet->user, 1001, WalletTransactionReasonEnum::Withdraw);
|
||||
$this->fail('ValidationException was not thrown');
|
||||
} catch (ValidationException $e) {
|
||||
$this->assertArrayHasKey('amount', $e->errors());
|
||||
}
|
||||
|
||||
$this->assertSame(1000, $wallet->fresh()->balance);
|
||||
$this->assertDatabaseCount('wallet_transactions', 0);
|
||||
}
|
||||
|
||||
public function test_pay_invoice_withdraws_amount_and_marks_invoice_paid(): void
|
||||
{
|
||||
$wallet = Wallet::factory()->create(['balance' => 2000000]);
|
||||
$invoice = Invoice::factory()->create(['user_id' => $wallet->user_id, 'amount' => 1200000]);
|
||||
|
||||
$paid = $this->service->payInvoice($wallet->user, $invoice);
|
||||
|
||||
$this->assertSame(InvoiceStatusEnum::Paid, $paid->status);
|
||||
$this->assertSame(InvoicePaymentMethodEnum::Wallet, $paid->payment_method);
|
||||
$this->assertNotNull($paid->paid_at);
|
||||
$this->assertSame(800000, $wallet->fresh()->balance);
|
||||
$this->assertDatabaseHas('wallet_transactions', [
|
||||
'wallet_id' => $wallet->id,
|
||||
'type' => WalletTransactionTypeEnum::Withdrawal->value,
|
||||
'reason' => WalletTransactionReasonEnum::InvoicePayment->value,
|
||||
'amount' => 1200000,
|
||||
'invoice_id' => $invoice->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_pay_invoice_dispatches_payment_confirmed_sms(): void
|
||||
{
|
||||
config(['services.iranpayamak.patterns.payment_confirmed' => 'pattern-code']);
|
||||
$wallet = Wallet::factory()->create(['balance' => 500000]);
|
||||
$invoice = Invoice::factory()->create(['user_id' => $wallet->user_id, 'amount' => 400000]);
|
||||
|
||||
$this->service->payInvoice($wallet->user, $invoice);
|
||||
|
||||
Queue::assertPushed(SendPatternSms::class);
|
||||
}
|
||||
|
||||
public function test_pay_invoice_throws_when_invoice_is_not_pending(): void
|
||||
{
|
||||
$wallet = Wallet::factory()->create(['balance' => 2000000]);
|
||||
$invoice = Invoice::factory()->paid()->create(['user_id' => $wallet->user_id, 'amount' => 1000]);
|
||||
|
||||
try {
|
||||
$this->service->payInvoice($wallet->user, $invoice);
|
||||
$this->fail('ValidationException was not thrown');
|
||||
} catch (ValidationException $e) {
|
||||
$this->assertArrayHasKey('invoice', $e->errors());
|
||||
}
|
||||
|
||||
$this->assertSame(2000000, $wallet->fresh()->balance);
|
||||
$this->assertDatabaseCount('wallet_transactions', 0);
|
||||
}
|
||||
|
||||
public function test_pay_invoice_throws_and_keeps_invoice_pending_when_balance_is_insufficient(): void
|
||||
{
|
||||
$wallet = Wallet::factory()->create(['balance' => 1000]);
|
||||
$invoice = Invoice::factory()->create(['user_id' => $wallet->user_id, 'amount' => 1200000]);
|
||||
|
||||
$this->expectException(ValidationException::class);
|
||||
|
||||
try {
|
||||
$this->service->payInvoice($wallet->user, $invoice);
|
||||
} finally {
|
||||
$this->assertSame(InvoiceStatusEnum::Pending, $invoice->fresh()->status);
|
||||
$this->assertSame(1000, $wallet->fresh()->balance);
|
||||
Queue::assertNothingPushed();
|
||||
}
|
||||
}
|
||||
|
||||
public function test_record_gateway_invoice_payment_creates_transaction_without_changing_balance(): void
|
||||
{
|
||||
$wallet = Wallet::factory()->create(['balance' => 9000]);
|
||||
$invoice = Invoice::factory()->create(['user_id' => $wallet->user_id, 'amount' => 700000]);
|
||||
$payment = Payment::factory()->paid()->create(['payer_id' => $wallet->user_id, 'invoice_id' => $invoice->id, 'amount' => 700000]);
|
||||
|
||||
$transaction = $this->service->recordGatewayInvoicePayment($invoice, $payment);
|
||||
|
||||
$this->assertSame(9000, $wallet->fresh()->balance);
|
||||
$this->assertDatabaseHas('wallet_transactions', [
|
||||
'id' => $transaction->id,
|
||||
'wallet_id' => $wallet->id,
|
||||
'type' => WalletTransactionTypeEnum::Withdrawal->value,
|
||||
'reason' => WalletTransactionReasonEnum::InvoicePayment->value,
|
||||
'amount' => 700000,
|
||||
'balance_after' => 9000,
|
||||
'invoice_id' => $invoice->id,
|
||||
'payment_id' => $payment->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_record_gateway_invoice_payment_creates_wallet_when_user_has_none(): void
|
||||
{
|
||||
$invoice = Invoice::factory()->create(['amount' => 500000]);
|
||||
$payment = Payment::factory()->paid()->create(['payer_id' => $invoice->user_id, 'invoice_id' => $invoice->id]);
|
||||
|
||||
$transaction = $this->service->recordGatewayInvoicePayment($invoice, $payment);
|
||||
|
||||
$this->assertDatabaseHas('wallets', ['user_id' => $invoice->user_id, 'balance' => 0]);
|
||||
$this->assertSame(0, $transaction->balance_after);
|
||||
$this->assertInstanceOf(WalletTransaction::class, $transaction);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user