PaymentService.php 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763
  1. <?php
  2. namespace App\Services;
  3. use App\Enums\ServicePackageStatusEnum;
  4. use App\Enums\PaymentSplitStatusEnum;
  5. use App\Enums\PaymentStatusEnum;
  6. use App\Exceptions\PaymentFailedException;
  7. use App\Exceptions\PaymentException;
  8. use App\Models\ServicePackage;
  9. use App\Models\ClientPaymentMethod;
  10. use App\Models\Payment;
  11. use App\Models\PaymentSplit;
  12. use App\Models\Schedule;
  13. use App\Services\Pagarme\PagarmePaymentService;
  14. use Carbon\Carbon;
  15. use Illuminate\Auth\Access\AuthorizationException;
  16. use Illuminate\Database\Eloquent\Collection;
  17. use Illuminate\Support\Collection as SupportCollection;
  18. use Illuminate\Support\Facades\Auth;
  19. use Illuminate\Support\Facades\DB;
  20. use Illuminate\Support\Str;
  21. class PaymentService
  22. {
  23. public function __construct(
  24. private readonly PagarmePaymentService $pagarmePaymentService,
  25. ) {}
  26. public function getAll(): Collection
  27. {
  28. return Payment::query()
  29. ->with(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule'])
  30. ->orderBy('created_at', 'desc')
  31. ->get();
  32. }
  33. public function findById(int $id): ?Payment
  34. {
  35. return Payment::query()
  36. ->with(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule'])
  37. ->find($id);
  38. }
  39. public function create(array $data): Payment
  40. {
  41. return Payment::create($data);
  42. }
  43. public function update(int $id, array $data): ?Payment
  44. {
  45. $model = $this->findById($id);
  46. if (! $model) {
  47. return null;
  48. }
  49. $model->update($data);
  50. return $model->fresh();
  51. }
  52. public function delete(int $id): bool
  53. {
  54. $model = $this->findById($id);
  55. if (! $model) {
  56. return false;
  57. }
  58. return $model->delete();
  59. }
  60. //
  61. public function platformFees(): array
  62. {
  63. return $this->pagarmePaymentService->platformFeeRates();
  64. }
  65. //
  66. public function paySchedule(
  67. int $scheduleId,
  68. string $paymentMethod,
  69. ?int $clientPaymentMethodId = null,
  70. array $options = [],
  71. ): Payment {
  72. $schedule = Schedule::query()
  73. ->with(['client', 'provider', 'customSchedule.serviceType'])
  74. ->findOrFail($scheduleId);
  75. if ($schedule->client?->user_id !== (int) Auth::id()) {
  76. throw new AuthorizationException;
  77. }
  78. return $this->processAcceptedSchedulePayment(
  79. schedule: $schedule,
  80. paymentMethod: $paymentMethod,
  81. clientPaymentMethodId: $clientPaymentMethodId,
  82. options: $options,
  83. );
  84. }
  85. private function processAcceptedSchedulePayment(
  86. Schedule $schedule,
  87. string $paymentMethod,
  88. ?int $clientPaymentMethodId = null,
  89. array $options = [],
  90. ): Payment {
  91. $schedule->loadMissing(['client', 'provider', 'customSchedule.serviceType']);
  92. if ($schedule->status !== 'accepted') {
  93. throw new PaymentException;
  94. }
  95. if (! in_array($paymentMethod, ['credit_card', 'pix'], true)) {
  96. throw new PaymentException;
  97. }
  98. if (! $schedule->provider_id || ! $schedule->provider) {
  99. throw new PaymentException;
  100. }
  101. if ((float) $schedule->total_amount <= 0) {
  102. throw new PaymentException;
  103. }
  104. if (empty($schedule->provider->recipient_id)) {
  105. throw new PaymentException;
  106. }
  107. $this->ensureScheduleCanBePaidIndividually($schedule);
  108. $existingPayment = Payment::query()
  109. ->where('schedule_id', $schedule->id)
  110. ->whereIn('status', [
  111. PaymentStatusEnum::PENDING->value,
  112. PaymentStatusEnum::PROCESSING->value,
  113. PaymentStatusEnum::AUTHORIZED->value,
  114. PaymentStatusEnum::PAID->value,
  115. ])
  116. ->latest('id')
  117. ->first();
  118. if ($existingPayment) {
  119. if ($this->isIncompleteGatewayPayment($existingPayment)) {
  120. $existingPayment->forceFill([
  121. 'status' => PaymentStatusEnum::FAILED,
  122. 'failed_at' => now(),
  123. 'failure_message' => 'Pagamento pendente sem retorno do gateway.',
  124. ])->save();
  125. }
  126. elseif ($this->isExpiredPixPayment($existingPayment)) {
  127. $existingPayment->forceFill([
  128. 'status' => PaymentStatusEnum::FAILED,
  129. 'failed_at' => now(),
  130. 'failure_message' => 'Pagamento Pix expirado.',
  131. ])->save();
  132. PaymentSplit::query()
  133. ->where('payment_id', $existingPayment->id)
  134. ->update(['status' => PaymentSplitStatusEnum::FAILED]);
  135. }
  136. else {
  137. if ($existingPayment->payment_method !== $paymentMethod && $existingPayment->status !== PaymentStatusEnum::PAID) {
  138. throw new PaymentException;
  139. }
  140. $this->syncScheduleStatusAfterPayment($schedule, $existingPayment);
  141. return $existingPayment;
  142. }
  143. }
  144. $clientPaymentMethod = null;
  145. $cardId = null;
  146. if ($paymentMethod === 'credit_card') {
  147. if (! $clientPaymentMethodId && empty(data_get($options, 'card_id'))) {
  148. throw new PaymentException;
  149. }
  150. if ($clientPaymentMethodId) {
  151. $clientPaymentMethod = ClientPaymentMethod::query()
  152. ->where('client_id', $schedule->client_id)
  153. ->where('id', $clientPaymentMethodId)
  154. ->where('is_active', true)
  155. ->first();
  156. if (! $clientPaymentMethod) {
  157. throw new PaymentException;
  158. }
  159. }
  160. $cardId = data_get($options, 'card_id', $clientPaymentMethod?->gateway_card_id);
  161. if (empty($cardId)) {
  162. throw new PaymentException;
  163. }
  164. }
  165. $serviceAmount = (float) $schedule->total_amount;
  166. $amounts = $this->pagarmePaymentService->calculatePaymentAmounts(
  167. serviceAmount: $serviceAmount,
  168. paymentMethod: $paymentMethod,
  169. schedule: $schedule,
  170. );
  171. $payment = Payment::create([
  172. 'schedule_id' => $schedule->id,
  173. 'client_id' => $schedule->client_id,
  174. 'provider_id' => $schedule->provider_id,
  175. 'client_payment_method_id' => $paymentMethod === 'credit_card' ? ($clientPaymentMethod?->id ?? null) : null,
  176. 'gateway_provider' => 'pagarme',
  177. 'gateway_code' => 'payment-'.(string) Str::uuid(),
  178. 'payment_method' => $paymentMethod,
  179. 'status' => PaymentStatusEnum::PENDING,
  180. 'gross_amount' => data_get($amounts, 'gross_amount'),
  181. 'gateway_fee_amount' => 0,
  182. 'platform_fee_amount' => data_get($amounts, 'platform_fee_amount'),
  183. 'net_amount' => data_get($amounts, 'gross_amount'),
  184. 'currency' => 'BRL',
  185. 'installments' => 1,
  186. 'expires_at' => $paymentMethod === 'pix' ? Carbon::now()->addMinutes(30) : null,
  187. 'metadata' => [
  188. 'service_amount' => number_format(data_get($amounts, 'service_amount'), 2, '.', ''),
  189. 'platform_fee' => number_format(data_get($amounts, 'platform_fee_amount'), 2, '.', ''),
  190. ],
  191. ]);
  192. PaymentSplit::create([
  193. 'payment_id' => $payment->id,
  194. 'provider_id' => $schedule->provider_id,
  195. 'gateway_provider' => 'pagarme',
  196. 'gateway_transfer_target_reference' => $schedule->provider->recipient_id,
  197. 'gateway_transfer_target_label' => 'recipient',
  198. 'status' => PaymentSplitStatusEnum::PENDING,
  199. 'gross_amount' => $serviceAmount,
  200. 'gateway_fee_amount' => 0,
  201. 'net_amount' => $serviceAmount,
  202. 'metadata' => [
  203. 'schedule_id' => (string) $schedule->id,
  204. ],
  205. ]);
  206. $schedule->ensureCustomerPhone(data_get($options, 'phone'));
  207. try {
  208. $orderResponse = $this->pagarmePaymentService->processPayment(
  209. payment: $payment,
  210. schedule: $schedule,
  211. paymentMethod: $paymentMethod,
  212. cardId: $cardId,
  213. options: $options,
  214. );
  215. } catch (\Throwable $e) {
  216. $payment->forceFill([
  217. 'status' => PaymentStatusEnum::FAILED,
  218. 'failed_at' => now(),
  219. 'failure_message' => $e->getMessage(),
  220. ])->save();
  221. PaymentSplit::query()
  222. ->where('payment_id', $payment->id)
  223. ->update(['status' => PaymentSplitStatusEnum::FAILED]);
  224. throw $e;
  225. }
  226. $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);
  227. $this->syncScheduleStatusAfterPayment($schedule, $payment);
  228. if ($payment->status === PaymentStatusEnum::FAILED) {
  229. throw new PaymentFailedException;
  230. }
  231. return $payment;
  232. }
  233. public function payServicePackage(
  234. int $servicePackageId,
  235. string $paymentMethod,
  236. ?int $clientPaymentMethodId = null,
  237. array $options = [],
  238. ): Payment {
  239. $userId = (int) Auth::id();
  240. if (! in_array($paymentMethod, ['credit_card', 'pix'], true)) {
  241. throw new PaymentException;
  242. }
  243. $paymentData = DB::transaction(function () use (
  244. $servicePackageId,
  245. $userId,
  246. $paymentMethod,
  247. $clientPaymentMethodId,
  248. $options,
  249. ): array {
  250. $servicePackage = ServicePackage::query()
  251. ->lockForUpdate()
  252. ->with(['client', 'provider', 'items.schedule.client', 'items.schedule.provider', 'items.schedule.customSchedule.serviceType'])
  253. ->findOrFail($servicePackageId);
  254. if ($servicePackage->client?->user_id !== $userId) {
  255. throw new AuthorizationException;
  256. }
  257. $schedules = $servicePackage->items->pluck('schedule')->filter()->values();
  258. $this->validateServicePackageForPayment($servicePackage, $schedules);
  259. $existingPayment = Payment::query()
  260. ->where('service_package_id', $servicePackage->id)
  261. ->whereIn('status', [
  262. PaymentStatusEnum::PENDING->value,
  263. PaymentStatusEnum::PROCESSING->value,
  264. PaymentStatusEnum::AUTHORIZED->value,
  265. PaymentStatusEnum::PAID->value,
  266. ])
  267. ->latest('id')
  268. ->first();
  269. if ($existingPayment) {
  270. if ($this->isExpiredPixPayment($existingPayment)) {
  271. $existingPayment->forceFill([
  272. 'status' => PaymentStatusEnum::FAILED,
  273. 'failed_at' => now(),
  274. 'failure_message' => 'Pagamento Pix expirado.',
  275. ])->save();
  276. PaymentSplit::query()
  277. ->where('payment_id', $existingPayment->id)
  278. ->update(['status' => PaymentSplitStatusEnum::FAILED]);
  279. } elseif ($this->isStaleIncompleteGatewayPayment($existingPayment)) {
  280. if ($existingPayment->payment_method !== $paymentMethod) {
  281. throw new PaymentException;
  282. }
  283. [, $cardId] = $this->resolveCard(
  284. clientId: $servicePackage->client_id,
  285. paymentMethod: $paymentMethod,
  286. clientPaymentMethodId: $clientPaymentMethodId ?? $existingPayment->client_payment_method_id,
  287. cardId: data_get($options, 'card_id'),
  288. );
  289. $this->servicePackagePaymentTotals($schedules, $paymentMethod);
  290. return [
  291. 'payment' => $existingPayment,
  292. 'schedules' => $schedules,
  293. 'cardId' => $cardId,
  294. ];
  295. } else {
  296. if ($existingPayment->payment_method !== $paymentMethod && $existingPayment->status !== PaymentStatusEnum::PAID) {
  297. throw new PaymentException;
  298. }
  299. return ['existing' => $existingPayment];
  300. }
  301. }
  302. if ($servicePackage->status !== ServicePackageStatusEnum::OPEN) {
  303. throw new PaymentException;
  304. }
  305. [$clientPaymentMethod, $cardId] = $this->resolveCard(
  306. clientId: $servicePackage->client_id,
  307. paymentMethod: $paymentMethod,
  308. clientPaymentMethodId: $clientPaymentMethodId,
  309. cardId: data_get($options, 'card_id'),
  310. );
  311. $totals = $this->servicePackagePaymentTotals($schedules, $paymentMethod);
  312. $payment = Payment::create([
  313. 'schedule_id' => null,
  314. 'service_package_id' => $servicePackage->id,
  315. 'client_id' => $servicePackage->client_id,
  316. 'provider_id' => $servicePackage->provider_id,
  317. 'client_payment_method_id' => $paymentMethod === 'credit_card' ? $clientPaymentMethod?->id : null,
  318. 'gateway_provider' => 'pagarme',
  319. 'gateway_code' => 'payment-'.(string) Str::uuid(),
  320. 'payment_method' => $paymentMethod,
  321. 'status' => PaymentStatusEnum::PENDING,
  322. 'gross_amount' => data_get($totals, 'gross_amount'),
  323. 'gateway_fee_amount' => 0,
  324. 'platform_fee_amount' => data_get($totals, 'platform_fee_amount'),
  325. 'net_amount' => data_get($totals, 'gross_amount'),
  326. 'currency' => 'BRL',
  327. 'installments' => 1,
  328. 'expires_at' => $paymentMethod === 'pix' ? Carbon::now()->addMinutes(30) : null,
  329. 'metadata' => [
  330. 'service_package_id' => (string) $servicePackage->id,
  331. 'schedule_ids' => $schedules->pluck('id')->map(fn ($id) => (string) $id)->all(),
  332. 'service_amount' => number_format(data_get($totals, 'service_amount'), 2, '.', ''),
  333. 'platform_fee' => number_format(data_get($totals, 'platform_fee_amount'), 2, '.', ''),
  334. ],
  335. ]);
  336. PaymentSplit::create([
  337. 'payment_id' => $payment->id,
  338. 'provider_id' => $servicePackage->provider_id,
  339. 'gateway_provider' => 'pagarme',
  340. 'gateway_transfer_target_reference' => $servicePackage->provider->recipient_id,
  341. 'gateway_transfer_target_label' => 'recipient',
  342. 'status' => PaymentSplitStatusEnum::PENDING,
  343. 'gross_amount' => data_get($totals, 'service_amount'),
  344. 'gateway_fee_amount' => 0,
  345. 'net_amount' => data_get($totals, 'service_amount'),
  346. 'metadata' => [
  347. 'service_package_id' => (string) $servicePackage->id,
  348. 'schedule_ids' => $schedules->pluck('id')->map(fn ($id) => (string) $id)->all(),
  349. ],
  350. ]);
  351. return compact('payment', 'schedules', 'cardId');
  352. });
  353. if (data_get($paymentData, 'existing')) {
  354. $existingPayment = data_get($paymentData, 'existing');
  355. $this->syncPaymentTargets($existingPayment);
  356. return $existingPayment->fresh(['client.user', 'provider.user', 'servicePackage.items.schedule']);
  357. }
  358. /** @var Payment $payment */
  359. $payment = data_get($paymentData, 'payment');
  360. /** @var SupportCollection $schedules */
  361. $schedules = data_get($paymentData, 'schedules');
  362. try {
  363. $schedules->first()->ensureCustomerPhone(data_get($options, 'phone'));
  364. $orderResponse = $this->pagarmePaymentService->processServicePackagePayment(
  365. payment: $payment,
  366. schedules: $schedules,
  367. paymentMethod: $paymentMethod,
  368. cardId: data_get($paymentData, 'cardId'),
  369. options: $options,
  370. );
  371. } catch (\Throwable $e) {
  372. $this->failPayment($payment, $e->getMessage());
  373. throw $e;
  374. }
  375. $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);
  376. $this->syncPaymentTargets($payment);
  377. if ($payment->status === PaymentStatusEnum::FAILED) {
  378. throw new PaymentFailedException;
  379. }
  380. return $payment->fresh(['client.user', 'provider.user', 'servicePackage.items.schedule']);
  381. }
  382. //
  383. public function getOrCreatePixPayment(Schedule $schedule): Payment
  384. {
  385. $this->ensureScheduleCanBePaidIndividually($schedule);
  386. $existingPayment = Payment::query()
  387. ->where('schedule_id', $schedule->id)
  388. ->where('payment_method', 'pix')
  389. ->whereIn('status', [
  390. PaymentStatusEnum::PENDING->value,
  391. PaymentStatusEnum::PROCESSING->value,
  392. PaymentStatusEnum::AUTHORIZED->value,
  393. PaymentStatusEnum::PAID->value,
  394. ])
  395. ->latest('id')
  396. ->first();
  397. if ($existingPayment && $this->isExpiredPixPayment($existingPayment)) {
  398. $existingPayment->forceFill([
  399. 'status' => PaymentStatusEnum::FAILED,
  400. 'failed_at' => Carbon::now(),
  401. 'failure_message' => 'Pagamento Pix expirado.',
  402. ])->save();
  403. PaymentSplit::query()
  404. ->where('payment_id', $existingPayment->id)
  405. ->update(['status' => PaymentSplitStatusEnum::FAILED]);
  406. $existingPayment = null;
  407. }
  408. if ($existingPayment) {
  409. if ($this->isIncompleteGatewayPayment($existingPayment)) {
  410. $existingPayment->forceFill([
  411. 'status' => PaymentStatusEnum::FAILED,
  412. 'failed_at' => Carbon::now(),
  413. 'failure_message' => 'Pagamento pendente sem retorno do gateway.',
  414. ])->save();
  415. PaymentSplit::query()
  416. ->where('payment_id', $existingPayment->id)
  417. ->update(['status' => PaymentSplitStatusEnum::FAILED]);
  418. } else {
  419. $this->syncScheduleStatusAfterPayment($schedule, $existingPayment);
  420. return $existingPayment;
  421. }
  422. }
  423. return $this->processAcceptedSchedulePayment(
  424. schedule: $schedule,
  425. paymentMethod: 'pix',
  426. );
  427. }
  428. //
  429. private function isExpiredPixPayment(Payment $payment): bool
  430. {
  431. if ($payment->payment_method !== 'pix') {
  432. return false;
  433. }
  434. if ($payment->status === PaymentStatusEnum::PAID) {
  435. return false;
  436. }
  437. return $payment->expires_at !== null
  438. && $payment->expires_at->isPast();
  439. }
  440. private function isIncompleteGatewayPayment(Payment $payment): bool
  441. {
  442. return $payment->status === PaymentStatusEnum::PENDING
  443. && empty($payment->gateway_entity_reference)
  444. && empty($payment->gateway_operation_reference)
  445. && empty($payment->gateway_payload);
  446. }
  447. private function isStaleIncompleteGatewayPayment(Payment $payment): bool
  448. {
  449. return $this->isIncompleteGatewayPayment($payment)
  450. && ($payment->created_at?->lte(now()->subMinutes(5)) ?? false);
  451. }
  452. public function syncScheduleStatusAfterPayment(Schedule $schedule, Payment $payment): void
  453. {
  454. if ($payment->status !== PaymentStatusEnum::PAID) {
  455. return;
  456. }
  457. if ($schedule->status !== 'paid') {
  458. $schedule->update(['status' => 'paid']);
  459. }
  460. $this->syncServicePackagesForSchedule($schedule);
  461. }
  462. public function syncPaymentTargets(Payment $payment): void
  463. {
  464. if ($payment->status !== PaymentStatusEnum::PAID) {
  465. return;
  466. }
  467. if ($payment->service_package_id) {
  468. DB::transaction(function () use ($payment): void {
  469. $servicePackage = ServicePackage::query()->lockForUpdate()->with('items')->find($payment->service_package_id);
  470. if (! $servicePackage) {
  471. return;
  472. }
  473. $scheduleIds = $servicePackage->items->pluck('schedule_id')->filter()->unique();
  474. Schedule::query()
  475. ->whereIn('id', $scheduleIds)
  476. ->where('status', 'accepted')
  477. ->update(['status' => 'paid']);
  478. if ($servicePackage->status !== ServicePackageStatusEnum::PAID) {
  479. $servicePackage->update(['status' => ServicePackageStatusEnum::PAID->value]);
  480. }
  481. });
  482. return;
  483. }
  484. $payment->loadMissing('schedule');
  485. if ($payment->schedule) {
  486. $this->syncScheduleStatusAfterPayment($payment->schedule, $payment);
  487. }
  488. }
  489. private function syncServicePackagesForSchedule(Schedule $schedule): void
  490. {
  491. ServicePackage::query()
  492. ->whereHas('items', fn ($query) => $query->where('schedule_id', $schedule->id))
  493. ->with('items')
  494. ->get()
  495. ->each(fn (ServicePackage $servicePackage) => $this->syncServicePackageStatusAfterPayments($servicePackage));
  496. }
  497. private function syncServicePackageStatusAfterPayments(ServicePackage $servicePackage): void
  498. {
  499. $servicePackage->loadMissing('items');
  500. $scheduleIds = $servicePackage->items
  501. ->pluck('schedule_id')
  502. ->filter()
  503. ->unique()
  504. ->values();
  505. if ($scheduleIds->isEmpty()) {
  506. return;
  507. }
  508. $paidSchedulesCount = Schedule::query()
  509. ->whereIn('id', $scheduleIds)
  510. ->where('status', 'paid')
  511. ->count();
  512. if ($paidSchedulesCount !== $scheduleIds->count()) {
  513. return;
  514. }
  515. if ($servicePackage->status !== ServicePackageStatusEnum::PAID) {
  516. $servicePackage->update(['status' => ServicePackageStatusEnum::PAID->value]);
  517. }
  518. }
  519. private function validateServicePackageForPayment(ServicePackage $servicePackage, SupportCollection $schedules): void
  520. {
  521. if (! in_array($servicePackage->status, [ServicePackageStatusEnum::OPEN, ServicePackageStatusEnum::PAID], true)) {
  522. throw new PaymentException;
  523. }
  524. if ($schedules->isEmpty()) {
  525. throw new PaymentException;
  526. }
  527. $providerIds = $schedules->pluck('provider_id')->filter()->unique()->values();
  528. $clientIds = $schedules->pluck('client_id')->unique()->values();
  529. if (! $servicePackage->provider_id || $providerIds->count() !== 1 || (int) $providerIds->first() !== $servicePackage->provider_id) {
  530. throw new PaymentException;
  531. }
  532. if ($clientIds->count() !== 1 || (int) $clientIds->first() !== $servicePackage->client_id) {
  533. throw new PaymentException;
  534. }
  535. if (empty($servicePackage->provider?->recipient_id)) {
  536. throw new PaymentException;
  537. }
  538. foreach ($schedules as $schedule) {
  539. $expectedStatus = $servicePackage->status === ServicePackageStatusEnum::PAID ? 'paid' : 'accepted';
  540. if ($schedule->status !== $expectedStatus) {
  541. throw new PaymentException;
  542. }
  543. if ((float) $schedule->total_amount <= 0) {
  544. throw new PaymentException;
  545. }
  546. }
  547. }
  548. private function resolveCard(
  549. int $clientId,
  550. string $paymentMethod,
  551. ?int $clientPaymentMethodId,
  552. ?string $cardId,
  553. ): array {
  554. if ($paymentMethod !== 'credit_card') {
  555. return [null, null];
  556. }
  557. if (! $clientPaymentMethodId && empty($cardId)) {
  558. throw new PaymentException;
  559. }
  560. $clientPaymentMethod = $clientPaymentMethodId
  561. ? ClientPaymentMethod::query()
  562. ->where('client_id', $clientId)
  563. ->where('id', $clientPaymentMethodId)
  564. ->where('is_active', true)
  565. ->first()
  566. : null;
  567. if ($clientPaymentMethodId && ! $clientPaymentMethod) {
  568. throw new PaymentException;
  569. }
  570. $cardId = $cardId ?: $clientPaymentMethod?->gateway_card_id;
  571. if (empty($cardId)) {
  572. throw new PaymentException;
  573. }
  574. return [$clientPaymentMethod, $cardId];
  575. }
  576. private function servicePackagePaymentTotals(SupportCollection $schedules, string $paymentMethod): array
  577. {
  578. return $schedules->reduce(function (array $totals, Schedule $schedule) use ($paymentMethod): array {
  579. $amounts = $this->pagarmePaymentService->calculatePaymentAmounts(
  580. serviceAmount: (float) $schedule->total_amount,
  581. paymentMethod: $paymentMethod,
  582. schedule: $schedule,
  583. );
  584. $schedule->setAttribute('payment_gross_amount', data_get($amounts, 'gross_amount'));
  585. return [
  586. 'service_amount' => round(data_get($totals, 'service_amount') + data_get($amounts, 'service_amount'), 2),
  587. 'platform_fee_amount' => round(data_get($totals, 'platform_fee_amount') + data_get($amounts, 'platform_fee_amount'), 2),
  588. 'gross_amount' => round(data_get($totals, 'gross_amount') + data_get($amounts, 'gross_amount'), 2),
  589. ];
  590. }, [
  591. 'service_amount' => 0.0,
  592. 'platform_fee_amount' => 0.0,
  593. 'gross_amount' => 0.0,
  594. ]);
  595. }
  596. private function failPayment(Payment $payment, string $message): void
  597. {
  598. $payment->forceFill([
  599. 'status' => PaymentStatusEnum::FAILED,
  600. 'failed_at' => now(),
  601. 'failure_message' => $message,
  602. ])->save();
  603. PaymentSplit::query()
  604. ->where('payment_id', $payment->id)
  605. ->update(['status' => PaymentSplitStatusEnum::FAILED]);
  606. }
  607. private function ensureScheduleCanBePaidIndividually(Schedule $schedule): void
  608. {
  609. $belongsToMultiItemServicePackage = ServicePackage::query()
  610. ->where('status', ServicePackageStatusEnum::OPEN->value)
  611. ->whereHas('items', fn ($query) => $query->where('schedule_id', $schedule->id))
  612. ->whereHas('items', null, '>', 1)
  613. ->exists();
  614. if ($belongsToMultiItemServicePackage) {
  615. throw new PaymentException;
  616. }
  617. }
  618. }