forked from admin/services_core
Giant Update
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Wallet;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Models\Wallet;
|
||||
use App\Models\WalletLedgerEntry;
|
||||
use App\Models\WalletTransaction;
|
||||
use App\Models\WalletWithdrawal;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class WalletService
|
||||
{
|
||||
public function wallets(User $user, ?int $tenantId)
|
||||
{
|
||||
$wallets = collect([$this->walletFor($user, 'user')]);
|
||||
if ($tenantId && ($user->isMasterAdmin() || $user->isTenantOwner())) {
|
||||
$wallets->push($this->walletFor(Tenant::findOrFail($tenantId), 'tenant'));
|
||||
}
|
||||
|
||||
return $wallets->values();
|
||||
}
|
||||
|
||||
public function ledger(Wallet $wallet, User $actor, ?int $tenantId, array $filters)
|
||||
{
|
||||
$this->authorizeWallet($wallet, $actor, $tenantId);
|
||||
|
||||
return $wallet->ledgerEntries()->with('transaction:id,uuid,transaction_number,type,status,description,created_at')
|
||||
->latest('id')->paginate($filters['per_page'] ?? 10);
|
||||
}
|
||||
|
||||
public function transfer(Wallet $source, Wallet $destination, float $amount, User $actor, ?int $tenantId): WalletTransaction
|
||||
{
|
||||
$this->authorizeWallet($source, $actor, $tenantId);
|
||||
$this->authorizeWallet($destination, $actor, $tenantId);
|
||||
if ($source->is($destination)) {
|
||||
throw ValidationException::withMessages(['destination_wallet_id' => 'Dompet tujuan harus berbeda.']);
|
||||
}
|
||||
if ($amount <= 0) {
|
||||
throw ValidationException::withMessages(['amount' => 'Nominal harus lebih dari nol.']);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($source, $destination, $amount, $actor) {
|
||||
$wallets = Wallet::whereIn('id', [$source->id, $destination->id])->lockForUpdate()->get()->keyBy('id');
|
||||
$from = $wallets[$source->id];
|
||||
$to = $wallets[$destination->id];
|
||||
if ((float) $from->available_balance < $amount) {
|
||||
throw ValidationException::withMessages(['amount' => 'Saldo tidak mencukupi.']);
|
||||
}
|
||||
$transaction = $this->newTransaction('transfer', $amount, $actor, 'Pindah saldo antar dompet');
|
||||
$this->post($from, $transaction, 'debit', $amount);
|
||||
$this->post($to, $transaction, 'credit', $amount);
|
||||
$transaction->update(['status' => 'completed', 'completed_at' => now()]);
|
||||
|
||||
return $transaction->load('entries.wallet');
|
||||
});
|
||||
}
|
||||
|
||||
public function requestWithdrawal(Wallet $wallet, array $data, User $actor, ?int $tenantId): WalletWithdrawal
|
||||
{
|
||||
$this->authorizeWallet($wallet, $actor, $tenantId);
|
||||
|
||||
return DB::transaction(function () use ($wallet, $data, $actor) {
|
||||
$locked = Wallet::lockForUpdate()->findOrFail($wallet->id);
|
||||
$total = (float) $data['amount'] + (float) ($data['fee'] ?? 0);
|
||||
if ((float) $locked->available_balance < $total) {
|
||||
throw ValidationException::withMessages(['amount' => 'Saldo tidak mencukupi.']);
|
||||
}
|
||||
$transaction = $this->newTransaction('withdrawal', $data['amount'], $actor, 'Permintaan penarikan saldo', $data['fee'] ?? 0);
|
||||
$this->post($locked, $transaction, 'debit', $total);
|
||||
|
||||
return WalletWithdrawal::create([
|
||||
'uuid' => Str::uuid(), 'wallet_id' => $locked->id, 'wallet_transaction_id' => $transaction->id,
|
||||
'requested_by' => $actor->id, 'amount' => $data['amount'], 'fee' => $data['fee'] ?? 0,
|
||||
'bank_code' => $data['bank_code'], 'account_number' => $data['account_number'],
|
||||
'account_name' => $data['account_name'], 'status' => 'pending_approval',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
private function walletFor(Model $owner, string $type): Wallet
|
||||
{
|
||||
return Wallet::firstOrCreate(
|
||||
['owner_type' => $owner->getMorphClass(), 'owner_id' => $owner->getKey(), 'wallet_type' => $type, 'currency' => 'IDR'],
|
||||
['uuid' => Str::uuid(), 'status' => 'active']
|
||||
);
|
||||
}
|
||||
|
||||
private function newTransaction(string $type, float $amount, User $actor, string $description, float $fee = 0): WalletTransaction
|
||||
{
|
||||
return WalletTransaction::create([
|
||||
'uuid' => Str::uuid(), 'transaction_number' => 'WLT-'.now()->format('YmdHis').'-'.Str::upper(Str::random(6)),
|
||||
'type' => $type, 'status' => 'processing', 'initiated_by' => $actor->id, 'amount' => $amount,
|
||||
'fee' => $fee, 'idempotency_key' => (string) Str::uuid(), 'description' => $description,
|
||||
]);
|
||||
}
|
||||
|
||||
private function post(Wallet $wallet, WalletTransaction $transaction, string $type, float $amount): void
|
||||
{
|
||||
$before = (float) $wallet->available_balance;
|
||||
$after = $type === 'credit' ? $before + $amount : $before - $amount;
|
||||
WalletLedgerEntry::create([
|
||||
'wallet_id' => $wallet->id, 'wallet_transaction_id' => $transaction->id, 'entry_type' => $type,
|
||||
'balance_bucket' => 'available', 'amount' => $amount, 'balance_before' => $before,
|
||||
'balance_after' => $after, 'description' => $transaction->description,
|
||||
]);
|
||||
$wallet->update(['available_balance' => $after, 'version' => $wallet->version + 1]);
|
||||
}
|
||||
|
||||
private function authorizeWallet(Wallet $wallet, User $actor, ?int $tenantId): void
|
||||
{
|
||||
if ($actor->isMasterAdmin()) {
|
||||
return;
|
||||
}
|
||||
$allowed = ($wallet->owner_type === $actor->getMorphClass() && (int) $wallet->owner_id === (int) $actor->id)
|
||||
|| ($actor->isTenantOwner() && $wallet->owner_type === (new Tenant)->getMorphClass() && (int) $wallet->owner_id === (int) $tenantId);
|
||||
abort_unless($allowed, 403, 'Dompet tidak dapat diakses.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user