Completed
Push — master ( e964f8...9c0f53 )
by Moecasts
04:26
created

Wallet::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 Moecasts\Laravel\Wallet\Models;
4
5
use Illuminate\Database\Eloquent\Model;
6
use Illuminate\Database\Eloquent\Relations\HasMany;
7
use Illuminate\Database\Eloquent\Relations\MorphMany;
8
use Illuminate\Database\Eloquent\Relations\MorphTo;
9
use Illuminate\Support\Facades\DB;
10
use Moecasts\Laravel\Wallet\Exceptions\AmountInvalid;
11
use Moecasts\Laravel\Wallet\Exceptions\ExchangeInvalid;
12
use Moecasts\Laravel\Wallet\Exceptions\InsufficientFunds;
13
use Moecasts\Laravel\Wallet\Interfaces\Assemblable;
14
use Moecasts\Laravel\Wallet\Interfaces\Exchangeable;
15
use Moecasts\Laravel\Wallet\Interfaces\Transferable;
16
use Moecasts\Laravel\Wallet\Models\Transaction;
17
use Moecasts\Laravel\Wallet\Tax;
18
use Moecasts\Laravel\Wallet\Traits\CanPay;
19
use Moecasts\Laravel\Wallet\WalletProxy;
20
use Ramsey\Uuid\Uuid;
21
22
class Wallet extends Model
23
{
24
    use CanPay;
0 ignored issues
show
introduced by
The trait Moecasts\Laravel\Wallet\Traits\CanPay requires some properties which are not provided by Moecasts\Laravel\Wallet\Models\Wallet: $withdraw, $deposit, $fromWallet, $toWallet
Loading history...
25
26
    protected $fillable = [
27
        'holder_type',
28
        'holder_id',
29
        'currency',
30
        'balance'
31
    ];
32
33 22
    public function holder(): MorphTo
34
    {
35 22
        return $this->morphTo();
36
    }
37
38 21
    public function transactions(): HasMany
39
    {
40 21
        return $this->hasMany(Transaction::class);
41
    }
42
43 4
    public function holderTransfers(): MorphMany
44
    {
45 4
        return $this->holder->transfers();
46
    }
47
48 3
    public function transfers(): HasMany
49
    {
50 3
        return $this->hasMany(Transfer::class, 'from_wallet_id');
51
    }
52
53 20
    public function deposit(float $amount, ?array $meta = null, bool $confirmed = true): Transaction
54
    {
55 20
        $this->checkAmount($amount);
0 ignored issues
show
Bug introduced by
$amount of type double is incompatible with the type integer expected by parameter $amount of Moecasts\Laravel\Wallet\...s\Wallet::checkAmount(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

55
        $this->checkAmount(/** @scrutinizer ignore-type */ $amount);
Loading history...
56
57 20
        $amount = (int) ($amount * $this->coefficient($this->currency));
58
59 20
        return $this->change(Transaction::TYPE_DEPOSIT, $amount, $meta, $confirmed);
60
    }
61
62 21
    private function checkAmount(int $amount): void
63
    {
64 21
        if ($amount < 0) {
65 1
            throw new AmountInvalid(trans('wallet::errors.price_positive'));
0 ignored issues
show
Bug introduced by
It seems like trans('wallet::errors.price_positive') can also be of type array; however, parameter $message of Moecasts\Laravel\Wallet\...tInvalid::__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

65
            throw new AmountInvalid(/** @scrutinizer ignore-type */ trans('wallet::errors.price_positive'));
Loading history...
66
        }
67 21
    }
68
69 21
    protected function change(string $type, int $amount, ?array $meta, bool $confirmed): Transaction
70
    {
71
        return DB::transaction(function () use ($type, $amount, $meta, $confirmed) {
72 21
            if ($confirmed) {
73 21
                $this->addBalance($amount);
74
            }
75
76 21
            return $this->transactions()->create([
77 21
                'type' => $type,
78 21
                'holder_type' => $this->holder->getMorphClass(),
79 21
                'holder_id' => $this->holder->getKey(),
80 21
                'wallet_id' => $this->getKey(),
81 21
                'uuid' => Uuid::uuid4()->toString(),
82 21
                'confirmed' => $confirmed,
83 21
                'amount' => $amount,
84 21
                'meta' => $meta,
85
            ]);
86 21
        });
87
    }
88
89 21
    protected function addBalance(float $amount): bool
90
    {
91 21
        $newBalance = $this->attributes['balance'] + $amount;
92 21
        $this->balance = $newBalance;
93 21
        $finalBalance = $newBalance / $this->coefficient($this->attributes['currency']);
94
95
        return
96
            // update database wallet
97 21
            $this->save() &&
98
99
            // update static wallet
100 21
            WalletProxy::set($this->getKey(), $finalBalance);
101
    }
102
103 22
    public function getBalanceAttribute(): float
104
    {
105 22
        $this->exists or $this->save();
106
107 22
        if (! WalletProxy::has($this->getKey())) {
108 5
            $balance = $this->attributes['balance'] / $this->coefficient($this->attributes['currency']);
109 5
            WalletProxy::set($this->getKey(), (float) ($balance ?? 0));
110
        }
111
112 22
        return WalletProxy::get($this->getKey());
113
    }
114
115 1
    public function safeTransfer(Transferable $transferable, float $amount, ?array $meta = null, string $action = Transfer::ACTION_TRANSFER): ?Transfer
116
    {
117
        try {
118 1
            return $this->transfer($transferable, $amount, $meta, $action);
119 1
        } catch (\Throwable $throwable) {
120 1
            return null;
121
        }
122
    }
123
124 9
    public function transfer(Transferable $transferable, float $amount, ?array $meta = null, string $action = Transfer::ACTION_TRANSFER): Transfer
125
    {
126 9
        $wallet = $transferable->getReceiptWallet($this->currency);
127
128
        return DB::transaction(function () use ($transferable, $amount, $wallet, $meta, $action) {
129 9
            $fee = Tax::fee($transferable, $wallet, $amount);
130 9
            $withdraw = $this->withdraw($amount + $fee, $meta);
131 7
            $deposit = $wallet->deposit($amount, $meta);
132 7
            return $this->assemble($transferable, $wallet, $withdraw, $deposit, $action);
133 9
        });
134
    }
135
136 12
    public function withdraw(float $amount, ?array $meta = null, bool $confirmed = true): Transaction
137
    {
138 12
        if (! $this->canWithdraw($amount)) {
139 5
            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 Moecasts\Laravel\Wallet\...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

139
            throw new InsufficientFunds(/** @scrutinizer ignore-type */ trans('wallet::errors.insufficient_funds'));
Loading history...
140
        }
141
142 10
        return $this->forceWithdraw($amount, $meta, $confirmed);
143
    }
144
145 12
    public function canWithdraw($amount): bool
146
    {
147 12
        return $this->balance >= $amount;
148
    }
149
150 14
    public function forceWithdraw(float $amount, ?array $meta = null, bool $confirmed = true): Transaction
151
    {
152 14
        $amount = (int) ($amount * $this->coefficient($this->currency));
153
154 14
        $this->checkAmount($amount);
155
156 14
        return $this->change(Transaction::TYPE_WITHDRAW, -$amount, $meta, $confirmed);
157
    }
158
159 11
    protected function assemble(Assemblable $transferable, Wallet $wallet, Transaction $withdraw, Transaction $deposit, string $action = Transfer::ACTION_PAID): Transfer
160
    {
161 11
        return \app('moecasts.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

161
        return \app('moecasts.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...
162 11
            'action' => $action,
163 11
            'deposit_id' => $deposit->getKey(),
164 11
            'withdraw_id' => $withdraw->getKey(),
165 11
            'from_type' => $this->holder->getMorphClass(),
166 11
            'from_id' => $this->holder->getKey(),
167 11
            'from_wallet_id' => $this->getKey(),
168 11
            'to_type' => $transferable->getMorphClass(),
0 ignored issues
show
Bug introduced by
The method getMorphClass() does not exist on Moecasts\Laravel\Wallet\Interfaces\Assemblable. It seems like you code against a sub-type of said class. However, the method does not exist in Moecasts\Laravel\Wallet\Interfaces\Transferable or Moecasts\Laravel\Wallet\Interfaces\Exchangeable or Moecasts\Laravel\Wallet\Interfaces\Product. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

168
            'to_type' => $transferable->/** @scrutinizer ignore-call */ getMorphClass(),
Loading history...
169 11
            'to_id' => $transferable->getKey(),
0 ignored issues
show
Bug introduced by
The method getKey() does not exist on Moecasts\Laravel\Wallet\Interfaces\Assemblable. It seems like you code against a sub-type of said class. However, the method does not exist in Moecasts\Laravel\Wallet\Interfaces\Transferable or Moecasts\Laravel\Wallet\Interfaces\Exchangeable or Moecasts\Laravel\Wallet\Interfaces\Product. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

169
            'to_id' => $transferable->/** @scrutinizer ignore-call */ getKey(),
Loading history...
170 11
            'to_wallet_id' => $wallet->getKey(),
171 11
            'fee' => \abs($withdraw->amount) - \abs($deposit->amount),
172 11
            'uuid' => Uuid::uuid4()->toString(),
173
        ]);
174
    }
175
176 2
    public function forceTransfer(Transferable $transferable, float $amount, ?array $meta = null, string $action = Transfer::ACTION_TRANSFER): Transfer
177
    {
178 2
        $wallet = $transferable->getReceiptWallet($this->currency);
179
180
        return DB::transaction(function () use ($transferable, $amount, $wallet, $meta, $action) {
181 2
            $fee = Tax::fee($transferable, $wallet, $amount);
182 2
            $withdraw = $this->forceWithdraw($amount + $fee, $meta);
183 2
            $deposit = $wallet->deposit($amount, $meta);
184 2
            return $this->assemble($transferable, $wallet, $withdraw, $deposit, $action);
185 2
        });
186
    }
187
188 4
    public function exchange(string $currency, float $amount, ?array $meta = null, $action = Transfer::ACTION_EXCHANGE): Transfer
189
    {
190 4
        $wallet = $this->holder->getWallet($currency);
191
192 4
        $exchangeRate = config('wallet.exchange.' . $this->currency . '.' . $currency);
193
194 4
        if (! $exchangeRate ||
195 4
            ! $this->holder instanceof Exchangeable) {
196 3
            throw new ExchangeInvalid(trans('wallet::errors.exchange_unsupported'));
0 ignored issues
show
Bug introduced by
It seems like trans('wallet::errors.exchange_unsupported') can also be of type array; however, parameter $message of Moecasts\Laravel\Wallet\...eInvalid::__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

196
            throw new ExchangeInvalid(/** @scrutinizer ignore-type */ trans('wallet::errors.exchange_unsupported'));
Loading history...
197
        }
198
199 1
        $exchangedAmount = $amount * $exchangeRate;
200
201
        return DB::transaction(function () use ($wallet, $amount, $exchangedAmount, $meta, $action) {
202 1
            $fee = Tax::fee($this->holder, $wallet, $amount);
203 1
            $withdraw = $this->withdraw($amount + $fee, $meta);
204 1
            $deposit = $wallet->deposit($exchangedAmount, $meta);
205 1
            return $this->assemble($this->holder, $wallet, $withdraw, $deposit, $action);
206 1
        });
207
    }
208
209 1
    public function safeExchange(string $currency, float $amount, ?array $meta = null, $action = Transfer::ACTION_EXCHANGE): ?Transfer
210
    {
211
        try {
212 1
            return $this->exchange($currency, $amount, $meta, $action);
213 1
        } catch (\Throwable $throwable) {
214 1
            return null;
215
        }
216
    }
217
218 1
    public function forceExchange(string $currency, float $amount, ?array $meta = null, $action = Transfer::ACTION_EXCHANGE): Transfer
219
    {
220 1
        $wallet = $this->holder->getWallet($currency);
221
222 1
        $exchangeRate = config('wallet.exchange.' . $this->currency . '.' . $currency);
223
224 1
        if (! $exchangeRate) {
225
            throw new ExchangeInvalid(trans('wallet::errors.exchange_unsupported'));
0 ignored issues
show
Bug introduced by
It seems like trans('wallet::errors.exchange_unsupported') can also be of type array; however, parameter $message of Moecasts\Laravel\Wallet\...eInvalid::__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

225
            throw new ExchangeInvalid(/** @scrutinizer ignore-type */ trans('wallet::errors.exchange_unsupported'));
Loading history...
226
        }
227
228 1
        $exchangedAmount = $amount * $exchangeRate;
229
230
        return DB::transaction(function () use ($wallet, $amount, $exchangedAmount, $meta, $action) {
231 1
            $fee = Tax::fee($this->holder, $wallet, $amount);
232 1
            $withdraw = $this->forceWithdraw($amount + $fee, $meta);
233 1
            $deposit = $wallet->deposit($exchangedAmount, $meta);
234 1
            return $this->assemble($this->holder, $wallet, $withdraw, $deposit, $action);
235 1
        });
236
    }
237
238 3
    public function refreshBalance(): bool
239
    {
240 3
        $balance = $this->getAvailableBalance();
241
242 3
        $this->attributes['balance'] = $balance;
243
244 3
        WalletProxy::set($this->getKey(), $balance);
245
246 3
        return $this->save();
247
    }
248
249 3
    public function getAvailableBalance(): int
250
    {
251 3
        return $this->transactions()
252 3
            ->where('wallet_id', $this->getKey())
253 3
            ->where('confirmed', true)
254 3
            ->sum('amount');
255
    }
256
257 22
    public function coefficient(string $currency = ''): float
258
    {
259 22
        return config('wallet.coefficient.' . $currency , 100.);
260
    }
261
}
262