feat: implement wallet and invoice factories, enhance wallet service methods, and add comprehensive tests
This commit is contained in:
@@ -27,12 +27,12 @@ class WalletService
|
||||
|
||||
public function forUser(User $user): Wallet
|
||||
{
|
||||
return Wallet::firstOrCreate(['user_id' => $user->id]);
|
||||
return Wallet::firstOrCreate(['user_id' => $user->id], ['balance' => 0]);
|
||||
}
|
||||
|
||||
public function system(): Wallet
|
||||
{
|
||||
return Wallet::firstOrCreate(['user_id' => null]);
|
||||
return Wallet::firstOrCreate(['user_id' => null], ['balance' => 0]);
|
||||
}
|
||||
|
||||
public function deposit(User $user, int $amount, WalletTransactionReasonEnum $reason, ?Payment $payment = null): WalletTransaction
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\DocumentModalityEnum;
|
||||
use App\Enums\InvoicePaymentMethodEnum;
|
||||
use App\Enums\InvoiceStatusEnum;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Invoice>
|
||||
*/
|
||||
class InvoiceFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'number' => fake()->unique()->numerify('#####'),
|
||||
'amount' => fake()->randomElement(DocumentModalityEnum::cases())->price(),
|
||||
'status' => InvoiceStatusEnum::Pending,
|
||||
'payment_method' => null,
|
||||
'paid_at' => null,
|
||||
'document_id' => null,
|
||||
'user_id' => User::factory(),
|
||||
];
|
||||
}
|
||||
|
||||
public function paid(InvoicePaymentMethodEnum $method = InvoicePaymentMethodEnum::Wallet): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => InvoiceStatusEnum::Paid,
|
||||
'payment_method' => $method,
|
||||
'paid_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function canceled(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => InvoiceStatusEnum::Canceled,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\PaymentPurposeEnum;
|
||||
use App\Enums\PaymentStatusEnum;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Payment>
|
||||
*/
|
||||
class PaymentFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'track_id' => fake()->unique()->numerify('##########'),
|
||||
'amount' => fake()->numberBetween(100000, 2000000),
|
||||
'status' => PaymentStatusEnum::Pending,
|
||||
'purpose' => PaymentPurposeEnum::Invoice,
|
||||
'ref_number' => null,
|
||||
'verified_at' => null,
|
||||
'invoice_id' => null,
|
||||
'payer_id' => User::factory(),
|
||||
];
|
||||
}
|
||||
|
||||
public function paid(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => PaymentStatusEnum::Paid,
|
||||
'ref_number' => fake()->numerify('############'),
|
||||
'verified_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function walletTopUp(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'purpose' => PaymentPurposeEnum::WalletTopUp,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Wallet>
|
||||
*/
|
||||
class WalletFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'balance' => 0,
|
||||
'user_id' => User::factory(),
|
||||
];
|
||||
}
|
||||
|
||||
public function system(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'user_id' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
# Testing Guide
|
||||
|
||||
Tests are written **only on explicit request**. When asked, follow this document exactly and keep the
|
||||
coverage matrix at the bottom up to date so remaining gaps are always visible.
|
||||
|
||||
## 1. What we test (and what we don't)
|
||||
|
||||
Business logic lives in `app/Services/`. Models are mostly relations and `$fillable`. Testing is
|
||||
prioritised accordingly:
|
||||
|
||||
| Priority | Layer | Why |
|
||||
|----------|-------|-----|
|
||||
| P1 | Services | Money, auth, scheduling, case flow — the only place real logic lives |
|
||||
| P1 | Form Requests | Cheap, catches every invalid input path before it reaches a service |
|
||||
| P1 | HTTP (critical routes) | Authentication, authorization/ownership, payment callback |
|
||||
| P2 | Remaining HTTP | Doctor & Admin CRUD |
|
||||
| P3 | Enums | Only methods with logic (`price()`, `processingHours()`, `fromDate()`, …) |
|
||||
| P3 | Models | Only scopes, accessors and helper methods — never relations or `$fillable` |
|
||||
|
||||
**Never test:** Eloquent relations, `$fillable`/`$casts`, framework behaviour, API Resources on their
|
||||
own (they are asserted through HTTP tests), Vue components (no JS test stack in this project).
|
||||
|
||||
## 2. Directory structure — one source file ↔ one test file
|
||||
|
||||
Test files mirror the source path. This is what makes gap detection mechanical.
|
||||
|
||||
```
|
||||
app/Services/Wallet/WalletService.php → tests/Feature/Services/Wallet/WalletServiceTest.php
|
||||
app/Http/Requests/Client/PaymentRequest.php → tests/Feature/Requests/Client/PaymentRequestTest.php
|
||||
app/Http/Controllers/Client/PaymentController.php → tests/Feature/Http/Client/PaymentControllerTest.php
|
||||
app/Enums/ShiftDayEnum.php → tests/Unit/Enums/ShiftDayEnumTest.php
|
||||
app/Models/Shift.php → tests/Feature/Models/ShiftTest.php
|
||||
```
|
||||
|
||||
- `tests/Unit/` — no database, no facades. Enums only.
|
||||
- `tests/Feature/` — boots the app, uses `RefreshDatabase` (sqlite `:memory:`, already configured in `phpunit.xml`).
|
||||
- The default `ExampleTest` placeholders have been removed; never re-add them.
|
||||
|
||||
## 3. Conventions
|
||||
|
||||
- PHPUnit 11, class-based tests extending `Tests\TestCase`, `RefreshDatabase` trait on every Feature test.
|
||||
- Method names: `test_{action}_{expected}_when_{condition}` in snake_case, e.g.
|
||||
`test_deposit_increases_balance`, `test_withdraw_throws_when_balance_is_insufficient`.
|
||||
- One behaviour per test. Arrange / Act / Assert, separated by a blank line, no comments.
|
||||
- Resolve services from the container (`app(WalletService::class)`), never `new`.
|
||||
- Every model touched by a test must have a factory in `database/factories/`. Create the missing factory
|
||||
first, in the same style as the existing ones. Factories must produce valid rows with all foreign keys.
|
||||
- Assert outcomes, not implementation: `assertDatabaseHas`, returned values, thrown exceptions,
|
||||
`assertInertia` props / JSON structure for HTTP.
|
||||
- Use enums in assertions (`DocumentStatusEnum::Pending`), never raw strings.
|
||||
|
||||
### Fakes — never hit a real external system
|
||||
|
||||
| Dependency | Fake |
|
||||
|------------|------|
|
||||
| Zibal (`ZibalPaymentService`), IranPayamak (`IranPayamakService`) | `Http::fake([...])` with `Http::assertSent` |
|
||||
| `SendPatternSms` job / `SmsNotificationService` | `Queue::fake()` + `Queue::assertPushed(SendPatternSms::class)` |
|
||||
| `FileStorageService`, `Medium` | `Storage::fake('public')`, `Storage::fake('private')` |
|
||||
| Time-dependent logic (shifts, OTP expiry, processing hours) | `Carbon::setTestNow(...)` |
|
||||
| Authenticated user | `$this->actingAs(User::factory()->create())` |
|
||||
| Spatie roles/permissions | seed `PermissionSeeder` in the test, then `$user->givePermissionTo(...)` / `assignRole(...)` |
|
||||
|
||||
## 4. Required coverage per layer
|
||||
|
||||
**Service** — happy path; every guard / exception branch; every side effect (DB rows, dispatched
|
||||
jobs, transactions written); for money code, balance invariants before and after.
|
||||
|
||||
**Form Request** — `authorize()` when it is not `return true`; each rule with an invalid input →
|
||||
`assertSessionHasErrors('field')` / 422; one fully valid payload passes.
|
||||
|
||||
**HTTP** — guest is redirected to login; wrong role or non-owner gets 403/404; happy path returns
|
||||
the expected status and persisted state; Inertia page tests assert the component name and key props.
|
||||
|
||||
**Enum** — every method containing logic, including edge values.
|
||||
|
||||
**Model** — each scope filters correctly; each accessor / helper method (`isExpired`, `fullUrl`, `humanSize`, …).
|
||||
|
||||
## 5. Workflow when asked to write tests
|
||||
|
||||
1. Read this file. If the user names a target, test that; otherwise take the next `☐` row by priority (P1 → P3).
|
||||
2. Create missing factories first.
|
||||
3. Write the test file at the mirrored path (section 2).
|
||||
4. Run `php artisan test --filter={ClassName}` and make it green. Then run the full suite.
|
||||
5. Flip the row to `☑` in the matrix below. Add rows for any new Service / Request / Controller / Enum that exists in the code but is missing from the matrix.
|
||||
6. Do not modify unrelated tests or source files.
|
||||
|
||||
### Detecting gaps mechanically
|
||||
|
||||
The matrix is the source of truth, but this catches rows that were never added. Anything it lists
|
||||
that is not in the matrix must be added as `☐` or, if it has no logic worth testing, as `—`:
|
||||
|
||||
```bash
|
||||
for f in $(find app/Services app/Http/Requests app/Http/Controllers app/Enums -name '*.php' ! -name 'Controller.php' ! -name 'EnumHasOptions.php'); do t=$(echo "$f" | sed -E 's#^app/Services/#tests/Feature/Services/#; s#^app/Http/Requests/#tests/Feature/Requests/#; s#^app/Http/Controllers/#tests/Feature/Http/#; s#^app/Enums/#tests/Unit/Enums/#; s#\.php$#Test.php#'); [ -f "$t" ] || echo "MISSING $t"; done
|
||||
```
|
||||
|
||||
## 6. Coverage matrix
|
||||
|
||||
Legend: `☐` not written · `☑` written · `—` intentionally not tested
|
||||
|
||||
### Services
|
||||
|
||||
| Priority | Source | Test | Status |
|
||||
|----------|--------|------|--------|
|
||||
| P1 | `app/Services/Wallet/WalletService.php` | `tests/Feature/Services/Wallet/WalletServiceTest.php` | ☑ |
|
||||
| P1 | `app/Services/Wallet/WithdrawRequestService.php` | `tests/Feature/Services/Wallet/WithdrawRequestServiceTest.php` | ☐ |
|
||||
| P1 | `app/Services/Invoice/InvoiceService.php` | `tests/Feature/Services/Invoice/InvoiceServiceTest.php` | ☐ |
|
||||
| P1 | `app/Services/Payment/ZibalPaymentService.php` | `tests/Feature/Services/Payment/ZibalPaymentServiceTest.php` | ☐ |
|
||||
| P1 | `app/Services/Auth/OtpService.php` | `tests/Feature/Services/Auth/OtpServiceTest.php` | ☐ |
|
||||
| P1 | `app/Services/Auth/AuthService.php` | `tests/Feature/Services/Auth/AuthServiceTest.php` | ☐ |
|
||||
| P1 | `app/Services/Auth/RegistrationService.php` | `tests/Feature/Services/Auth/RegistrationServiceTest.php` | ☐ |
|
||||
| P1 | `app/Services/Shift/ShiftScheduleService.php` | `tests/Feature/Services/Shift/ShiftScheduleServiceTest.php` | ☐ |
|
||||
| P1 | `app/Services/Document/CaseService.php` | `tests/Feature/Services/Document/CaseServiceTest.php` | ☐ |
|
||||
| P1 | `app/Services/Document/FastRequestService.php` | `tests/Feature/Services/Document/FastRequestServiceTest.php` | ☐ |
|
||||
| P2 | `app/Services/Document/DocumentCodeService.php` | `tests/Feature/Services/Document/DocumentCodeServiceTest.php` | ☐ |
|
||||
| P2 | `app/Services/Sms/SmsNotificationService.php` | `tests/Feature/Services/Sms/SmsNotificationServiceTest.php` | ☐ |
|
||||
| P2 | `app/Services/FileStorageService.php` | `tests/Feature/Services/FileStorageServiceTest.php` | ☐ |
|
||||
| P3 | `app/Services/Sms/IranPayamakService.php` | `tests/Feature/Services/Sms/IranPayamakServiceTest.php` | ☐ |
|
||||
|
||||
### Form Requests
|
||||
|
||||
| Priority | Source | Test | Status |
|
||||
|----------|--------|------|--------|
|
||||
| P1 | `app/Http/Requests/Client/Auth/UserRegistrationRequest.php` | `tests/Feature/Requests/Client/Auth/UserRegistrationRequestTest.php` | ☐ |
|
||||
| P1 | `app/Http/Requests/Client/Auth/VerifyOtpRequest.php` | `tests/Feature/Requests/Client/Auth/VerifyOtpRequestTest.php` | ☐ |
|
||||
| P1 | `app/Http/Requests/Client/PaymentRequest.php` | `tests/Feature/Requests/Client/PaymentRequestTest.php` | ☐ |
|
||||
| P1 | `app/Http/Requests/Client/WalletDepositRequest.php` | `tests/Feature/Requests/Client/WalletDepositRequestTest.php` | ☐ |
|
||||
| P1 | `app/Http/Requests/Client/WithdrawRequestStoreRequest.php` | `tests/Feature/Requests/Client/WithdrawRequestStoreRequestTest.php` | ☐ |
|
||||
| P1 | `app/Http/Requests/Client/BankCardStoreRequest.php` | `tests/Feature/Requests/Client/BankCardStoreRequestTest.php` | ☐ |
|
||||
| P1 | `app/Http/Requests/Client/FastRequestRequest.php` | `tests/Feature/Requests/Client/FastRequestRequestTest.php` | ☐ |
|
||||
| P2 | `app/Http/Requests/Client/CaseRejectRequest.php` | `tests/Feature/Requests/Client/CaseRejectRequestTest.php` | ☐ |
|
||||
| P2 | `app/Http/Requests/Client/ReportSubmitRequest.php` | `tests/Feature/Requests/Client/ReportSubmitRequestTest.php` | ☐ |
|
||||
| P2 | `app/Http/Requests/Client/ProfileUpdateRequest.php` | `tests/Feature/Requests/Client/ProfileUpdateRequestTest.php` | ☐ |
|
||||
| P2 | `app/Http/Requests/Admin/WithdrawRejectRequest.php` | `tests/Feature/Requests/Admin/WithdrawRejectRequestTest.php` | ☐ |
|
||||
| P2 | `app/Http/Requests/Admin/DocumentRequest.php` | `tests/Feature/Requests/Admin/DocumentRequestTest.php` | ☐ |
|
||||
| P2 | `app/Http/Requests/Admin/ShiftRequest.php` | `tests/Feature/Requests/Admin/ShiftRequestTest.php` | ☐ |
|
||||
| P2 | `app/Http/Requests/Admin/UserRequest.php` | `tests/Feature/Requests/Admin/UserRequestTest.php` | ☐ |
|
||||
| P2 | `app/Http/Requests/Admin/PetRequest.php` | `tests/Feature/Requests/Admin/PetRequestTest.php` | ☐ |
|
||||
| P2 | `app/Http/Requests/Admin/ReportRequest.php` | `tests/Feature/Requests/Admin/ReportRequestTest.php` | ☐ |
|
||||
| P2 | `app/Http/Requests/Admin/SettingProfileRequest.php` | `tests/Feature/Requests/Admin/SettingProfileRequestTest.php` | ☐ |
|
||||
|
||||
### HTTP
|
||||
|
||||
| Priority | Source | Test | Status |
|
||||
|----------|--------|------|--------|
|
||||
| P1 | `app/Http/Controllers/Client/AuthController.php` | `tests/Feature/Http/Client/AuthControllerTest.php` | ☐ |
|
||||
| P1 | `app/Http/Controllers/Client/PaymentController.php` | `tests/Feature/Http/Client/PaymentControllerTest.php` | ☐ |
|
||||
| P1 | `app/Http/Controllers/Client/WalletController.php` | `tests/Feature/Http/Client/WalletControllerTest.php` | ☐ |
|
||||
| P1 | `app/Http/Controllers/Client/FastRequestController.php` | `tests/Feature/Http/Client/FastRequestControllerTest.php` | ☐ |
|
||||
| P1 | `app/Http/Controllers/Client/InvoiceController.php` | `tests/Feature/Http/Client/InvoiceControllerTest.php` | ☐ |
|
||||
| P1 | `app/Http/Controllers/Admin/WithdrawRequestController.php` | `tests/Feature/Http/Admin/WithdrawRequestControllerTest.php` | ☐ |
|
||||
| P2 | `app/Http/Controllers/Client/Doctor/CaseController.php` | `tests/Feature/Http/Client/Doctor/CaseControllerTest.php` | ☐ |
|
||||
| P2 | `app/Http/Controllers/Client/Doctor/ReportController.php` | `tests/Feature/Http/Client/Doctor/ReportControllerTest.php` | ☐ |
|
||||
| P2 | `app/Http/Controllers/Client/Doctor/ShiftController.php` | `tests/Feature/Http/Client/Doctor/ShiftControllerTest.php` | ☐ |
|
||||
| P2 | `app/Http/Controllers/Client/Doctor/DocumentController.php` | `tests/Feature/Http/Client/Doctor/DocumentControllerTest.php` | ☐ |
|
||||
| P2 | `app/Http/Controllers/Client/Doctor/DashboardController.php` | `tests/Feature/Http/Client/Doctor/DashboardControllerTest.php` | ☐ |
|
||||
| P2 | `app/Http/Controllers/Client/MediaController.php` | `tests/Feature/Http/Client/MediaControllerTest.php` | ☐ |
|
||||
| P2 | `app/Http/Controllers/Client/ProfileController.php` | `tests/Feature/Http/Client/ProfileControllerTest.php` | ☐ |
|
||||
| P2 | `app/Http/Controllers/Admin/AuthController.php` | `tests/Feature/Http/Admin/AuthControllerTest.php` | ☐ |
|
||||
| P2 | `app/Http/Controllers/Admin/DocumentController.php` | `tests/Feature/Http/Admin/DocumentControllerTest.php` | ☐ |
|
||||
| P2 | `app/Http/Controllers/Admin/ShiftController.php` | `tests/Feature/Http/Admin/ShiftControllerTest.php` | ☐ |
|
||||
| P2 | `app/Http/Controllers/Admin/UserController.php` | `tests/Feature/Http/Admin/UserControllerTest.php` | ☐ |
|
||||
| P2 | `app/Http/Controllers/Admin/ReportController.php` | `tests/Feature/Http/Admin/ReportControllerTest.php` | ☐ |
|
||||
| P2 | `app/Http/Controllers/Admin/PetController.php` | `tests/Feature/Http/Admin/PetControllerTest.php` | ☐ |
|
||||
| P2 | `app/Http/Controllers/Admin/MediumController.php` | `tests/Feature/Http/Admin/MediumControllerTest.php` | ☐ |
|
||||
| P2 | `app/Http/Controllers/Admin/SettingUserController.php` | `tests/Feature/Http/Admin/SettingUserControllerTest.php` | ☐ |
|
||||
| P3 | `app/Http/Controllers/Admin/Role/RoleController.php` | `tests/Feature/Http/Admin/Role/RoleControllerTest.php` | ☐ |
|
||||
| P3 | `app/Http/Controllers/Admin/Role/PermissionController.php` | `tests/Feature/Http/Admin/Role/PermissionControllerTest.php` | ☐ |
|
||||
| P3 | `app/Http/Controllers/Admin/PanelController.php` | `tests/Feature/Http/Admin/PanelControllerTest.php` | ☐ |
|
||||
|
||||
### Enums
|
||||
|
||||
| Priority | Source | Test | Status |
|
||||
|----------|--------|------|--------|
|
||||
| P3 | `app/Enums/ShiftDayEnum.php` | `tests/Unit/Enums/ShiftDayEnumTest.php` | ☐ |
|
||||
| P3 | `app/Enums/DocumentModalityEnum.php` | `tests/Unit/Enums/DocumentModalityEnumTest.php` | ☐ |
|
||||
| P3 | `app/Enums/DocumentPriorityEnum.php` | `tests/Unit/Enums/DocumentPriorityEnumTest.php` | ☐ |
|
||||
| P3 | `app/Enums/EnumHasOptions.php` (via any enum using it) | `tests/Unit/Enums/EnumHasOptionsTest.php` | ☐ |
|
||||
| — | all other enums (values only) | | — |
|
||||
|
||||
### Models
|
||||
|
||||
| Priority | Source | Test | Status |
|
||||
|----------|--------|------|--------|
|
||||
| P3 | `app/Models/Shift.php` (`scopeActive`, `scopeOpen`) | `tests/Feature/Models/ShiftTest.php` | ☐ |
|
||||
| P3 | `app/Models/Medium.php` (`fullUrl`, `humanSize`, `exists`, `isPublic`, `isPrivate`, `deleteFile`) | `tests/Feature/Models/MediumTest.php` | ☐ |
|
||||
| P3 | `app/Models/OtpCode.php` (`isExpired`) | `tests/Feature/Models/OtpCodeTest.php` | ☐ |
|
||||
| — | all other models (relations / fillable only) | | — |
|
||||
|
||||
### Factories
|
||||
|
||||
Existing: `User`, `Pet`, `Document`, `DocumentAssignment`, `Medium`, `MediaAssignment`, `Report`.
|
||||
|
||||
Missing — create when first needed by a test:
|
||||
|
||||
| Model | Status |
|
||||
|-------|--------|
|
||||
| `Wallet` | ☑ |
|
||||
| `WalletTransaction` | ☐ |
|
||||
| `WithdrawRequest` | ☐ |
|
||||
| `BankCard` | ☐ |
|
||||
| `Invoice` | ☑ |
|
||||
| `Payment` | ☑ |
|
||||
| `OtpCode` | ☐ |
|
||||
| `Shift` | ☐ |
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
// use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_the_application_returns_a_successful_response(): void
|
||||
{
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_that_true_is_true(): void
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user