PaymentService.php 35 KB

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