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

HasWallet::change()   A

Complexity

Conditions 4
Paths 1

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 4.026

Importance

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

182
        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...
183 9
            'deposit_id' => $deposit->getKey(),
184 9
            'withdraw_id' => $withdraw->getKey(),
185 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

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

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

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

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