Completed
Pull Request — master (#40)
by Бабичев
05:13
created

HasWallet::safeTransfer()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 4
dl 0
loc 6
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\BalanceIsEmpty;
6
use Bavix\Wallet\Exceptions\InsufficientFunds;
7
use Bavix\Wallet\Interfaces\Wallet;
8
use Bavix\Wallet\Models\Transaction;
9
use Bavix\Wallet\Models\Transfer;
10
use Bavix\Wallet\Models\Wallet as WalletModel;
11
use Bavix\Wallet\Services\CommonService;
12
use Bavix\Wallet\Services\ProxyService;
13
use Bavix\Wallet\Services\WalletService;
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 31
    public function deposit(int $amount, ?array $meta = null, bool $confirmed = true): Transaction
43
    {
44 31
        app(WalletService::class)->checkAmount($amount);
45 28
        return $this->change(Transaction::TYPE_DEPOSIT, $amount, $meta, $confirmed);
46
    }
47
48
    /**
49
     * this method adds a new transaction to the translation table
50
     *
51
     * @param string $type
52
     * @param int $amount
53
     * @param array|null $meta
54
     * @param bool $confirmed
55
     * @return Transaction
56
     * @throws
57
     */
58 28
    protected function change(string $type, int $amount, ?array $meta, bool $confirmed): Transaction
59
    {
60
        return DB::transaction(function () use ($type, $amount, $meta, $confirmed) {
61
62
            /**
63
             * @var WalletModel $wallet
64
             */
65 28
            $wallet = app(WalletService::class)
66 28
                ->getWallet($this);
0 ignored issues
show
Bug introduced by
$this of type Bavix\Wallet\Traits\HasWallet is incompatible with the type Bavix\Wallet\Interfaces\Wallet expected by parameter $object of Bavix\Wallet\Services\WalletService::getWallet(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

66
                ->getWallet(/** @scrutinizer ignore-type */ $this);
Loading history...
67
68 28
            if ($confirmed) {
69 28
                $this->addBalance($wallet, $amount);
70
            }
71
72 28
            return $this->transactions()->create([
73 28
                'type' => $type,
74 28
                'wallet_id' => $wallet->getKey(),
75 28
                'uuid' => Uuid::uuid4()->toString(),
76 28
                'confirmed' => $confirmed,
77 28
                'amount' => $amount,
78 28
                'meta' => $meta,
79
            ]);
80 28
        });
81
    }
82
83
    /**
84
     * This method automatically updates the balance in the
85
     * database and the project statics
86
     *
87
     * @param WalletModel $wallet
88
     * @param int $amount
89
     * @return bool
90
     */
91 28
    protected function addBalance(WalletModel $wallet, int $amount): bool
92
    {
93 28
        $newBalance = $this->getBalanceAttribute() + $amount;
94 28
        $wallet->balance = $newBalance;
95
96 28
        if ($wallet->save()) {
97 28
            $proxy = app(ProxyService::class);
98 28
            $proxy->set($wallet->getKey(), $newBalance);
0 ignored issues
show
Bug introduced by
It seems like $wallet->getKey() can also be of type boolean and null; however, parameter $key of Bavix\Wallet\Services\ProxyService::set() 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

98
            $proxy->set(/** @scrutinizer ignore-type */ $wallet->getKey(), $newBalance);
Loading history...
99 28
            return true;
100
        }
101
102
        return false;
103
    }
104
105
    /**
106
     * Magic laravel framework method, makes it
107
     *  possible to call property balance
108
     *
109
     * Example:
110
     *  $user1 = User::first()->load('wallet');
111
     *  $user2 = User::first()->load('wallet');
112
     *
113
     * Without static:
114
     *  var_dump($user1->balance, $user2->balance); // 100 100
115
     *  $user1->deposit(100);
116
     *  $user2->deposit(100);
117
     *  var_dump($user1->balance, $user2->balance); // 200 200
118
     *
119
     * With static:
120
     *  var_dump($user1->balance, $user2->balance); // 100 100
121
     *  $user1->deposit(100);
122
     *  var_dump($user1->balance); // 200
123
     *  $user2->deposit(100);
124
     *  var_dump($user2->balance); // 300
125
     *
126
     * @return int
127
     * @throws
128
     */
129 38
    public function getBalanceAttribute(): int
130
    {
131 38
        if ($this instanceof WalletModel) {
132 38
            $this->exists or $this->save();
133 38
            $proxy = app(ProxyService::class);
134 38
            if (!$proxy->has($this->getKey())) {
0 ignored issues
show
Bug introduced by
It seems like $this->getKey() can also be of type boolean and null; however, parameter $key of Bavix\Wallet\Services\ProxyService::has() 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

134
            if (!$proxy->has(/** @scrutinizer ignore-type */ $this->getKey())) {
Loading history...
135 38
                $proxy->set($this->getKey(), (int)($this->attributes['balance'] ?? 0));
0 ignored issues
show
Bug introduced by
It seems like $this->getKey() can also be of type boolean and null; however, parameter $key of Bavix\Wallet\Services\ProxyService::set() 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

135
                $proxy->set(/** @scrutinizer ignore-type */ $this->getKey(), (int)($this->attributes['balance'] ?? 0));
Loading history...
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...
136
            }
137
138 38
            return $proxy[$this->getKey()];
139
        }
140
141 38
        return $this->wallet->balance;
142
    }
143
144
    /**
145
     * all user actions on wallets will be in this method
146
     *
147
     * @return MorphMany
148
     */
149 28
    public function transactions(): MorphMany
150
    {
151 28
        return ($this instanceof WalletModel ? $this->holder : $this)
152 28
            ->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

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

207
            throw new BalanceIsEmpty(/** @scrutinizer ignore-type */ trans('wallet::errors.wallet_empty'));
Loading history...
208
        }
209
210 27
        if (!$this->canWithdraw($amount)) {
211 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

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

258
        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...
259 17
            'status' => $status,
260 17
            'deposit_id' => $deposit->getKey(),
261 17
            'withdraw_id' => $withdraw->getKey(),
262 17
            'from_type' => ($this instanceof WalletModel ? $this : $this->wallet)->getMorphClass(),
263 17
            'from_id' => ($this instanceof WalletModel ? $this : $this->wallet)->getKey(),
264 17
            'to_type' => $wallet->getMorphClass(),
265 17
            'to_id' => $wallet->getKey(),
266 17
            'fee' => \abs($withdraw->amount) - \abs($deposit->amount),
267 17
            'uuid' => Uuid::uuid4()->toString(),
268
        ]);
269
    }
270
271
    /**
272
     * the forced transfer is needed when the user does not have the money and we drive it.
273
     * Sometimes you do. Depends on business logic.
274
     *
275
     * @param Wallet $wallet
276
     * @param int $amount
277
     * @param array|null $meta
278
     * @param string $status
279
     * @return Transfer
280
     */
281 6
    public function forceTransfer(Wallet $wallet, int $amount, ?array $meta = null, string $status = Transfer::STATUS_TRANSFER): Transfer
282
    {
283
        return DB::transaction(function () use ($amount, $wallet, $meta, $status) {
284 6
            $fee = app(WalletService::class)
285 6
                ->fee($wallet, $amount);
286
287 6
            $withdraw = $this->forceWithdraw($amount + $fee, $meta);
288 6
            $deposit = $wallet->deposit($amount, $meta);
289 6
            return $this->assemble($wallet, $withdraw, $deposit, $status);
290 6
        });
291
    }
292
293
    /**
294
     * the transfer table is used to confirm the payment
295
     * this method receives all transfers
296
     *
297
     * @return MorphMany
298
     */
299 10
    public function transfers(): MorphMany
300
    {
301 10
        return app(WalletService::class)
302 10
            ->getWallet($this)
0 ignored issues
show
Bug introduced by
$this of type Bavix\Wallet\Traits\HasWallet is incompatible with the type Bavix\Wallet\Interfaces\Wallet expected by parameter $object of Bavix\Wallet\Services\WalletService::getWallet(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

302
            ->getWallet(/** @scrutinizer ignore-type */ $this)
Loading history...
303 10
            ->morphMany(config('wallet.transfer.model'), 'from');
304
    }
305
306
    /**
307
     * Get default Wallet
308
     * this method is used for Eager Loading
309
     *
310
     * @return MorphOne|WalletModel
311
     */
312 38
    public function wallet(): MorphOne
313
    {
314 38
        return ($this instanceof WalletModel ? $this->holder : $this)
315 38
            ->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

315
            ->/** @scrutinizer ignore-call */ morphOne(config('wallet.wallet.model'), 'holder')
Loading history...
316 38
            ->where('slug', config('wallet.wallet.default.slug'))
317 38
            ->withDefault([
318 38
                'name' => config('wallet.wallet.default.name'),
319 38
                'slug' => config('wallet.wallet.default.slug'),
320 38
                'balance' => 0,
321
            ]);
322
    }
323
324
}
325