Completed
Pull Request — master (#14)
by Бабичев
03:26
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\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 amount of checks for errors
35
     *
36
     * @param int $amount
37
     * @throws
38
     */
39 28
    private function checkAmount(int $amount): void
40
    {
41 28
        if ($amount < 0) {
42 3
            throw new AmountInvalid(trans('wallet::errors.price_positive'));
43
        }
44 25
    }
45
46
    /**
47
     * Forced to withdraw funds from system
48
     *
49
     * @param int $amount
50
     * @param array|null $meta
51
     * @param bool $confirmed
52
     *
53
     * @return Transaction
54
     */
55 24
    public function forceWithdraw(int $amount, ?array $meta = null, bool $confirmed = true): Transaction
56
    {
57 24
        $this->checkAmount($amount);
58 24
        return $this->change(Transaction::TYPE_WITHDRAW, -$amount, $meta, $confirmed);
59
    }
60
61
    /**
62
     * The input means in the system
63
     *
64
     * @param int $amount
65
     * @param array|null $meta
66
     * @param bool $confirmed
67
     *
68
     * @return Transaction
69
     */
70 28
    public function deposit(int $amount, ?array $meta = null, bool $confirmed = true): Transaction
71
    {
72 28
        $this->checkAmount($amount);
73 25
        return $this->change(Transaction::TYPE_DEPOSIT, $amount, $meta, $confirmed);
74
    }
75
76
    /**
77
     * Withdrawals from the system
78
     *
79
     * @param int $amount
80
     * @param array|null $meta
81
     * @param bool $confirmed
82
     *
83
     * @return Transaction
84
     */
85 30
    public function withdraw(int $amount, ?array $meta = null, bool $confirmed = true): Transaction
86
    {
87 30
        if ($amount && !$this->balance) {
88 13
            throw new BalanceIsEmpty(trans('wallet::errors.wallet_empty'));
89
        }
90
91 24
        if (!$this->canWithdraw($amount)) {
92 1
            throw new InsufficientFunds(trans('wallet::errors.insufficient_funds'));
93
        }
94
95 23
        return $this->forceWithdraw($amount, $meta, $confirmed);
96
    }
97
98
    /**
99
     * Checks if you can withdraw funds
100
     *
101
     * @param int $amount
102
     * @return bool
103
     */
104 24
    public function canWithdraw(int $amount): bool
105
    {
106 24
        return $this->balance >= $amount;
107
    }
108
109
    /**
110
     * A method that transfers funds from host to host
111
     *
112
     * @param Wallet $wallet
113
     * @param int $amount
114
     * @param array|null $meta
115
     * @return Transfer
116
     * @throws
117
     */
118 13
    public function transfer(Wallet $wallet, int $amount, ?array $meta = null): Transfer
119
    {
120
        return DB::transaction(function () use ($amount, $wallet, $meta) {
121 13
            $fee = Tax::fee($wallet, $amount);
122 13
            $withdraw = $this->withdraw($amount + $fee, $meta);
123 13
            $deposit = $wallet->deposit($amount, $meta);
124 13
            return $this->assemble($wallet, $withdraw, $deposit);
125 13
        });
126
    }
127
128
    /**
129
     * This method ignores errors that occur when transferring funds
130
     *
131
     * @param Wallet $wallet
132
     * @param int $amount
133
     * @param array|null $meta
134
     * @return null|Transfer
135
     */
136 3
    public function safeTransfer(Wallet $wallet, int $amount, ?array $meta = null): ?Transfer
137
    {
138
        try {
139 3
            return $this->transfer($wallet, $amount, $meta);
140 3
        } catch (\Throwable $throwable) {
141 3
            return null;
142
        }
143
    }
144
145
    /**
146
     * the forced transfer is needed when the user does not have the money and we drive it.
147
     * Sometimes you do. Depends on business logic.
148
     *
149
     * @param Wallet $wallet
150
     * @param int $amount
151
     * @param array|null $meta
152
     * @return Transfer
153
     */
154 5
    public function forceTransfer(Wallet $wallet, int $amount, ?array $meta = null): Transfer
155
    {
156
        return DB::transaction(function () use ($amount, $wallet, $meta) {
157 5
            $fee = Tax::fee($wallet, $amount);
158 5
            $withdraw = $this->forceWithdraw($amount + $fee, $meta);
159 5
            $deposit = $wallet->deposit($amount, $meta);
160 5
            return $this->assemble($wallet, $withdraw, $deposit);
161 5
        });
162
    }
163
164
    /**
165
     * this method adds a new transfer to the transfer table
166
     *
167
     * @param Wallet $wallet
168
     * @param Transaction $withdraw
169
     * @param Transaction $deposit
170
     * @return Transfer
171
     * @throws
172
     */
173 14
    protected function assemble(Wallet $wallet, Transaction $withdraw, Transaction $deposit): Transfer
174
    {
175
        /**
176
         * @var Model $wallet
177
         */
178 14
        return \app('bavix.wallet::transfer')->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

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

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

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

232
            ->/** @scrutinizer ignore-call */ morphMany(config('wallet.transaction.model'), 'payable');
Loading history...
233
    }
234
235
    /**
236
     * the transfer table is used to confirm the payment
237
     * this method receives all transfers
238
     *
239
     * @return MorphMany
240
     */
241 7
    public function transfers(): MorphMany
242
    {
243 7
        return ($this instanceof WalletModel ? $this->holder : $this)
244 7
            ->morphMany(config('wallet.transfer.model'), 'from');
245
    }
246
247
    /**
248
     * Get default Wallet
249
     * this method is used for Eager Loading
250
     *
251
     * @return MorphOne|WalletModel
252
     */
253 34
    public function wallet(): MorphOne
254
    {
255 34
        return ($this instanceof WalletModel ? $this->holder : $this)
256 34
            ->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

256
            ->/** @scrutinizer ignore-call */ morphOne(config('wallet.wallet.model'), 'holder')
Loading history...
257 34
            ->withDefault([
258 34
                'name' => config('wallet.wallet.default.name'),
259 34
                'slug' => config('wallet.wallet.default.slug'),
260 34
                'balance' => 0,
261
            ]);
262
    }
263
264
    /**
265
     * Magic laravel framework method, makes it
266
     *  possible to call property balance
267
     *
268
     * Example:
269
     *  $user1 = User::first()->load('wallet');
270
     *  $user2 = User::first()->load('wallet');
271
     *
272
     * Without static:
273
     *  var_dump($user1->balance, $user2->balance); // 100 100
274
     *  $user1->deposit(100);
275
     *  $user2->deposit(100);
276
     *  var_dump($user1->balance, $user2->balance); // 200 200
277
     *
278
     * With static:
279
     *  var_dump($user1->balance, $user2->balance); // 100 100
280
     *  $user1->deposit(100);
281
     *  var_dump($user1->balance); // 200
282
     *  $user2->deposit(100);
283
     *  var_dump($user2->balance); // 300
284
     *
285
     * @return int
286
     * @throws
287
     */
288 34
    public function getBalanceAttribute(): int
289
    {
290 34
        if ($this instanceof WalletModel) {
291 34
            $this->exists or $this->save();
292 34
            if (!WalletProxy::has($this->getKey())) {
293 34
                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...
294
            }
295
296 34
            return WalletProxy::get($this->getKey());
297
        }
298
299 34
        return $this->wallet->balance;
300
    }
301
302
    /**
303
     * This method automatically updates the balance in the
304
     * database and the project statics
305
     *
306
     * @param WalletModel $wallet
307
     * @param int $amount
308
     * @return bool
309
     */
310 25
    protected function addBalance(WalletModel $wallet, int $amount): bool
311
    {
312 25
        $newBalance = $this->getBalanceAttribute() + $amount;
313 25
        $wallet->balance = $newBalance;
314
315
        return
316
            // update database wallet
317 25
            $wallet->save() &&
318
319
            // update static wallet
320 25
            WalletProxy::set($wallet->getKey(), $newBalance);
321
    }
322
323
}
324