PaymentService.php 35 KB

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