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

HasWallet   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 291
Duplicated Lines 0 %

Test Coverage

Coverage 98.85%

Importance

Changes 0
Metric Value
eloc 75
dl 0
loc 291
ccs 86
cts 87
cp 0.9885
rs 10
c 0
b 0
f 0
wmc 27

14 Methods

Rating   Name   Duplication   Size   Complexity  
A forceTransfer() 0 9 1
A addBalance() 0 12 2
A withdraw() 0 11 4
A assemble() 0 15 3
A transfer() 0 9 1
A getBalanceAttribute() 0 13 4
A forceWithdraw() 0 4 1
A transactions() 0 4 2
A safeTransfer() 0 6 2
A canWithdraw() 0 3 1
A transfers() 0 5 1
A deposit() 0 4 1
A wallet() 0 9 2
A change() 0 21 2
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\ProxyService;
12
use Bavix\Wallet\Services\WalletService;
13
use Illuminate\Database\Eloquent\Model;
14
use Illuminate\Database\Eloquent\Relations\MorphMany;
15
use Illuminate\Database\Eloquent\Relations\MorphOne;
16
use Illuminate\Support\Collection;
17
use Illuminate\Support\Facades\DB;
18
use Ramsey\Uuid\Uuid;
19
20
/**
21
 * Trait HasWallet
22
 *
23
 * @package Bavix\Wallet\Traits
24
 *
25
 * @property-read WalletModel $wallet
26
 * @property-read Collection|WalletModel[] $wallets
27
 * @property-read int $balance
28
 */
29
trait HasWallet
30
{
31
32
    /**
33
     * The input means in the system
34
     *
35
     * @param int $amount
36
     * @param array|null $meta
37
     * @param bool $confirmed
38
     *
39
     * @return Transaction
40
     */
41 31
    public function deposit(int $amount, ?array $meta = null, bool $confirmed = true): Transaction
42
    {
43 31
        app(WalletService::class)->checkAmount($amount);
44 28
        return $this->change(Transaction::TYPE_DEPOSIT, $amount, $meta, $confirmed);
45
    }
46
47
    /**
48
     * this method adds a new transaction to the translation table
49
     *
50
     * @param string $type
51
     * @param int $amount
52
     * @param array|null $meta
53
     * @param bool $confirmed
54
     * @return Transaction
55
     * @throws
56
     */
57 28
    protected function change(string $type, int $amount, ?array $meta, bool $confirmed): Transaction
58
    {
59
        return DB::transaction(function () use ($type, $amount, $meta, $confirmed) {
60
61
            /**
62
             * @var WalletModel $wallet
63
             */
64 28
            $wallet = app(WalletService::class)
65 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

65
                ->getWallet(/** @scrutinizer ignore-type */ $this);
Loading history...
66
67 28
            if ($confirmed) {
68 28
                $this->addBalance($wallet, $amount);
69
            }
70
71 28
            return $this->transactions()->create([
72 28
                'type' => $type,
73 28
                'wallet_id' => $wallet->getKey(),
74 28
                'uuid' => Uuid::uuid4()->toString(),
75 28
                'confirmed' => $confirmed,
76 28
                'amount' => $amount,
77 28
                'meta' => $meta,
78
            ]);
79 28
        });
80
    }
81
82
    /**
83
     * This method automatically updates the balance in the
84
     * database and the project statics
85
     *
86
     * @param WalletModel $wallet
87
     * @param int $amount
88
     * @return bool
89
     */
90 28
    protected function addBalance(WalletModel $wallet, int $amount): bool
91
    {
92 28
        $newBalance = $this->getBalanceAttribute() + $amount;
93 28
        $wallet->balance = $newBalance;
94
95 28
        if ($wallet->save()) {
96 28
            $proxy = app(ProxyService::class);
97 28
            $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

97
            $proxy->set(/** @scrutinizer ignore-type */ $wallet->getKey(), $newBalance);
Loading history...
98 28
            return true;
99
        }
100
101
        return false;
102
    }
103
104
    /**
105
     * Magic laravel framework method, makes it
106
     *  possible to call property balance
107
     *
108
     * Example:
109
     *  $user1 = User::first()->load('wallet');
110
     *  $user2 = User::first()->load('wallet');
111
     *
112
     * Without static:
113
     *  var_dump($user1->balance, $user2->balance); // 100 100
114
     *  $user1->deposit(100);
115
     *  $user2->deposit(100);
116
     *  var_dump($user1->balance, $user2->balance); // 200 200
117
     *
118
     * With static:
119
     *  var_dump($user1->balance, $user2->balance); // 100 100
120
     *  $user1->deposit(100);
121
     *  var_dump($user1->balance); // 200
122
     *  $user2->deposit(100);
123
     *  var_dump($user2->balance); // 300
124
     *
125
     * @return int
126
     * @throws
127
     */
128 38
    public function getBalanceAttribute(): int
129
    {
130 38
        if ($this instanceof WalletModel) {
131 38
            $this->exists or $this->save();
132 38
            $proxy = app(ProxyService::class);
133 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

133
            if (!$proxy->has(/** @scrutinizer ignore-type */ $this->getKey())) {
Loading history...
134 38
                $proxy->set($this->getKey(), (int)($this->attributes['balance'] ?? 0));
0 ignored issues
show
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...
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

134
                $proxy->set(/** @scrutinizer ignore-type */ $this->getKey(), (int)($this->attributes['balance'] ?? 0));
Loading history...
135
            }
136
137 38
            return $proxy[$this->getKey()];
138
        }
139
140 38
        return $this->wallet->balance;
141
    }
142
143
    /**
144
     * all user actions on wallets will be in this method
145
     *
146
     * @return MorphMany
147
     */
148 28
    public function transactions(): MorphMany
149
    {
150 28
        return ($this instanceof WalletModel ? $this->holder : $this)
151 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

151
            ->/** @scrutinizer ignore-call */ morphMany(config('wallet.transaction.model'), 'payable');
Loading history...
152
    }
153
154
    /**
155
     * This method ignores errors that occur when transferring funds
156
     *
157
     * @param Wallet $wallet
158
     * @param int $amount
159
     * @param array|null $meta
160
     * @param string $status
161
     * @return null|Transfer
162
     */
163 3
    public function safeTransfer(Wallet $wallet, int $amount, ?array $meta = null, string $status = Transfer::STATUS_TRANSFER): ?Transfer
164
    {
165
        try {
166 3
            return $this->transfer($wallet, $amount, $meta, $status);
167 3
        } catch (\Throwable $throwable) {
168 3
            return null;
169
        }
170
    }
171
172
    /**
173
     * A method that transfers funds from host to host
174
     *
175
     * @param Wallet $wallet
176
     * @param int $amount
177
     * @param array|null $meta
178
     * @param string $status
179
     * @return Transfer
180
     * @throws
181
     */
182 15
    public function transfer(Wallet $wallet, int $amount, ?array $meta = null, string $status = Transfer::STATUS_TRANSFER): Transfer
183
    {
184
        return DB::transaction(function () use ($amount, $wallet, $meta, $status) {
185 15
            $fee = app(WalletService::class)
186 15
                ->fee($wallet, $amount);
187
188 15
            $withdraw = $this->withdraw($amount + $fee, $meta);
189 15
            $deposit = $wallet->deposit($amount, $meta);
190 15
            return $this->assemble($wallet, $withdraw, $deposit, $status);
191 15
        });
192
    }
193
194
    /**
195
     * Withdrawals from the system
196
     *
197
     * @param int $amount
198
     * @param array|null $meta
199
     * @param bool $confirmed
200
     *
201
     * @return Transaction
202
     */
203 33
    public function withdraw(int $amount, ?array $meta = null, bool $confirmed = true): Transaction
204
    {
205 33
        if ($amount && !$this->balance) {
206 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

206
            throw new BalanceIsEmpty(/** @scrutinizer ignore-type */ trans('wallet::errors.wallet_empty'));
Loading history...
207
        }
208
209 27
        if (!$this->canWithdraw($amount)) {
210 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

210
            throw new InsufficientFunds(/** @scrutinizer ignore-type */ trans('wallet::errors.insufficient_funds'));
Loading history...
211
        }
212
213 26
        return $this->forceWithdraw($amount, $meta, $confirmed);
214
    }
215
216
    /**
217
     * Checks if you can withdraw funds
218
     *
219
     * @param int $amount
220
     * @return bool
221
     */
222 27
    public function canWithdraw(int $amount): bool
223
    {
224 27
        return $this->balance >= $amount;
225
    }
226
227
    /**
228
     * Forced to withdraw funds from system
229
     *
230
     * @param int $amount
231
     * @param array|null $meta
232
     * @param bool $confirmed
233
     *
234
     * @return Transaction
235
     */
236 27
    public function forceWithdraw(int $amount, ?array $meta = null, bool $confirmed = true): Transaction
237
    {
238 27
        app(WalletService::class)->checkAmount($amount);
239 27
        return $this->change(Transaction::TYPE_WITHDRAW, -$amount, $meta, $confirmed);
240
    }
241
242
    /**
243
     * this method adds a new transfer to the transfer table
244
     *
245
     * @param Wallet $wallet
246
     * @param Transaction $withdraw
247
     * @param Transaction $deposit
248
     * @param string $status
249
     * @return Transfer
250
     * @throws
251
     */
252 17
    protected function assemble(Wallet $wallet, Transaction $withdraw, Transaction $deposit, string $status = Transfer::STATUS_PAID): Transfer
253
    {
254
        /**
255
         * @var Model $wallet
256
         */
257 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

257
        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...
258 17
            'status' => $status,
259 17
            'deposit_id' => $deposit->getKey(),
260 17
            'withdraw_id' => $withdraw->getKey(),
261 17
            'from_type' => ($this instanceof WalletModel ? $this : $this->wallet)->getMorphClass(),
262 17
            'from_id' => ($this instanceof WalletModel ? $this : $this->wallet)->getKey(),
263 17
            'to_type' => $wallet->getMorphClass(),
264 17
            'to_id' => $wallet->getKey(),
265 17
            'fee' => \abs($withdraw->amount) - \abs($deposit->amount),
266 17
            'uuid' => Uuid::uuid4()->toString(),
267
        ]);
268
    }
269
270
    /**
271
     * the forced transfer is needed when the user does not have the money and we drive it.
272
     * Sometimes you do. Depends on business logic.
273
     *
274
     * @param Wallet $wallet
275
     * @param int $amount
276
     * @param array|null $meta
277
     * @param string $status
278
     * @return Transfer
279
     */
280 6
    public function forceTransfer(Wallet $wallet, int $amount, ?array $meta = null, string $status = Transfer::STATUS_TRANSFER): Transfer
281
    {
282
        return DB::transaction(function () use ($amount, $wallet, $meta, $status) {
283 6
            $fee = app(WalletService::class)
284 6
                ->fee($wallet, $amount);
285
286 6
            $withdraw = $this->forceWithdraw($amount + $fee, $meta);
287 6
            $deposit = $wallet->deposit($amount, $meta);
288 6
            return $this->assemble($wallet, $withdraw, $deposit, $status);
289 6
        });
290
    }
291
292
    /**
293
     * the transfer table is used to confirm the payment
294
     * this method receives all transfers
295
     *
296
     * @return MorphMany
297
     */
298 10
    public function transfers(): MorphMany
299
    {
300 10
        return app(WalletService::class)
301 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

301
            ->getWallet(/** @scrutinizer ignore-type */ $this)
Loading history...
302 10
            ->morphMany(config('wallet.transfer.model'), 'from');
303
    }
304
305
    /**
306
     * Get default Wallet
307
     * this method is used for Eager Loading
308
     *
309
     * @return MorphOne|WalletModel
310
     */
311 38
    public function wallet(): MorphOne
312
    {
313 38
        return ($this instanceof WalletModel ? $this->holder : $this)
314 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

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