ProviderWithdrawalService.php 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. <?php
  2. namespace App\Services;
  3. use App\Data\Pagarme\Response\PagarmeTransferResponseData;
  4. use App\Enums\PaymentStatusEnum;
  5. use App\Enums\ProviderWithdrawalStatusEnum;
  6. use App\Models\PaymentSplit;
  7. use App\Models\Provider;
  8. use App\Models\ProviderWithdrawal;
  9. use App\Services\Pagarme\PagarmeTransferService;
  10. use Illuminate\Database\Eloquent\Collection;
  11. use Illuminate\Support\Facades\DB;
  12. use Illuminate\Support\Facades\Log;
  13. class ProviderWithdrawalService
  14. {
  15. public function __construct(
  16. private readonly PagarmeTransferService $pagarmeTransfer,
  17. ) {}
  18. public function getAvailableBalance(Provider $provider): float
  19. {
  20. $earnings = (float) PaymentSplit::query()
  21. ->where('provider_id', $provider->id)
  22. ->whereHas('payment', fn ($q) => $q->where('status', PaymentStatusEnum::PAID))
  23. ->whereHas('payment.schedule', fn ($q) => $q->where('status', 'finished'))
  24. ->sum('gross_amount');
  25. $withdrawn = (float) ProviderWithdrawal::query()
  26. ->where('provider_id', $provider->id)
  27. ->whereIn('status', [
  28. ProviderWithdrawalStatusEnum::PENDING_TRANSFER,
  29. ProviderWithdrawalStatusEnum::PROCESSING,
  30. ProviderWithdrawalStatusEnum::TRANSFERRED,
  31. ])
  32. ->sum('gross_amount');
  33. return max(0, $earnings - $withdrawn);
  34. }
  35. public function getPendingBalance(Provider $provider): float
  36. {
  37. return (float) PaymentSplit::query()
  38. ->where('provider_id', $provider->id)
  39. ->whereHas('payment', fn ($q) => $q->where('status', PaymentStatusEnum::PAID))
  40. ->whereHas('payment.schedule', fn ($q) => $q->whereNotIn('status', ['finished', 'cancelled', 'rejected']))
  41. ->sum('gross_amount');
  42. }
  43. public function requestWithdrawal(Provider $provider): ProviderWithdrawal
  44. {
  45. if (empty($provider->recipient_id)) {
  46. throw new \RuntimeException('Prestador nao possui recipient_id no Pagar.me.');
  47. }
  48. return DB::transaction(function () use ($provider) {
  49. Provider::query()
  50. ->where('id', $provider->id)
  51. ->lockForUpdate()
  52. ->first();
  53. $pending = ProviderWithdrawal::query()
  54. ->where('provider_id', $provider->id)
  55. ->whereIn('status', [
  56. ProviderWithdrawalStatusEnum::PENDING_TRANSFER,
  57. ProviderWithdrawalStatusEnum::PROCESSING,
  58. ])
  59. ->exists();
  60. if ($pending) {
  61. throw new \RuntimeException('Voce ja possui um saque em andamento.');
  62. }
  63. $available = $this->getAvailableBalance($provider);
  64. if ($available <= 0) {
  65. throw new \RuntimeException('Saldo disponivel insuficiente para saque.');
  66. }
  67. $amountInCents = (int) round($available * 100);
  68. $idempotencyKey = sprintf('withdrawal-%d-%s', $provider->id, now()->timestamp);
  69. $bankAccount = $provider->recipient_default_bank_account;
  70. $withdrawal = ProviderWithdrawal::create([
  71. 'provider_id' => $provider->id,
  72. 'recipient_id' => $provider->recipient_id,
  73. 'idempotency_key' => $idempotencyKey,
  74. 'gross_amount' => $available,
  75. 'net_amount' => $available,
  76. 'status' => ProviderWithdrawalStatusEnum::PENDING_TRANSFER,
  77. 'bank_account' => $bankAccount,
  78. 'metadata' => [
  79. 'provider_id' => $provider->id,
  80. 'amount' => $available,
  81. ],
  82. ]);
  83. try {
  84. $response = $this->pagarmeTransfer->createTransfer(
  85. $amountInCents,
  86. $provider->recipient_id,
  87. $idempotencyKey,
  88. );
  89. $transfer = PagarmeTransferResponseData::fromArray($response);
  90. $withdrawal->forceFill([
  91. 'transfer_id' => $transfer->id(),
  92. 'status' => $transfer->status() ?? ProviderWithdrawalStatusEnum::PROCESSING->value,
  93. 'type' => $transfer->type,
  94. 'gateway_fee_amount' => $transfer->fee ?? 0,
  95. 'net_amount' => max(0, $available - ($transfer->fee ?? 0) / 100),
  96. 'gateway_payload' => $response,
  97. ])->save();
  98. PaymentSplit::query()
  99. ->where('provider_id', $provider->id)
  100. ->whereNull('provider_withdrawal_id')
  101. ->whereHas('payment', fn ($q) => $q->where('status', PaymentStatusEnum::PAID))
  102. ->whereHas('payment.schedule', fn ($q) => $q->where('status', 'finished'))
  103. ->update(['provider_withdrawal_id' => $withdrawal->id]);
  104. return $withdrawal->fresh();
  105. } catch (\Throwable $e) {
  106. $withdrawal->forceFill([
  107. 'status' => ProviderWithdrawalStatusEnum::FAILED,
  108. 'failed_at' => now(),
  109. 'bank_response' => $e->getMessage(),
  110. ])->save();
  111. throw $e;
  112. }
  113. });
  114. }
  115. public function getWithdrawals(Provider $provider): Collection
  116. {
  117. return ProviderWithdrawal::query()
  118. ->where('provider_id', $provider->id)
  119. ->orderBy('created_at', 'desc')
  120. ->get();
  121. }
  122. public function getWithdrawal(int $id, Provider $provider): ProviderWithdrawal
  123. {
  124. return ProviderWithdrawal::query()
  125. ->where('id', $id)
  126. ->where('provider_id', $provider->id)
  127. ->firstOrFail();
  128. }
  129. public function handleTransferWebhook(array $data): void
  130. {
  131. $transferId = $data['id'] ?? null;
  132. if (empty($transferId)) {
  133. return;
  134. }
  135. $withdrawal = ProviderWithdrawal::query()
  136. ->where('transfer_id', $transferId)
  137. ->first();
  138. if (! $withdrawal) {
  139. Log::channel('pagarme')->warning('ProviderWithdrawal not found for transfer webhook', [
  140. 'transfer_id' => $transferId,
  141. ]);
  142. return;
  143. }
  144. $status = $data['status'] ?? null;
  145. if ($status === ProviderWithdrawalStatusEnum::TRANSFERRED->value) {
  146. $withdrawal->forceFill([
  147. 'status' => ProviderWithdrawalStatusEnum::TRANSFERRED,
  148. 'completed_at' => now(),
  149. ])->save();
  150. } elseif (in_array($status, [ProviderWithdrawalStatusEnum::FAILED->value, ProviderWithdrawalStatusEnum::CANCELED->value], true)) {
  151. $withdrawal->paymentSplits()
  152. ->update(['provider_withdrawal_id' => null]);
  153. $withdrawal->forceFill([
  154. 'status' => ProviderWithdrawalStatusEnum::fromString($status),
  155. 'failed_at' => $status === ProviderWithdrawalStatusEnum::FAILED->value ? now() : null,
  156. 'bank_response' => $status === ProviderWithdrawalStatusEnum::FAILED->value ? ($data['bank_response'] ?? null) : null,
  157. ])->save();
  158. } elseif ($status === ProviderWithdrawalStatusEnum::PROCESSING->value) {
  159. $withdrawal->forceFill([
  160. 'status' => ProviderWithdrawalStatusEnum::PROCESSING,
  161. ])->save();
  162. }
  163. }
  164. }