with(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule']) ->orderBy('created_at', 'desc') ->get(); } public function findById(int $id): ?Payment { return Payment::query() ->with(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule']) ->find($id); } public function create(array $data): Payment { return Payment::create($data); } public function update(int $id, array $data): ?Payment { $model = $this->findById($id); if (! $model) { return null; } $model->update($data); return $model->fresh(); } public function delete(int $id): bool { $model = $this->findById($id); if (! $model) { return false; } return $model->delete(); } // public function platformFees(): array { return $this->pagarmePaymentService->platformFeeRates(); } public function payServicePackage( int $servicePackageId, string $paymentMethod, ?int $clientPaymentMethodId = null, array $options = [], ): Payment { $userId = (int) Auth::id(); if (! in_array($paymentMethod, ['credit_card', 'pix'], true)) { throw new PaymentException; } $paymentData = DB::transaction(function () use ( $servicePackageId, $userId, $paymentMethod, $clientPaymentMethodId, $options, ): array { $servicePackage = ServicePackage::query() ->lockForUpdate() ->with(['client', 'provider', 'items.schedule.client', 'items.schedule.provider', 'items.schedule.customSchedule.serviceType']) ->findOrFail($servicePackageId); if ($servicePackage->client?->user_id !== $userId) { throw new AuthorizationException; } $schedules = $servicePackage->items->pluck('schedule')->filter()->values(); $this->validateServicePackageForPayment($servicePackage, $schedules); $existingPayment = Payment::query() ->where('service_package_id', $servicePackage->id) ->whereIn('status', [ PaymentStatusEnum::PENDING->value, PaymentStatusEnum::PROCESSING->value, PaymentStatusEnum::AUTHORIZED->value, PaymentStatusEnum::PAID->value, ]) ->latest('id') ->first(); if ($existingPayment) { if ($this->isExpiredPixPayment($existingPayment)) { $existingPayment->forceFill([ 'status' => PaymentStatusEnum::FAILED, 'failed_at' => now(), 'failure_message' => 'Pagamento Pix expirado.', ])->save(); PaymentSplit::query() ->where('payment_id', $existingPayment->id) ->update(['status' => PaymentSplitStatusEnum::FAILED]); } elseif ($this->isStaleIncompleteGatewayPayment($existingPayment)) { if ($existingPayment->payment_method !== $paymentMethod) { throw new PaymentException; } [, $cardId] = $this->resolveCard( clientId: $servicePackage->client_id, paymentMethod: $paymentMethod, clientPaymentMethodId: $clientPaymentMethodId ?? $existingPayment->client_payment_method_id, cardId: data_get($options, 'card_id'), ); $this->servicePackagePaymentTotals($schedules, $paymentMethod); return [ 'payment' => $existingPayment, 'schedules' => $schedules, 'cardId' => $cardId, ]; } else { if ($existingPayment->payment_method !== $paymentMethod && $existingPayment->status !== PaymentStatusEnum::PAID) { throw new PaymentException; } return ['existing' => $existingPayment]; } } if ($servicePackage->status !== ServicePackageStatusEnum::OPEN) { throw new PaymentException; } [$clientPaymentMethod, $cardId] = $this->resolveCard( clientId: $servicePackage->client_id, paymentMethod: $paymentMethod, clientPaymentMethodId: $clientPaymentMethodId, cardId: data_get($options, 'card_id'), ); $totals = $this->servicePackagePaymentTotals($schedules, $paymentMethod); $payment = Payment::create([ 'schedule_id' => null, 'service_package_id' => $servicePackage->id, 'client_id' => $servicePackage->client_id, 'provider_id' => $servicePackage->provider_id, 'client_payment_method_id' => $paymentMethod === 'credit_card' ? $clientPaymentMethod?->id : null, 'gateway_provider' => 'pagarme', 'gateway_code' => 'payment-'.(string) Str::uuid(), 'payment_method' => $paymentMethod, 'status' => PaymentStatusEnum::PENDING, 'gross_amount' => data_get($totals, 'gross_amount'), 'gateway_fee_amount' => 0, 'platform_fee_amount' => data_get($totals, 'platform_fee_amount'), 'net_amount' => data_get($totals, 'gross_amount'), 'currency' => 'BRL', 'installments' => 1, 'expires_at' => $paymentMethod === 'pix' ? Carbon::now()->addMinutes(30) : null, 'metadata' => [ 'service_package_id' => (string) $servicePackage->id, 'schedule_ids' => $schedules->pluck('id')->map(fn ($id) => (string) $id)->all(), 'service_amount' => number_format(data_get($totals, 'service_amount'), 2, '.', ''), 'platform_fee' => number_format(data_get($totals, 'platform_fee_amount'), 2, '.', ''), ], ]); PaymentSplit::create([ 'payment_id' => $payment->id, 'provider_id' => $servicePackage->provider_id, 'gateway_provider' => 'pagarme', 'gateway_transfer_target_reference' => $servicePackage->provider->recipient_id, 'gateway_transfer_target_label' => 'recipient', 'status' => PaymentSplitStatusEnum::PENDING, 'gross_amount' => data_get($totals, 'service_amount'), 'gateway_fee_amount' => 0, 'net_amount' => data_get($totals, 'service_amount'), 'metadata' => [ 'service_package_id' => (string) $servicePackage->id, 'schedule_ids' => $schedules->pluck('id')->map(fn ($id) => (string) $id)->all(), ], ]); return compact('payment', 'schedules', 'cardId'); }); if (data_get($paymentData, 'existing')) { $existingPayment = data_get($paymentData, 'existing'); $this->syncPaymentTargets($existingPayment); return $existingPayment->fresh(['client.user', 'provider.user', 'servicePackage.items.schedule']); } /** @var Payment $payment */ $payment = data_get($paymentData, 'payment'); /** @var SupportCollection $schedules */ $schedules = data_get($paymentData, 'schedules'); try { $schedules->first()->ensureCustomerPhone(data_get($options, 'phone')); $orderResponse = $this->pagarmePaymentService->processServicePackagePayment( payment: $payment, schedules: $schedules, paymentMethod: $paymentMethod, cardId: data_get($paymentData, 'cardId'), options: $options, ); } catch (\Throwable $e) { $this->failPayment($payment, $e->getMessage()); throw $e; } $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse); $this->syncPaymentTargets($payment); if ($payment->status === PaymentStatusEnum::FAILED) { throw new PaymentFailedException; } return $payment->fresh(['client.user', 'provider.user', 'servicePackage.items.schedule']); } // public function getOrCreateServicePackagePixPayment(ServicePackage $servicePackage): Payment { $userId = (int) Auth::id(); if ($servicePackage->client?->user_id !== $userId) { throw new AuthorizationException; } $existingPayment = Payment::query() ->where('service_package_id', $servicePackage->id) ->where('payment_method', 'pix') ->whereIn('status', [ PaymentStatusEnum::PENDING->value, PaymentStatusEnum::PROCESSING->value, PaymentStatusEnum::AUTHORIZED->value, PaymentStatusEnum::PAID->value, ]) ->latest('id') ->first(); if ($existingPayment && $this->isExpiredPixPayment($existingPayment)) { $existingPayment->forceFill([ 'status' => PaymentStatusEnum::FAILED, 'failed_at' => Carbon::now(), 'failure_message' => 'Pagamento Pix expirado.', ])->save(); PaymentSplit::query() ->where('payment_id', $existingPayment->id) ->update(['status' => PaymentSplitStatusEnum::FAILED]); $existingPayment = null; } if ($existingPayment) { if ($this->isIncompleteGatewayPayment($existingPayment)) { $existingPayment->forceFill([ 'status' => PaymentStatusEnum::FAILED, 'failed_at' => Carbon::now(), 'failure_message' => 'Pagamento pendente sem retorno do gateway.', ])->save(); PaymentSplit::query() ->where('payment_id', $existingPayment->id) ->update(['status' => PaymentSplitStatusEnum::FAILED]); } else { $this->syncPaymentTargets($existingPayment); return $existingPayment; } } $paymentData = DB::transaction(function () use ($servicePackage, $userId): array { $servicePackage = ServicePackage::query() ->lockForUpdate() ->with(['client', 'provider', 'items.schedule.client', 'items.schedule.provider', 'items.schedule.customSchedule.serviceType']) ->findOrFail($servicePackage->id); if ($servicePackage->client?->user_id !== $userId) { throw new AuthorizationException; } $schedules = $servicePackage->items->pluck('schedule')->filter()->values(); $this->validateServicePackageForPayment($servicePackage, $schedules); if ($servicePackage->status !== ServicePackageStatusEnum::OPEN) { throw new PaymentException; } $totals = $this->servicePackagePaymentTotals($schedules, 'pix'); $payment = Payment::create([ 'schedule_id' => null, 'service_package_id' => $servicePackage->id, 'client_id' => $servicePackage->client_id, 'provider_id' => $servicePackage->provider_id, 'client_payment_method_id' => null, 'gateway_provider' => 'pagarme', 'gateway_code' => 'payment-'.(string) Str::uuid(), 'payment_method' => 'pix', 'status' => PaymentStatusEnum::PENDING, 'gross_amount' => data_get($totals, 'gross_amount'), 'gateway_fee_amount' => 0, 'platform_fee_amount' => data_get($totals, 'platform_fee_amount'), 'net_amount' => data_get($totals, 'gross_amount'), 'currency' => 'BRL', 'installments' => 1, 'expires_at' => Carbon::now()->addMinutes(30), 'metadata' => [ 'service_package_id' => (string) $servicePackage->id, 'schedule_ids' => $schedules->pluck('id')->map(fn ($id) => (string) $id)->all(), 'service_amount' => number_format(data_get($totals, 'service_amount'), 2, '.', ''), 'platform_fee' => number_format(data_get($totals, 'platform_fee_amount'), 2, '.', ''), ], ]); PaymentSplit::create([ 'payment_id' => $payment->id, 'provider_id' => $servicePackage->provider_id, 'gateway_provider' => 'pagarme', 'gateway_transfer_target_reference' => $servicePackage->provider->recipient_id, 'gateway_transfer_target_label' => 'recipient', 'status' => PaymentSplitStatusEnum::PENDING, 'gross_amount' => data_get($totals, 'service_amount'), 'gateway_fee_amount' => 0, 'net_amount' => data_get($totals, 'service_amount'), 'metadata' => [ 'service_package_id' => (string) $servicePackage->id, 'schedule_ids' => $schedules->pluck('id')->map(fn ($id) => (string) $id)->all(), ], ]); return compact('payment', 'schedules'); }); /** @var Payment $payment */ $payment = data_get($paymentData, 'payment'); /** @var SupportCollection $schedules */ $schedules = data_get($paymentData, 'schedules'); try { $orderResponse = $this->pagarmePaymentService->processServicePackagePayment( payment: $payment, schedules: $schedules, paymentMethod: 'pix', cardId: null, options: [], ); } catch (\Throwable $e) { $this->failPayment($payment, $e->getMessage()); throw $e; } $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse); $this->syncPaymentTargets($payment); return $payment->fresh(['client.user', 'provider.user', 'servicePackage.items.schedule']); } // private function isExpiredPixPayment(Payment $payment): bool { if ($payment->payment_method !== 'pix') { return false; } if ($payment->status === PaymentStatusEnum::PAID) { return false; } return $payment->expires_at !== null && $payment->expires_at->isPast(); } private function isIncompleteGatewayPayment(Payment $payment): bool { return $payment->status === PaymentStatusEnum::PENDING && empty($payment->gateway_entity_reference) && empty($payment->gateway_operation_reference) && empty($payment->gateway_payload); } private function isStaleIncompleteGatewayPayment(Payment $payment): bool { return $this->isIncompleteGatewayPayment($payment) && ($payment->created_at?->lte(now()->subMinutes(5)) ?? false); } public function syncScheduleStatusAfterPayment(Schedule $schedule, Payment $payment): void { if ($payment->status !== PaymentStatusEnum::PAID) { return; } if ($schedule->status !== 'paid') { $schedule->update(['status' => 'paid']); } $this->syncServicePackagesForSchedule($schedule); } public function syncPaymentTargets(Payment $payment): void { if ($payment->status !== PaymentStatusEnum::PAID) { return; } if ($payment->service_package_id) { DB::transaction(function () use ($payment): void { $servicePackage = ServicePackage::query()->lockForUpdate()->with('items')->find($payment->service_package_id); if (! $servicePackage) { return; } $scheduleIds = $servicePackage->items->pluck('schedule_id')->filter()->unique(); Schedule::query() ->whereIn('id', $scheduleIds) ->where('status', 'accepted') ->update(['status' => 'paid']); if ($servicePackage->status !== ServicePackageStatusEnum::PAID) { $servicePackage->update(['status' => ServicePackageStatusEnum::PAID->value]); } }); return; } $payment->loadMissing('schedule'); if ($payment->schedule) { $this->syncScheduleStatusAfterPayment($payment->schedule, $payment); } } private function syncServicePackagesForSchedule(Schedule $schedule): void { ServicePackage::query() ->whereHas('items', fn ($query) => $query->where('schedule_id', $schedule->id)) ->with('items') ->get() ->each(fn (ServicePackage $servicePackage) => $this->syncServicePackageStatusAfterPayments($servicePackage)); } private function syncServicePackageStatusAfterPayments(ServicePackage $servicePackage): void { $servicePackage->loadMissing('items'); $scheduleIds = $servicePackage->items ->pluck('schedule_id') ->filter() ->unique() ->values(); if ($scheduleIds->isEmpty()) { return; } $paidSchedulesCount = Schedule::query() ->whereIn('id', $scheduleIds) ->where('status', 'paid') ->count(); if ($paidSchedulesCount !== $scheduleIds->count()) { return; } if ($servicePackage->status !== ServicePackageStatusEnum::PAID) { $servicePackage->update(['status' => ServicePackageStatusEnum::PAID->value]); } } private function validateServicePackageForPayment(ServicePackage $servicePackage, SupportCollection $schedules): void { if (! in_array($servicePackage->status, [ServicePackageStatusEnum::OPEN, ServicePackageStatusEnum::PAID], true)) { throw new PaymentException; } if ($schedules->isEmpty()) { throw new PaymentException; } $providerIds = $schedules->pluck('provider_id')->filter()->unique()->values(); $clientIds = $schedules->pluck('client_id')->unique()->values(); if (! $servicePackage->provider_id || $providerIds->count() !== 1 || (int) $providerIds->first() !== $servicePackage->provider_id) { throw new PaymentException; } if ($clientIds->count() !== 1 || (int) $clientIds->first() !== $servicePackage->client_id) { throw new PaymentException; } if (empty($servicePackage->provider?->recipient_id)) { throw new PaymentException; } foreach ($schedules as $schedule) { $expectedStatus = $servicePackage->status === ServicePackageStatusEnum::PAID ? 'paid' : 'accepted'; if ($schedule->status !== $expectedStatus) { throw new PaymentException; } if ((float) $schedule->total_amount <= 0) { throw new PaymentException; } } } private function resolveCard( int $clientId, string $paymentMethod, ?int $clientPaymentMethodId, ?string $cardId, ): array { if ($paymentMethod !== 'credit_card') { return [null, null]; } if (! $clientPaymentMethodId && empty($cardId)) { throw new PaymentException; } $clientPaymentMethod = $clientPaymentMethodId ? ClientPaymentMethod::query() ->where('client_id', $clientId) ->where('id', $clientPaymentMethodId) ->where('is_active', true) ->first() : null; if ($clientPaymentMethodId && ! $clientPaymentMethod) { throw new PaymentException; } $cardId = $cardId ?: $clientPaymentMethod?->gateway_card_id; if (empty($cardId)) { throw new PaymentException; } return [$clientPaymentMethod, $cardId]; } private function servicePackagePaymentTotals(SupportCollection $schedules, string $paymentMethod): array { return $schedules->reduce(function (array $totals, Schedule $schedule) use ($paymentMethod): array { $amounts = $this->pagarmePaymentService->calculatePaymentAmounts( serviceAmount: (float) $schedule->total_amount, paymentMethod: $paymentMethod, schedule: $schedule, ); $schedule->setAttribute('payment_gross_amount', data_get($amounts, 'gross_amount')); return [ 'service_amount' => round(data_get($totals, 'service_amount') + data_get($amounts, 'service_amount'), 2), 'platform_fee_amount' => round(data_get($totals, 'platform_fee_amount') + data_get($amounts, 'platform_fee_amount'), 2), 'gross_amount' => round(data_get($totals, 'gross_amount') + data_get($amounts, 'gross_amount'), 2), ]; }, [ 'service_amount' => 0.0, 'platform_fee_amount' => 0.0, 'gross_amount' => 0.0, ]); } private function failPayment(Payment $payment, string $message): void { $payment->forceFill([ 'status' => PaymentStatusEnum::FAILED, 'failed_at' => now(), 'failure_message' => $message, ])->save(); PaymentSplit::query() ->where('payment_id', $payment->id) ->update(['status' => PaymentSplitStatusEnum::FAILED]); } }