Completed
Pull Request — master (#14)
by Бабичев
02:38
created

HasWallet::getWallet()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 9
ccs 0
cts 6
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\AmountInvalid;
6
use Bavix\Wallet\Exceptions\BalanceIsEmpty;
7
use Bavix\Wallet\Exceptions\InsufficientFunds;
8
use Bavix\Wallet\Tax;
9
use Bavix\Wallet\Interfaces\Wallet;
10
use Bavix\Wallet\Models\Wallet as WalletModel;
11
use Bavix\Wallet\Models\Transaction;
12
use Bavix\Wallet\Models\Transfer;
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 variable is used for the cache, so as not to request wallets many times.
35
     * WalletProxy keeps the money wallets in the memory to avoid errors when you
36
     * purchase/transfer, etc.
37
     *
38
     * @var array
39
     */
40
    private $_wallets = [];
41
42
    /**
43
     * The amount of checks for errors
44
     *
45
     * @param int $amount
46
     * @throws
47
     */
48 17
    private function checkAmount(int $amount): void
49
    {
50 17
        if ($amount < 0) {
51 2
            throw new AmountInvalid(trans('wallet::errors.price_positive'));
52
        }
53 15
    }
54
55
    /**
56
     * Forced to withdraw funds from system
57
     *
58
     * @param int $amount
59
     * @param array|null $meta
60
     * @param bool $confirmed
61
     *
62
     * @return Transaction
63
     */
64 15
    public function forceWithdraw(int $amount, ?array $meta = null, bool $confirmed = true): Transaction
65
    {
66 15
        $this->checkAmount($amount);
67 15
        return $this->change(-$amount, $meta, $confirmed);
68
    }
69
70
    /**
71
     * The input means in the system
72
     *
73
     * @param int $amount
74
     * @param array|null $meta
75
     * @param bool $confirmed
76
     *
77
     * @return Transaction
78
     */
79 17
    public function deposit(int $amount, ?array $meta = null, bool $confirmed = true): Transaction
80
    {
81 17
        $this->checkAmount($amount);
82 15
        return $this->change($amount, $meta, $confirmed);
83
    }
84
85
    /**
86
     * Withdrawals from the system
87
     *
88
     * @param int $amount
89
     * @param array|null $meta
90
     * @param bool $confirmed
91
     *
92
     * @return Transaction
93
     */
94 18
    public function withdraw(int $amount, ?array $meta = null, bool $confirmed = true): Transaction
95
    {
96 18
        if (!$this->balance) {
97 9
            throw new BalanceIsEmpty(trans('wallet::errors.wallet_empty'));
98
        }
99
100 14
        if (!$this->canWithdraw($amount)) {
101
            throw new InsufficientFunds(trans('wallet::errors.insufficient_funds'));
102
        }
103
104 14
        return $this->forceWithdraw($amount, $meta, $confirmed);
105
    }
106
107
    /**
108
     * Checks if you can withdraw funds
109
     *
110
     * @param int $amount
111
     * @return bool
112
     */
113 14
    public function canWithdraw(int $amount): bool
114
    {
115 14
        return $this->balance >= $amount;
116
    }
117
118
    /**
119
     * A method that transfers funds from host to host
120
     *
121
     * @param Wallet $wallet
122
     * @param int $amount
123
     * @param array|null $meta
124
     * @return Transfer
125
     * @throws
126
     */
127 8
    public function transfer(Wallet $wallet, int $amount, ?array $meta = null): Transfer
128
    {
129
        return DB::transaction(function() use ($amount, $wallet, $meta) {
130 8
            $fee = Tax::fee($wallet, $amount);
131 8
            $withdraw = $this->withdraw($amount + $fee, $meta);
132 8
            $deposit = $wallet->deposit($amount, $meta);
133 8
            return $this->assemble($wallet, $withdraw, $deposit);
134 8
        });
135
    }
136
137
    /**
138
     * This method ignores errors that occur when transferring funds
139
     *
140
     * @param Wallet $wallet
141
     * @param int $amount
142
     * @param array|null $meta
143
     * @return null|Transfer
144
     */
145 2
    public function safeTransfer(Wallet $wallet, int $amount, ?array $meta = null): ?Transfer
146
    {
147
        try {
148 2
            return $this->transfer($wallet, $amount, $meta);
149 2
        } catch (\Throwable $throwable) {
150 2
            return null;
151
        }
152
    }
153
154
    /**
155
     * the forced transfer is needed when the user does not have the money and we drive it.
156
     * Sometimes you do. Depends on business logic.
157
     *
158
     * @param Wallet $wallet
159
     * @param int $amount
160
     * @param array|null $meta
161
     * @return Transfer
162
     */
163 4
    public function forceTransfer(Wallet $wallet, int $amount, ?array $meta = null): Transfer
164
    {
165
        return DB::transaction(function() use ($amount, $wallet, $meta) {
166 4
            $fee = Tax::fee($wallet, $amount);
167 4
            $withdraw = $this->forceWithdraw($amount + $fee, $meta);
168 4
            $deposit = $wallet->deposit($amount, $meta);
169 4
            return $this->assemble($wallet, $withdraw, $deposit);
170 4
        });
171
    }
172
173
    /**
174
     * this method adds a new transfer to the transfer table
175
     *
176
     * @param Wallet $wallet
177
     * @param Transaction $withdraw
178
     * @param Transaction $deposit
179
     * @return Transfer
180
     * @throws
181
     */
182 9
    protected function assemble(Wallet $wallet, Transaction $withdraw, Transaction $deposit): Transfer
183
    {
184
        /**
185
         * @var Model $wallet
186
         */
187 9
        return \app(config('wallet.transfer.model'))->create([
0 ignored issues
show
Bug introduced by
The method create() does not exist on Illuminate\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

187
        return \app(config('wallet.transfer.model'))->/** @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...
188 9
            'deposit_id' => $deposit->getKey(),
189 9
            'withdraw_id' => $withdraw->getKey(),
190 9
            'from_type' => $this->getMorphClass(),
0 ignored issues
show
Bug introduced by
It seems like getMorphClass() 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

190
            'from_type' => $this->/** @scrutinizer ignore-call */ getMorphClass(),
Loading history...
191 9
            'from_id' => $this->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

191
            'from_id' => $this->/** @scrutinizer ignore-call */ getKey(),
Loading history...
192 9
            'to_type' => $wallet->getMorphClass(),
193 9
            'to_id' => $wallet->getKey(),
194 9
            'fee' => $withdraw->amount - $deposit->amount,
195 9
            'uuid' => Uuid::uuid4()->toString(),
196
        ]);
197
    }
198
199
    /**
200
     * this method adds a new transaction to the translation table
201
     *
202
     * @param int $amount
203
     * @param array|null $meta
204
     * @param bool $confirmed
205
     * @return Transaction
206
     * @throws
207
     */
208 15
    protected function change(int $amount, ?array $meta, bool $confirmed): Transaction
209
    {
210
        return DB::transaction(function() use ($amount, $meta, $confirmed) {
211
212 15
            if ($this instanceof WalletModel) {
213
                $payable = $this->holder;
214
                $wallet = $this;
215
            } else {
216 15
                $payable = $this;
217 15
                $wallet = $this->wallet;
218
            }
219
220 15
            if ($confirmed) {
221 15
                $this->addBalance($wallet, $amount);
222
            }
223
224 15
            return $this->transactions()->create([
225 15
                'type' => $amount > 0 ? 'deposit' : 'withdraw',
226 15
                'payable_type' => $payable->getMorphClass(),
227 15
                'payable_id' => $payable->getKey(),
228 15
                'wallet_id' => $wallet->getKey(),
229 15
                'uuid' => Uuid::uuid4()->toString(),
230 15
                'confirmed' => $confirmed,
231 15
                'amount' => $amount,
232 15
                'meta' => $meta,
233
            ]);
234 15
        });
235
    }
236
237
    /**
238
     * all user actions on wallets will be in this method
239
     *
240
     * @return MorphMany
241
     */
242 15
    public function transactions(): MorphMany
243
    {
244 15
        return $this->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

244
        return $this->/** @scrutinizer ignore-call */ morphMany(config('wallet.transaction.model'), 'payable');
Loading history...
245
    }
246
247
    /**
248
     * the transfer table is used to confirm the payment
249
     * this method receives all transfers
250
     *
251
     * @return MorphMany
252
     */
253 4
    public function transfers(): MorphMany
254
    {
255 4
        return $this->morphMany(config('wallet.transfer.model'), 'from');
256
    }
257
258
    /**
259
     * method of obtaining all wallets
260
     *
261
     * @return MorphMany
262
     */
263
    public function wallets(): MorphMany
264
    {
265
        return $this->morphMany(config('wallet.wallet.model'), 'holder');
266
    }
267
268
    /**
269
     * Get wallet by slug
270
     *
271
     *  $user->wallet->balance // 200
272
     *  or short recording $user->balance; // 200
273
     *
274
     *  $defaultSlug = config('wallet.wallet.default.slug');
275
     *  $user->getWallet($defaultSlug)->balance; // 200
276
     *
277
     *  $user->getWallet('usd')->balance; // 50
278
     *  $user->getWallet('rub')->balance; // 100
279
     *
280
     * @param string $slug
281
     * @return WalletModel|null
282
     */
283
    public function getWallet(string $slug): ?WalletModel
284
    {
285
        if (!\array_key_exists($slug, $this->_wallets)) {
286
            $this->_wallets[$slug] = $this->wallets()
287
                ->where('slug', $slug)
288
                ->first();
289
        }
290
291
        return $this->_wallets[$slug];
292
    }
293
294
    /**
295
     * Get default Wallet
296
     * this method is used for Eager Loading
297
     *
298
     * @return MorphOne|WalletModel
299
     */
300 19
    public function wallet(): MorphOne
301
    {
302 19
        return $this->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

302
        return $this->/** @scrutinizer ignore-call */ morphOne(config('wallet.wallet.model'), 'holder')
Loading history...
303 19
            ->withDefault([
304 19
                'name' => config('wallet.wallet.default.name'),
305 19
                'slug' => config('wallet.wallet.default.slug'),
306 19
                'balance' => 0,
307
            ]);
308
    }
309
310
    /**
311
     * Magic laravel framework method, makes it
312
     *  possible to call property balance
313
     *
314
     * Example:
315
     *  $user1 = User::first()->load('wallet');
316
     *  $user2 = User::first()->load('wallet');
317
     *
318
     * Without static:
319
     *  var_dump($user1->balance, $user2->balance); // 100 100
320
     *  $user1->deposit(100);
321
     *  $user2->deposit(100);
322
     *  var_dump($user1->balance, $user2->balance); // 200 200
323
     *
324
     * With static:
325
     *  var_dump($user1->balance, $user2->balance); // 100 100
326
     *  $user1->deposit(100);
327
     *  var_dump($user1->balance); // 200
328
     *  $user2->deposit(100);
329
     *  var_dump($user2->balance); // 300
330
     *
331
     * @return int
332
     * @throws
333
     */
334 19
    public function getBalanceAttribute(): int
335
    {
336 19
        if ($this instanceof WalletModel) {
337 19
            $this->exists or $this->save();
338 19
            if (!WalletProxy::has($this->getKey())) {
339 19
                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...
340
            }
341
342 19
            return WalletProxy::get($this->getKey());
343
        }
344
345 19
        return $this->wallet->balance;
346
    }
347
348
    /**
349
     * This method automatically updates the balance in the
350
     * database and the project statics
351
     *
352
     * @param WalletModel $wallet
353
     * @param int $amount
354
     * @return bool
355
     */
356 15
    protected function addBalance(WalletModel $wallet, int $amount): bool
357
    {
358 15
        $newBalance = $this->getBalanceAttribute() + $amount;
359 15
        $wallet->balance = $newBalance;
360
361
        return
362
            // update database wallet
363 15
            $wallet->save() &&
364
365
            // update static wallet
366 15
            WalletProxy::set($wallet->getKey(), $newBalance);
367
    }
368
369
}
370