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

HasWallet::holderTransfers()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 7
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\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
        /**
46
         * @var WalletService $walletService
47
         */
48 31
        $walletService = \app(WalletService::class);
49 31
        $walletService->checkAmount($amount);
50
51
        /**
52
         * @var WalletModel $wallet
53
         */
54 28
        $wallet = $walletService->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

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

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

97
                $proxy->set(/** @scrutinizer ignore-type */ $this->getKey(), (int)($this->attributes['balance'] ?? 0));
Loading history...
98
            }
99
100 38
            return $proxy[$this->getKey()];
101
        }
102
103 38
        return $this->wallet->balance;
104
    }
105
106
    /**
107
     * all user actions on wallets will be in this method
108
     *
109
     * @return MorphMany
110
     */
111 28
    public function transactions(): MorphMany
112
    {
113 28
        return ($this instanceof WalletModel ? $this->holder : $this)
114 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

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

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

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

210
        $wallet = $walletService->getWallet(/** @scrutinizer ignore-type */ $this);
Loading history...
211
212 27
        $transactions = \app(CommonService::class)->enforce($wallet, [
213 27
            (new Operation())
214 27
                ->setType(Transaction::TYPE_WITHDRAW)
215 27
                ->setConfirmed($confirmed)
216 27
                ->setAmount(-$amount)
217 27
                ->setMeta($meta)
218
        ]);
219
220 27
        return \current($transactions);
221
    }
222
223
    /**
224
     * this method adds a new transfer to the transfer table
225
     *
226
     * @param Wallet $wallet
227
     * @param Transaction $withdraw
228
     * @param Transaction $deposit
229
     * @param string $status
230
     * @return Transfer
231
     * @throws
232
     */
233 17
    protected function assemble(Wallet $wallet, Transaction $withdraw, Transaction $deposit, string $status = Transfer::STATUS_PAID): Transfer
234
    {
235
        /**
236
         * @var Model $wallet
237
         */
238 17
        return \app(Transfer::class)->create([
239 17
            'status' => $status,
240 17
            'deposit_id' => $deposit->getKey(),
241 17
            'withdraw_id' => $withdraw->getKey(),
242 17
            'from_type' => ($this instanceof WalletModel ? $this : $this->wallet)->getMorphClass(),
243 17
            'from_id' => ($this instanceof WalletModel ? $this : $this->wallet)->getKey(),
244 17
            'to_type' => $wallet->getMorphClass(),
245 17
            'to_id' => $wallet->getKey(),
246 17
            'fee' => \abs($withdraw->amount) - \abs($deposit->amount),
247 17
            'uuid' => Uuid::uuid4()->toString(),
248
        ]);
249
    }
250
251
    /**
252
     * the forced transfer is needed when the user does not have the money and we drive it.
253
     * Sometimes you do. Depends on business logic.
254
     *
255
     * @param Wallet $wallet
256
     * @param int $amount
257
     * @param array|null $meta
258
     * @param string $status
259
     * @return Transfer
260
     */
261 6
    public function forceTransfer(Wallet $wallet, int $amount, ?array $meta = null, string $status = Transfer::STATUS_TRANSFER): Transfer
262
    {
263
        return DB::transaction(function () use ($amount, $wallet, $meta, $status) {
264 6
            $fee = \app(WalletService::class)
265 6
                ->fee($wallet, $amount);
266
267 6
            $withdraw = $this->forceWithdraw($amount + $fee, $meta);
268 6
            $deposit = $wallet->deposit($amount, $meta);
269 6
            return $this->assemble($wallet, $withdraw, $deposit, $status);
270 6
        });
271
    }
272
273
    /**
274
     * the transfer table is used to confirm the payment
275
     * this method receives all transfers
276
     *
277
     * @return MorphMany
278
     */
279 10
    public function transfers(): MorphMany
280
    {
281 10
        return \app(WalletService::class)
282 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

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

295
            ->/** @scrutinizer ignore-call */ morphOne(config('wallet.wallet.model'), 'holder')
Loading history...
296 38
            ->where('slug', config('wallet.wallet.default.slug'))
297 38
            ->withDefault([
298 38
                'name' => config('wallet.wallet.default.name'),
299 38
                'slug' => config('wallet.wallet.default.slug'),
300 38
                'balance' => 0,
301
            ]);
302
    }
303
304
}
305