|
@@ -10,7 +10,10 @@ use App\Models\PaymentSplit;
|
|
|
use App\Models\Schedule;
|
|
use App\Models\Schedule;
|
|
|
use App\Services\Pagarme\PagarmePaymentService;
|
|
use App\Services\Pagarme\PagarmePaymentService;
|
|
|
use Carbon\Carbon;
|
|
use Carbon\Carbon;
|
|
|
|
|
+use Illuminate\Auth\Access\AuthorizationException;
|
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
|
|
|
+use Illuminate\Support\Collection as SupportCollection;
|
|
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Support\Str;
|
|
|
|
|
|
|
|
class PaymentService
|
|
class PaymentService
|
|
@@ -63,6 +66,8 @@ class PaymentService
|
|
|
return $model->delete();
|
|
return $model->delete();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ //
|
|
|
|
|
+
|
|
|
public function platformFees(): array
|
|
public function platformFees(): array
|
|
|
{
|
|
{
|
|
|
return $this->pagarmePaymentService->platformFeeRates();
|
|
return $this->pagarmePaymentService->platformFeeRates();
|
|
@@ -72,9 +77,8 @@ class PaymentService
|
|
|
|
|
|
|
|
public function payAcceptedSchedule(
|
|
public function payAcceptedSchedule(
|
|
|
Schedule $schedule,
|
|
Schedule $schedule,
|
|
|
- string $paymentMethod,
|
|
|
|
|
- ?int $clientPaymentMethodId = null,
|
|
|
|
|
- array $options = []
|
|
|
|
|
|
|
+ string $paymentMethod,
|
|
|
|
|
+ ?int $clientPaymentMethodId = null, array $options = []
|
|
|
): Payment {
|
|
): Payment {
|
|
|
$schedule->loadMissing(['client', 'provider', 'customSchedule.serviceType']);
|
|
$schedule->loadMissing(['client', 'provider', 'customSchedule.serviceType']);
|
|
|
|
|
|
|
@@ -116,7 +120,9 @@ class PaymentService
|
|
|
'failed_at' => now(),
|
|
'failed_at' => now(),
|
|
|
'failure_message' => 'Pagamento pendente sem retorno do gateway.',
|
|
'failure_message' => 'Pagamento pendente sem retorno do gateway.',
|
|
|
])->save();
|
|
])->save();
|
|
|
- } elseif ($this->isExpiredPixPayment($existingPayment)) {
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ elseif ($this->isExpiredPixPayment($existingPayment)) {
|
|
|
$existingPayment->forceFill([
|
|
$existingPayment->forceFill([
|
|
|
'status' => PaymentStatusEnum::FAILED,
|
|
'status' => PaymentStatusEnum::FAILED,
|
|
|
'failed_at' => now(),
|
|
'failed_at' => now(),
|
|
@@ -126,7 +132,9 @@ class PaymentService
|
|
|
PaymentSplit::query()
|
|
PaymentSplit::query()
|
|
|
->where('payment_id', $existingPayment->id)
|
|
->where('payment_id', $existingPayment->id)
|
|
|
->update(['status' => PaymentSplitStatusEnum::FAILED]);
|
|
->update(['status' => PaymentSplitStatusEnum::FAILED]);
|
|
|
- } else {
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ else {
|
|
|
if ($existingPayment->payment_method !== $paymentMethod && $existingPayment->status !== PaymentStatusEnum::PAID) {
|
|
if ($existingPayment->payment_method !== $paymentMethod && $existingPayment->status !== PaymentStatusEnum::PAID) {
|
|
|
throw new \InvalidArgumentException('Ja existe um pagamento em andamento para este agendamento.');
|
|
throw new \InvalidArgumentException('Ja existe um pagamento em andamento para este agendamento.');
|
|
|
}
|
|
}
|
|
@@ -215,11 +223,11 @@ class PaymentService
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
$orderResponse = $this->pagarmePaymentService->processPayment(
|
|
$orderResponse = $this->pagarmePaymentService->processPayment(
|
|
|
- payment: $payment,
|
|
|
|
|
- schedule: $schedule,
|
|
|
|
|
|
|
+ payment: $payment,
|
|
|
|
|
+ schedule: $schedule,
|
|
|
paymentMethod: $paymentMethod,
|
|
paymentMethod: $paymentMethod,
|
|
|
- cardId: $cardId,
|
|
|
|
|
- options: $options,
|
|
|
|
|
|
|
+ cardId: $cardId,
|
|
|
|
|
+ options: $options,
|
|
|
);
|
|
);
|
|
|
} catch (\Throwable $e) {
|
|
} catch (\Throwable $e) {
|
|
|
$payment->forceFill([
|
|
$payment->forceFill([
|
|
@@ -242,6 +250,160 @@ class PaymentService
|
|
|
return $payment;
|
|
return $payment;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ public function payAcceptedSchedules(
|
|
|
|
|
+ array|SupportCollection $schedules, string $paymentMethod,
|
|
|
|
|
+ ?int $clientPaymentMethodId = null, array $options = []
|
|
|
|
|
+ ): Payment {
|
|
|
|
|
+ $schedules = collect($schedules)
|
|
|
|
|
+ ->map(fn (Schedule|int $schedule) => $schedule instanceof Schedule ? $schedule : Schedule::findOrFail($schedule))
|
|
|
|
|
+ ->values();
|
|
|
|
|
+
|
|
|
|
|
+ $this->validateCartSchedules($schedules, $paymentMethod);
|
|
|
|
|
+
|
|
|
|
|
+ $firstSchedule = $schedules->first();
|
|
|
|
|
+
|
|
|
|
|
+ if (! $firstSchedule) {
|
|
|
|
|
+ throw new \InvalidArgumentException('Carrinho precisa ter ao menos um agendamento.');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $clientPaymentMethod = null;
|
|
|
|
|
+
|
|
|
|
|
+ $cardId = null;
|
|
|
|
|
+
|
|
|
|
|
+ if ($paymentMethod === 'credit_card') {
|
|
|
|
|
+ if (! $clientPaymentMethodId && empty($options['card_id'])) {
|
|
|
|
|
+ throw new \InvalidArgumentException('Cartao de pagamento ou card_id e obrigatorio.');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if ($clientPaymentMethodId) {
|
|
|
|
|
+ $clientPaymentMethod = ClientPaymentMethod::query()
|
|
|
|
|
+ ->where('client_id', $firstSchedule->client_id)
|
|
|
|
|
+ ->where('id', $clientPaymentMethodId)
|
|
|
|
|
+ ->where('is_active', true)
|
|
|
|
|
+ ->first();
|
|
|
|
|
+
|
|
|
|
|
+ if (! $clientPaymentMethod) {
|
|
|
|
|
+ throw new \InvalidArgumentException('Cartao de pagamento nao encontrado ou inativo para este cliente.');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $cardId = $options['card_id'] ?? $clientPaymentMethod?->gateway_card_id ?? null;
|
|
|
|
|
+
|
|
|
|
|
+ if (empty($cardId)) {
|
|
|
|
|
+ throw new \InvalidArgumentException('Cartao de pagamento invalido ou sem gateway_card_id do Pagar.me.');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $payment = DB::transaction(function () use ($schedules, $paymentMethod, $clientPaymentMethod, $firstSchedule) {
|
|
|
|
|
+ $totals = $this->cartPaymentTotals($schedules, $paymentMethod);
|
|
|
|
|
+
|
|
|
|
|
+ $providerIds = $schedules->pluck('provider_id')->unique()->values();
|
|
|
|
|
+
|
|
|
|
|
+ $payment = Payment::create([
|
|
|
|
|
+ 'schedule_id' => null,
|
|
|
|
|
+ 'client_id' => $firstSchedule->client_id,
|
|
|
|
|
+ 'provider_id' => $providerIds->count() === 1 ? $providerIds->first() : null,
|
|
|
|
|
+ 'client_payment_method_id' => $paymentMethod === 'credit_card' ? ($clientPaymentMethod?->id ?? null) : null,
|
|
|
|
|
+ 'gateway_provider' => 'pagarme',
|
|
|
|
|
+ 'gateway_code' => 'payment-'.(string) Str::uuid(),
|
|
|
|
|
+ 'payment_method' => $paymentMethod,
|
|
|
|
|
+ 'status' => PaymentStatusEnum::PENDING,
|
|
|
|
|
+ 'gross_amount' => $totals['gross_amount'],
|
|
|
|
|
+ 'gateway_fee_amount' => 0,
|
|
|
|
|
+ 'platform_fee_amount' => $totals['platform_fee_amount'],
|
|
|
|
|
+ 'net_amount' => $totals['gross_amount'],
|
|
|
|
|
+ 'currency' => 'BRL',
|
|
|
|
|
+ 'installments' => 1,
|
|
|
|
|
+ 'expires_at' => $paymentMethod === 'pix' ? Carbon::now()->addMinutes(30) : null,
|
|
|
|
|
+
|
|
|
|
|
+ 'metadata' => [
|
|
|
|
|
+ 'service_amount' => number_format($totals['service_amount'], 2, '.', ''),
|
|
|
|
|
+ 'platform_fee' => number_format($totals['platform_fee_amount'], 2, '.', ''),
|
|
|
|
|
+ 'schedule_ids' => $schedules->pluck('id')->map(fn ($id) => (string) $id)->all(),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ foreach ($schedules as $schedule) {
|
|
|
|
|
+ $amounts = $this->pagarmePaymentService->calculatePaymentAmounts(
|
|
|
|
|
+ serviceAmount: (float) $schedule->total_amount,
|
|
|
|
|
+ paymentMethod: $paymentMethod,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ $schedule->setAttribute('payment_gross_amount', $amounts['gross_amount']);
|
|
|
|
|
+
|
|
|
|
|
+ $payment->schedules()->attach($schedule->id);
|
|
|
|
|
+
|
|
|
|
|
+ PaymentSplit::create([
|
|
|
|
|
+ 'payment_id' => $payment->id,
|
|
|
|
|
+ 'provider_id' => $schedule->provider_id,
|
|
|
|
|
+ 'gateway_provider' => 'pagarme',
|
|
|
|
|
+ 'gateway_transfer_target_reference' => $schedule->provider->recipient_id,
|
|
|
|
|
+ 'gateway_transfer_target_label' => 'recipient',
|
|
|
|
|
+ 'status' => PaymentSplitStatusEnum::PENDING,
|
|
|
|
|
+ 'gross_amount' => (float) $schedule->total_amount,
|
|
|
|
|
+ 'gateway_fee_amount' => 0,
|
|
|
|
|
+ 'net_amount' => (float) $schedule->total_amount,
|
|
|
|
|
+
|
|
|
|
|
+ 'metadata' => [
|
|
|
|
|
+ 'schedule_id' => (string) $schedule->id,
|
|
|
|
|
+ ],
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $payment;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ $firstSchedule->ensureCustomerPhone($options['phone'] ?? null);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ $orderResponse = $this->pagarmePaymentService->processCartPayment(
|
|
|
|
|
+ payment: $payment,
|
|
|
|
|
+ schedules: $schedules,
|
|
|
|
|
+ paymentMethod: $paymentMethod,
|
|
|
|
|
+ cardId: $cardId,
|
|
|
|
|
+ options: $options,
|
|
|
|
|
+ );
|
|
|
|
|
+ } catch (\Throwable $e) {
|
|
|
|
|
+ $payment->forceFill([
|
|
|
|
|
+ 'status' => PaymentStatusEnum::FAILED,
|
|
|
|
|
+ 'failed_at' => now(),
|
|
|
|
|
+ 'failure_message' => $e->getMessage(),
|
|
|
|
|
+ ])->save();
|
|
|
|
|
+
|
|
|
|
|
+ PaymentSplit::query()
|
|
|
|
|
+ ->where('payment_id', $payment->id)
|
|
|
|
|
+ ->update(['status' => PaymentSplitStatusEnum::FAILED]);
|
|
|
|
|
+
|
|
|
|
|
+ throw $e;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);
|
|
|
|
|
+
|
|
|
|
|
+ $this->syncSchedulesStatusAfterPayment($payment);
|
|
|
|
|
+
|
|
|
|
|
+ return $payment->fresh(['client.user', 'provider.user', 'schedules']);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function payCart(array $data, int $userId): Payment
|
|
|
|
|
+ {
|
|
|
|
|
+ $schedules = $this->cartSchedules($data['schedule_ids']);
|
|
|
|
|
+
|
|
|
|
|
+ if ($schedules->contains(fn (Schedule $schedule) => $schedule->client?->user_id !== $userId)) {
|
|
|
|
|
+ throw new AuthorizationException;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $this->payAcceptedSchedules(
|
|
|
|
|
+ schedules: $schedules,
|
|
|
|
|
+ paymentMethod: $data['payment_method'],
|
|
|
|
|
+ clientPaymentMethodId: $data['client_payment_method_id'] ?? null,
|
|
|
|
|
+
|
|
|
|
|
+ options: [
|
|
|
|
|
+ 'phone' => $data['phone'] ?? null,
|
|
|
|
|
+ 'card_id' => $data['card_id'] ?? null,
|
|
|
|
|
+ ],
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
public function getOrCreatePixPayment(Schedule $schedule): Payment
|
|
public function getOrCreatePixPayment(Schedule $schedule): Payment
|
|
|
{
|
|
{
|
|
|
$existingPayment = Payment::query()
|
|
$existingPayment = Payment::query()
|
|
@@ -289,7 +451,7 @@ class PaymentService
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return $this->payAcceptedSchedule(
|
|
return $this->payAcceptedSchedule(
|
|
|
- schedule: $schedule,
|
|
|
|
|
|
|
+ schedule: $schedule,
|
|
|
paymentMethod: 'pix',
|
|
paymentMethod: 'pix',
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
@@ -326,4 +488,112 @@ class PaymentService
|
|
|
|
|
|
|
|
$schedule->update(['status' => 'paid']);
|
|
$schedule->update(['status' => 'paid']);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ public function syncSchedulesStatusAfterPayment(Payment $payment): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $payment->loadMissing('schedules');
|
|
|
|
|
+
|
|
|
|
|
+ if ($payment->schedules->isEmpty()) {
|
|
|
|
|
+ if ($payment->schedule) {
|
|
|
|
|
+ $this->syncScheduleStatusAfterPayment($payment->schedule, $payment);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $payment->schedules->each(fn (Schedule $schedule) => $this->syncScheduleStatusAfterPayment($schedule, $payment));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function validateCartSchedules(SupportCollection $schedules, string $paymentMethod): void
|
|
|
|
|
+ {
|
|
|
|
|
+ if ($schedules->isEmpty()) {
|
|
|
|
|
+ throw new \InvalidArgumentException('Carrinho precisa ter ao menos um agendamento.');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (! in_array($paymentMethod, ['credit_card', 'pix'], true)) {
|
|
|
|
|
+ throw new \InvalidArgumentException('Forma de pagamento invalida.');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $clientIds = $schedules->pluck('client_id')->unique()->values();
|
|
|
|
|
+
|
|
|
|
|
+ if ($clientIds->count() !== 1) {
|
|
|
|
|
+ throw new \InvalidArgumentException('Todos os agendamentos do carrinho precisam ser do mesmo cliente.');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $schedules->each(function (Schedule $schedule): void {
|
|
|
|
|
+ $schedule->loadMissing(['client', 'provider', 'customSchedule.serviceType']);
|
|
|
|
|
+
|
|
|
|
|
+ if ($schedule->status !== 'accepted') {
|
|
|
|
|
+ throw new \InvalidArgumentException("Agendamento {$schedule->id} precisa estar aceito para ser pago.");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (! $schedule->provider_id || ! $schedule->provider) {
|
|
|
|
|
+ throw new \InvalidArgumentException("Agendamento {$schedule->id} precisa ter prestador confirmado para gerar pagamento.");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if ((float) $schedule->total_amount <= 0) {
|
|
|
|
|
+ throw new \InvalidArgumentException("Agendamento {$schedule->id} precisa ter valor maior que zero para gerar pagamento.");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (empty($schedule->provider->recipient_id)) {
|
|
|
|
|
+ throw new \InvalidArgumentException("Prestador do agendamento {$schedule->id} precisa ter recipient_id do Pagar.me para receber split.");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $existingPayment = Payment::query()
|
|
|
|
|
+ ->where(function ($query) use ($schedule) {
|
|
|
|
|
+ $query->where('schedule_id', $schedule->id)
|
|
|
|
|
+ ->orWhereHas('schedules', fn ($query) => $query->where('schedules.id', $schedule->id));
|
|
|
|
|
+ })
|
|
|
|
|
+ ->whereIn('status', [
|
|
|
|
|
+ PaymentStatusEnum::PENDING->value,
|
|
|
|
|
+ PaymentStatusEnum::PROCESSING->value,
|
|
|
|
|
+ PaymentStatusEnum::AUTHORIZED->value,
|
|
|
|
|
+ PaymentStatusEnum::PAID->value,
|
|
|
|
|
+ ])
|
|
|
|
|
+ ->latest('id')
|
|
|
|
|
+ ->first();
|
|
|
|
|
+
|
|
|
|
|
+ if ($existingPayment) {
|
|
|
|
|
+ throw new \InvalidArgumentException("Ja existe um pagamento em andamento para o agendamento {$schedule->id}.");
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ //
|
|
|
|
|
+
|
|
|
|
|
+ private function cartSchedules(array $scheduleIds): SupportCollection
|
|
|
|
|
+ {
|
|
|
|
|
+ $schedules = Schedule::query()
|
|
|
|
|
+ ->with(['client', 'provider', 'customSchedule.serviceType'])
|
|
|
|
|
+ ->whereIn('id', $scheduleIds)
|
|
|
|
|
+ ->get()
|
|
|
|
|
+ ->sortBy(fn (Schedule $schedule) => array_search($schedule->id, $scheduleIds, true))
|
|
|
|
|
+ ->values();
|
|
|
|
|
+
|
|
|
|
|
+ if ($schedules->count() !== count($scheduleIds)) {
|
|
|
|
|
+ throw new \InvalidArgumentException('Um ou mais agendamentos nao foram encontrados.');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $schedules;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function cartPaymentTotals(SupportCollection $schedules, string $paymentMethod): array
|
|
|
|
|
+ {
|
|
|
|
|
+ return $schedules->reduce(function (array $totals, Schedule $schedule) use ($paymentMethod) {
|
|
|
|
|
+ $amounts = $this->pagarmePaymentService->calculatePaymentAmounts(
|
|
|
|
|
+ serviceAmount: (float) $schedule->total_amount,
|
|
|
|
|
+ paymentMethod: $paymentMethod,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'service_amount' => $totals['service_amount'] + $amounts['service_amount'],
|
|
|
|
|
+ 'platform_fee_amount' => $totals['platform_fee_amount'] + $amounts['platform_fee_amount'],
|
|
|
|
|
+ 'gross_amount' => $totals['gross_amount'] + $amounts['gross_amount'],
|
|
|
|
|
+ ];
|
|
|
|
|
+ }, [
|
|
|
|
|
+ 'service_amount' => 0,
|
|
|
|
|
+ 'platform_fee_amount' => 0,
|
|
|
|
|
+ 'gross_amount' => 0,
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|