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

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

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

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

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

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

227
        return $this->/** @scrutinizer ignore-call */ morphOne(config('wallet.wallet.model'), 'holder')
Loading history...
228 19
            ->withDefault([
229 19
                'name' => config('wallet.wallet.default.name'),
230 19
                'slug' => config('wallet.wallet.default.slug'),
231 19
                'balance' => 0,
232
            ]);
233
    }
234
235
    /**
236
     * Example:
237
     *  $user1 = User::first()->load('wallet');
238
     *  $user2 = User::first()->load('wallet');
239
     *
240
     * Without static:
241
     *  var_dump($user1->balance, $user2->balance); // 100 100
242
     *  $user1->deposit(100);
243
     *  $user2->deposit(100);
244
     *  var_dump($user1->balance, $user2->balance); // 200 200
245
     *
246
     * With static:
247
     *  var_dump($user1->balance, $user2->balance); // 100 100
248
     *  $user1->deposit(100);
249
     *  var_dump($user1->balance); // 200
250
     *  $user2->deposit(100);
251
     *  var_dump($user2->balance); // 300
252
     *
253
     * @return int
254
     * @throws
255
     */
256 19
    public function getBalanceAttribute(): int
257
    {
258 19
        if ($this instanceof WalletModel) {
259 19
            $this->exists or $this->save();
260 19
            if (!WalletProxy::has($this->getKey())) {
261 19
                WalletProxy::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...
262
            }
263
264 19
            return WalletProxy::get($this->getKey());
265
        }
266
267 19
        return $this->wallet->balance;
268
    }
269
270
    /**
271
     * @param WalletModel $wallet
272
     * @param int $amount
273
     * @return bool
274
     */
275 15
    protected function addBalance(WalletModel $wallet, int $amount): bool
276
    {
277 15
        $newBalance = $this->getBalanceAttribute() + $amount;
278 15
        $wallet->balance = $newBalance;
279
280 15
        if ($wallet->save()) {
281 15
            WalletProxy::set($wallet->getKey(), $newBalance);
282 15
            return true;
283
        }
284
285
        return false;
286
    }
287
288
}
289