where('provider_id', $provider->id) ->whereHas('payment', fn ($q) => $q->where('status', PaymentStatusEnum::PAID)) ->whereHas('payment.schedule', fn ($q) => $this->availableScheduleQuery($q)) ->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) => $this->pendingScheduleQuery($q)) ->sum('gross_amount'); } public function getPaymentSplits(Provider $provider): Collection { return PaymentSplit::query() ->where('provider_id', $provider->id) ->with(['payment.schedule.client.user']) ->orderBy('created_at', 'desc') ->get(); } 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) => $this->availableScheduleQuery($q)) ->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 getAllWithdrawals(): Collection { return ProviderWithdrawal::query() ->with(['provider.user', 'paymentSplits']) ->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(); } } private function availableScheduleQuery($query) { return $query ->where('code_verified', true) ->when( ! app()->environment('local', 'development'), fn ($query) => $query->whereRaw( $this->scheduleEndedAtExpression() . ' <= ?', [$this->withdrawalReleaseCutoff()] ) ); } private function pendingScheduleQuery($query) { return $query ->whereNotIn('status', ['cancelled', 'rejected']) ->where(function ($q) { $q->where('code_verified', false) ->orWhereRaw( $this->scheduleEndedAtExpression().' > ?', [$this->withdrawalReleaseCutoff()] ); }); } private function scheduleEndedAtExpression(): string { return match (DB::connection()->getDriverName()) { 'pgsql' => '(date + end_time)', 'sqlite' => "datetime(date || ' ' || end_time)", default => 'TIMESTAMP(date, end_time)', }; } private function withdrawalReleaseCutoff(): string { return Carbon::now()->subDays(5)->format('Y-m-d H:i:s'); } }