PaymentService.php 35 KB

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