| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255 |
- <?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 Carbon\Carbon;
- 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) => $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');
- }
- }
|