diff --git a/changelog.md b/changelog.md index 2c9f67ff5..d5292f204 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.3.0] +### Added +- Credit Limits +- WalletCreatedEvent + +### Updated +- Optimization of check for withdrawals; + ## [7.2.0] - 2021-12-07 ### Added - Added balance update event @@ -752,7 +760,8 @@ The operation is now executed in the transaction and updates the new `refund` fi - Exceptions: AmountInvalid, BalanceIsEmpty. - Models: Transfer, Transaction. -[Unreleased]: https://github.com/bavix/laravel-wallet/compare/7.2.0...develop +[Unreleased]: https://github.com/bavix/laravel-wallet/compare/7.3.0...develop +[7.3.0]: https://github.com/bavix/laravel-wallet/compare/7.2.0...7.3.0 [7.2.0]: https://github.com/bavix/laravel-wallet/compare/7.1.0...7.2.0 [7.1.0]: https://github.com/bavix/laravel-wallet/compare/7.0.0...7.1.0 [7.0.0]: https://github.com/bavix/laravel-wallet/compare/6.2.4...7.0.0 diff --git a/composer.json b/composer.json index a1c823830..8daf91e7b 100644 --- a/composer.json +++ b/composer.json @@ -32,19 +32,19 @@ "ext-json": "*" }, "require-dev": { - "brianium/paratest": "^6.3", + "brianium/paratest": "^6.4", "cknow/laravel-money": "^6.1", "ergebnis/phpstan-rules": "^1.0", "infection/infection": "~0.25", "laravel/cashier": "^13.6", "nunomaduro/collision": "^5.10", "orchestra/testbench": "^6.23", - "phpstan/phpstan": "^1.1", + "phpstan/phpstan": "^1.2", "phpunit/phpunit": "^9.5", "psalm/plugin-laravel": "^1.5", - "rector/rector": "^0.12.3", + "rector/rector": "^0.12", "symplify/easy-coding-standard": "^10.0", - "vimeo/psalm": "^4.12" + "vimeo/psalm": "^4.15" }, "suggest": { "bavix/laravel-wallet-swap": "Addition to the laravel-wallet library for quick setting of exchange rates", diff --git a/config/config.php b/config/config.php index 5170f3afd..5b2997497 100644 --- a/config/config.php +++ b/config/config.php @@ -10,8 +10,10 @@ use Bavix\Wallet\Internal\Assembler\TransferLazyDtoAssembler; use Bavix\Wallet\Internal\Assembler\TransferQueryAssembler; use Bavix\Wallet\Internal\Events\BalanceUpdatedEvent; +use Bavix\Wallet\Internal\Events\WalletCreatedEvent; use Bavix\Wallet\Internal\Repository\TransactionRepository; use Bavix\Wallet\Internal\Repository\TransferRepository; +use Bavix\Wallet\Internal\Repository\WalletRepository; use Bavix\Wallet\Internal\Service\ClockService; use Bavix\Wallet\Internal\Service\DatabaseService; use Bavix\Wallet\Internal\Service\DispatcherService; @@ -39,6 +41,7 @@ use Bavix\Wallet\Services\PurchaseService; use Bavix\Wallet\Services\RegulatorService; use Bavix\Wallet\Services\TaxService; +use Bavix\Wallet\Services\WalletService; return [ /** @@ -93,6 +96,7 @@ 'prepare' => PrepareService::class, 'purchase' => PurchaseService::class, 'tax' => TaxService::class, + 'wallet' => WalletService::class, ], /** @@ -101,6 +105,7 @@ 'repositories' => [ 'transaction' => TransactionRepository::class, 'transfer' => TransferRepository::class, + 'wallet' => WalletRepository::class, ], /** @@ -129,6 +134,7 @@ */ 'events' => [ 'balance_updated' => BalanceUpdatedEvent::class, + 'wallet_created' => WalletCreatedEvent::class, ], /** diff --git a/depfile.yaml b/depfile.yaml index e0366a819..797b6bc80 100644 --- a/depfile.yaml +++ b/depfile.yaml @@ -192,15 +192,18 @@ ruleset: - QueryInterface RepositoryInterface: + - InternalException - QueryInterface - DtoInterface - Model Repository: - RepositoryInterface - TransformInterface + - InternalException - ServiceInterface # json service only - QueryInterface - DtoInterface + - UIException - Model ServiceInterface: diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 510668eaa..79dd00801 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -15,11 +15,13 @@ - [Confirm](confirm) - [Exchange](exchange) - [Withdraw taxing](taxing) + - [Credit Limits](credit-limits) - Multi Wallets - [New Wallet](new-wallet) - [Transfer](wallet-transfer) + - [Transaction Filter](transaction-filter) - Purchases @@ -37,6 +39,7 @@ - Events - [BalanceUpdatedEvent](balance-updated-event) + - [WalletCreatedEvent](wallet-created-event) - [Event Customize](event-customize) - Additions diff --git a/docs/credit-limits.md b/docs/credit-limits.md new file mode 100644 index 000000000..5360985df --- /dev/null +++ b/docs/credit-limits.md @@ -0,0 +1,36 @@ +## Credit Limits + +If you need the ability to have wallets have a credit limit, then this functionality is for you. + +The functionality does nothing, it only allows you not to use "force" for most of the operations within the credit limit. You yourself write the logic for collecting interest, notifications on debts, etc. + +By default, the credit limit is zero. + +An example of working with a credit limit: +```php +/** + * @var \Bavix\Wallet\Interfaces\Customer $customer + * @var \Bavix\Wallet\Models\Wallet $wallet + * @var \Bavix\Wallet\Interfaces\Product $product + */ +$wallet = $customer->wallet; // get default wallet +$wallet->meta['credit'] = 10000; // credit limit +$wallet->save(); // update credit limit + +$wallet->balanceInt; // 0 +$product->getAmountProduct($customer); // 500 + +$wallet->pay($product); // success +$wallet->balanceInt; // -500 +``` + +For multi-wallets when creating: +```php +/** @var \Bavix\Wallet\Traits\HasWallets $user */ +$wallet = $user->createWallet([ + 'name' => 'My Wallet', + 'meta' => ['credit' => 500], +]); +``` + +It worked! diff --git a/docs/transaction-filter.md b/docs/transaction-filter.md new file mode 100644 index 000000000..87948a591 --- /dev/null +++ b/docs/transaction-filter.md @@ -0,0 +1,48 @@ +## Transaction Filter + +Often developers ask me about the `transactions` method. +Yes, this method displays ALL transactions for the wallet owner. +If you only need to filter one wallet at a time, now you can use the `walletTransactions` method. + +```php +/** @var \Bavix\Wallet\Models\Wallet $wallet */ + +// Before version 7.3 +$query = $wallet + ->transactions() + ->where('wallet_id', $wallet->getKey()); + +// 7.3+ +$query = $wallet->walletTransactions(); +``` + +Let's take a look at a livelier code example: +```php +$user->transactions()->count(); // 0 + +// default wallet +$user->deposit(100); +$user->wallet->deposit(200); +$user->wallet->withdraw(1); + +// usd +$usd = $user->createWallet(['name' => 'USD']); +$usd->deposit(100); + +// eur +$eur = $user->createWallet(['name' => 'EUR']); +$eur->deposit(100); + +$user->transactions()->count(); // 5 +$user->wallet->transactions()->count(); // 5 +$usd->transactions()->count(); // 5 +$eur->transactions()->count(); // 5 +// the transactions method returns data relative to the owner of the wallet, for all transactions + +$user->walletTransactions()->count(); // 3. we get the default wallet +$user->wallet->walletTransactions()->count(); // 3 +$usd->walletTransactions()->count(); // 1 +$eur->walletTransactions()->count(); // 1 +``` + +It worked! diff --git a/docs/wallet-created-event.md b/docs/wallet-created-event.md new file mode 100644 index 000000000..dc8abc1d3 --- /dev/null +++ b/docs/wallet-created-event.md @@ -0,0 +1,33 @@ +## Tracking the creation of wallets + +The events are similar to the events for updating the balance, only for the creation of a wallet. A frequent case of transferring data via websockets to the front-end. + +Version 7.3 introduces an interface to which you can subscribe. +This is done using standard Laravel methods. +More information in the [documentation](https://laravel.com/docs/8.x/events). + +```php +use Bavix\Wallet\Internal\Events\WalletCreatedEventInterface; + +protected $listen = [ + WalletCreatedEventInterface::class => [ + MyWalletCreatedListener::class, + ], +]; +``` + +And then we create a listener. + +```php +use Bavix\Wallet\Internal\Events\WalletCreatedEventInterface; + +class MyWalletCreatedListener +{ + public function handle(WalletCreatedEventInterface $event): void + { + // And then the implementation... + } +} +``` + +It worked! diff --git a/src/Interfaces/Wallet.php b/src/Interfaces/Wallet.php index 5dd9f9083..948b4cc01 100644 --- a/src/Interfaces/Wallet.php +++ b/src/Interfaces/Wallet.php @@ -12,6 +12,7 @@ use Bavix\Wallet\Internal\Exceptions\TransactionFailedException; use Bavix\Wallet\Models\Transaction; use Bavix\Wallet\Models\Transfer; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\RecordsNotFoundException; @@ -91,6 +92,8 @@ public function getBalanceAttribute(); public function getBalanceIntAttribute(): int; + public function walletTransactions(): HasMany; + public function transactions(): MorphMany; public function transfers(): MorphMany; diff --git a/src/Internal/Assembler/WalletCreatedEventAssembler.php b/src/Internal/Assembler/WalletCreatedEventAssembler.php new file mode 100644 index 000000000..cc938d789 --- /dev/null +++ b/src/Internal/Assembler/WalletCreatedEventAssembler.php @@ -0,0 +1,31 @@ +clockService = $clockService; + } + + public function create(Wallet $wallet): WalletCreatedEventInterface + { + return new WalletCreatedEvent( + $wallet->holder_type, + $wallet->holder_id, + $wallet->uuid, + (int) $wallet->getKey(), + $this->clockService->now() + ); + } +} diff --git a/src/Internal/Assembler/WalletCreatedEventAssemblerInterface.php b/src/Internal/Assembler/WalletCreatedEventAssemblerInterface.php new file mode 100644 index 000000000..e8c89a540 --- /dev/null +++ b/src/Internal/Assembler/WalletCreatedEventAssemblerInterface.php @@ -0,0 +1,13 @@ +holderType = $holderType; + $this->holderId = $holderId; + $this->walletUuid = $walletUuid; + $this->walletId = $walletId; + $this->createdAt = $createdAt; + } + + public function getHolderType(): string + { + return $this->holderType; + } + + public function getHolderId(): int + { + return $this->holderId; + } + + public function getWalletUuid(): string + { + return $this->walletUuid; + } + + public function getWalletId(): int + { + return $this->walletId; + } + + public function getCreatedAt(): DateTimeImmutable + { + return $this->createdAt; + } +} diff --git a/src/Internal/Events/WalletCreatedEventInterface.php b/src/Internal/Events/WalletCreatedEventInterface.php new file mode 100644 index 000000000..7cd058c90 --- /dev/null +++ b/src/Internal/Events/WalletCreatedEventInterface.php @@ -0,0 +1,20 @@ +wallet = $wallet; + } + + public function create(array $attributes): Wallet + { + $instance = $this->wallet->newInstance($attributes); + $instance::withoutEvents(static fn () => $instance->save()); + + return $instance; + } + + public function findById(int $id): ?Wallet + { + try { + return $this->getById($id); + } catch (ModelNotFoundException $modelNotFoundException) { + return null; + } + } + + public function findByUuid(string $uuid): ?Wallet + { + try { + return $this->getByUuid($uuid); + } catch (ModelNotFoundException $modelNotFoundException) { + return null; + } + } + + public function findBySlug(string $holderType, int $holderId, string $slug): ?Wallet + { + try { + return $this->getBySlug($holderType, $holderId, $slug); + } catch (ModelNotFoundException $modelNotFoundException) { + return null; + } + } + + /** @throws ModelNotFoundException */ + public function getById(int $id): Wallet + { + return $this->getBy(['id' => $id]); + } + + /** @throws ModelNotFoundException */ + public function getByUuid(string $uuid): Wallet + { + return $this->getBy(['uuid' => $uuid]); + } + + /** @throws ModelNotFoundException */ + public function getBySlug(string $holderType, int $holderId, string $slug): Wallet + { + return $this->getBy([ + 'holder_type' => $holderType, + 'holder_id' => $holderId, + 'slug' => $slug, + ]); + } + + /** @param array $attributes */ + private function getBy(array $attributes): Wallet + { + try { + $wallet = $this->wallet->newQuery()->where($attributes)->firstOrFail(); + assert($wallet instanceof Wallet); + + return $wallet; + } catch (EloquentModelNotFoundException $exception) { + throw new ModelNotFoundException( + $exception->getMessage(), + ExceptionInterface::MODEL_NOT_FOUND, + $exception + ); + } + } +} diff --git a/src/Internal/Repository/WalletRepositoryInterface.php b/src/Internal/Repository/WalletRepositoryInterface.php new file mode 100644 index 000000000..5889debf0 --- /dev/null +++ b/src/Internal/Repository/WalletRepositoryInterface.php @@ -0,0 +1,28 @@ +block($this, function () { - $whatIs = $this->balance; + $whatIs = $this->getBalanceAttribute(); $balance = $this->getAvailableBalanceAttribute(); if (app(MathServiceInterface::class)->compare($whatIs, $balance) === 0) { return true; @@ -134,8 +135,7 @@ public function getOriginalBalanceAttribute(): string */ public function getAvailableBalanceAttribute() { - return $this->transactions() - ->where('wallet_id', $this->getKey()) + return $this->walletTransactions() ->where('confirmed', true) ->sum('amount') ; @@ -158,6 +158,11 @@ public function holder(): MorphTo return $this->morphTo(); } + public function getCreditAttribute(): string + { + return (string) ($this->meta['credit'] ?? '0'); + } + public function getCurrencyAttribute(): string { return $this->meta['currency'] ?? Str::upper($this->slug); diff --git a/src/Services/CastService.php b/src/Services/CastService.php index 66dd03fb8..bdac66cb9 100644 --- a/src/Services/CastService.php +++ b/src/Services/CastService.php @@ -5,12 +5,31 @@ namespace Bavix\Wallet\Services; use Bavix\Wallet\Interfaces\Wallet; +use Bavix\Wallet\Internal\Assembler\WalletCreatedEventAssemblerInterface; +use Bavix\Wallet\Internal\Exceptions\ExceptionInterface; +use Bavix\Wallet\Internal\Service\DatabaseServiceInterface; +use Bavix\Wallet\Internal\Service\DispatcherServiceInterface; use Bavix\Wallet\Models\Wallet as WalletModel; use Illuminate\Database\Eloquent\Model; /** @psalm-internal */ final class CastService implements CastServiceInterface { + private WalletCreatedEventAssemblerInterface $walletCreatedEventAssembler; + private DispatcherServiceInterface $dispatcherService; + private DatabaseServiceInterface $databaseService; + + public function __construct( + WalletCreatedEventAssemblerInterface $walletCreatedEventAssembler, + DispatcherServiceInterface $dispatcherService, + DatabaseServiceInterface $databaseService + ) { + $this->walletCreatedEventAssembler = $walletCreatedEventAssembler; + $this->dispatcherService = $dispatcherService; + $this->databaseService = $databaseService; + } + + /** @throws ExceptionInterface */ public function getWallet(Wallet $object, bool $save = true): WalletModel { $wallet = $this->getModel($object); @@ -20,7 +39,12 @@ public function getWallet(Wallet $object, bool $save = true): WalletModel } if ($save && !$wallet->exists) { - $wallet->save(); + $this->databaseService->transaction(function () use ($wallet) { + $result = $wallet::withoutEvents(fn () => $wallet->save()); + $this->dispatcherService->dispatch($this->walletCreatedEventAssembler->create($wallet)); + + return $result; + }); } return $wallet; diff --git a/src/Services/ConsistencyService.php b/src/Services/ConsistencyService.php index e25845976..2f6048a13 100644 --- a/src/Services/ConsistencyService.php +++ b/src/Services/ConsistencyService.php @@ -53,15 +53,16 @@ public function checkPositive($amount): void public function checkPotential(Wallet $object, $amount, bool $allowZero = false): void { $wallet = $this->castService->getWallet($object, false); + $balance = $this->mathService->add($wallet->getBalanceAttribute(), $wallet->getCreditAttribute()); - if (($this->mathService->compare($amount, 0) !== 0) && !$wallet->getBalanceAttribute()) { + if (($this->mathService->compare($amount, 0) !== 0) && ($this->mathService->compare($balance, 0) === 0)) { throw new BalanceIsEmpty( $this->translatorService->get('wallet::errors.wallet_empty'), ExceptionInterface::BALANCE_IS_EMPTY ); } - if (!$wallet->canWithdraw($amount, $allowZero)) { + if (!$this->canWithdraw($balance, $amount, $allowZero)) { throw new InsufficientFunds( $this->translatorService->get('wallet::errors.insufficient_funds'), ExceptionInterface::INSUFFICIENT_FUNDS @@ -69,6 +70,24 @@ public function checkPotential(Wallet $object, $amount, bool $allowZero = false) } } + /** + * @param float|int|string $balance + * @param float|int|string $amount + */ + public function canWithdraw($balance, $amount, bool $allowZero = false): bool + { + $mathService = app(MathServiceInterface::class); + + /** + * Allow buying for free with a negative balance. + */ + if ($allowZero && !$mathService->compare($amount, 0)) { + return true; + } + + return $mathService->compare($balance, $amount) >= 0; + } + /** * @param TransferLazyDtoInterface[] $objects * diff --git a/src/Services/ConsistencyServiceInterface.php b/src/Services/ConsistencyServiceInterface.php index cdc0e4c69..e6b9aa127 100644 --- a/src/Services/ConsistencyServiceInterface.php +++ b/src/Services/ConsistencyServiceInterface.php @@ -27,6 +27,12 @@ public function checkPositive($amount): void; */ public function checkPotential(Wallet $object, $amount, bool $allowZero = false): void; + /** + * @param float|int|string $balance + * @param float|int|string $amount + */ + public function canWithdraw($balance, $amount, bool $allowZero = false): bool; + /** * @param TransferLazyDtoInterface[] $objects * diff --git a/src/Services/WalletService.php b/src/Services/WalletService.php new file mode 100644 index 000000000..0b95e2054 --- /dev/null +++ b/src/Services/WalletService.php @@ -0,0 +1,92 @@ +walletCreatedEventAssembler = $walletCreatedEventAssembler; + $this->uuidFactoryService = $uuidFactoryService; + $this->dispatcherService = $dispatcherService; + $this->walletRepository = $walletRepository; + } + + public function create(Model $model, array $data): Wallet + { + $wallet = $this->walletRepository->create(array_merge( + config('wallet.wallet.creating', []), + $data, + [ + 'uuid' => $this->uuidFactoryService->uuid4(), + 'holder_type' => $model->getMorphClass(), + 'holder_id' => $model->getKey(), + ] + )); + + $event = $this->walletCreatedEventAssembler->create($wallet); + $this->dispatcherService->dispatch($event); + + return $wallet; + } + + public function findBySlug(Model $model, string $slug): ?Wallet + { + return $this->walletRepository->findBySlug( + $model->getMorphClass(), + (int) $model->getKey(), + $slug + ); + } + + public function findByUuid(string $uuid): ?Wallet + { + return $this->walletRepository->findByUuid($uuid); + } + + public function findById(int $id): ?Wallet + { + return $this->walletRepository->findById($id); + } + + /** @throws ModelNotFoundException */ + public function getBySlug(Model $model, string $slug): Wallet + { + return $this->walletRepository->getBySlug( + $model->getMorphClass(), + (int) $model->getKey(), + $slug + ); + } + + /** @throws ModelNotFoundException */ + public function getByUuid(string $uuid): Wallet + { + return $this->walletRepository->getByUuid($uuid); + } + + /** @throws ModelNotFoundException */ + public function getById(int $id): Wallet + { + return $this->walletRepository->getById($id); + } +} diff --git a/src/Services/WalletServiceInterface.php b/src/Services/WalletServiceInterface.php new file mode 100644 index 000000000..454c60162 --- /dev/null +++ b/src/Services/WalletServiceInterface.php @@ -0,0 +1,29 @@ +getBalanceAttribute(); } + /** + * We receive transactions of the selected wallet. + */ + public function walletTransactions(): HasMany + { + return app(CastServiceInterface::class) + ->getWallet($this) + ->hasMany(config('wallet.transaction.model', Transaction::class), 'wallet_id') + ; + } + /** * all user actions on wallets will be in this method. */ @@ -167,16 +179,11 @@ public function withdraw($amount, ?array $meta = null, bool $confirmed = true): */ public function canWithdraw($amount, bool $allowZero = false): bool { - $math = app(MathServiceInterface::class); - - /** - * Allow buying for free with a negative balance. - */ - if ($allowZero && !$math->compare($amount, 0)) { - return true; - } + $mathService = app(MathServiceInterface::class); + $wallet = app(CastServiceInterface::class)->getWallet($this); + $balance = $mathService->add($this->getBalanceAttribute(), $wallet->getCreditAttribute()); - return $math->compare($this->balance, $amount) >= 0; + return app(ConsistencyServiceInterface::class)->canWithdraw($balance, $amount, $allowZero); } /** diff --git a/src/Traits/HasWalletFloat.php b/src/Traits/HasWalletFloat.php index 580f807d8..abf7ebcc1 100644 --- a/src/Traits/HasWalletFloat.php +++ b/src/Traits/HasWalletFloat.php @@ -161,6 +161,6 @@ public function getBalanceFloatAttribute() $decimalPlacesValue = $wallet->decimal_places; $decimalPlaces = $math->powTen($decimalPlacesValue); - return $math->div($wallet->balance, $decimalPlaces, $decimalPlacesValue); + return $math->div($wallet->getBalanceAttribute(), $decimalPlaces, $decimalPlacesValue); } } diff --git a/src/Traits/HasWallets.php b/src/Traits/HasWallets.php index 81a29ae38..80a5e1dde 100644 --- a/src/Traits/HasWallets.php +++ b/src/Traits/HasWallets.php @@ -5,12 +5,10 @@ namespace Bavix\Wallet\Traits; use function array_key_exists; -use Bavix\Wallet\Internal\Exceptions\ExceptionInterface; use Bavix\Wallet\Internal\Exceptions\ModelNotFoundException; -use Bavix\Wallet\Internal\Service\UuidFactoryServiceInterface; use Bavix\Wallet\Models\Wallet as WalletModel; +use Bavix\Wallet\Services\WalletServiceInterface; use function config; -use Illuminate\Database\Eloquent\ModelNotFoundException as EloquentModelNotFoundException; use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Support\Collection; @@ -77,18 +75,7 @@ public function getWalletOrFail(string $slug): WalletModel } if (!array_key_exists($slug, $this->_wallets)) { - try { - $this->_wallets[$slug] = $this->wallets() - ->where('slug', $slug) - ->firstOrFail() - ; - } catch (EloquentModelNotFoundException $exception) { - throw new ModelNotFoundException( - $exception->getMessage(), - ExceptionInterface::MODEL_NOT_FOUND, - $exception - ); - } + $this->_wallets[$slug] = app(WalletServiceInterface::class)->getBySlug($this, $slug); } return $this->_wallets[$slug]; @@ -104,13 +91,7 @@ public function wallets(): MorphMany public function createWallet(array $data): WalletModel { - /** @var WalletModel $wallet */ - $wallet = $this->wallets()->create(array_merge( - config('wallet.wallet.creating', []), - $data, - ['uuid' => app(UuidFactoryServiceInterface::class)->uuid4()] - )); - + $wallet = app(WalletServiceInterface::class)->create($this, $data); $this->_wallets[$wallet->slug] = $wallet; return $wallet; diff --git a/src/WalletServiceProvider.php b/src/WalletServiceProvider.php index 886f75bcc..96dd97c5a 100644 --- a/src/WalletServiceProvider.php +++ b/src/WalletServiceProvider.php @@ -18,12 +18,18 @@ use Bavix\Wallet\Internal\Assembler\TransferLazyDtoAssemblerInterface; use Bavix\Wallet\Internal\Assembler\TransferQueryAssembler; use Bavix\Wallet\Internal\Assembler\TransferQueryAssemblerInterface; +use Bavix\Wallet\Internal\Assembler\WalletCreatedEventAssembler; +use Bavix\Wallet\Internal\Assembler\WalletCreatedEventAssemblerInterface; use Bavix\Wallet\Internal\Events\BalanceUpdatedEvent; use Bavix\Wallet\Internal\Events\BalanceUpdatedEventInterface; +use Bavix\Wallet\Internal\Events\WalletCreatedEvent; +use Bavix\Wallet\Internal\Events\WalletCreatedEventInterface; use Bavix\Wallet\Internal\Repository\TransactionRepository; use Bavix\Wallet\Internal\Repository\TransactionRepositoryInterface; use Bavix\Wallet\Internal\Repository\TransferRepository; use Bavix\Wallet\Internal\Repository\TransferRepositoryInterface; +use Bavix\Wallet\Internal\Repository\WalletRepository; +use Bavix\Wallet\Internal\Repository\WalletRepositoryInterface; use Bavix\Wallet\Internal\Service\ClockService; use Bavix\Wallet\Internal\Service\ClockServiceInterface; use Bavix\Wallet\Internal\Service\DatabaseService; @@ -77,6 +83,8 @@ use Bavix\Wallet\Services\RegulatorServiceInterface; use Bavix\Wallet\Services\TaxService; use Bavix\Wallet\Services\TaxServiceInterface; +use Bavix\Wallet\Services\WalletService; +use Bavix\Wallet\Services\WalletServiceInterface; use function config; use function dirname; use function function_exists; @@ -153,6 +161,11 @@ private function repositories(array $configure): void TransferRepositoryInterface::class, $configure['transfer'] ?? TransferRepository::class ); + + $this->app->singleton( + WalletRepositoryInterface::class, + $configure['wallet'] ?? WalletRepository::class + ); } /** @@ -218,6 +231,7 @@ private function services(array $configure): void $this->app->singleton(PrepareServiceInterface::class, $configure['prepare'] ?? PrepareService::class); $this->app->singleton(PurchaseServiceInterface::class, $configure['purchase'] ?? PurchaseService::class); $this->app->singleton(TaxServiceInterface::class, $configure['tax'] ?? TaxService::class); + $this->app->singleton(WalletServiceInterface::class, $configure['wallet'] ?? WalletService::class); } private function assemblers(array $configure): void @@ -256,6 +270,11 @@ private function assemblers(array $configure): void TransferQueryAssemblerInterface::class, $configure['transfer_query'] ?? TransferQueryAssembler::class ); + + $this->app->singleton( + WalletCreatedEventAssemblerInterface::class, + $configure['wallet_created_event'] ?? WalletCreatedEventAssembler::class + ); } private function transformers(array $configure): void @@ -277,6 +296,11 @@ private function events(array $configure): void BalanceUpdatedEventInterface::class, $configure['balance_updated'] ?? BalanceUpdatedEvent::class ); + + $this->app->bind( + WalletCreatedEventInterface::class, + $configure['wallet_created'] ?? WalletCreatedEvent::class + ); } private function legacySingleton(): void diff --git a/tests/Infra/Listeners/WalletCreatedThrowListener.php b/tests/Infra/Listeners/WalletCreatedThrowListener.php new file mode 100644 index 000000000..8f85195cb --- /dev/null +++ b/tests/Infra/Listeners/WalletCreatedThrowListener.php @@ -0,0 +1,25 @@ +getHolderType(); + $uuid = $walletCreatedEvent->getWalletUuid(); + $createdAt = $walletCreatedEvent->getCreatedAt()->format(DateTimeInterface::ATOM); + + $message = hash('sha256', $holderType.$uuid.$createdAt); + $code = $walletCreatedEvent->getWalletId() + $walletCreatedEvent->getHolderId(); + assert($code > 1); + + throw new UnknownEventException($message, $code); + } +} diff --git a/tests/Units/Domain/CreditWalletTest.php b/tests/Units/Domain/CreditWalletTest.php new file mode 100644 index 000000000..9fd918c5f --- /dev/null +++ b/tests/Units/Domain/CreditWalletTest.php @@ -0,0 +1,54 @@ +create(); + $wallet = $user->createWallet([ + 'name' => 'Credit USD', + 'slug' => 'credit-usd', + 'meta' => ['credit' => 10000, 'currency' => 'USD'], + ]); + + $transaction = $wallet->deposit(1000); + self::assertNotNull($transaction); + + self::assertSame(1000, $wallet->balanceInt); + self::assertSame(10., (float) $wallet->balanceFloat); + + $transaction = $wallet->withdraw(10000); + self::assertNotNull($transaction); + self::assertSame(-9000, $wallet->balanceInt); + } + + public function testCreditLimitBalanceZero(): void + { + /** @var UserMulti $user */ + $user = UserMultiFactory::new()->create(); + $wallet = $user->createWallet([ + 'name' => 'Credit USD', + 'slug' => 'credit-usd', + 'meta' => ['credit' => 10000, 'currency' => 'USD'], + ]); + + self::assertSame(0, $wallet->balanceInt); + self::assertSame(0., (float) $wallet->balanceFloat); + + $transaction = $wallet->withdraw(10000); + self::assertNotNull($transaction); + self::assertSame(-10000, $wallet->balanceInt); + } +} diff --git a/tests/Units/Domain/EventTest.php b/tests/Units/Domain/EventTest.php index 3b7a2e42e..309db3e96 100644 --- a/tests/Units/Domain/EventTest.php +++ b/tests/Units/Domain/EventTest.php @@ -5,17 +5,21 @@ namespace Bavix\Wallet\Test\Units\Domain; use Bavix\Wallet\Internal\Events\BalanceUpdatedEventInterface; +use Bavix\Wallet\Internal\Events\WalletCreatedEventInterface; use Bavix\Wallet\Internal\Exceptions\ExceptionInterface; use Bavix\Wallet\Internal\Exceptions\UnknownEventException; use Bavix\Wallet\Internal\Service\ClockServiceInterface; use Bavix\Wallet\Internal\Service\DatabaseServiceInterface; +use Bavix\Wallet\Internal\Service\UuidFactoryServiceInterface; use Bavix\Wallet\Test\Infra\Factories\BuyerFactory; use Bavix\Wallet\Test\Infra\Listeners\BalanceUpdatedThrowDateListener; use Bavix\Wallet\Test\Infra\Listeners\BalanceUpdatedThrowIdListener; use Bavix\Wallet\Test\Infra\Listeners\BalanceUpdatedThrowUuidListener; +use Bavix\Wallet\Test\Infra\Listeners\WalletCreatedThrowListener; use Bavix\Wallet\Test\Infra\Models\Buyer; use Bavix\Wallet\Test\Infra\Services\ClockFakeService; use Bavix\Wallet\Test\Infra\TestCase; +use DateTimeInterface; use Illuminate\Support\Facades\Event; /** @@ -45,7 +49,7 @@ public function testBalanceUpdatedThrowIdListener(): void /** @var Buyer $buyer */ $buyer = BuyerFactory::new()->create(); - self::assertSame(0, $buyer->wallet->balanceInt); + self::assertSame(0, $buyer->wallet->balanceInt); // auto create wallet // unit $this->expectException(UnknownEventException::class); @@ -73,6 +77,31 @@ public function testBalanceUpdatedThrowDateListener(): void $buyer->deposit(789); } + public function testWalletCreatedThrowListener(): void + { + $this->app->bind(ClockServiceInterface::class, ClockFakeService::class); + + Event::listen(WalletCreatedEventInterface::class, WalletCreatedThrowListener::class); + + /** @var Buyer $buyer */ + $buyer = BuyerFactory::new()->create(); + + $uuidFactoryService = app(UuidFactoryServiceInterface::class); + $buyer->wallet->uuid = $uuidFactoryService->uuid4(); + + $holderType = $buyer->getMorphClass(); + $uuid = $buyer->wallet->uuid; + $createdAt = app(ClockServiceInterface::class)->now()->format(DateTimeInterface::ATOM); + + $message = hash('sha256', $holderType.$uuid.$createdAt); + + // unit + $this->expectException(UnknownEventException::class); + $this->expectExceptionMessage($message); + + $buyer->getBalanceIntAttribute(); + } + /** * @throws ExceptionInterface */ diff --git a/tests/Units/Domain/MultiWalletTest.php b/tests/Units/Domain/MultiWalletTest.php index d0ac3cf85..a064145ac 100644 --- a/tests/Units/Domain/MultiWalletTest.php +++ b/tests/Units/Domain/MultiWalletTest.php @@ -234,6 +234,28 @@ public function testWithdraw(): void $wallet->withdraw(1); } + public function testWalletTransactions(): void + { + /** @var UserMulti $user */ + $user = UserMultiFactory::new()->create(); + $usd = $user->createWallet(['name' => 'USD']); + $eur = $user->createWallet(['name' => 'EUR']); + + $usd->deposit(100); + $eur->deposit(200); + $eur->withdraw(50); + + self::assertSame(3, $user->transactions()->count()); + self::assertSame(3, $user->wallet->transactions()->count()); + self::assertSame(3, $usd->transactions()->count()); + self::assertSame(3, $eur->transactions()->count()); + + self::assertSame(0, $user->walletTransactions()->count()); + self::assertSame(0, $user->wallet->walletTransactions()->count()); + self::assertSame(1, $usd->walletTransactions()->count()); + self::assertSame(2, $eur->walletTransactions()->count()); + } + public function testInvalidWithdraw(): void { $this->expectException(BalanceIsEmpty::class); diff --git a/tests/Units/Service/WalletTest.php b/tests/Units/Service/WalletTest.php new file mode 100644 index 000000000..6a49b4130 --- /dev/null +++ b/tests/Units/Service/WalletTest.php @@ -0,0 +1,71 @@ +create(); + + $uuidFactoryService = app(UuidFactoryServiceInterface::class); + $walletService = app(WalletServiceInterface::class); + + $uuid = $uuidFactoryService->uuid4(); + + self::assertNull($walletService->findBySlug($buyer, 'default')); + self::assertNull($walletService->findByUuid($uuid)); + self::assertNull($walletService->findById(-1)); + + $buyer->wallet->uuid = $uuid; // @hack + $buyer->deposit(100); + + self::assertNotNull($walletService->findBySlug($buyer, 'default')); + self::assertNotNull($walletService->findByUuid($uuid)); + self::assertNotNull($walletService->findById((int) $buyer->wallet->getKey())); + } + + public function testGetBySlug(): void + { + $this->expectException(ModelNotFoundException::class); + $this->expectExceptionCode(ExceptionInterface::MODEL_NOT_FOUND); + + /** @var Buyer $buyer */ + $buyer = BuyerFactory::new()->create(); + $walletService = app(WalletServiceInterface::class); + + $walletService->getBySlug($buyer, 'default'); + } + + public function testGetById(): void + { + $this->expectException(ModelNotFoundException::class); + $this->expectExceptionCode(ExceptionInterface::MODEL_NOT_FOUND); + + app(WalletServiceInterface::class)->getById(-1); + } + + public function testGetByUuid(): void + { + $this->expectException(ModelNotFoundException::class); + $this->expectExceptionCode(ExceptionInterface::MODEL_NOT_FOUND); + + $uuidFactoryService = app(UuidFactoryServiceInterface::class); + + app(WalletServiceInterface::class)->getByUuid($uuidFactoryService->uuid4()); + } +}