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

HasWallet::transfers()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
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
            if (!$this->wallet->exists) {
176
                $this->wallet->save();
177
            }
178
179 15
            return $this->transactions()->create([
180 15
                'type' => $amount > 0 ? 'deposit' : 'withdraw',
181 15
                'payable_type' => $this->getMorphClass(),
182 15
                'payable_id' => $this->getKey(),
183 15
                'wallet_id' => $this->wallet->id,
184 15
                'uuid' => Uuid::uuid4()->toString(),
185 15
                'confirmed' => $confirmed,
186 15
                'amount' => $amount,
187 15
                'meta' => $meta,
188
            ]);
189 15
        });
190
    }
191
192
    /**
193
     * @return MorphMany
194
     */
195 15
    public function transactions(): MorphMany
196
    {
197 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

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

221
        return $this->/** @scrutinizer ignore-call */ morphOne(config('wallet.wallet.model'), 'holder')
Loading history...
222 19
            ->withDefault([
223 19
                'name' => config('wallet.wallet.default.name'),
224 19
                'slug' => config('wallet.wallet.default.slug'),
225 19
                'balance' => 0,
226
            ]);
227
    }
228
229
    /**
230
     * Example:
231
     *  $user1 = User::first()->load('wallet');
232
     *  $user2 = User::first()->load('wallet');
233
     *
234
     * Without static:
235
     *  var_dump($user1->balance, $user2->balance); // 100 100
236
     *  $user1->deposit(100);
237
     *  $user2->deposit(100);
238
     *  var_dump($user1->balance, $user2->balance); // 200 200
239
     *
240
     * With static:
241
     *  var_dump($user1->balance, $user2->balance); // 100 100
242
     *  $user1->deposit(100);
243
     *  var_dump($user1->balance); // 200
244
     *  $user2->deposit(100);
245
     *  var_dump($user2->balance); // 300
246
     *
247
     * @return int
248
     */
249 19
    public function getBalanceAttribute(): int
250
    {
251 19
        if ($this instanceof WalletModel) {
252 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...
253
        }
254
255 19
        return $this->wallet->balance;
256
    }
257
258
    /**
259
     * @param int $amount
260
     * @return bool
261
     */
262 15
    protected function addBalance(int $amount): bool
263
    {
264 15
        $wallet = $this;
265
266 15
        if (!($this instanceof WalletModel)) {
267 15
            $this->getBalanceAttribute();
268 15
            $wallet = $this->wallet;
269
        }
270
271 15
        $wallet->balance += $amount;
272 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

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