Passed
Pull Request — master (#36)
by Бабичев
04:43
created

HasWallet::holderTransfers()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Bavix\Wallet\Traits;
4
5
use Bavix\Wallet\Exceptions\AmountInvalid;
6
use Bavix\Wallet\Exceptions\BalanceIsEmpty;
7
use Bavix\Wallet\Exceptions\InsufficientFunds;
8
use Bavix\Wallet\Interfaces\Wallet;
9
use Bavix\Wallet\Models\Transaction;
10
use Bavix\Wallet\Models\Transfer;
11
use Bavix\Wallet\Models\Wallet as WalletModel;
12
use Bavix\Wallet\Tax;
13
use Bavix\Wallet\WalletProxy;
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 30
    public function deposit(int $amount, ?array $meta = null, bool $confirmed = true): Transaction
43
    {
44 30
        $this->checkAmount($amount);
45 27
        return $this->change(Transaction::TYPE_DEPOSIT, $amount, $meta, $confirmed);
46
    }
47
48
    /**
49
     * The amount of checks for errors
50
     *
51
     * @param int $amount
52
     * @throws
53
     */
54 30
    private function checkAmount(int $amount): void
55
    {
56 30
        if ($amount < 0) {
57 3
            throw new AmountInvalid(trans('wallet::errors.price_positive'));
0 ignored issues
show
Bug introduced by
It seems like trans('wallet::errors.price_positive') can also be of type array; however, parameter $message of Bavix\Wallet\Exceptions\...tInvalid::__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

57
            throw new AmountInvalid(/** @scrutinizer ignore-type */ trans('wallet::errors.price_positive'));
Loading history...
58
        }
59 27
    }
60
61
    /**
62
     * this method adds a new transaction to the translation table
63
     *
64
     * @param string $type
65
     * @param int $amount
66
     * @param array|null $meta
67
     * @param bool $confirmed
68
     * @return Transaction
69
     * @throws
70
     */
71 27
    protected function change(string $type, int $amount, ?array $meta, bool $confirmed): Transaction
72
    {
73
        return DB::transaction(function () use ($type, $amount, $meta, $confirmed) {
74
75 27
            $wallet = $this;
76 27
            if (!($this instanceof WalletModel)) {
77 22
                $wallet = $this->wallet;
78
            }
79
80 27
            if ($confirmed) {
81 27
                $this->addBalance($wallet, $amount);
82
            }
83
84 27
            return $this->transactions()->create([
85 27
                'type' => $type,
86 27
                'wallet_id' => $wallet->getKey(),
0 ignored issues
show
Bug introduced by
It seems like getKey() 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

86
                'wallet_id' => $wallet->/** @scrutinizer ignore-call */ getKey(),
Loading history...
87 27
                'uuid' => Uuid::uuid4()->toString(),
88 27
                'confirmed' => $confirmed,
89 27
                'amount' => $amount,
90 27
                'meta' => $meta,
91
            ]);
92 27
        });
93
    }
94
95
    /**
96
     * This method automatically updates the balance in the
97
     * database and the project statics
98
     *
99
     * @param WalletModel $wallet
100
     * @param int $amount
101
     * @return bool
102
     */
103 27
    protected function addBalance(WalletModel $wallet, int $amount): bool
104
    {
105 27
        $newBalance = $this->getBalanceAttribute() + $amount;
106 27
        $wallet->balance = $newBalance;
107
108
        return
109
            // update database wallet
110 27
            $wallet->save() &&
111
112
            // update static wallet
113 27
            WalletProxy::set($wallet->getKey(), $newBalance);
114
    }
115
116
    /**
117
     * Magic laravel framework method, makes it
118
     *  possible to call property balance
119
     *
120
     * Example:
121
     *  $user1 = User::first()->load('wallet');
122
     *  $user2 = User::first()->load('wallet');
123
     *
124
     * Without static:
125
     *  var_dump($user1->balance, $user2->balance); // 100 100
126
     *  $user1->deposit(100);
127
     *  $user2->deposit(100);
128
     *  var_dump($user1->balance, $user2->balance); // 200 200
129
     *
130
     * With static:
131
     *  var_dump($user1->balance, $user2->balance); // 100 100
132
     *  $user1->deposit(100);
133
     *  var_dump($user1->balance); // 200
134
     *  $user2->deposit(100);
135
     *  var_dump($user2->balance); // 300
136
     *
137
     * @return int
138
     * @throws
139
     */
140 36
    public function getBalanceAttribute(): int
141
    {
142 36
        if ($this instanceof WalletModel) {
143 36
            $this->exists or $this->save();
144 36
            if (!WalletProxy::has($this->getKey())) {
145 36
                WalletProxy::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...
146
            }
147
148 36
            return WalletProxy::get($this->getKey());
149
        }
150
151 36
        return $this->wallet->balance;
152
    }
153
154
    /**
155
     * all user actions on wallets will be in this method
156
     *
157
     * @return MorphMany
158
     */
159 27
    public function transactions(): MorphMany
160
    {
161 27
        return ($this instanceof WalletModel ? $this->holder : $this)
162 27
            ->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

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

215
            throw new BalanceIsEmpty(/** @scrutinizer ignore-type */ trans('wallet::errors.wallet_empty'));
Loading history...
216
        }
217
218 26
        if (!$this->canWithdraw($amount)) {
219 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

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

266
        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...
267 16
            'status' => $status,
268 16
            'deposit_id' => $deposit->getKey(),
269 16
            'withdraw_id' => $withdraw->getKey(),
270 16
            'from_type' => ($this instanceof WalletModel ? $this : $this->wallet)->getMorphClass(),
271 16
            'from_id' => ($this instanceof WalletModel ? $this : $this->wallet)->getKey(),
272 16
            'to_type' => $wallet->getMorphClass(),
273 16
            'to_id' => $wallet->getKey(),
274 16
            'fee' => \abs($withdraw->amount) - \abs($deposit->amount),
275 16
            'uuid' => Uuid::uuid4()->toString(),
276
        ]);
277
    }
278
279
    /**
280
     * the forced transfer is needed when the user does not have the money and we drive it.
281
     * Sometimes you do. Depends on business logic.
282
     *
283
     * @param Wallet $wallet
284
     * @param int $amount
285
     * @param array|null $meta
286
     * @param string $status
287
     * @return Transfer
288
     */
289 6
    public function forceTransfer(Wallet $wallet, int $amount, ?array $meta = null, string $status = Transfer::STATUS_TRANSFER): Transfer
290
    {
291
        return DB::transaction(function () use ($amount, $wallet, $meta, $status) {
292 6
            $fee = Tax::fee($wallet, $amount);
293 6
            $withdraw = $this->forceWithdraw($amount + $fee, $meta);
294 6
            $deposit = $wallet->deposit($amount, $meta);
295 6
            return $this->assemble($wallet, $withdraw, $deposit, $status);
296 6
        });
297
    }
298
299
    /**
300
     * the transfer table is used to confirm the payment
301
     * this method receives all transfers
302
     *
303
     * @return MorphMany
304
     */
305 9
    public function transfers(): MorphMany
306
    {
307 9
        if (!($this instanceof WalletModel)) {
308 9
            return $this->wallet->transfers();
309
        }
310
311 9
        return $this->morphMany(config('wallet.transfer.model'), 'from');
312
    }
313
314
    /**
315
     * Get default Wallet
316
     * this method is used for Eager Loading
317
     *
318
     * @return MorphOne|WalletModel
319
     */
320 36
    public function wallet(): MorphOne
321
    {
322
        return $this
323 36
            ->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

323
            ->/** @scrutinizer ignore-call */ morphOne(config('wallet.wallet.model'), 'holder')
Loading history...
324 36
            ->where('slug', config('wallet.wallet.default.slug'))
325 36
            ->withDefault([
326 36
                'name' => config('wallet.wallet.default.name'),
327 36
                'slug' => config('wallet.wallet.default.slug'),
328 36
                'balance' => 0,
329
            ]);
330
    }
331
332
}
333