PaymentService.php 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033
  1. <?php
  2. namespace App\Services;
  3. use App\Enums\ServicePackageStatusEnum;
  4. use App\Enums\NotificationTypeEnum;
  5. use App\Enums\PaymentSplitStatusEnum;
  6. use App\Enums\PaymentStatusEnum;
  7. use App\Exceptions\PaymentFailedException;
  8. use App\Exceptions\PaymentException;
  9. use App\Jobs\StartScheduleJob;
  10. use App\Models\ServicePackage;
  11. use App\Models\ClientPaymentMethod;
  12. use App\Models\Payment;
  13. use App\Models\PaymentSplit;
  14. use App\Models\Schedule;
  15. use App\Models\ScheduleProposal;
  16. use App\Services\Pagarme\PagarmePaymentService;
  17. use Carbon\Carbon;
  18. use Illuminate\Auth\Access\AuthorizationException;
  19. use Illuminate\Database\Eloquent\Collection;
  20. use Illuminate\Support\Collection as SupportCollection;
  21. use Illuminate\Support\Facades\Auth;
  22. use Illuminate\Support\Facades\DB;
  23. use Illuminate\Support\Str;
  24. class PaymentService
  25. {
  26. public function __construct(
  27. private readonly PagarmePaymentService $pagarmePaymentService,
  28. ) {}
  29. public function getAll(): Collection
  30. {
  31. return Payment::query()
  32. ->with(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule'])
  33. ->orderBy('created_at', 'desc')
  34. ->get();
  35. }
  36. public function findById(int $id): ?Payment
  37. {
  38. return Payment::query()
  39. ->with(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule'])
  40. ->find($id);
  41. }
  42. public function create(array $data): Payment
  43. {
  44. return Payment::create($data);
  45. }
  46. public function update(int $id, array $data): ?Payment
  47. {
  48. $model = $this->findById($id);
  49. if (! $model) {
  50. return null;
  51. }
  52. $model->update($data);
  53. return $model->fresh();
  54. }
  55. public function delete(int $id): bool
  56. {
  57. $model = $this->findById($id);
  58. if (! $model) {
  59. return false;
  60. }
  61. return $model->delete();
  62. }
  63. //
  64. public function platformFees(): array
  65. {
  66. return $this->pagarmePaymentService->platformFeeRates();
  67. }
  68. public function payServicePackage(
  69. int $servicePackageId,
  70. string $paymentMethod,
  71. ?int $clientPaymentMethodId = null,
  72. array $options = [],
  73. ): Payment {
  74. $userId = (int) Auth::id();
  75. if (! in_array($paymentMethod, ['credit_card', 'pix'], true)) {
  76. throw new PaymentException;
  77. }
  78. $paymentData = DB::transaction(function () use (
  79. $servicePackageId,
  80. $userId,
  81. $paymentMethod,
  82. $clientPaymentMethodId,
  83. $options,
  84. ): array {
  85. $servicePackage = ServicePackage::query()
  86. ->lockForUpdate()
  87. ->with(['client', 'provider', 'items.schedule.client', 'items.schedule.provider', 'items.schedule.customSchedule.serviceType'])
  88. ->findOrFail($servicePackageId);
  89. if ($servicePackage->client?->user_id !== $userId) {
  90. throw new AuthorizationException;
  91. }
  92. $schedules = $this->activePackageSchedules($servicePackage);
  93. $this->validateServicePackageForPayment($servicePackage, $schedules);
  94. $existingPayment = Payment::query()
  95. ->where('service_package_id', $servicePackage->id)
  96. ->whereIn('status', [
  97. PaymentStatusEnum::PENDING->value,
  98. PaymentStatusEnum::PROCESSING->value,
  99. PaymentStatusEnum::AUTHORIZED->value,
  100. PaymentStatusEnum::PAID->value,
  101. ])
  102. ->latest('id')
  103. ->first();
  104. if ($existingPayment) {
  105. if ($this->isExpiredPixPayment($existingPayment)) {
  106. $existingPayment->forceFill([
  107. 'status' => PaymentStatusEnum::FAILED,
  108. 'failed_at' => now(),
  109. 'failure_message' => 'Pagamento Pix expirado.',
  110. ])->save();
  111. PaymentSplit::query()
  112. ->where('payment_id', $existingPayment->id)
  113. ->update(['status' => PaymentSplitStatusEnum::FAILED]);
  114. } elseif ($this->isStaleIncompleteGatewayPayment($existingPayment)) {
  115. if ($existingPayment->payment_method !== $paymentMethod) {
  116. throw new PaymentException;
  117. }
  118. [, $cardId] = $this->resolveCard(
  119. clientId: $servicePackage->client_id,
  120. paymentMethod: $paymentMethod,
  121. clientPaymentMethodId: $clientPaymentMethodId ?? $existingPayment->client_payment_method_id,
  122. cardId: data_get($options, 'card_id'),
  123. );
  124. $this->servicePackagePaymentTotals($schedules, $paymentMethod);
  125. return [
  126. 'payment' => $existingPayment,
  127. 'schedules' => $schedules,
  128. 'cardId' => $cardId,
  129. ];
  130. } else {
  131. if ($existingPayment->payment_method !== $paymentMethod && $existingPayment->status !== PaymentStatusEnum::PAID) {
  132. throw new PaymentException;
  133. }
  134. return ['existing' => $existingPayment];
  135. }
  136. }
  137. if ($servicePackage->status !== ServicePackageStatusEnum::OPEN) {
  138. throw new PaymentException;
  139. }
  140. [$clientPaymentMethod, $cardId] = $this->resolveCard(
  141. clientId: $servicePackage->client_id,
  142. paymentMethod: $paymentMethod,
  143. clientPaymentMethodId: $clientPaymentMethodId,
  144. cardId: data_get($options, 'card_id'),
  145. );
  146. $totals = $this->servicePackagePaymentTotals($schedules, $paymentMethod);
  147. $payment = Payment::create([
  148. 'schedule_id' => null,
  149. 'service_package_id' => $servicePackage->id,
  150. 'client_id' => $servicePackage->client_id,
  151. 'provider_id' => $servicePackage->provider_id,
  152. 'client_payment_method_id' => $paymentMethod === 'credit_card' ? $clientPaymentMethod?->id : null,
  153. 'gateway_provider' => 'pagarme',
  154. 'gateway_code' => 'payment-'.(string) Str::uuid(),
  155. 'payment_method' => $paymentMethod,
  156. 'status' => PaymentStatusEnum::PENDING,
  157. 'gross_amount' => data_get($totals, 'gross_amount'),
  158. 'gateway_fee_amount' => 0,
  159. 'platform_fee_amount' => data_get($totals, 'platform_fee_amount'),
  160. 'net_amount' => data_get($totals, 'gross_amount'),
  161. 'currency' => 'BRL',
  162. 'installments' => 1,
  163. 'expires_at' => $paymentMethod === 'pix' ? Carbon::now()->addMinutes(30) : null,
  164. 'metadata' => [
  165. 'service_package_id' => (string) $servicePackage->id,
  166. 'schedule_ids' => $schedules->pluck('id')->map(fn ($id) => (string) $id)->all(),
  167. 'service_amount' => number_format(data_get($totals, 'service_amount'), 2, '.', ''),
  168. 'platform_fee' => number_format(data_get($totals, 'platform_fee_amount'), 2, '.', ''),
  169. ],
  170. ]);
  171. PaymentSplit::create([
  172. 'payment_id' => $payment->id,
  173. 'provider_id' => $servicePackage->provider_id,
  174. 'gateway_provider' => 'pagarme',
  175. 'gateway_transfer_target_reference' => $servicePackage->provider->recipient_id,
  176. 'gateway_transfer_target_label' => 'recipient',
  177. 'status' => PaymentSplitStatusEnum::PENDING,
  178. 'gross_amount' => data_get($totals, 'service_amount'),
  179. 'gateway_fee_amount' => 0,
  180. 'net_amount' => data_get($totals, 'service_amount'),
  181. 'metadata' => [
  182. 'service_package_id' => (string) $servicePackage->id,
  183. 'schedule_ids' => $schedules->pluck('id')->map(fn ($id) => (string) $id)->all(),
  184. ],
  185. ]);
  186. return compact('payment', 'schedules', 'cardId');
  187. });
  188. if (data_get($paymentData, 'existing')) {
  189. $existingPayment = data_get($paymentData, 'existing');
  190. $this->syncPaymentTargets($existingPayment);
  191. return $existingPayment->fresh(['client.user', 'provider.user', 'servicePackage.items.schedule']);
  192. }
  193. /** @var Payment $payment */
  194. $payment = data_get($paymentData, 'payment');
  195. /** @var SupportCollection $schedules */
  196. $schedules = data_get($paymentData, 'schedules');
  197. try {
  198. $schedules->first()->ensureCustomerPhone(data_get($options, 'phone'));
  199. $orderResponse = $this->pagarmePaymentService->processServicePackagePayment(
  200. payment: $payment,
  201. schedules: $schedules,
  202. paymentMethod: $paymentMethod,
  203. cardId: data_get($paymentData, 'cardId'),
  204. options: $options,
  205. );
  206. } catch (\Throwable $e) {
  207. $this->failPayment($payment, $e->getMessage());
  208. throw $e;
  209. }
  210. $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);
  211. $this->syncPaymentTargets($payment);
  212. if ($payment->status === PaymentStatusEnum::FAILED) {
  213. throw new PaymentFailedException;
  214. }
  215. return $payment->fresh(['client.user', 'provider.user', 'servicePackage.items.schedule']);
  216. }
  217. //
  218. public function getOrCreateServicePackagePixPayment(ServicePackage $servicePackage): Payment
  219. {
  220. $userId = (int) Auth::id();
  221. if ($servicePackage->client?->user_id !== $userId) {
  222. throw new AuthorizationException;
  223. }
  224. $existingPayment = Payment::query()
  225. ->where('service_package_id', $servicePackage->id)
  226. ->where('payment_method', 'pix')
  227. ->whereIn('status', [
  228. PaymentStatusEnum::PENDING->value,
  229. PaymentStatusEnum::PROCESSING->value,
  230. PaymentStatusEnum::AUTHORIZED->value,
  231. PaymentStatusEnum::PAID->value,
  232. ])
  233. ->latest('id')
  234. ->first();
  235. if ($existingPayment && $this->isExpiredPixPayment($existingPayment)) {
  236. $existingPayment->forceFill([
  237. 'status' => PaymentStatusEnum::FAILED,
  238. 'failed_at' => Carbon::now(),
  239. 'failure_message' => 'Pagamento Pix expirado.',
  240. ])->save();
  241. PaymentSplit::query()
  242. ->where('payment_id', $existingPayment->id)
  243. ->update(['status' => PaymentSplitStatusEnum::FAILED]);
  244. $existingPayment = null;
  245. }
  246. if ($existingPayment) {
  247. if ($this->isIncompleteGatewayPayment($existingPayment)) {
  248. $existingPayment->forceFill([
  249. 'status' => PaymentStatusEnum::FAILED,
  250. 'failed_at' => Carbon::now(),
  251. 'failure_message' => 'Pagamento pendente sem retorno do gateway.',
  252. ])->save();
  253. PaymentSplit::query()
  254. ->where('payment_id', $existingPayment->id)
  255. ->update(['status' => PaymentSplitStatusEnum::FAILED]);
  256. } else {
  257. $this->syncPaymentTargets($existingPayment);
  258. return $existingPayment;
  259. }
  260. }
  261. $paymentData = DB::transaction(function () use ($servicePackage, $userId): array {
  262. $servicePackage = ServicePackage::query()
  263. ->lockForUpdate()
  264. ->with(['client', 'provider', 'items.schedule.client', 'items.schedule.provider', 'items.schedule.customSchedule.serviceType'])
  265. ->findOrFail($servicePackage->id);
  266. if ($servicePackage->client?->user_id !== $userId) {
  267. throw new AuthorizationException;
  268. }
  269. $schedules = $this->activePackageSchedules($servicePackage);
  270. $this->validateServicePackageForPayment($servicePackage, $schedules);
  271. if ($servicePackage->status !== ServicePackageStatusEnum::OPEN) {
  272. throw new PaymentException;
  273. }
  274. $totals = $this->servicePackagePaymentTotals($schedules, 'pix');
  275. $payment = Payment::create([
  276. 'schedule_id' => null,
  277. 'service_package_id' => $servicePackage->id,
  278. 'client_id' => $servicePackage->client_id,
  279. 'provider_id' => $servicePackage->provider_id,
  280. 'client_payment_method_id' => null,
  281. 'gateway_provider' => 'pagarme',
  282. 'gateway_code' => 'payment-'.(string) Str::uuid(),
  283. 'payment_method' => 'pix',
  284. 'status' => PaymentStatusEnum::PENDING,
  285. 'gross_amount' => data_get($totals, 'gross_amount'),
  286. 'gateway_fee_amount' => 0,
  287. 'platform_fee_amount' => data_get($totals, 'platform_fee_amount'),
  288. 'net_amount' => data_get($totals, 'gross_amount'),
  289. 'currency' => 'BRL',
  290. 'installments' => 1,
  291. 'expires_at' => Carbon::now()->addMinutes(30),
  292. 'metadata' => [
  293. 'service_package_id' => (string) $servicePackage->id,
  294. 'schedule_ids' => $schedules->pluck('id')->map(fn ($id) => (string) $id)->all(),
  295. 'service_amount' => number_format(data_get($totals, 'service_amount'), 2, '.', ''),
  296. 'platform_fee' => number_format(data_get($totals, 'platform_fee_amount'), 2, '.', ''),
  297. ],
  298. ]);
  299. PaymentSplit::create([
  300. 'payment_id' => $payment->id,
  301. 'provider_id' => $servicePackage->provider_id,
  302. 'gateway_provider' => 'pagarme',
  303. 'gateway_transfer_target_reference' => $servicePackage->provider->recipient_id,
  304. 'gateway_transfer_target_label' => 'recipient',
  305. 'status' => PaymentSplitStatusEnum::PENDING,
  306. 'gross_amount' => data_get($totals, 'service_amount'),
  307. 'gateway_fee_amount' => 0,
  308. 'net_amount' => data_get($totals, 'service_amount'),
  309. 'metadata' => [
  310. 'service_package_id' => (string) $servicePackage->id,
  311. 'schedule_ids' => $schedules->pluck('id')->map(fn ($id) => (string) $id)->all(),
  312. ],
  313. ]);
  314. return compact('payment', 'schedules');
  315. });
  316. /** @var Payment $payment */
  317. $payment = data_get($paymentData, 'payment');
  318. /** @var SupportCollection $schedules */
  319. $schedules = data_get($paymentData, 'schedules');
  320. try {
  321. $schedules->first()->ensureCustomerPhone();
  322. $orderResponse = $this->pagarmePaymentService->processServicePackagePayment(
  323. payment: $payment,
  324. schedules: $schedules,
  325. paymentMethod: 'pix',
  326. cardId: null,
  327. options: [],
  328. );
  329. } catch (\Throwable $e) {
  330. $this->failPayment($payment, $e->getMessage());
  331. throw $e;
  332. }
  333. $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);
  334. $this->syncPaymentTargets($payment);
  335. return $payment->fresh(['client.user', 'provider.user', 'servicePackage.items.schedule']);
  336. }
  337. public function payScheduleProposal(
  338. int $proposalId,
  339. string $paymentMethod,
  340. ?int $clientPaymentMethodId = null,
  341. array $options = [],
  342. ): Payment {
  343. $userId = (int) Auth::id();
  344. if (! in_array($paymentMethod, ['credit_card', 'pix'], true)) {
  345. throw new PaymentException;
  346. }
  347. $paymentData = DB::transaction(function () use (
  348. $proposalId,
  349. $userId,
  350. $paymentMethod,
  351. $clientPaymentMethodId,
  352. $options,
  353. ): array {
  354. $proposal = ScheduleProposal::query()
  355. ->lockForUpdate()
  356. ->with(['provider', 'schedule.client'])
  357. ->findOrFail($proposalId);
  358. $schedule = $proposal->schedule;
  359. $provider = $proposal->provider;
  360. if ($schedule?->client?->user_id !== $userId) {
  361. throw new AuthorizationException;
  362. }
  363. $schedule->setAttribute('total_amount', app(CustomScheduleService::class)->resolveProposalAmount($schedule, $provider));
  364. $schedules = collect([$schedule]);
  365. $existingPayment = Payment::query()
  366. ->where('metadata->schedule_proposal_id', (string) $proposal->id)
  367. ->whereIn('status', [
  368. PaymentStatusEnum::PENDING->value,
  369. PaymentStatusEnum::PROCESSING->value,
  370. PaymentStatusEnum::AUTHORIZED->value,
  371. PaymentStatusEnum::PAID->value,
  372. ])
  373. ->latest('id')
  374. ->first();
  375. if (! $existingPayment && $schedule->provider_id) {
  376. throw new PaymentException;
  377. }
  378. if ($existingPayment) {
  379. if ($this->isExpiredPixPayment($existingPayment)) {
  380. $existingPayment->forceFill([
  381. 'status' => PaymentStatusEnum::FAILED,
  382. 'failed_at' => now(),
  383. 'failure_message' => 'Pagamento Pix expirado.',
  384. ])->save();
  385. PaymentSplit::query()
  386. ->where('payment_id', $existingPayment->id)
  387. ->update(['status' => PaymentSplitStatusEnum::FAILED]);
  388. } elseif ($this->isStaleIncompleteGatewayPayment($existingPayment)) {
  389. if ($existingPayment->payment_method !== $paymentMethod) {
  390. throw new PaymentException;
  391. }
  392. [, $cardId] = $this->resolveCard(
  393. clientId: $schedule->client_id,
  394. paymentMethod: $paymentMethod,
  395. clientPaymentMethodId: $clientPaymentMethodId ?? $existingPayment->client_payment_method_id,
  396. cardId: data_get($options, 'card_id'),
  397. );
  398. $this->servicePackagePaymentTotals($schedules, $paymentMethod);
  399. return [
  400. 'payment' => $existingPayment,
  401. 'schedules' => $schedules,
  402. 'cardId' => $cardId,
  403. ];
  404. } else {
  405. if ($existingPayment->payment_method !== $paymentMethod && $existingPayment->status !== PaymentStatusEnum::PAID) {
  406. throw new PaymentException;
  407. }
  408. return ['existing' => $existingPayment];
  409. }
  410. }
  411. [$clientPaymentMethod, $cardId] = $this->resolveCard(
  412. clientId: $schedule->client_id,
  413. paymentMethod: $paymentMethod,
  414. clientPaymentMethodId: $clientPaymentMethodId,
  415. cardId: data_get($options, 'card_id'),
  416. );
  417. $totals = $this->servicePackagePaymentTotals($schedules, $paymentMethod);
  418. $payment = Payment::create([
  419. 'schedule_id' => $schedule->id,
  420. 'service_package_id' => null,
  421. 'client_id' => $schedule->client_id,
  422. 'provider_id' => $provider->id,
  423. 'client_payment_method_id' => $paymentMethod === 'credit_card' ? $clientPaymentMethod?->id : null,
  424. 'gateway_provider' => 'pagarme',
  425. 'gateway_code' => 'payment-'.(string) Str::uuid(),
  426. 'payment_method' => $paymentMethod,
  427. 'status' => PaymentStatusEnum::PENDING,
  428. 'gross_amount' => data_get($totals, 'gross_amount'),
  429. 'gateway_fee_amount' => 0,
  430. 'platform_fee_amount' => data_get($totals, 'platform_fee_amount'),
  431. 'net_amount' => data_get($totals, 'gross_amount'),
  432. 'currency' => 'BRL',
  433. 'installments' => 1,
  434. 'expires_at' => $paymentMethod === 'pix' ? Carbon::now()->addMinutes(30) : null,
  435. 'metadata' => [
  436. 'schedule_proposal_id' => (string) $proposal->id,
  437. 'schedule_id' => (string) $schedule->id,
  438. 'service_amount' => number_format(data_get($totals, 'service_amount'), 2, '.', ''),
  439. 'platform_fee' => number_format(data_get($totals, 'platform_fee_amount'), 2, '.', ''),
  440. ],
  441. ]);
  442. PaymentSplit::create([
  443. 'payment_id' => $payment->id,
  444. 'provider_id' => $provider->id,
  445. 'gateway_provider' => 'pagarme',
  446. 'gateway_transfer_target_reference' => $provider->recipient_id,
  447. 'gateway_transfer_target_label' => 'recipient',
  448. 'status' => PaymentSplitStatusEnum::PENDING,
  449. 'gross_amount' => data_get($totals, 'service_amount'),
  450. 'gateway_fee_amount' => 0,
  451. 'net_amount' => data_get($totals, 'service_amount'),
  452. 'metadata' => [
  453. 'schedule_proposal_id' => (string) $proposal->id,
  454. 'schedule_id' => (string) $schedule->id,
  455. ],
  456. ]);
  457. return compact('payment', 'schedules', 'cardId');
  458. });
  459. if (data_get($paymentData, 'existing')) {
  460. $existingPayment = data_get($paymentData, 'existing');
  461. $this->syncPaymentTargets($existingPayment);
  462. return $existingPayment->fresh(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule']);
  463. }
  464. /** @var Payment $payment */
  465. $payment = data_get($paymentData, 'payment');
  466. /** @var SupportCollection $schedules */
  467. $schedules = data_get($paymentData, 'schedules');
  468. try {
  469. $schedules->first()->ensureCustomerPhone(data_get($options, 'phone'));
  470. $orderResponse = $this->pagarmePaymentService->processServicePackagePayment(
  471. payment: $payment,
  472. schedules: $schedules,
  473. paymentMethod: $paymentMethod,
  474. cardId: data_get($paymentData, 'cardId'),
  475. options: $options,
  476. );
  477. } catch (\Throwable $e) {
  478. $this->failPayment($payment, $e->getMessage());
  479. throw $e;
  480. }
  481. $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);
  482. $this->syncPaymentTargets($payment);
  483. if ($payment->status === PaymentStatusEnum::FAILED) {
  484. throw new PaymentFailedException;
  485. }
  486. return $payment->fresh(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule']);
  487. }
  488. public function getOrCreateScheduleProposalPixPayment(ScheduleProposal $proposal): Payment
  489. {
  490. $userId = (int) Auth::id();
  491. $proposal->loadMissing('schedule.client');
  492. $schedule = $proposal->schedule;
  493. if ($schedule?->client?->user_id !== $userId) {
  494. throw new AuthorizationException;
  495. }
  496. $existingPayment = Payment::query()
  497. ->where('metadata->schedule_proposal_id', (string) $proposal->id)
  498. ->where('payment_method', 'pix')
  499. ->whereIn('status', [
  500. PaymentStatusEnum::PENDING->value,
  501. PaymentStatusEnum::PROCESSING->value,
  502. PaymentStatusEnum::AUTHORIZED->value,
  503. PaymentStatusEnum::PAID->value,
  504. ])
  505. ->latest('id')
  506. ->first();
  507. if ($existingPayment && $this->isExpiredPixPayment($existingPayment)) {
  508. $existingPayment->forceFill([
  509. 'status' => PaymentStatusEnum::FAILED,
  510. 'failed_at' => Carbon::now(),
  511. 'failure_message' => 'Pagamento Pix expirado.',
  512. ])->save();
  513. PaymentSplit::query()
  514. ->where('payment_id', $existingPayment->id)
  515. ->update(['status' => PaymentSplitStatusEnum::FAILED]);
  516. $existingPayment = null;
  517. }
  518. if ($existingPayment) {
  519. if ($this->isIncompleteGatewayPayment($existingPayment)) {
  520. $existingPayment->forceFill([
  521. 'status' => PaymentStatusEnum::FAILED,
  522. 'failed_at' => Carbon::now(),
  523. 'failure_message' => 'Pagamento pendente sem retorno do gateway.',
  524. ])->save();
  525. PaymentSplit::query()
  526. ->where('payment_id', $existingPayment->id)
  527. ->update(['status' => PaymentSplitStatusEnum::FAILED]);
  528. } else {
  529. $this->syncPaymentTargets($existingPayment);
  530. return $existingPayment;
  531. }
  532. }
  533. $paymentData = DB::transaction(function () use ($proposal, $userId): array {
  534. $proposal = ScheduleProposal::query()
  535. ->lockForUpdate()
  536. ->with(['provider', 'schedule.client'])
  537. ->findOrFail($proposal->id);
  538. $schedule = $proposal->schedule;
  539. $provider = $proposal->provider;
  540. if ($schedule?->client?->user_id !== $userId) {
  541. throw new AuthorizationException;
  542. }
  543. if ($schedule->provider_id) {
  544. throw new PaymentException;
  545. }
  546. $schedule->setAttribute('total_amount', app(CustomScheduleService::class)->resolveProposalAmount($schedule, $provider));
  547. $schedules = collect([$schedule]);
  548. $totals = $this->servicePackagePaymentTotals($schedules, 'pix');
  549. $payment = Payment::create([
  550. 'schedule_id' => $schedule->id,
  551. 'service_package_id' => null,
  552. 'client_id' => $schedule->client_id,
  553. 'provider_id' => $provider->id,
  554. 'client_payment_method_id' => null,
  555. 'gateway_provider' => 'pagarme',
  556. 'gateway_code' => 'payment-'.(string) Str::uuid(),
  557. 'payment_method' => 'pix',
  558. 'status' => PaymentStatusEnum::PENDING,
  559. 'gross_amount' => data_get($totals, 'gross_amount'),
  560. 'gateway_fee_amount' => 0,
  561. 'platform_fee_amount' => data_get($totals, 'platform_fee_amount'),
  562. 'net_amount' => data_get($totals, 'gross_amount'),
  563. 'currency' => 'BRL',
  564. 'installments' => 1,
  565. 'expires_at' => Carbon::now()->addMinutes(30),
  566. 'metadata' => [
  567. 'schedule_proposal_id' => (string) $proposal->id,
  568. 'schedule_id' => (string) $schedule->id,
  569. 'service_amount' => number_format(data_get($totals, 'service_amount'), 2, '.', ''),
  570. 'platform_fee' => number_format(data_get($totals, 'platform_fee_amount'), 2, '.', ''),
  571. ],
  572. ]);
  573. PaymentSplit::create([
  574. 'payment_id' => $payment->id,
  575. 'provider_id' => $provider->id,
  576. 'gateway_provider' => 'pagarme',
  577. 'gateway_transfer_target_reference' => $provider->recipient_id,
  578. 'gateway_transfer_target_label' => 'recipient',
  579. 'status' => PaymentSplitStatusEnum::PENDING,
  580. 'gross_amount' => data_get($totals, 'service_amount'),
  581. 'gateway_fee_amount' => 0,
  582. 'net_amount' => data_get($totals, 'service_amount'),
  583. 'metadata' => [
  584. 'schedule_proposal_id' => (string) $proposal->id,
  585. 'schedule_id' => (string) $schedule->id,
  586. ],
  587. ]);
  588. return compact('payment', 'schedules');
  589. });
  590. /** @var Payment $payment */
  591. $payment = data_get($paymentData, 'payment');
  592. /** @var SupportCollection $schedules */
  593. $schedules = data_get($paymentData, 'schedules');
  594. try {
  595. $schedules->first()->ensureCustomerPhone();
  596. $orderResponse = $this->pagarmePaymentService->processServicePackagePayment(
  597. payment: $payment,
  598. schedules: $schedules,
  599. paymentMethod: 'pix',
  600. cardId: null,
  601. options: [],
  602. );
  603. } catch (\Throwable $e) {
  604. $this->failPayment($payment, $e->getMessage());
  605. throw $e;
  606. }
  607. $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);
  608. $this->syncPaymentTargets($payment);
  609. return $payment->fresh(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule']);
  610. }
  611. //
  612. private function isExpiredPixPayment(Payment $payment): bool
  613. {
  614. if ($payment->payment_method !== 'pix') {
  615. return false;
  616. }
  617. if ($payment->status === PaymentStatusEnum::PAID) {
  618. return false;
  619. }
  620. return $payment->expires_at !== null
  621. && $payment->expires_at->isPast();
  622. }
  623. private function isIncompleteGatewayPayment(Payment $payment): bool
  624. {
  625. return $payment->status === PaymentStatusEnum::PENDING
  626. && empty($payment->gateway_entity_reference)
  627. && empty($payment->gateway_operation_reference)
  628. && empty($payment->gateway_payload);
  629. }
  630. private function isStaleIncompleteGatewayPayment(Payment $payment): bool
  631. {
  632. return $this->isIncompleteGatewayPayment($payment)
  633. && ($payment->created_at?->lte(now()->subMinutes(5)) ?? false);
  634. }
  635. public function syncScheduleStatusAfterPayment(Schedule $schedule, Payment $payment): void
  636. {
  637. if ($payment->status !== PaymentStatusEnum::PAID) {
  638. return;
  639. }
  640. if ($schedule->status !== 'paid') {
  641. $schedule->update(['status' => 'paid']);
  642. }
  643. $this->syncServicePackagesForSchedule($schedule);
  644. }
  645. public function syncPaymentTargets(Payment $payment): void
  646. {
  647. if ($payment->status !== PaymentStatusEnum::PAID) {
  648. return;
  649. }
  650. if ($payment->service_package_id) {
  651. $paidSchedules = DB::transaction(function () use ($payment): SupportCollection {
  652. $servicePackage = ServicePackage::query()->lockForUpdate()->with('items')->find($payment->service_package_id);
  653. if (! $servicePackage) {
  654. return collect();
  655. }
  656. $scheduleIds = $servicePackage->items->pluck('schedule_id')->filter()->unique();
  657. $schedulesToPay = Schedule::query()
  658. ->with('provider')
  659. ->whereIn('id', $scheduleIds)
  660. ->where('status', 'accepted')
  661. ->get();
  662. Schedule::query()
  663. ->whereIn('id', $schedulesToPay->pluck('id'))
  664. ->where('status', 'accepted')
  665. ->update(['status' => 'paid']);
  666. if ($servicePackage->status !== ServicePackageStatusEnum::PAID) {
  667. $servicePackage->update(['status' => ServicePackageStatusEnum::PAID->value]);
  668. }
  669. return $schedulesToPay->toBase();
  670. });
  671. $paidSchedules->each(fn (Schedule $schedule) => $this->notifyProviderAndScheduleStart($schedule));
  672. return;
  673. }
  674. $payment->loadMissing('schedule');
  675. if (! $payment->schedule) {
  676. return;
  677. }
  678. $proposalId = data_get($payment->metadata, 'schedule_proposal_id');
  679. if ($proposalId && ! $payment->schedule->provider_id) {
  680. $servicePackage = app(CustomScheduleService::class)->acceptProposal((int) $proposalId);
  681. $payment->update([
  682. 'schedule_id' => null,
  683. 'service_package_id' => $servicePackage->id,
  684. ]);
  685. $this->syncPaymentTargets($payment->fresh());
  686. return;
  687. }
  688. $this->syncScheduleStatusAfterPayment($payment->schedule, $payment);
  689. }
  690. private function notifyProviderAndScheduleStart(Schedule $schedule): void
  691. {
  692. if ($schedule->provider_id && $schedule->provider) {
  693. app(NotificationService::class)->create([
  694. 'title' => __('notifications.payment_confirmed_title'),
  695. 'description' => __('notifications.payment_confirmed_description'),
  696. 'origin' => 'schedule',
  697. 'origin_id' => $schedule->id,
  698. 'type' => NotificationTypeEnum::SCHEDULE_PROVIDER_START->value,
  699. 'user_id' => $schedule->provider->user_id,
  700. ]);
  701. }
  702. $dateCleaned = Carbon::parse($schedule->date)->format('Y-m-d');
  703. StartScheduleJob::dispatch($schedule->id)
  704. ->delay(Carbon::parse($dateCleaned . ' ' . $schedule->start_time)->subHour());
  705. }
  706. private function syncServicePackagesForSchedule(Schedule $schedule): void
  707. {
  708. ServicePackage::query()
  709. ->whereHas('items', fn ($query) => $query->where('schedule_id', $schedule->id))
  710. ->with('items')
  711. ->get()
  712. ->each(fn (ServicePackage $servicePackage) => $this->syncServicePackageStatusAfterPayments($servicePackage));
  713. }
  714. private function syncServicePackageStatusAfterPayments(ServicePackage $servicePackage): void
  715. {
  716. $servicePackage->loadMissing('items');
  717. $scheduleIds = $servicePackage->items
  718. ->pluck('schedule_id')
  719. ->filter()
  720. ->unique()
  721. ->values();
  722. if ($scheduleIds->isEmpty()) {
  723. return;
  724. }
  725. $paidSchedulesCount = Schedule::query()
  726. ->whereIn('id', $scheduleIds)
  727. ->where('status', 'paid')
  728. ->count();
  729. if ($paidSchedulesCount !== $scheduleIds->count()) {
  730. return;
  731. }
  732. if ($servicePackage->status !== ServicePackageStatusEnum::PAID) {
  733. $servicePackage->update(['status' => ServicePackageStatusEnum::PAID->value]);
  734. }
  735. }
  736. private function activePackageSchedules(ServicePackage $servicePackage): SupportCollection
  737. {
  738. return $servicePackage->items
  739. ->pluck('schedule')
  740. ->filter()
  741. ->reject(fn (Schedule $schedule) => in_array($schedule->status, ['cancelled', 'rejected'], true))
  742. ->values();
  743. }
  744. private function validateServicePackageForPayment(ServicePackage $servicePackage, SupportCollection $schedules): void
  745. {
  746. if (! in_array($servicePackage->status, [ServicePackageStatusEnum::OPEN, ServicePackageStatusEnum::PAID], true)) {
  747. throw new PaymentException;
  748. }
  749. if ($schedules->isEmpty()) {
  750. throw new PaymentException;
  751. }
  752. $providerIds = $schedules->pluck('provider_id')->filter()->unique()->values();
  753. $clientIds = $schedules->pluck('client_id')->unique()->values();
  754. if (! $servicePackage->provider_id || $providerIds->count() !== 1 || (int) $providerIds->first() !== $servicePackage->provider_id) {
  755. throw new PaymentException;
  756. }
  757. if ($clientIds->count() !== 1 || (int) $clientIds->first() !== $servicePackage->client_id) {
  758. throw new PaymentException;
  759. }
  760. if (empty($servicePackage->provider?->recipient_id)) {
  761. throw new PaymentException;
  762. }
  763. foreach ($schedules as $schedule) {
  764. $expectedStatus = $servicePackage->status === ServicePackageStatusEnum::PAID ? 'paid' : 'accepted';
  765. if ($schedule->status !== $expectedStatus) {
  766. throw new PaymentException;
  767. }
  768. if ((float) $schedule->total_amount <= 0) {
  769. throw new PaymentException;
  770. }
  771. }
  772. }
  773. private function resolveCard(
  774. int $clientId,
  775. string $paymentMethod,
  776. ?int $clientPaymentMethodId,
  777. ?string $cardId,
  778. ): array {
  779. if ($paymentMethod !== 'credit_card') {
  780. return [null, null];
  781. }
  782. if (! $clientPaymentMethodId && empty($cardId)) {
  783. throw new PaymentException;
  784. }
  785. $clientPaymentMethod = $clientPaymentMethodId
  786. ? ClientPaymentMethod::query()
  787. ->where('client_id', $clientId)
  788. ->where('id', $clientPaymentMethodId)
  789. ->where('is_active', true)
  790. ->first()
  791. : null;
  792. if ($clientPaymentMethodId && ! $clientPaymentMethod) {
  793. throw new PaymentException;
  794. }
  795. $cardId = $cardId ?: $clientPaymentMethod?->gateway_card_id;
  796. if (empty($cardId)) {
  797. throw new PaymentException;
  798. }
  799. return [$clientPaymentMethod, $cardId];
  800. }
  801. private function servicePackagePaymentTotals(SupportCollection $schedules, string $paymentMethod): array
  802. {
  803. return $schedules->reduce(function (array $totals, Schedule $schedule) use ($paymentMethod): array {
  804. $amounts = $this->pagarmePaymentService->calculatePaymentAmounts(
  805. serviceAmount: (float) $schedule->total_amount,
  806. paymentMethod: $paymentMethod,
  807. schedule: $schedule,
  808. );
  809. $schedule->setAttribute('payment_gross_amount', data_get($amounts, 'gross_amount'));
  810. return [
  811. 'service_amount' => round(data_get($totals, 'service_amount') + data_get($amounts, 'service_amount'), 2),
  812. 'platform_fee_amount' => round(data_get($totals, 'platform_fee_amount') + data_get($amounts, 'platform_fee_amount'), 2),
  813. 'gross_amount' => round(data_get($totals, 'gross_amount') + data_get($amounts, 'gross_amount'), 2),
  814. ];
  815. }, [
  816. 'service_amount' => 0.0,
  817. 'platform_fee_amount' => 0.0,
  818. 'gross_amount' => 0.0,
  819. ]);
  820. }
  821. private function failPayment(Payment $payment, string $message): void
  822. {
  823. $payment->forceFill([
  824. 'status' => PaymentStatusEnum::FAILED,
  825. 'failed_at' => now(),
  826. 'failure_message' => $message,
  827. ])->save();
  828. PaymentSplit::query()
  829. ->where('payment_id', $payment->id)
  830. ->update(['status' => PaymentSplitStatusEnum::FAILED]);
  831. }
  832. }