Completed
Pull Request — master (#40)
by Бабичев
05:21
created

HasWallet::addBalance()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 2
dl 0
loc 12
ccs 0
cts 8
cp 0
crap 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Bavix\Wallet\Traits;
4
5
use Bavix\Wallet\Exceptions\BalanceIsEmpty;
6
use Bavix\Wallet\Exceptions\InsufficientFunds;
7
use Bavix\Wallet\Interfaces\Wallet;
8
use Bavix\Wallet\Models\Transaction;
9
use Bavix\Wallet\Models\Transfer;
10
use Bavix\Wallet\Models\Wallet as WalletModel;
11
use Bavix\Wallet\Services\CommonService;
12
use Bavix\Wallet\Services\ProxyService;
13
use Bavix\Wallet\Services\WalletService;
14
use Illuminate\Database\Eloquent\Model;
15
use Illuminate\Database\Eloquent\Relations\MorphMany;
16
use Illuminate\Database\Eloquent\Relations\MorphOne;
17
use Illuminate\Support\Collection;
18
use Illuminate\Support\Facades\DB;
19
use Ramsey\Uuid\Uuid;
20
21
/**
22
 * Trait HasWallet
23
 *
24
 * @package Bavix\Wallet\Traits
25
 *
26
 * @property-read WalletModel $wallet
27
 * @property-read Collection|WalletModel[] $wallets
28
 * @property-read int $balance
29
 */
30
trait HasWallet
31
{
32
33
    /**
34
     * The input means in the system
35
     *
36
     * @param int $amount
37
     * @param array|null $meta
38
     * @param bool $confirmed
39
     *
40
     * @return Transaction
41
     */
42 31
    public function deposit(int $amount, ?array $meta = null, bool $confirmed = true): Transaction
43
    {
44 31
        app(WalletService::class)->checkAmount($amount);
45
46
        /**
47
         * @var WalletModel $wallet
48
         */
49 28
        $wallet = app(WalletService::class)
50 28
            ->getWallet($this);
0 ignored issues
show
Bug introduced by
$this of type Bavix\Wallet\Traits\HasWallet is incompatible with the type Bavix\Wallet\Interfaces\Wallet expected by parameter $object of Bavix\Wallet\Services\WalletService::getWallet(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

50
            ->getWallet(/** @scrutinizer ignore-type */ $this);
Loading history...
51
52 28
        $transactions = app(CommonService::class)->enforce(true, $wallet, [
53 28
            (new \Bavix\Wallet\Objects\Transaction())
54 28
                ->setType(Transaction::TYPE_DEPOSIT)
55 28
                ->setConfirmed($confirmed)
56 28
                ->setAmount($amount)
57 28
                ->setMeta($meta)
58
        ]);
59
60 28
        return $transactions[0];
61
    }
62
63
    /**
64
     * This method automatically updates the balance in the
65
     * database and the project statics
66
     *
67
     * @param WalletModel $wallet
68
     * @param int $amount
69
     * @return bool
70
     */
71
    protected function addBalance(WalletModel $wallet, int $amount): bool
72
    {
73
        $newBalance = $this->getBalanceAttribute() + $amount;
74
        $wallet->balance = $newBalance;
75
76
        if ($wallet->save()) {
77
            $proxy = app(ProxyService::class);
78
            $proxy->set($wallet->getKey(), $newBalance);
0 ignored issues
show
Bug introduced by
It seems like $wallet->getKey() can also be of type boolean and null; however, parameter $key of Bavix\Wallet\Services\ProxyService::set() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

78
            $proxy->set(/** @scrutinizer ignore-type */ $wallet->getKey(), $newBalance);
Loading history...
79
            return true;
80
        }
81
82
        return false;
83
    }
84
85
    /**
86
     * Magic laravel framework method, makes it
87
     *  possible to call property balance
88
     *
89
     * Example:
90
     *  $user1 = User::first()->load('wallet');
91
     *  $user2 = User::first()->load('wallet');
92
     *
93
     * Without static:
94
     *  var_dump($user1->balance, $user2->balance); // 100 100
95
     *  $user1->deposit(100);
96
     *  $user2->deposit(100);
97
     *  var_dump($user1->balance, $user2->balance); // 200 200
98
     *
99
     * With static:
100
     *  var_dump($user1->balance, $user2->balance); // 100 100
101
     *  $user1->deposit(100);
102
     *  var_dump($user1->balance); // 200
103
     *  $user2->deposit(100);
104
     *  var_dump($user2->balance); // 300
105
     *
106
     * @return int
107
     * @throws
108
     */
109 38
    public function getBalanceAttribute(): int
110
    {
111 38
        if ($this instanceof WalletModel) {
112 38
            $this->exists or $this->save();
113 38
            $proxy = app(ProxyService::class);
114 38
            if (!$proxy->has($this->getKey())) {
0 ignored issues
show
Bug introduced by
It seems like $this->getKey() can also be of type boolean and null; however, parameter $key of Bavix\Wallet\Services\ProxyService::has() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

114
            if (!$proxy->has(/** @scrutinizer ignore-type */ $this->getKey())) {
Loading history...
115 38
                $proxy->set($this->getKey(), (int)($this->attributes['balance'] ?? 0));
0 ignored issues
show
Bug introduced by
It seems like $this->getKey() can also be of type boolean and null; however, parameter $key of Bavix\Wallet\Services\ProxyService::set() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

115
                $proxy->set(/** @scrutinizer ignore-type */ $this->getKey(), (int)($this->attributes['balance'] ?? 0));
Loading history...
Bug Best Practice introduced by
The property $attributes is declared protected in Illuminate\Database\Eloquent\Model. Since you implement __get, consider adding a @property or @property-read.
Loading history...
116
            }
117
118 38
            return $proxy[$this->getKey()];
119
        }
120
121 38
        return $this->wallet->balance;
122
    }
123
124
    /**
125
     * all user actions on wallets will be in this method
126
     *
127
     * @return MorphMany
128
     */
129 28
    public function transactions(): MorphMany
130
    {
131 28
        return ($this instanceof WalletModel ? $this->holder : $this)
132 28
            ->morphMany(config('wallet.transaction.model'), 'payable');
0 ignored issues
show
Bug introduced by
It seems like morphMany() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

132
            ->/** @scrutinizer ignore-call */ morphMany(config('wallet.transaction.model'), 'payable');
Loading history...
133
    }
134
135
    /**
136
     * This method ignores errors that occur when transferring funds
137
     *
138
     * @param Wallet $wallet
139
     * @param int $amount
140
     * @param array|null $meta
141
     * @param string $status
142
     * @return null|Transfer
143
     */
144 3
    public function safeTransfer(Wallet $wallet, int $amount, ?array $meta = null, string $status = Transfer::STATUS_TRANSFER): ?Transfer
145
    {
146
        try {
147 3
            return $this->transfer($wallet, $amount, $meta, $status);
148 3
        } catch (\Throwable $throwable) {
149 3
            return null;
150
        }
151
    }
152
153
    /**
154
     * A method that transfers funds from host to host
155
     *
156
     * @param Wallet $wallet
157
     * @param int $amount
158
     * @param array|null $meta
159
     * @param string $status
160
     * @return Transfer
161
     * @throws
162
     */
163 15
    public function transfer(Wallet $wallet, int $amount, ?array $meta = null, string $status = Transfer::STATUS_TRANSFER): Transfer
164
    {
165
        return DB::transaction(function () use ($amount, $wallet, $meta, $status) {
166 15
            $fee = app(WalletService::class)
167 15
                ->fee($wallet, $amount);
168
169 15
            $withdraw = $this->withdraw($amount + $fee, $meta);
170 15
            $deposit = $wallet->deposit($amount, $meta);
171 15
            return $this->assemble($wallet, $withdraw, $deposit, $status);
172 15
        });
173
    }
174
175
    /**
176
     * Withdrawals from the system
177
     *
178
     * @param int $amount
179
     * @param array|null $meta
180
     * @param bool $confirmed
181
     *
182
     * @return Transaction
183
     */
184 33
    public function withdraw(int $amount, ?array $meta = null, bool $confirmed = true): Transaction
185
    {
186 33
        if ($amount && !$this->balance) {
187 14
            throw new BalanceIsEmpty(trans('wallet::errors.wallet_empty'));
0 ignored issues
show
Bug introduced by
It seems like trans('wallet::errors.wallet_empty') can also be of type array; however, parameter $message of Bavix\Wallet\Exceptions\...eIsEmpty::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

187
            throw new BalanceIsEmpty(/** @scrutinizer ignore-type */ trans('wallet::errors.wallet_empty'));
Loading history...
188
        }
189
190 27
        if (!$this->canWithdraw($amount)) {
191 1
            throw new InsufficientFunds(trans('wallet::errors.insufficient_funds'));
0 ignored issues
show
Bug introduced by
It seems like trans('wallet::errors.insufficient_funds') can also be of type array; however, parameter $message of Bavix\Wallet\Exceptions\...entFunds::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

191
            throw new InsufficientFunds(/** @scrutinizer ignore-type */ trans('wallet::errors.insufficient_funds'));
Loading history...
192
        }
193
194 26
        return $this->forceWithdraw($amount, $meta, $confirmed);
195
    }
196
197
    /**
198
     * Checks if you can withdraw funds
199
     *
200
     * @param int $amount
201
     * @return bool
202
     */
203 27
    public function canWithdraw(int $amount): bool
204
    {
205 27
        return $this->balance >= $amount;
206
    }
207
208
    /**
209
     * Forced to withdraw funds from system
210
     *
211
     * @param int $amount
212
     * @param array|null $meta
213
     * @param bool $confirmed
214
     *
215
     * @return Transaction
216
     */
217 27
    public function forceWithdraw(int $amount, ?array $meta = null, bool $confirmed = true): Transaction
218
    {
219 27
        app(WalletService::class)->checkAmount($amount);
220
221
        /**
222
         * @var WalletModel $wallet
223
         */
224 27
        $wallet = app(WalletService::class)
225 27
            ->getWallet($this);
0 ignored issues
show
Bug introduced by
$this of type Bavix\Wallet\Traits\HasWallet is incompatible with the type Bavix\Wallet\Interfaces\Wallet expected by parameter $object of Bavix\Wallet\Services\WalletService::getWallet(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

225
            ->getWallet(/** @scrutinizer ignore-type */ $this);
Loading history...
226
227 27
        $transactions = app(CommonService::class)->enforce(true, $wallet, [
228 27
            (new \Bavix\Wallet\Objects\Transaction())
229 27
                ->setType(Transaction::TYPE_WITHDRAW)
230 27
                ->setConfirmed($confirmed)
231 27
                ->setAmount(-$amount)
232 27
                ->setMeta($meta)
233
        ]);
234
235 27
        return $transactions[0];
236
    }
237
238
    /**
239
     * this method adds a new transfer to the transfer table
240
     *
241
     * @param Wallet $wallet
242
     * @param Transaction $withdraw
243
     * @param Transaction $deposit
244
     * @param string $status
245
     * @return Transfer
246
     * @throws
247
     */
248 17
    protected function assemble(Wallet $wallet, Transaction $withdraw, Transaction $deposit, string $status = Transfer::STATUS_PAID): Transfer
249
    {
250
        /**
251
         * @var Model $wallet
252
         */
253 17
        return \app('bavix.wallet::transfer')->create([
0 ignored issues
show
Bug introduced by
The method create() does not exist on Illuminate\Contracts\Foundation\Application. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

253
        return \app('bavix.wallet::transfer')->/** @scrutinizer ignore-call */ create([

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
254 17
            'status' => $status,
255 17
            'deposit_id' => $deposit->getKey(),
256 17
            'withdraw_id' => $withdraw->getKey(),
257 17
            'from_type' => ($this instanceof WalletModel ? $this : $this->wallet)->getMorphClass(),
258 17
            'from_id' => ($this instanceof WalletModel ? $this : $this->wallet)->getKey(),
259 17
            'to_type' => $wallet->getMorphClass(),
260 17
            'to_id' => $wallet->getKey(),
261 17
            'fee' => \abs($withdraw->amount) - \abs($deposit->amount),
262 17
            'uuid' => Uuid::uuid4()->toString(),
263
        ]);
264
    }
265
266
    /**
267
     * the forced transfer is needed when the user does not have the money and we drive it.
268
     * Sometimes you do. Depends on business logic.
269
     *
270
     * @param Wallet $wallet
271
     * @param int $amount
272
     * @param array|null $meta
273
     * @param string $status
274
     * @return Transfer
275
     */
276 6
    public function forceTransfer(Wallet $wallet, int $amount, ?array $meta = null, string $status = Transfer::STATUS_TRANSFER): Transfer
277
    {
278
        return DB::transaction(function () use ($amount, $wallet, $meta, $status) {
279 6
            $fee = app(WalletService::class)
280 6
                ->fee($wallet, $amount);
281
282 6
            $withdraw = $this->forceWithdraw($amount + $fee, $meta);
283 6
            $deposit = $wallet->deposit($amount, $meta);
284 6
            return $this->assemble($wallet, $withdraw, $deposit, $status);
285 6
        });
286
    }
287
288
    /**
289
     * the transfer table is used to confirm the payment
290
     * this method receives all transfers
291
     *
292
     * @return MorphMany
293
     */
294 10
    public function transfers(): MorphMany
295
    {
296 10
        return app(WalletService::class)
297 10
            ->getWallet($this)
0 ignored issues
show
Bug introduced by
$this of type Bavix\Wallet\Traits\HasWallet is incompatible with the type Bavix\Wallet\Interfaces\Wallet expected by parameter $object of Bavix\Wallet\Services\WalletService::getWallet(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

297
            ->getWallet(/** @scrutinizer ignore-type */ $this)
Loading history...
298 10
            ->morphMany(config('wallet.transfer.model'), 'from');
299
    }
300
301
    /**
302
     * Get default Wallet
303
     * this method is used for Eager Loading
304
     *
305
     * @return MorphOne|WalletModel
306
     */
307 38
    public function wallet(): MorphOne
308
    {
309 38
        return ($this instanceof WalletModel ? $this->holder : $this)
310 38
            ->morphOne(config('wallet.wallet.model'), 'holder')
0 ignored issues
show
Bug introduced by
It seems like morphOne() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

310
            ->/** @scrutinizer ignore-call */ morphOne(config('wallet.wallet.model'), 'holder')
Loading history...
311 38
            ->where('slug', config('wallet.wallet.default.slug'))
312 38
            ->withDefault([
313 38
                'name' => config('wallet.wallet.default.name'),
314 38
                'slug' => config('wallet.wallet.default.slug'),
315 38
                'balance' => 0,
316
            ]);
317
    }
318
319
}
320