PaymentService.php 35 KB

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