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

HasWallet::forceTransfer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 4
dl 0
loc 9
ccs 6
cts 6
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\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\Objects\Operation;
12
use Bavix\Wallet\Services\CommonService;
13
use Bavix\Wallet\Services\ProxyService;
14
use Bavix\Wallet\Services\WalletService;
15
use Illuminate\Database\Eloquent\Model;
16
use Illuminate\Database\Eloquent\Relations\MorphMany;
17
use Illuminate\Database\Eloquent\Relations\MorphOne;
18
use Illuminate\Support\Collection;
19
use Illuminate\Support\Facades\DB;
20
use Ramsey\Uuid\Uuid;
21
22
/**
23
 * Trait HasWallet
24
 *
25
 * @package Bavix\Wallet\Traits
26
 *
27
 * @property-read WalletModel $wallet
28
 * @property-read Collection|WalletModel[] $wallets
29
 * @property-read int $balance
30
 */
31
trait HasWallet
32
{
33
34
    /**
35
     * The input means in the system
36
     *
37
     * @param int $amount
38
     * @param array|null $meta
39
     * @param bool $confirmed
40
     *
41
     * @return Transaction
42
     */
43 31
    public function deposit(int $amount, ?array $meta = null, bool $confirmed = true): Transaction
44
    {
45 31
        app(WalletService::class)->checkAmount($amount);
46
47
        /**
48
         * @var WalletModel $wallet
49
         */
50 28
        $wallet = app(WalletService::class)
51 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

51
            ->getWallet(/** @scrutinizer ignore-type */ $this);
Loading history...
52
53 28
        $transactions = app(CommonService::class)->enforce($wallet, [
54 28
            (new Operation())
55 28
                ->setType(Transaction::TYPE_DEPOSIT)
56 28
                ->setConfirmed($confirmed)
57 28
                ->setAmount($amount)
58 28
                ->setMeta($meta)
59
        ]);
60
61 28
        return \current($transactions);
62
    }
63
64
    /**
65
     * Magic laravel framework method, makes it
66
     *  possible to call property balance
67
     *
68
     * Example:
69
     *  $user1 = User::first()->load('wallet');
70
     *  $user2 = User::first()->load('wallet');
71
     *
72
     * Without static:
73
     *  var_dump($user1->balance, $user2->balance); // 100 100
74
     *  $user1->deposit(100);
75
     *  $user2->deposit(100);
76
     *  var_dump($user1->balance, $user2->balance); // 200 200
77
     *
78
     * With static:
79
     *  var_dump($user1->balance, $user2->balance); // 100 100
80
     *  $user1->deposit(100);
81
     *  var_dump($user1->balance); // 200
82
     *  $user2->deposit(100);
83
     *  var_dump($user2->balance); // 300
84
     *
85
     * @return int
86
     * @throws
87
     */
88 38
    public function getBalanceAttribute(): int
89
    {
90 38
        if ($this instanceof WalletModel) {
91 38
            $this->exists or $this->save();
92 38
            $proxy = app(ProxyService::class);
93 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

93
            if (!$proxy->has(/** @scrutinizer ignore-type */ $this->getKey())) {
Loading history...
94 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

94
                $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...
95
            }
96
97 38
            return $proxy[$this->getKey()];
98
        }
99
100 38
        return $this->wallet->balance;
101
    }
102
103
    /**
104
     * all user actions on wallets will be in this method
105
     *
106
     * @return MorphMany
107
     */
108 28
    public function transactions(): MorphMany
109
    {
110 28
        return ($this instanceof WalletModel ? $this->holder : $this)
111 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

111
            ->/** @scrutinizer ignore-call */ morphMany(config('wallet.transaction.model'), 'payable');
Loading history...
112
    }
113
114
    /**
115
     * This method ignores errors that occur when transferring funds
116
     *
117
     * @param Wallet $wallet
118
     * @param int $amount
119
     * @param array|null $meta
120
     * @param string $status
121
     * @return null|Transfer
122
     */
123 3
    public function safeTransfer(Wallet $wallet, int $amount, ?array $meta = null, string $status = Transfer::STATUS_TRANSFER): ?Transfer
124
    {
125
        try {
126 3
            return $this->transfer($wallet, $amount, $meta, $status);
127 3
        } catch (\Throwable $throwable) {
128 3
            return null;
129
        }
130
    }
131
132
    /**
133
     * A method that transfers funds from host to host
134
     *
135
     * @param Wallet $wallet
136
     * @param int $amount
137
     * @param array|null $meta
138
     * @param string $status
139
     * @return Transfer
140
     * @throws
141
     */
142 15
    public function transfer(Wallet $wallet, int $amount, ?array $meta = null, string $status = Transfer::STATUS_TRANSFER): Transfer
143
    {
144
        return DB::transaction(function () use ($amount, $wallet, $meta, $status) {
145 15
            $fee = app(WalletService::class)
146 15
                ->fee($wallet, $amount);
147
148 15
            $withdraw = $this->withdraw($amount + $fee, $meta);
149 15
            $deposit = $wallet->deposit($amount, $meta);
150 15
            return $this->assemble($wallet, $withdraw, $deposit, $status);
151 15
        });
152
    }
153
154
    /**
155
     * Withdrawals from the system
156
     *
157
     * @param int $amount
158
     * @param array|null $meta
159
     * @param bool $confirmed
160
     *
161
     * @return Transaction
162
     */
163 33
    public function withdraw(int $amount, ?array $meta = null, bool $confirmed = true): Transaction
164
    {
165 33
        if ($amount && !$this->balance) {
166 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

166
            throw new BalanceIsEmpty(/** @scrutinizer ignore-type */ trans('wallet::errors.wallet_empty'));
Loading history...
167
        }
168
169 27
        if (!$this->canWithdraw($amount)) {
170 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

170
            throw new InsufficientFunds(/** @scrutinizer ignore-type */ trans('wallet::errors.insufficient_funds'));
Loading history...
171
        }
172
173 26
        return $this->forceWithdraw($amount, $meta, $confirmed);
174
    }
175
176
    /**
177
     * Checks if you can withdraw funds
178
     *
179
     * @param int $amount
180
     * @return bool
181
     */
182 27
    public function canWithdraw(int $amount): bool
183
    {
184 27
        return $this->balance >= $amount;
185
    }
186
187
    /**
188
     * Forced to withdraw funds from system
189
     *
190
     * @param int $amount
191
     * @param array|null $meta
192
     * @param bool $confirmed
193
     *
194
     * @return Transaction
195
     */
196 27
    public function forceWithdraw(int $amount, ?array $meta = null, bool $confirmed = true): Transaction
197
    {
198 27
        app(WalletService::class)->checkAmount($amount);
199
200
        /**
201
         * @var WalletModel $wallet
202
         */
203 27
        $wallet = app(WalletService::class)
204 27
            ->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

204
            ->getWallet(/** @scrutinizer ignore-type */ $this);
Loading history...
205
206 27
        $transactions = app(CommonService::class)->enforce($wallet, [
207 27
            (new Operation())
208 27
                ->setType(Transaction::TYPE_WITHDRAW)
209 27
                ->setConfirmed($confirmed)
210 27
                ->setAmount(-$amount)
211 27
                ->setMeta($meta)
212
        ]);
213
214 27
        return \current($transactions);
215
    }
216
217
    /**
218
     * this method adds a new transfer to the transfer table
219
     *
220
     * @param Wallet $wallet
221
     * @param Transaction $withdraw
222
     * @param Transaction $deposit
223
     * @param string $status
224
     * @return Transfer
225
     * @throws
226
     */
227 17
    protected function assemble(Wallet $wallet, Transaction $withdraw, Transaction $deposit, string $status = Transfer::STATUS_PAID): Transfer
228
    {
229
        /**
230
         * @var Model $wallet
231
         */
232 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

232
        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...
233 17
            'status' => $status,
234 17
            'deposit_id' => $deposit->getKey(),
235 17
            'withdraw_id' => $withdraw->getKey(),
236 17
            'from_type' => ($this instanceof WalletModel ? $this : $this->wallet)->getMorphClass(),
237 17
            'from_id' => ($this instanceof WalletModel ? $this : $this->wallet)->getKey(),
238 17
            'to_type' => $wallet->getMorphClass(),
239 17
            'to_id' => $wallet->getKey(),
240 17
            'fee' => \abs($withdraw->amount) - \abs($deposit->amount),
241 17
            'uuid' => Uuid::uuid4()->toString(),
242
        ]);
243
    }
244
245
    /**
246
     * the forced transfer is needed when the user does not have the money and we drive it.
247
     * Sometimes you do. Depends on business logic.
248
     *
249
     * @param Wallet $wallet
250
     * @param int $amount
251
     * @param array|null $meta
252
     * @param string $status
253
     * @return Transfer
254
     */
255 6
    public function forceTransfer(Wallet $wallet, int $amount, ?array $meta = null, string $status = Transfer::STATUS_TRANSFER): Transfer
256
    {
257
        return DB::transaction(function () use ($amount, $wallet, $meta, $status) {
258 6
            $fee = app(WalletService::class)
259 6
                ->fee($wallet, $amount);
260
261 6
            $withdraw = $this->forceWithdraw($amount + $fee, $meta);
262 6
            $deposit = $wallet->deposit($amount, $meta);
263 6
            return $this->assemble($wallet, $withdraw, $deposit, $status);
264 6
        });
265
    }
266
267
    /**
268
     * the transfer table is used to confirm the payment
269
     * this method receives all transfers
270
     *
271
     * @return MorphMany
272
     */
273 10
    public function transfers(): MorphMany
274
    {
275 10
        return app(WalletService::class)
276 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

276
            ->getWallet(/** @scrutinizer ignore-type */ $this)
Loading history...
277 10
            ->morphMany(config('wallet.transfer.model'), 'from');
278
    }
279
280
    /**
281
     * Get default Wallet
282
     * this method is used for Eager Loading
283
     *
284
     * @return MorphOne|WalletModel
285
     */
286 38
    public function wallet(): MorphOne
287
    {
288 38
        return ($this instanceof WalletModel ? $this->holder : $this)
289 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

289
            ->/** @scrutinizer ignore-call */ morphOne(config('wallet.wallet.model'), 'holder')
Loading history...
290 38
            ->where('slug', config('wallet.wallet.default.slug'))
291 38
            ->withDefault([
292 38
                'name' => config('wallet.wallet.default.name'),
293 38
                'slug' => config('wallet.wallet.default.slug'),
294 38
                'balance' => 0,
295
            ]);
296
    }
297
298
}
299