diff --git a/README.md b/README.md index 30625af..335bc91 100644 --- a/README.md +++ b/README.md @@ -7,60 +7,65 @@ [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/hpwebdeveloper/laravel-pay-pocket/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/hpwebdeveloper/laravel-pay-pocket/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) [![Imports](https://github.com/HPWebdeveloper/laravel-pay-pocket/actions/workflows/check_imports.yml/badge.svg?branch=main)](https://github.com/HPWebdeveloper/laravel-pay-pocket/actions/workflows/check_imports.yml) - **Laravel Pay Pocket** is a package designed for Laravel applications, offering the flexibility to manage multiple wallet types within two dedicated database tables, `wallets` and `wallets_logs`. **Demo** https://github.com/HPWebdeveloper/demo-pay-pocket **Note:** This package does not handle payments from payment platforms, but instead offers the concept of virtual money, deposit, and withdrawal. -* **Author**: Hamed Panjeh -* **Vendor**: hpwebdeveloper -* **Package**: laravel-pay-pocket -* **Alias name**: Laravel PPP (Laravel Pay Pocket Package) -* **Version**: `1.x` -* **PHP Version**: 8.1+ -* **Laravel Version**: `10.x` -* **[Composer](https://getcomposer.org/):** `composer require hpwebdeveloper/laravel-pay-pocket` - +- **Author**: Hamed Panjeh +- **Vendor**: hpwebdeveloper +- **Package**: laravel-pay-pocket +- **Alias name**: Laravel PPP (Laravel Pay Pocket Package) +- **Version**: `1.x` +- **PHP Version**: 8.1+ +- **Laravel Version**: `10.x` +- **[Composer](https://getcomposer.org/):** `composer require hpwebdeveloper/laravel-pay-pocket` ### Support Policy -| Version | Laravel | PHP | Release date | End of improvements | End of support | -|---------|----------------|---------------|--------------|---------------------|----------------| -| 1.x | ^10.0 | 8.1, 8.2, 8.3 | Nov 30, 2023 | Mar 1, 2024 | | | -| x.x | | | | | | | - +| Version | Laravel | PHP | Release date | End of improvements | End of support | +| ------- | ------- | ------------- | ------------ | ------------------- | -------------- | +| 1.x | ^10.0 | 8.1, 8.2, 8.3 | Nov 30, 2023 | Mar 1, 2024 | | +| x.x | | | | | | ## Installation: -- **Step 1:** You can install the package via composer: +- **Step 1:** You can install the package via composer: ```bash composer require hpwebdeveloper/laravel-pay-pocket ``` -- **Step 2:** Publish and run the migrations with: +- **Step 2:** Publish and run the migrations with: ```bash php artisan vendor:publish --tag="pay-pocket-migrations" php artisan migrate ``` + You have successfully added two dedicated database tables, `wallets` and `wallets_logs`, without making any modifications to the `users` table. -- **Step 3:** Publish the wallet types using +- **Step 3:** Publish the wallet types using ```bash php artisan vendor:publish --tag="pay-pocket-wallets" +php artisan vendor:publish --tag="config" ``` This command will automatically publish the `WalletEnums.php` file into your application's `app/Enums` directory. +## Updating + +If updating from version `<= 1.0.3`, new migration and config files have been added to support the new [Transaction Info Feature](#transaction-info) + +Follow the [Installation](#installation) Steps 2 and 3 to update your migrations. + ## Preparation ### Prepare User Model -To use this package you need to implement the `WalletOperations` into `User` model and utilize the `ManagesWallet` trait. +To use this package you need to implement the `WalletOperations` into `User` model and utilize the `ManagesWallet` trait. ```php @@ -75,9 +80,10 @@ class User extends Authenticatable implements WalletOperations ### Prepare Wallets -In Laravel Pay Pocket, you have the flexibility to define the order in which wallets are prioritized for payments through the use of Enums. The order of wallets in the Enum file determines their priority level. The first wallet listed has the highest priority and will be used first for deducting order values. +In Laravel Pay Pocket, you have the flexibility to define the order in which wallets are prioritized for payments through the use of Enums. The order of wallets in the Enum file determines their priority level. The first wallet listed has the highest priority and will be used first for deducting order values. For example, consider the following wallet types defined in the Enum class (published in step 3 of installation): + ```php namespace App\Enums; @@ -88,48 +94,84 @@ enum WalletEnums: string } ``` -**You have complete freedom to name your wallets as per your requirements and even add more wallet types to the Enum list.** +**You have complete freedom to name your wallets as per your requirements and even add more wallet types to the Enum list.** In this particular setup, `wallet_1` (`WALLET1`) is given the **highest priority**. When an order payment is processed, the system will first attempt to use `wallet_1` to cover the cost. If `wallet_1` does not have sufficient funds, `wallet_2` (`WALLET2`) will be used next. ### Example: + If the balance in `wallet_1` is 10 and the balance in `wallet_2` is 20, and you need to pay an order value of 15, the payment process will first utilize the entire balance of `wallet_1`. Since `wallet_1`'s balance is insufficient to cover the full amount, the remaining 5 will be deducted from `wallet_2`. After the payment, `wallet_2` will have a remaining balance of 15." ## Usage, APIs and Operations: + ### Deposit +```php +deposit(type: 'wallet_1', amount: 123.45, notes: null) +``` + +Deposit funds into `wallet_1` + ```php $user = auth()->user(); +$user->deposit('wallet_1', 123.45); +``` -$user->deposit('wallet_1', 123.45); // Deposit funds into 'wallet_1' +Deposit funds into `wallet_2` -$user->deposit('wallet_2', 67.89); // Deposit funds into 'wallet_2' +```php +$user = auth()->user(); +$user->deposit('wallet_2', 67.89); +``` -// Or using provided facade +Or using provided facade +```php use HPWebdeveloper\LaravelPayPocket\Facades\LaravelPayPocket; +$user = auth()->user(); LaravelPayPocket::deposit($user, 'wallet_1', 123.45); ``` + Note: `wallet_1` and `wallet_2` must already be defined in the `WalletEnums`. +#### Transaction Info ([#8][i8]) + +In a case where you want to enter descriptions for a particular transaction, the `$notes` param allows you to provide information about why a transaction happened. + +```php +$user = auth()->user(); +$user->deposit('wallet_1', 67.89, 'You ordered pizza.'); +``` + ### Pay + +```php +pay(amount: 12.34, notes: null) +``` + +Pay the value using the total combined balance available across all allowed wallets + ```php -// Pay the value using the total combined balance available across all wallets +$user = auth()->user(); $user->pay(12.34); - -// Or using provided facade +``` +Or using provided facade + +```php use HPWebdeveloper\LaravelPayPocket\Facades\LaravelPayPocket; +$user = auth()->user(); LaravelPayPocket::pay($user, 12.34); ``` ### Balance -- **Wallets** +- **Wallets** + ```php $user->walletBalance // Total combined balance available across all wallets @@ -138,7 +180,8 @@ $user->walletBalance // Total combined balance available across all wallets LaravelPayPocket::checkBalance($user); ``` -- **Particular Wallet** +- **Particular Wallet** + ```php $user->getWalletBalanceByType('wallet_1') // Balance available in wallet_1 $user->getWalletBalanceByType('wallet_2') // Balance available in wallet_2 @@ -149,7 +192,8 @@ LaravelPayPocket::walletBalanceByType($user, 'wallet_1'); ``` ### Exceptions -Upon examining the `src/Exceptions` directory within the source code, + +Upon examining the `src/Exceptions` directory within the source code, you will discover a variety of exceptions tailored to address each scenario of invalid entry. Review the [demo](https://github.com/HPWebdeveloper/demo-pay-pocket) that accounts for some of the exceptions. ### Log @@ -157,7 +201,6 @@ you will discover a variety of exceptions tailored to address each scenario of i A typical `wallets_logs` table. ![Laravel Pay Pocket Log](https://github.com/HPWebdeveloper/laravel-pay-pocket/assets/16323354/a242d335-8bd2-4af1-aa38-4e95b8870941) - ## Testing ```bash @@ -185,10 +228,12 @@ Please review [our security policy](../../security/policy) on how to report secu ## Credits -- [Hamed Panjeh](https://github.com/HPWebdeveloper) -- [All Contributors](../../contributors) -- Icon in the above image: pocket by Creative Mahira from [Noun Project](https://thenounproject.com/browse/icons/term/pocket/) (CC BY 3.0) +- [Hamed Panjeh](https://github.com/HPWebdeveloper) +- [All Contributors](../../contributors) +- Icon in the above image: pocket by Creative Mahira from [Noun Project](https://thenounproject.com/browse/icons/term/pocket/) (CC BY 3.0) ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. + +[i8]: Tag link (will be updated soon) diff --git a/config/pay-pocket.php b/config/pay-pocket.php index 4cb9728..5c8e375 100644 --- a/config/pay-pocket.php +++ b/config/pay-pocket.php @@ -1,6 +1,19 @@ 12, + 'log_reference_prefix' => null, + 'log_reference_generator' => null, ]; diff --git a/database/migrations/add_notes_and_reference_columns_to_wallets_logs_table.php.stub b/database/migrations/add_notes_and_reference_columns_to_wallets_logs_table.php.stub new file mode 100644 index 0000000..b9ac4ca --- /dev/null +++ b/database/migrations/add_notes_and_reference_columns_to_wallets_logs_table.php.stub @@ -0,0 +1,38 @@ +string('notes')->nullable()->after('status'); + } + if (!Schema::hasColumn('wallets_logs', 'reference')) { + $table->string('reference')->nullable()->after('ip'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('wallets_logs', function (Blueprint $table) { + if (Schema::hasColumn('wallets_logs', 'notes')) { + $table->dropColumn('notes'); + } + if (Schema::hasColumn('wallets_logs', 'reference')) { + $table->dropColumn('reference'); + } + }); + } +}; diff --git a/src/Interfaces/WalletOperations.php b/src/Interfaces/WalletOperations.php index 42742ef..47e132d 100644 --- a/src/Interfaces/WalletOperations.php +++ b/src/Interfaces/WalletOperations.php @@ -10,7 +10,7 @@ public function getWalletBalanceByType(string $walletType); public function hasSufficientBalance($value): bool; - public function pay(int|float $orderValue); + public function pay(int|float $orderValue, ?string $notes = null); - public function deposit(string $type, int|float $amount): bool; + public function deposit(string $type, int|float $amount, ?string $notes = null): bool; } diff --git a/src/LaravelPayPocketServiceProvider.php b/src/LaravelPayPocketServiceProvider.php index f58c9ae..b8baa3a 100644 --- a/src/LaravelPayPocketServiceProvider.php +++ b/src/LaravelPayPocketServiceProvider.php @@ -18,7 +18,11 @@ public function configurePackage(Package $package): void ->name('laravel-pay-pocket') ->hasConfigFile() ->hasViews() - ->hasMigrations('create_wallets_logs_table', 'create_wallets_table'); + ->hasMigrations( + 'create_wallets_logs_table', + 'create_wallets_table', + 'add_notes_and_reference_columns_to_wallets_logs_table' + ); } public function bootingPackage() @@ -26,5 +30,15 @@ public function bootingPackage() $this->publishes([ __DIR__.'/../Enums/' => app_path('Enums'), ], 'pay-pocket-wallets'); + + $this->publishes([ + __DIR__.'/../config/pay-pocket.php' => config_path('pay-pocket.php'), + ], 'config'); + } + + public function registeringPackage() + { + // Automatically apply the package configuration + $this->mergeConfigFrom(__DIR__.'/../config/pay-pocket.php', 'pay-pocket'); } } diff --git a/src/Models/WalletsLog.php b/src/Models/WalletsLog.php index ca751e2..b7a4130 100644 --- a/src/Models/WalletsLog.php +++ b/src/Models/WalletsLog.php @@ -16,7 +16,7 @@ class WalletsLog extends Model use HasFactory; protected $fillable = [ - 'from', 'to', 'type', 'ip', 'value', 'wallet_name', + 'from', 'to', 'type', 'ip', 'value', 'wallet_name', 'notes', 'reference', ]; public function loggable(): MorphTo diff --git a/src/Services/PocketServices.php b/src/Services/PocketServices.php index 1fe31cc..91db495 100644 --- a/src/Services/PocketServices.php +++ b/src/Services/PocketServices.php @@ -4,14 +4,14 @@ class PocketServices { - public function deposit($user, $type, $amount) + public function deposit($user, $type, $amount, $notes = null) { - return $user->deposit($type, $amount); + return $user->deposit($type, $amount, $notes); } - public function pay($user, $orderValue) + public function pay($user, $orderValue, $notes = null) { - return $user->pay($orderValue); + return $user->pay($orderValue, $notes); } public function checkBalance($user) diff --git a/src/Traits/BalanceOperation.php b/src/Traits/BalanceOperation.php index ae895ef..abbd4a5 100644 --- a/src/Traits/BalanceOperation.php +++ b/src/Traits/BalanceOperation.php @@ -2,12 +2,14 @@ namespace HPWebdeveloper\LaravelPayPocket\Traits; +use Illuminate\Support\Str; + trait BalanceOperation { protected $createdLog; /** - * Check if Balance is more than zero. + * Check if Balance is more than zero. */ public function hasBalance(): bool { @@ -15,32 +17,40 @@ public function hasBalance(): bool } /** - * Decrement Balance and create a log entry. + * Decrement Balance and create a log entry. */ - public function decrementAndCreateLog($value): void + public function decrementAndCreateLog($value, $notes = null): void { - $this->createLog('dec', $value); + $this->createLog('dec', $value, $notes); $this->decrement('balance', $value); } /** - * Increment Balance and create a log entry. + * Increment Balance and create a log entry. */ - public function incrementAndCreateLog($value): void + public function incrementAndCreateLog($value, $notes = null): void { - $this->createLog('inc', $value); + $this->createLog('inc', $value, $notes); $this->increment('balance', $value); } /** - * Create a new log record + * Create a new log record */ - protected function createLog($logType, $value): void + protected function createLog($logType, $value, $notes = null): void { $currentBalance = $this->balance ?? 0; $newBalance = $logType === 'dec' ? $currentBalance - $value : $currentBalance + $value; + $refGen = config('pay-pocket.log_reference_generator', [ + Str::class, 'random', [config('pay-pocket.log_reference_length', 12)], + ]); + $reference = config('pay-pocket.reference_string_prefix', ''); + $reference .= isset($refGen[0], $refGen[1]) + ? $refGen[0]::{$refGen[1]}(...$refGen[2] ?? []) + : Str::random(config('pay-pocket.log_reference_length', 12)); + $this->createdLog = $this->logs()->create([ 'wallet_name' => $this->type->value, 'from' => $currentBalance, @@ -48,6 +58,8 @@ protected function createLog($logType, $value): void 'type' => $logType, 'ip' => \Request::ip(), 'value' => $value, + 'notes' => $notes, + 'reference' => $reference, ]); $this->createdLog->changeStatus('Done'); diff --git a/src/Traits/HandlesDeposit.php b/src/Traits/HandlesDeposit.php index 108c1ac..03cbaf8 100644 --- a/src/Traits/HandlesDeposit.php +++ b/src/Traits/HandlesDeposit.php @@ -15,7 +15,7 @@ trait HandlesDeposit /** * Deposit an amount to the user's wallet of a specific type. */ - public function deposit(string $type, int|float $amount): bool + public function deposit(string $type, int|float $amount, ?string $notes = null): bool { $depositable = $this->getDepositableTypes(); @@ -27,10 +27,10 @@ public function deposit(string $type, int|float $amount): bool throw new InvalidValueException(); } - DB::transaction(function () use ($type, $amount) { + DB::transaction(function () use ($type, $amount, $notes) { $type = WalletEnums::tryFrom($type); $wallet = $this->wallets()->firstOrCreate(['type' => $type]); - $wallet->incrementAndCreateLog($amount); + $wallet->incrementAndCreateLog($amount, $notes); }); return true; diff --git a/src/Traits/HandlesPayment.php b/src/Traits/HandlesPayment.php index 2e31745..4bfa835 100644 --- a/src/Traits/HandlesPayment.php +++ b/src/Traits/HandlesPayment.php @@ -13,13 +13,13 @@ trait HandlesPayment * * @throws InsufficientBalanceException */ - public function pay(int|float $orderValue): void + public function pay(int|float $orderValue, ?string $notes = null): void { if (! $this->hasSufficientBalance($orderValue)) { throw new InsufficientBalanceException('Insufficient balance to cover the order.'); } - DB::transaction(function () use ($orderValue) { + DB::transaction(function () use ($orderValue, $notes) { $remainingOrderValue = $orderValue; $walletsInOrder = $this->wallets()->whereIn('type', $this->walletsInOrder())->get(); @@ -30,7 +30,7 @@ public function pay(int|float $orderValue): void } $amountToDeduct = min($wallet->balance, $remainingOrderValue); - $wallet->decrementAndCreateLog($amountToDeduct); + $wallet->decrementAndCreateLog($amountToDeduct, $notes); $remainingOrderValue -= $amountToDeduct; if ($remainingOrderValue <= 0) { diff --git a/tests/OperationsWithFacadeTest.php b/tests/OperationsWithFacadeTest.php index 24a5c5c..2421416 100644 --- a/tests/OperationsWithFacadeTest.php +++ b/tests/OperationsWithFacadeTest.php @@ -1,6 +1,7 @@ toBeFloat(234.56); expect(LaravelPayPocket::checkBalance($user))->toBeFloat(234.56); - }); test('user can deposit two times', function () { @@ -34,7 +34,6 @@ expect(LaravelPayPocket::walletBalanceByType($user, 'wallet_2'))->toBeFloat(1023.68); expect(LaravelPayPocket::checkBalance($user))->toBeFloat(1023.68); - }); test('user can pay order', function () { @@ -97,3 +96,36 @@ expect(LaravelPayPocket::checkBalance($user))->toBeFloat(0.12); }); + +test('notes can be added during deposit', function () { + $user = User::factory()->create(); + + $type = 'wallet_2'; + + $description = \Illuminate\Support\Str::random(); + LaravelPayPocket::deposit($user, $type, 234.56, $description); + + expect(WalletsLog::where('notes', $description)->exists())->toBe(true); +}); + +test('notes can be added during payment', function () { + $user = User::factory()->create(); + + $type = 'wallet_2'; + + $description = \Illuminate\Support\Str::random(); + LaravelPayPocket::deposit($user, $type, 234.56); + LaravelPayPocket::pay($user, 234.56, $description); + + expect(WalletsLog::where('notes', $description)->exists())->toBe(true); +}); + +test('transaction reference is added to wallet log', function () { + $user = User::factory()->create(); + + $type = 'wallet_2'; + + LaravelPayPocket::deposit($user, $type, 234.56); + + expect(WalletsLog::whereNotNull('reference')->exists())->toBe(true); +}); diff --git a/tests/OperationsWithoutFacadeTest.php b/tests/OperationsWithoutFacadeTest.php index 66e09db..e64143f 100644 --- a/tests/OperationsWithoutFacadeTest.php +++ b/tests/OperationsWithoutFacadeTest.php @@ -1,5 +1,6 @@ getWalletBalanceByType('wallet_2'))->toBeFloat(234.56); expect($user->walletBalance)->toBeFloat(234.56); - }); test('user can deposit two times', function () { @@ -33,7 +33,6 @@ expect($user->getWalletBalanceByType('wallet_2'))->toBeFloat(1023.68); expect($user->walletBalance)->toBeFloat(1023.68); - }); test('user can pay order', function () { @@ -98,3 +97,36 @@ expect($user->walletBalance)->toBeFloat(0.12); }); + +test('notes can be added during deposit', function () { + $user = User::factory()->create(); + + $type = 'wallet_2'; + + $description = \Illuminate\Support\Str::random(); + $user->deposit($type, 234.56, $description); + + expect(WalletsLog::where('notes', $description)->exists())->toBe(true); +}); + +test('notes can be added during payment', function () { + $user = User::factory()->create(); + + $type = 'wallet_2'; + + $description = \Illuminate\Support\Str::random(); + $user->deposit($type, 234.56); + $user->pay(234.56, $description); + + expect(WalletsLog::where('notes', $description)->exists())->toBe(true); +}); + +test('transaction reference is added to wallet log', function () { + $user = User::factory()->create(); + + $type = 'wallet_2'; + + $user->deposit($type, 234.56); + + expect(WalletsLog::whereNotNull('reference')->exists())->toBe(true); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index 55ad4e2..1dfaf4c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -11,6 +11,30 @@ class TestCase extends Orchestra { use RefreshDatabase; + public function getEnvironmentSetUp($app) + { + config()->set('app.key', 'base64:EWcFBKBT8lKlGK8nQhTHY+wg19QlfmbhtO9Qnn3NfcA='); + + config()->set('database.default', 'testing'); + + /* + $migration = include __DIR__.'/../database/migrations/create_laravel-pay-pocket_table.php.stub'; + $migration->up(); + */ + + $migration = include __DIR__.'/database/migrations/create_users_tables.php'; + $migration->up(); + + $migration = include __DIR__.'/../database/migrations/create_wallets_logs_table.php.stub'; + $migration->up(); + + $migration = include __DIR__.'/../database/migrations/create_wallets_table.php.stub'; + $migration->up(); + + $migration = include __DIR__.'/../database/migrations/add_notes_and_reference_columns_to_wallets_logs_table.php.stub'; + $migration->up(); + } + protected function setUp(): void { parent::setUp(); @@ -34,25 +58,4 @@ protected function getPackageProviders($app) LaravelPayPocketServiceProvider::class, ]; } - - public function getEnvironmentSetUp($app) - { - config()->set('app.key', 'base64:EWcFBKBT8lKlGK8nQhTHY+wg19QlfmbhtO9Qnn3NfcA='); - - config()->set('database.default', 'testing'); - - /* - $migration = include __DIR__.'/../database/migrations/create_laravel-pay-pocket_table.php.stub'; - $migration->up(); - */ - - $migration = include __DIR__.'/database/migrations/create_users_tables.php'; - $migration->up(); - - $migration = include __DIR__.'/../database/migrations/create_wallets_logs_table.php.stub'; - $migration->up(); - - $migration = include __DIR__.'/../database/migrations/create_wallets_table.php.stub'; - $migration->up(); - } }