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

HasWallet::balance()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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

179
        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...
180 10
            'deposit_id' => $deposit->getKey(),
181 10
            'withdraw_id' => $withdraw->getKey(),
182 10
            '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

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

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

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

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