├── .github └── workflows │ └── run-tests.yml ├── .styleci.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── config ├── .gitkeep └── config.php ├── database └── migrations │ └── create_wallet_plus_tables.php.stub └── src ├── Contracts └── WalletTransaction.php ├── Models ├── Traits │ └── HasWallets.php ├── Wallet.php ├── WalletLedger.php └── WalletType.php └── WalletPlusServiceProvider.php /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: true 11 | matrix: 12 | php: [7.4] 13 | laravel: [6.*] 14 | dependency-version: [prefer-lowest, prefer-stable] 15 | include: 16 | - laravel: 6.* 17 | testbench: 4.* 18 | 19 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v1 24 | 25 | - name: Cache dependencies 26 | uses: actions/cache@v1 27 | with: 28 | path: ~/.composer/cache/files 29 | key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 30 | 31 | - name: Setup PHP 32 | uses: shivammathur/setup-php@v1 33 | with: 34 | php-version: ${{ matrix.php }} 35 | extension-csv: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick 36 | coverage: none 37 | 38 | - name: Install dependencies 39 | run: | 40 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 41 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest 42 | 43 | - name: Execute tests 44 | run: vendor/bin/phpunit 45 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | disabled: 4 | - single_class_element_per_statement 5 | - self_accessor 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-wallet-plus` will be documented in this file 4 | 5 | ## 1.0.0 - 201X-XX-XX 6 | 7 | - initial release 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) CoreProc bvba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Wallet Plus 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/coreproc/laravel-wallet-plus.svg?style=flat-square)](https://packagist.org/packages/coreproc/laravel-wallet-plus) 4 | [![Quality Score](https://img.shields.io/scrutinizer/g/coreproc/laravel-wallet-plus.svg?style=flat-square)](https://scrutinizer-ci.com/g/coreproc/laravel-wallet-plus) 5 | [![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/coreproc/laravel-wallet-plus/run-tests?label=tests)](https://github.com/coreproc/laravel-wallet-plus/actions?query=workflow%3Arun-tests+branch%3Amaster) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/coreproc/laravel-wallet-plus.svg?style=flat-square)](https://packagist.org/packages/coreproc/laravel-wallet-plus) 7 | 8 | Easily add a virtual wallet to your Laravel models. Features multiple wallets and a ledger system to help keep track of all transactions in the wallets. 9 | 10 | ## Installation 11 | 12 | You can install the package via composer: 13 | 14 | ```bash 15 | composer require coreproc/laravel-wallet-plus 16 | ``` 17 | 18 | You can publish the migration with: 19 | 20 | ```bash 21 | php artisan vendor:publish --provider="CoreProc\WalletPlus\WalletPlusServiceProvider" --tag="migrations" 22 | ``` 23 | 24 | After the migration file has been published you can create the wallet-plus tables by running the migration: 25 | 26 | ```bash 27 | php artisan migrate 28 | ``` 29 | 30 | ## Usage 31 | 32 | First, you'll need to add the `HasWallets` trait to your model. 33 | 34 | ```php 35 | use CoreProc\WalletPlus\Models\Traits\HasWallets; 36 | use Illuminate\Foundation\Auth\User as Authenticatable; 37 | use Illuminate\Notifications\Notifiable; 38 | 39 | class User extends Authenticatable 40 | { 41 | use Notifiable, HasWallets; 42 | } 43 | ``` 44 | 45 | By adding the `HasWallets` trait, you've essentially added all the wallet relationships to the model. 46 | 47 | You can start by creating a wallet for the given model. 48 | 49 | ```php 50 | $user = User::find(1); 51 | 52 | $wallet = $user->wallets()->create(); 53 | ``` 54 | 55 | You can then increment the wallet balance by: 56 | 57 | ```php 58 | $wallet->incrementBalance(100); 59 | ``` 60 | 61 | Or decrement the balance by: 62 | 63 | ```php 64 | $wallet->decrementBalance(100); 65 | ``` 66 | 67 | To get the balance of the wallet, you can use the `balance` accessor: 68 | 69 | ```php 70 | $wallet->balance; 71 | ``` 72 | 73 | A wallet can be accessed using the `wallet()` method in the model: 74 | 75 | ```php 76 | $user->wallet(); 77 | ``` 78 | 79 | You can set up multiple types of wallets by defining a `WalletType`. Simply create a wallet type entry in the 80 | `wallet_types` table and create a wallet using this wallet type. 81 | 82 | ```php 83 | use CoreProc\WalletPlus\Models\WalletType; 84 | 85 | $walletType = WalletType::create([ 86 | 'name' => 'Peso Wallet', 87 | 'decimals' => 2, // Set how many decimal points your wallet accepts here. Defaults to 0. 88 | ]); 89 | 90 | $user->wallets()->create(['wallet_type_id' => $walletType->id]); 91 | ``` 92 | 93 | You can access a model's particular wallet type by using the `wallet()` method as well: 94 | 95 | ```php 96 | $pesoWallet = $user->wallet('Peso Wallet'); // This method also accepts the ID of the wallet type as a parameter 97 | 98 | $pesoWallet->incrementBalance(100); 99 | 100 | $pesoWallet->balance; // Returns the updated balance without having to refresh the model. 101 | ``` 102 | 103 | All movements made in the wallet are recorded in the `wallet_ledgers` table. 104 | 105 | ### Defining Transactions 106 | 107 | Ideally, we want to record all transactions concerning the wallet by linking it to a transaction model. Let's say we 108 | have a `PurchaseTransaction` model which holds the data of a purchase the user makes in our app. 109 | 110 | ```php 111 | use Illuminate\Database\Eloquent\Model; 112 | 113 | class PurchaseTransaction extends Model 114 | { 115 | // 116 | } 117 | ``` 118 | 119 | We can link this `PurchaseTransaction` to the wallet ledger by implementing the `WalletTransaction` contract to this 120 | model and using this transaction to decrement (or increment, whatever the case may be) the wallet balance. 121 | 122 | ```php 123 | use CoreProc\WalletPlus\Contracts\WalletTransaction; 124 | use Illuminate\Database\Eloquent\Model; 125 | 126 | class PurchaseTransaction extends Model implements WalletTransaction 127 | { 128 | public function getAmount() 129 | { 130 | return $this->amount; 131 | } 132 | } 133 | ``` 134 | 135 | Now we can use this in the wallet: 136 | 137 | ```php 138 | $wallet = $user->wallet('Peso Wallet'); 139 | 140 | $purchaseTransaction = PurchaseTransaction::create([ 141 | ..., 142 | 'amount' => 100, 143 | ]); 144 | 145 | $wallet->decrementBalance($purchaseTransaction); 146 | ``` 147 | 148 | By doing this, you will be able to see in the `wallet_ledgers` table the transaction that is related to the movement 149 | in the wallet. 150 | 151 | ### Testing 152 | 153 | ``` bash 154 | composer test 155 | ``` 156 | 157 | ### Changelog 158 | 159 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 160 | 161 | ## Contributing 162 | 163 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 164 | 165 | ### Security 166 | 167 | If you discover any security related issues, please email chris.bautista@coreproc.ph instead of using the issue tracker. 168 | 169 | ## Credits 170 | 171 | - [Chris Bautista](https://github.com/chrisbjr) 172 | - [All Contributors](../../contributors) 173 | 174 | ## License 175 | 176 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 177 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coreproc/laravel-wallet-plus", 3 | "description": "Easily add a virtual wallet to your Laravel models. Features multiple wallets and a ledger system to help keep track of all transactions in the wallets.", 4 | "keywords": [ 5 | "coreproc", 6 | "laravel-wallet-plus" 7 | ], 8 | "homepage": "https://github.com/coreproc/laravel-wallet-plus", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Chris Bautista", 13 | "email": "chris.bautista@coreproc.ph", 14 | "homepage": "https://coreproc.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^7.3 || ^8.0" 20 | }, 21 | "require-dev": { 22 | "symfony/var-dumper": "^4.3 || ^5.1 || ^6.0", 23 | "phpunit/phpunit": "^8.2 || ^9.3" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "CoreProc\\WalletPlus\\": "src" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "CoreProc\\WalletPlus\\Tests\\": "tests" 33 | } 34 | }, 35 | "scripts": { 36 | "test": "vendor/bin/phpunit", 37 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 38 | }, 39 | "config": { 40 | "sort-packages": true 41 | }, 42 | "extra": { 43 | "laravel": { 44 | "providers": [ 45 | "CoreProc\\WalletPlus\\WalletPlusServiceProvider" 46 | ] 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoreProc/laravel-wallet-plus/9601dec6485cbdf1c762ea1ef04e5dd5d560335f/config/.gitkeep -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoreProc/laravel-wallet-plus/9601dec6485cbdf1c762ea1ef04e5dd5d560335f/config/config.php -------------------------------------------------------------------------------- /database/migrations/create_wallet_plus_tables.php.stub: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('name'); 19 | $table->integer('decimals')->default(0); 20 | $table->timestamps(); 21 | }); 22 | 23 | Schema::create('wallets', function (Blueprint $table) { 24 | $table->bigIncrements('id'); 25 | $table->nullableMorphs('user'); 26 | $table->unsignedBigInteger('wallet_type_id')->nullable(); 27 | $table->bigInteger('raw_balance')->default(0); 28 | $table->timestamps(); 29 | 30 | $table->foreign('wallet_type_id')->references('id')->on('wallet_types')->onDelete('set null'); 31 | }); 32 | 33 | Schema::create('wallet_ledgers', function (Blueprint $table) { 34 | $table->bigIncrements('id'); 35 | $table->unsignedBigInteger('wallet_id')->nullable(); 36 | $table->nullableMorphs('transaction'); 37 | $table->bigInteger('amount')->default(0); 38 | $table->bigInteger('running_raw_balance')->default(0); 39 | $table->timestamps(); 40 | 41 | $table->foreign('wallet_id')->references('id')->on('wallets')->onDelete('set null'); 42 | }); 43 | } 44 | 45 | /** 46 | * Reverse the migrations. 47 | * 48 | * @return void 49 | */ 50 | public function down() 51 | { 52 | Schema::dropIfExists('wallet_types'); 53 | Schema::dropIfExists('wallets'); 54 | Schema::dropIfExists('wallet_ledgers'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Contracts/WalletTransaction.php: -------------------------------------------------------------------------------- 1 | morphMany(Wallet::class, 'user'); 12 | } 13 | 14 | /** 15 | * @param int|string|null $walletType Can either be the name, or the wallet type ID. Can also be null if you're not 16 | * using wallet types. 17 | * @return Wallet 18 | */ 19 | public function wallet($walletType = null) 20 | { 21 | if(is_null($walletType)) { 22 | return $this->wallets()->whereNull('wallet_type_id')->first(); 23 | } 24 | 25 | if(is_int($walletType)) { 26 | return $this->wallets()->where('wallet_type_id', $walletType)->first(); 27 | } 28 | 29 | if(is_string($walletType)) { 30 | return $this->wallets()->whereHas('walletType', function($q) use ($walletType) { 31 | return $q->where('name', $walletType); 32 | })->first(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Models/Wallet.php: -------------------------------------------------------------------------------- 1 | morphTo('user'); 21 | } 22 | 23 | public function walletType() 24 | { 25 | return $this->belongsTo(WalletType::class); 26 | } 27 | 28 | public function walletLedgers() 29 | { 30 | return $this->hasMany(WalletLedger::class); 31 | } 32 | 33 | public function getBalanceAttribute() 34 | { 35 | if (empty($this->walletType->decimals)) { 36 | return $this->raw_balance; 37 | } 38 | 39 | return $this->raw_balance / pow(10, $this->walletType->decimals); 40 | } 41 | 42 | /** 43 | * @param $transaction WalletTransaction|integer|float|double 44 | * @return Wallet 45 | * @throws Exception 46 | */ 47 | public function incrementBalance($transaction) 48 | { 49 | if (is_numeric($transaction)) { 50 | $amount = $this->convertToWalletTypeInteger($transaction); 51 | $this->increment('raw_balance', $amount); 52 | $this->createWalletLedgerEntry($amount, $this->raw_balance); 53 | 54 | return $this; 55 | } 56 | 57 | if (! $transaction instanceof WalletTransaction) { 58 | throw new Exception('Increment balance expects parameter to be a float or a WalletTransaction object.'); 59 | } 60 | 61 | $this->increment('raw_balance', $transaction->getAmount() * pow(10, $this->walletType->decimals)); 62 | 63 | // Record in ledger 64 | $this->createWalletLedgerEntry($transaction, $this->raw_balance); 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * @param $transaction WalletTransaction|integer|float|double 71 | * @return Wallet 72 | * @throws Exception 73 | */ 74 | public function decrementBalance($transaction) 75 | { 76 | if (is_numeric($transaction)) { 77 | $amount = $this->convertToWalletTypeInteger($transaction); 78 | $this->decrement('raw_balance', $amount); 79 | $this->createWalletLedgerEntry($amount, $this->raw_balance, 'decrement'); 80 | 81 | return $this; 82 | } 83 | 84 | if (! $transaction instanceof WalletTransaction) { 85 | throw new Exception('Decrement balance expects parameter to be a number or a WalletTransaction object.'); 86 | } 87 | 88 | $this->decrement('raw_balance', $transaction->getAmount() * pow(10, $this->walletType->decimals)); 89 | 90 | // Record in ledger 91 | $this->createWalletLedgerEntry($transaction, $this->raw_balance, 'decrement'); 92 | 93 | return $this; 94 | } 95 | 96 | /** 97 | * @param $transaction 98 | * @param $newRunningRawBalance 99 | * @param string $type 100 | * @return mixed 101 | * @throws Exception 102 | */ 103 | private function createWalletLedgerEntry($transaction, $newRunningRawBalance, $type = 'increment') 104 | { 105 | if (is_numeric($transaction)) { 106 | if ($type === 'decrement') { 107 | $transaction = -$transaction; 108 | } 109 | 110 | return WalletLedger::query()->create([ 111 | 'wallet_id' => $this->id, 112 | 'amount' => $transaction, 113 | 'running_raw_balance' => $newRunningRawBalance, 114 | ]); 115 | } 116 | 117 | if (! $transaction instanceof WalletTransaction) { 118 | throw new Exception('Wallet ledger entries expect first parameter to be numeric or a WalletTransaction ' . 119 | 'instance'); 120 | } 121 | 122 | $amount = $this->convertToWalletTypeInteger($transaction->getAmount()); 123 | 124 | if ($type === 'decrement') { 125 | $amount = -$amount; 126 | } 127 | 128 | return WalletLedger::query()->create([ 129 | 'wallet_id' => $this->id, 130 | 'transaction_id' => $transaction->id, 131 | 'transaction_type' => get_class($transaction), 132 | 'amount' => $amount, 133 | 'running_raw_balance' => $newRunningRawBalance, 134 | ]); 135 | } 136 | 137 | /** 138 | * Converts the given value to an integer that is compatible with this wallet's type. 139 | * 140 | * @param int $value 141 | * @return float|int 142 | */ 143 | private function convertToWalletTypeInteger($value) 144 | { 145 | if (empty($this->walletType) || $this->walletType->decimals === 0) { 146 | return $value; 147 | } 148 | 149 | return (int)($value * pow(10, $this->walletType->decimals)); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Models/WalletLedger.php: -------------------------------------------------------------------------------- 1 | belongsTo(Wallet::class); 20 | } 21 | 22 | public function transaction() 23 | { 24 | return $this->morphTo('transaction'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Models/WalletType.php: -------------------------------------------------------------------------------- 1 | publishes([ 17 | __DIR__ . '/../database/migrations/create_wallet_plus_tables.php.stub' => 18 | $this->getMigrationFileName($filesystem), 19 | ], 'migrations'); 20 | } 21 | 22 | /** 23 | * Register the application services. 24 | */ 25 | public function register() 26 | { 27 | // $this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'wallet-plus'); 28 | } 29 | 30 | protected function getMigrationFileName(Filesystem $filesystem) 31 | { 32 | $timestamp = date('Y_m_d_His'); 33 | 34 | return Collection::make($this->app->databasePath() . DIRECTORY_SEPARATOR . 'migrations' . DIRECTORY_SEPARATOR) 35 | ->flatMap(function ($path) use ($filesystem) { 36 | return $filesystem->glob($path . '*_create_wallet_plus_tables.php'); 37 | })->push($this->app->databasePath() . "/migrations/{$timestamp}_create_wallet_plus_tables.php") 38 | ->first(); 39 | } 40 | } 41 | --------------------------------------------------------------------------------