bavix/laravel-wallet

View on GitHub
src/Traits/HasWallet.php

Summary

Maintainability
A
1 hr
Test Coverage
A
100%
<?php

declare(strict_types=1);

namespace Bavix\Wallet\Traits;

use function app;
use Bavix\Wallet\Exceptions\AmountInvalid;
use Bavix\Wallet\Exceptions\BalanceIsEmpty;
use Bavix\Wallet\Exceptions\InsufficientFunds;
use Bavix\Wallet\External\Contracts\ExtraDtoInterface;
use Bavix\Wallet\Interfaces\Wallet;
use Bavix\Wallet\Internal\Exceptions\ExceptionInterface;
use Bavix\Wallet\Internal\Exceptions\TransactionFailedException;
use Bavix\Wallet\Internal\Service\MathServiceInterface;
use Bavix\Wallet\Models\Transaction;
use Bavix\Wallet\Models\Transfer;
use Bavix\Wallet\Models\Wallet as WalletModel;
use Bavix\Wallet\Services\AtomicServiceInterface;
use Bavix\Wallet\Services\CastServiceInterface;
use Bavix\Wallet\Services\ConsistencyServiceInterface;
use Bavix\Wallet\Services\PrepareServiceInterface;
use Bavix\Wallet\Services\RegulatorServiceInterface;
use Bavix\Wallet\Services\TransactionServiceInterface;
use Bavix\Wallet\Services\TransferServiceInterface;
use function config;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\RecordsNotFoundException;

/**
 * Trait HasWallet.
 *
 * @property WalletModel $wallet
 * @property string $balance
 * @property int $balanceInt
 *
 * @psalm-require-extends \Illuminate\Database\Eloquent\Model
 *
 * @psalm-require-implements \Bavix\Wallet\Interfaces\Wallet
 */
trait HasWallet
{
    use MorphOneWallet;

    /**
     * The input means in the system.
     *
     * @throws AmountInvalid
     * @throws RecordsNotFoundException
     * @throws TransactionFailedException
     * @throws ExceptionInterface
     */
    public function deposit(int|string $amount, ?array $meta = null, bool $confirmed = true): Transaction
    {
        return app(AtomicServiceInterface::class)->block(
            $this,
            fn () => app(TransactionServiceInterface::class)
                ->makeOne($this, Transaction::TYPE_DEPOSIT, $amount, $meta, $confirmed)
        );
    }

    /**
     * Magic laravel framework method, makes it possible to call property balance.
     */
    public function getBalanceAttribute(): string
    {
        /** @var Wallet $this */
        return app(RegulatorServiceInterface::class)->amount(app(CastServiceInterface::class)->getWallet($this, false));
    }

    public function getBalanceIntAttribute(): int
    {
        return (int) $this->getBalanceAttribute();
    }

    /**
     * We receive transactions of the selected wallet.
     *
     * @return HasMany<Transaction>
     */
    public function walletTransactions(): HasMany
    {
        return app(CastServiceInterface::class)
            ->getWallet($this, false)
            ->hasMany(config('wallet.transaction.model', Transaction::class), 'wallet_id');
    }

    /**
     * all user actions on wallets will be in this method.
     *
     * @return MorphMany<Transaction>
     */
    public function transactions(): MorphMany
    {
        return app(CastServiceInterface::class)
            ->getHolder($this)
            ->morphMany(config('wallet.transaction.model', Transaction::class), 'payable');
    }

    /**
     * This method ignores errors that occur when transferring funds.
     */
    public function safeTransfer(
        Wallet $wallet,
        int|string $amount,
        ExtraDtoInterface|array|null $meta = null
    ): ?Transfer {
        try {
            return $this->transfer($wallet, $amount, $meta);
        } catch (ExceptionInterface) {
            return null;
        }
    }

    /**
     * A method that transfers funds from host to host.
     *
     * @throws AmountInvalid
     * @throws BalanceIsEmpty
     * @throws InsufficientFunds
     * @throws RecordsNotFoundException
     * @throws TransactionFailedException
     * @throws ExceptionInterface
     */
    public function transfer(Wallet $wallet, int|string $amount, ExtraDtoInterface|array|null $meta = null): Transfer
    {
        return app(AtomicServiceInterface::class)->block($this, function () use ($wallet, $amount, $meta): Transfer {
            /** @var Wallet $this */
            app(ConsistencyServiceInterface::class)->checkPotential($this, $amount);

            return $this->forceTransfer($wallet, $amount, $meta);
        });
    }

    /**
     * Withdrawals from the system.
     *
     * @throws AmountInvalid
     * @throws BalanceIsEmpty
     * @throws InsufficientFunds
     * @throws RecordsNotFoundException
     * @throws TransactionFailedException
     * @throws ExceptionInterface
     */
    public function withdraw(int|string $amount, ?array $meta = null, bool $confirmed = true): Transaction
    {
        return app(AtomicServiceInterface::class)->block($this, function () use (
            $amount,
            $meta,
            $confirmed
        ): Transaction {
            /** @var Wallet $this */
            app(ConsistencyServiceInterface::class)->checkPotential($this, $amount);

            return $this->forceWithdraw($amount, $meta, $confirmed);
        });
    }

    /**
     * Checks if you can withdraw funds.
     */
    public function canWithdraw(int|string $amount, bool $allowZero = false): bool
    {
        $mathService = app(MathServiceInterface::class);
        $wallet = app(CastServiceInterface::class)->getWallet($this);
        $balance = $mathService->add($this->getBalanceAttribute(), $wallet->getCreditAttribute());

        return app(ConsistencyServiceInterface::class)->canWithdraw($balance, $amount, $allowZero);
    }

    /**
     * Forced to withdraw funds from system.
     *
     * @throws AmountInvalid
     * @throws RecordsNotFoundException
     * @throws TransactionFailedException
     * @throws ExceptionInterface
     */
    public function forceWithdraw(
        int|string $amount,
        array|null $meta = null,
        bool $confirmed = true
    ): Transaction {
        return app(AtomicServiceInterface::class)->block(
            $this,
            fn () => app(TransactionServiceInterface::class)
                ->makeOne($this, Transaction::TYPE_WITHDRAW, $amount, $meta, $confirmed)
        );
    }

    /**
     * the forced transfer is needed when the user does not have the money, and we drive it. Sometimes you do. Depends
     * on business logic.
     *
     * @throws AmountInvalid
     * @throws RecordsNotFoundException
     * @throws TransactionFailedException
     * @throws ExceptionInterface
     */
    public function forceTransfer(
        Wallet $wallet,
        int|string $amount,
        ExtraDtoInterface|array|null $meta = null
    ): Transfer {
        return app(AtomicServiceInterface::class)->block($this, function () use ($wallet, $amount, $meta): Transfer {
            $transferLazyDto = app(PrepareServiceInterface::class)
                ->transferLazy($this, $wallet, Transfer::STATUS_TRANSFER, $amount, $meta);

            $transfers = app(TransferServiceInterface::class)->apply([$transferLazyDto]);

            return current($transfers);
        });
    }

    /**
     * the transfer table is used to confirm the payment this method receives all transfers.
     *
     * @return HasMany<Transfer>
     */
    public function transfers(): HasMany
    {
        /** @var Wallet $this */
        return app(CastServiceInterface::class)
            ->getWallet($this, false)
            ->hasMany(config('wallet.transfer.model', Transfer::class), 'from_id');
    }

    /**
     * returns all the receiving transfers to this wallet.
     *
     * @return HasMany<Transfer>
     */
    public function receivedTransfers(): HasMany
    {
        return app(CastServiceInterface::class)
            ->getWallet($this, false)
            ->hasMany(config('wallet.transfer.model', Transfer::class), 'to_id');
    }
}