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

HasWallet::wallet()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 0
dl 0
loc 7
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\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 Illuminate\Database\Eloquent\Model;
12
use Illuminate\Database\Eloquent\Relations\MorphMany;
13
use Illuminate\Database\Eloquent\Relations\MorphOne;
14
use Illuminate\Support\Collection;
15
use Illuminate\Support\Facades\DB;
16
use Ramsey\Uuid\Uuid;
17
18
/**
19
 * Trait HasWallet
20
 *
21
 * @package Bavix\Wallet\Traits
22
 *
23
 * @property-read WalletModel $wallet
24
 * @property-read Collection|WalletModel[] $wallets
25
 * @property-read int $balance
26
 */
27
trait HasWallet
28
{
29
30
    /**
31
     * @param int $amount
32
     * @throws
33
     */
34 19
    private function checkAmount(int $amount): void
35
    {
36 19
        if ($amount <= 0) {
37 4
            throw new AmountInvalid('The amount must be greater than zero');
38
        }
39 15
    }
40
41
    /**
42
     * @param int $amount
43
     * @param array|null $meta
44
     * @param bool $confirmed
45
     *
46
     * @return Transaction
47
     */
48 17
    public function forceWithdraw(int $amount, ?array $meta = null, bool $confirmed = true): Transaction
49
    {
50 17
        $this->checkAmount($amount);
51 15
        return $this->change(-$amount, $meta, $confirmed);
52
    }
53
54
    /**
55
     * @param int $amount
56
     * @param array|null $meta
57
     * @param bool $confirmed
58
     *
59
     * @return Transaction
60
     */
61 17
    public function deposit(int $amount, ?array $meta = null, bool $confirmed = true): Transaction
62
    {
63 17
        $this->checkAmount($amount);
64 15
        return $this->change($amount, $meta, $confirmed);
65
    }
66
67
    /**
68
     * @param int $amount
69
     * @param array|null $meta
70
     * @param bool $confirmed
71
     *
72
     * @return Transaction
73
     */
74 18
    public function withdraw(int $amount, ?array $meta = null, bool $confirmed = true): Transaction
75
    {
76 18
        if (!$this->canWithdraw($amount)) {
77 7
            throw new BalanceIsEmpty('Balance insufficient for write-off');
78
        }
79
80 16
        return $this->forceWithdraw($amount, $meta, $confirmed);
81
    }
82
83
    /**
84
     * @param int $amount
85
     * @return bool
86
     */
87 18
    public function canWithdraw(int $amount): bool
88
    {
89 18
        return $this->balance >= $amount;
90
    }
91
92
    /**
93
     * @param Wallet $wallet
94
     * @param int $amount
95
     * @param array|null $meta
96
     * @return Transfer
97
     * @throws
98
     */
99 8
    public function transfer(Wallet $wallet, int $amount, ?array $meta = null): Transfer
100
    {
101
        return DB::transaction(function() use ($amount, $wallet, $meta) {
102 8
            $withdraw = $this->withdraw($amount, $meta);
103 8
            $deposit = $wallet->deposit($amount, $meta);
104 8
            return $this->assemble($wallet, $withdraw, $deposit);
105 8
        });
106
    }
107
108
    /**
109
     * @param Wallet $wallet
110
     * @param int $amount
111
     * @param array|null $meta
112
     * @return null|Transfer
113
     */
114 2
    public function safeTransfer(Wallet $wallet, int $amount, ?array $meta = null): ?Transfer
115
    {
116
        try {
117 2
            return $this->transfer($wallet, $amount, $meta);
118 2
        } catch (\Throwable $throwable) {
119 2
            return null;
120
        }
121
    }
122
123
    /**
124
     * @param Wallet $wallet
125
     * @param int $amount
126
     * @param array|null $meta
127
     * @return Transfer
128
     */
129 4
    public function forceTransfer(Wallet $wallet, int $amount, ?array $meta = null): Transfer
130
    {
131
        return DB::transaction(function() use ($amount, $wallet, $meta) {
132 4
            $withdraw = $this->forceWithdraw($amount, $meta);
133 4
            $deposit = $wallet->deposit($amount, $meta);
134 4
            return $this->assemble($wallet, $withdraw, $deposit);
135 4
        });
136
    }
137
138
    /**
139
     * @param Wallet $wallet
140
     * @param Transaction $withdraw
141
     * @param Transaction $deposit
142
     * @return Transfer
143
     * @throws
144
     */
145 9
    protected function assemble(Wallet $wallet, Transaction $withdraw, Transaction $deposit): Transfer
146
    {
147
        /**
148
         * @var Model $wallet
149
         */
150 9
        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

150
        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...
151 9
            'deposit_id' => $deposit->getKey(),
152 9
            'withdraw_id' => $withdraw->getKey(),
153 9
            '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

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

154
            'from_id' => $this->/** @scrutinizer ignore-call */ getKey(),
Loading history...
155 9
            'to_type' => $wallet->getMorphClass(),
156 9
            'to_id' => $wallet->getKey(),
157 9
            'uuid' => Uuid::uuid4()->toString(),
158
        ]);
159
    }
160
161
    /**
162
     * @param int $amount
163
     * @param array|null $meta
164
     * @param bool $confirmed
165
     * @return Transaction
166
     * @throws
167
     */
168 15
    protected function change(int $amount, ?array $meta, bool $confirmed): Transaction
169
    {
170
        return DB::transaction(function () use ($amount, $meta, $confirmed) {
171 15
            if ($confirmed) {
172 15
                $this->addBalance($amount);
173
            }
174
175 15
            return $this->transactions()->create([
176 15
                'type' => $amount > 0 ? 'deposit' : 'withdraw',
177 15
                'payable_type' => $this->getMorphClass(),
178 15
                'payable_id' => $this->getKey(),
179 15
                'uuid' => Uuid::uuid4()->toString(),
180 15
                'confirmed' => $confirmed,
181 15
                'amount' => $amount,
182 15
                'meta' => $meta,
183
            ]);
184 15
        });
185
    }
186
187
    /**
188
     * @return MorphMany
189
     */
190 15
    public function transactions(): MorphMany
191
    {
192 15
        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

192
        return $this->/** @scrutinizer ignore-call */ morphMany(config('wallet.transaction.model'), 'payable');
Loading history...
193
    }
194
195
    /**
196
     * @return MorphMany
197
     */
198 4
    public function transfers(): MorphMany
199
    {
200 4
        return $this->morphMany(config('wallet.transfer.model'), 'from');
201
    }
202
203
    /**
204
     * @return MorphMany
205
     */
206
    public function wallets(): MorphMany
207
    {
208
        return $this->morphMany(config('wallet.wallet.model'), 'holder');
209
    }
210
211
    /**
212
     * @return MorphOne
213
     */
214 19
    public function wallet(): MorphOne
215
    {
216 19
        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

216
        return $this->/** @scrutinizer ignore-call */ morphOne(config('wallet.wallet.model'), 'holder')
Loading history...
217 19
            ->withDefault([
218 19
                'name' => config('wallet.wallet.default.name'),
219 19
                'slug' => config('wallet.wallet.default.slug'),
220 19
                'balance' => 0,
221
            ]);
222
    }
223
224
    /**
225
     * Example:
226
     *  $user1 = User::first()->load('balance');
227
     *  $user2 = User::first()->load('balance');
228
     *
229
     * Without static:
230
     *  var_dump($user1->balance, $user2->balance); // 100 100
231
     *  $user1->deposit(100);
232
     *  $user2->deposit(100);
233
     *  var_dump($user1->balance, $user2->balance); // 200 200
234
     *
235
     * With static:
236
     *  var_dump($user1->balance, $user2->balance); // 100 100
237
     *  $user1->deposit(100);
238
     *  var_dump($user1->balance); // 200
239
     *  $user2->deposit(100);
240
     *  var_dump($user2->balance); // 300
241
     *
242
     * @return int
243
     */
244 19
    public function getBalanceAttribute(): int
245
    {
246 19
        if ($this instanceof WalletModel) {
247 19
            return (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...
248
        }
249
250 19
        if (!\array_key_exists('wallet', $this->relations)) {
251 19
            $this->load('wallet');
0 ignored issues
show
Bug introduced by
It seems like load() 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

251
            $this->/** @scrutinizer ignore-call */ 
252
                   load('wallet');
Loading history...
252
        }
253
254 19
        return $this->wallet->balance;
255
    }
256
257
    /**
258
     * @param int $amount
259
     * @return bool
260
     */
261 15
    protected function addBalance(int $amount): bool
262
    {
263 15
        $wallet = $this;
264
265 15
        if (!($this instanceof WalletModel)) {
266 15
            $this->getBalanceAttribute();
267 15
            $wallet = $this->wallet;
268
        }
269
270 15
        $wallet->balance += $amount;
271 15
        return $wallet->save();
0 ignored issues
show
Bug introduced by
It seems like save() 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

271
        return $wallet->/** @scrutinizer ignore-call */ save();
Loading history...
272
    }
273
274
}
275