|
|
@@ -0,0 +1,197 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace App\Services;
|
|
|
+
|
|
|
+use App\Data\Pagarme\Response\PagarmeTransferResponseData;
|
|
|
+use App\Enums\PaymentStatusEnum;
|
|
|
+use App\Enums\ProviderWithdrawalStatusEnum;
|
|
|
+use App\Models\PaymentSplit;
|
|
|
+use App\Models\Provider;
|
|
|
+use App\Models\ProviderWithdrawal;
|
|
|
+use App\Services\Pagarme\PagarmeTransferService;
|
|
|
+use Illuminate\Database\Eloquent\Collection;
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
+use Illuminate\Support\Facades\Log;
|
|
|
+
|
|
|
+class ProviderWithdrawalService
|
|
|
+{
|
|
|
+ public function __construct(
|
|
|
+ private readonly PagarmeTransferService $pagarmeTransfer,
|
|
|
+ ) {}
|
|
|
+
|
|
|
+ public function getAvailableBalance(Provider $provider): float
|
|
|
+ {
|
|
|
+ $earnings = (float) PaymentSplit::query()
|
|
|
+ ->where('provider_id', $provider->id)
|
|
|
+ ->whereHas('payment', fn ($q) => $q->where('status', PaymentStatusEnum::PAID))
|
|
|
+ ->whereHas('payment.schedule', fn ($q) => $q->where('status', 'finished'))
|
|
|
+ ->sum('gross_amount');
|
|
|
+
|
|
|
+ $withdrawn = (float) ProviderWithdrawal::query()
|
|
|
+ ->where('provider_id', $provider->id)
|
|
|
+ ->whereIn('status', [
|
|
|
+ ProviderWithdrawalStatusEnum::PENDING_TRANSFER,
|
|
|
+ ProviderWithdrawalStatusEnum::PROCESSING,
|
|
|
+ ProviderWithdrawalStatusEnum::TRANSFERRED,
|
|
|
+ ])
|
|
|
+ ->sum('gross_amount');
|
|
|
+
|
|
|
+ return max(0, $earnings - $withdrawn);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function getPendingBalance(Provider $provider): float
|
|
|
+ {
|
|
|
+ return (float) PaymentSplit::query()
|
|
|
+ ->where('provider_id', $provider->id)
|
|
|
+ ->whereHas('payment', fn ($q) => $q->where('status', PaymentStatusEnum::PAID))
|
|
|
+ ->whereHas('payment.schedule', fn ($q) => $q->whereNotIn('status', ['finished', 'cancelled', 'rejected']))
|
|
|
+ ->sum('gross_amount');
|
|
|
+ }
|
|
|
+
|
|
|
+ public function requestWithdrawal(Provider $provider): ProviderWithdrawal
|
|
|
+ {
|
|
|
+ if (empty($provider->recipient_id)) {
|
|
|
+ throw new \RuntimeException('Prestador nao possui recipient_id no Pagar.me.');
|
|
|
+ }
|
|
|
+
|
|
|
+ return DB::transaction(function () use ($provider) {
|
|
|
+ Provider::query()
|
|
|
+ ->where('id', $provider->id)
|
|
|
+ ->lockForUpdate()
|
|
|
+ ->first();
|
|
|
+
|
|
|
+ $pending = ProviderWithdrawal::query()
|
|
|
+ ->where('provider_id', $provider->id)
|
|
|
+ ->whereIn('status', [
|
|
|
+ ProviderWithdrawalStatusEnum::PENDING_TRANSFER,
|
|
|
+ ProviderWithdrawalStatusEnum::PROCESSING,
|
|
|
+ ])
|
|
|
+ ->exists();
|
|
|
+
|
|
|
+ if ($pending) {
|
|
|
+ throw new \RuntimeException('Voce ja possui um saque em andamento.');
|
|
|
+ }
|
|
|
+
|
|
|
+ $available = $this->getAvailableBalance($provider);
|
|
|
+
|
|
|
+ if ($available <= 0) {
|
|
|
+ throw new \RuntimeException('Saldo disponivel insuficiente para saque.');
|
|
|
+ }
|
|
|
+
|
|
|
+ $amountInCents = (int) round($available * 100);
|
|
|
+
|
|
|
+ $idempotencyKey = sprintf('withdrawal-%d-%s', $provider->id, now()->timestamp);
|
|
|
+
|
|
|
+ $bankAccount = $provider->recipient_default_bank_account;
|
|
|
+
|
|
|
+ $withdrawal = ProviderWithdrawal::create([
|
|
|
+ 'provider_id' => $provider->id,
|
|
|
+ 'recipient_id' => $provider->recipient_id,
|
|
|
+ 'idempotency_key' => $idempotencyKey,
|
|
|
+ 'gross_amount' => $available,
|
|
|
+ 'net_amount' => $available,
|
|
|
+ 'status' => ProviderWithdrawalStatusEnum::PENDING_TRANSFER,
|
|
|
+ 'bank_account' => $bankAccount,
|
|
|
+ 'metadata' => [
|
|
|
+ 'provider_id' => $provider->id,
|
|
|
+ 'amount' => $available,
|
|
|
+ ],
|
|
|
+ ]);
|
|
|
+
|
|
|
+ try {
|
|
|
+ $response = $this->pagarmeTransfer->createTransfer(
|
|
|
+ $amountInCents,
|
|
|
+ $provider->recipient_id,
|
|
|
+ $idempotencyKey,
|
|
|
+ );
|
|
|
+
|
|
|
+ $transfer = PagarmeTransferResponseData::fromArray($response);
|
|
|
+
|
|
|
+ $withdrawal->forceFill([
|
|
|
+ 'transfer_id' => $transfer->id(),
|
|
|
+ 'status' => $transfer->status() ?? ProviderWithdrawalStatusEnum::PROCESSING->value,
|
|
|
+ 'type' => $transfer->type,
|
|
|
+ 'gateway_fee_amount' => $transfer->fee ?? 0,
|
|
|
+ 'net_amount' => max(0, $available - ($transfer->fee ?? 0) / 100),
|
|
|
+ 'gateway_payload' => $response,
|
|
|
+ ])->save();
|
|
|
+
|
|
|
+ PaymentSplit::query()
|
|
|
+ ->where('provider_id', $provider->id)
|
|
|
+ ->whereNull('provider_withdrawal_id')
|
|
|
+ ->whereHas('payment', fn ($q) => $q->where('status', PaymentStatusEnum::PAID))
|
|
|
+ ->whereHas('payment.schedule', fn ($q) => $q->where('status', 'finished'))
|
|
|
+ ->update(['provider_withdrawal_id' => $withdrawal->id]);
|
|
|
+
|
|
|
+ return $withdrawal->fresh();
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ $withdrawal->forceFill([
|
|
|
+ 'status' => ProviderWithdrawalStatusEnum::FAILED,
|
|
|
+ 'failed_at' => now(),
|
|
|
+ 'bank_response' => $e->getMessage(),
|
|
|
+ ])->save();
|
|
|
+
|
|
|
+ throw $e;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ public function getWithdrawals(Provider $provider): Collection
|
|
|
+ {
|
|
|
+ return ProviderWithdrawal::query()
|
|
|
+ ->where('provider_id', $provider->id)
|
|
|
+ ->orderBy('created_at', 'desc')
|
|
|
+ ->get();
|
|
|
+ }
|
|
|
+
|
|
|
+ public function getWithdrawal(int $id, Provider $provider): ProviderWithdrawal
|
|
|
+ {
|
|
|
+ return ProviderWithdrawal::query()
|
|
|
+ ->where('id', $id)
|
|
|
+ ->where('provider_id', $provider->id)
|
|
|
+ ->firstOrFail();
|
|
|
+ }
|
|
|
+
|
|
|
+ public function handleTransferWebhook(array $data): void
|
|
|
+ {
|
|
|
+ $transferId = $data['id'] ?? null;
|
|
|
+
|
|
|
+ if (empty($transferId)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $withdrawal = ProviderWithdrawal::query()
|
|
|
+ ->where('transfer_id', $transferId)
|
|
|
+ ->first();
|
|
|
+
|
|
|
+ if (! $withdrawal) {
|
|
|
+ Log::channel('pagarme')->warning('ProviderWithdrawal not found for transfer webhook', [
|
|
|
+ 'transfer_id' => $transferId,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $status = $data['status'] ?? null;
|
|
|
+
|
|
|
+ if ($status === ProviderWithdrawalStatusEnum::TRANSFERRED->value) {
|
|
|
+ $withdrawal->forceFill([
|
|
|
+ 'status' => ProviderWithdrawalStatusEnum::TRANSFERRED,
|
|
|
+ 'completed_at' => now(),
|
|
|
+ ])->save();
|
|
|
+ } elseif (in_array($status, [ProviderWithdrawalStatusEnum::FAILED->value, ProviderWithdrawalStatusEnum::CANCELED->value], true)) {
|
|
|
+ $withdrawal->paymentSplits()
|
|
|
+ ->update(['provider_withdrawal_id' => null]);
|
|
|
+
|
|
|
+ $withdrawal->forceFill([
|
|
|
+ 'status' => ProviderWithdrawalStatusEnum::fromString($status),
|
|
|
+ 'failed_at' => $status === ProviderWithdrawalStatusEnum::FAILED->value ? now() : null,
|
|
|
+ 'bank_response' => $status === ProviderWithdrawalStatusEnum::FAILED->value ? ($data['bank_response'] ?? null) : null,
|
|
|
+ ])->save();
|
|
|
+ } elseif ($status === ProviderWithdrawalStatusEnum::PROCESSING->value) {
|
|
|
+ $withdrawal->forceFill([
|
|
|
+ 'status' => ProviderWithdrawalStatusEnum::PROCESSING,
|
|
|
+ ])->save();
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|