PaymentService.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. <?php
  2. namespace App\Services;
  3. use App\Data\Pagarme\Request\PagarmeCustomerRequestData\PagarmeCustomerAddressRequestData;
  4. use App\Data\Pagarme\Request\PagarmeCustomerRequestData\PagarmeCustomerPhonesRequestData\PagarmeCustomerPhoneData;
  5. use App\Data\Pagarme\Request\PagarmeCustomerRequestData\PagarmeCustomerPhonesRequestData\PagarmeCustomerPhonesRequestData;
  6. use App\Data\Pagarme\Request\PagarmeCustomerRequestData\PagarmeCustomerRequestData;
  7. use App\Data\Pagarme\Request\PagarmeOrderRequestData\PagarmeOrderItemData;
  8. use App\Data\Pagarme\Request\PagarmeOrderRequestData\PagarmeOrderPaymentData\PagarmeOrderCreditCardData;
  9. use App\Data\Pagarme\Request\PagarmeOrderRequestData\PagarmeOrderPaymentData\PagarmeOrderPixAdditionalInformationData;
  10. use App\Data\Pagarme\Request\PagarmeOrderRequestData\PagarmeOrderPaymentData\PagarmeOrderPixData;
  11. use App\Data\Pagarme\Request\PagarmeOrderRequestData\PagarmeOrderPaymentData\PagarmeOrderSplitData;
  12. use App\Data\Pagarme\Request\PagarmeOrderRequestData\PagarmeOrderPaymentData\PagarmeOrderSplitOptionsData;
  13. use App\Data\Pagarme\Request\PagarmeOrderRequestData\PagarmeOrderRequestData;
  14. use App\Enums\PaymentSplitStatusEnum;
  15. use App\Enums\PaymentStatusEnum;
  16. use App\Models\Address;
  17. use App\Models\ClientPaymentMethod;
  18. use App\Models\Payment;
  19. use App\Models\PaymentSplit;
  20. use App\Models\Schedule;
  21. use App\Services\Pagarme\PagarmePaymentService;
  22. use Carbon\Carbon;
  23. use Illuminate\Database\Eloquent\Collection;
  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'])
  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'])
  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 payAcceptedSchedule(
  66. Schedule $schedule,
  67. string $paymentMethod,
  68. ?int $clientPaymentMethodId = null,
  69. array $options = []
  70. ): Payment {
  71. $schedule->loadMissing(['client', 'provider', 'customSchedule.serviceType']);
  72. if ($schedule->status !== 'accepted') {
  73. throw new \InvalidArgumentException('Agendamento precisa estar aceito para ser pago.');
  74. }
  75. if (! in_array($paymentMethod, ['credit_card', 'pix'], true)) {
  76. throw new \InvalidArgumentException('Forma de pagamento invalida.');
  77. }
  78. if (! $schedule->provider_id || ! $schedule->provider) {
  79. throw new \InvalidArgumentException('Agendamento precisa ter prestador confirmado para gerar pagamento.');
  80. }
  81. if ((float) $schedule->total_amount <= 0) {
  82. throw new \InvalidArgumentException('Agendamento precisa ter valor maior que zero para gerar pagamento.');
  83. }
  84. if (empty($schedule->provider->recipient_id)) {
  85. throw new \InvalidArgumentException('Prestador precisa ter recipient_id do Pagar.me para receber split.');
  86. }
  87. $existingPayment = Payment::query()
  88. ->where('schedule_id', $schedule->id)
  89. ->whereIn('status', [
  90. PaymentStatusEnum::PENDING->value,
  91. PaymentStatusEnum::PROCESSING->value,
  92. PaymentStatusEnum::AUTHORIZED->value,
  93. PaymentStatusEnum::PAID->value,
  94. ])
  95. ->latest('id')
  96. ->first();
  97. if ($existingPayment) {
  98. if ($this->isIncompleteGatewayPayment($existingPayment)) {
  99. $existingPayment->forceFill([
  100. 'status' => PaymentStatusEnum::FAILED,
  101. 'failed_at' => now(),
  102. 'failure_message' => 'Pagamento pendente sem retorno do gateway.',
  103. ])->save();
  104. } else {
  105. if ($existingPayment->payment_method !== $paymentMethod && $existingPayment->status !== PaymentStatusEnum::PAID) {
  106. throw new \InvalidArgumentException('Ja existe um pagamento em andamento para este agendamento.');
  107. }
  108. $this->syncScheduleStatusAfterPayment($schedule, $existingPayment);
  109. return $existingPayment;
  110. }
  111. }
  112. $clientPaymentMethod = null;
  113. if ($paymentMethod === 'credit_card') {
  114. if (! $clientPaymentMethodId && empty($options['card_id'])) {
  115. throw new \InvalidArgumentException('Cartao de pagamento ou card_id e obrigatorio.');
  116. }
  117. if ($clientPaymentMethodId) {
  118. $clientPaymentMethod = ClientPaymentMethod::query()
  119. ->where('client_id', $schedule->client_id)
  120. ->where('id', $clientPaymentMethodId)
  121. ->where('is_active', true)
  122. ->first();
  123. if (! $clientPaymentMethod) {
  124. throw new \InvalidArgumentException('Cartao de pagamento nao encontrado ou inativo para este cliente.');
  125. }
  126. }
  127. if (
  128. empty($clientPaymentMethod?->gateway_card_id)
  129. && empty($options['card_id'])
  130. ) {
  131. throw new \InvalidArgumentException('Cartao de pagamento invalido ou sem gateway_card_id do Pagar.me.');
  132. }
  133. }
  134. $serviceAmount = (float) $schedule->total_amount;
  135. $platformFee = round($serviceAmount * 0.11, 2);
  136. $grossAmount = round($serviceAmount + $platformFee, 2);
  137. $items = $this->buildOrderItems($schedule, $grossAmount);
  138. $this->ensureCustomerPhoneForPayment($schedule, $options);
  139. $customer = $this->buildCustomerPayload(schedule: $schedule, options: $options, requirePhone: true);
  140. $platformRecipientId = config('services.pagarme.platform_recipient_id');
  141. if ($platformFee > 0 && empty($platformRecipientId)) {
  142. throw new \InvalidArgumentException('PAGARME_PLATFORM_RECIPIENT_ID precisa estar configurado para receber a taxa da plataforma no split.');
  143. }
  144. $payment = Payment::create([
  145. 'schedule_id' => $schedule->id,
  146. 'client_id' => $schedule->client_id,
  147. 'provider_id' => $schedule->provider_id,
  148. 'client_payment_method_id' => $paymentMethod === 'credit_card' ? $clientPaymentMethod->id : null,
  149. 'gateway_provider' => 'pagarme',
  150. 'gateway_code' => 'payment-'.(string) Str::uuid(),
  151. 'payment_method' => $paymentMethod,
  152. 'status' => PaymentStatusEnum::PENDING,
  153. 'gross_amount' => $grossAmount,
  154. 'gateway_fee_amount' => 0,
  155. 'platform_fee_amount' => $platformFee,
  156. 'net_amount' => $grossAmount,
  157. 'currency' => 'BRL',
  158. 'installments' => 1,
  159. 'expires_at' => $paymentMethod === 'pix' ? Carbon::now()->addMinutes(30) : null,
  160. 'metadata' => [
  161. 'service_amount' => number_format($serviceAmount, 2, '.', ''),
  162. 'platform_fee' => number_format($platformFee, 2, '.', ''),
  163. ],
  164. ]);
  165. $transfer = PaymentSplit::create([
  166. 'payment_id' => $payment->id,
  167. 'provider_id' => $schedule->provider_id,
  168. 'gateway_provider' => 'pagarme',
  169. 'gateway_transfer_target_reference' => $schedule->provider->recipient_id,
  170. 'gateway_transfer_target_label' => 'recipient',
  171. 'status' => PaymentSplitStatusEnum::PENDING,
  172. 'gross_amount' => $serviceAmount,
  173. 'gateway_fee_amount' => 0,
  174. 'net_amount' => $serviceAmount,
  175. 'metadata' => [
  176. 'schedule_id' => (string) $schedule->id,
  177. ],
  178. ]);
  179. $split = PagarmeOrderRequestData::splitFromTransfers(collect([$transfer]));
  180. if ($platformFee > 0) {
  181. $split[] = new PagarmeOrderSplitData(
  182. amount: PagarmeOrderRequestData::amountInCents($platformFee),
  183. recipientId: $platformRecipientId,
  184. type: 'flat',
  185. options: new PagarmeOrderSplitOptionsData(
  186. chargeProcessingFee: true,
  187. chargeRemainderFee: true,
  188. liable: true,
  189. ),
  190. );
  191. }
  192. $pixOptions = config('services.pagarme.pix_disable_split')
  193. ? []
  194. : ['split' => $split];
  195. try {
  196. $creditCardReference = $paymentMethod === 'credit_card'
  197. ? $this->resolveCreditCardReference($clientPaymentMethod, $options)
  198. : [];
  199. $orderResponse = $paymentMethod === 'credit_card'
  200. ? $this->pagarmePaymentService->createOrderWithCreditCard(
  201. payment: $payment,
  202. items: $items,
  203. customer: $customer,
  204. creditCard: new PagarmeOrderCreditCardData(
  205. installments: 1,
  206. statementDescriptor: Str::limit((string) config('app.name', 'SOFTPAR'), 13, ''),
  207. operationType: 'auth_and_capture',
  208. cardId: $creditCardReference['card_id'],
  209. ),
  210. options: [
  211. 'split' => $split,
  212. ],
  213. )
  214. : $this->pagarmePaymentService->createOrderWithPix(
  215. payment: $payment,
  216. items: $items,
  217. customer: $customer,
  218. pix: new PagarmeOrderPixData(
  219. expiresIn: 1800,
  220. additionalInformation: [
  221. new PagarmeOrderPixAdditionalInformationData(
  222. name: 'Agendamento',
  223. value: (string) $schedule->id,
  224. ),
  225. ],
  226. ),
  227. options: $pixOptions,
  228. );
  229. } catch (\Throwable $e) {
  230. $payment->forceFill([
  231. 'status' => PaymentStatusEnum::FAILED,
  232. 'failed_at' => now(),
  233. 'failure_message' => $e->getMessage(),
  234. ])->save();
  235. $transfer->update(['status' => PaymentSplitStatusEnum::FAILED]);
  236. throw $e;
  237. }
  238. $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);
  239. $this->syncScheduleStatusAfterPayment($schedule, $payment);
  240. return $payment;
  241. }
  242. //
  243. private function buildOrderItems(Schedule $schedule, float $grossAmount): array
  244. {
  245. $description = $schedule->customSchedule?->serviceType?->description
  246. ?? "Servico {$schedule->id}";
  247. return [new PagarmeOrderItemData(
  248. code: "schedule-{$schedule->id}",
  249. amount: PagarmeOrderRequestData::amountInCents($grossAmount),
  250. quantity: 1,
  251. description: $description,
  252. )];
  253. }
  254. private function buildCustomerPayload(
  255. Schedule $schedule,
  256. array $options = [],
  257. bool $requirePhone = true
  258. ): PagarmeCustomerRequestData {
  259. $client = $schedule->client;
  260. $user = $client->user()->first(['id', 'name', 'email', 'phone']);
  261. $address = Address::with(['city.state', 'state'])->find($schedule->address_id);
  262. foreach ([
  263. 'nome' => $user?->name,
  264. 'email' => $user?->email,
  265. 'documento' => $client->document,
  266. ] as $field => $value) {
  267. if ($value === null || $value === '') {
  268. throw new \InvalidArgumentException("Cliente precisa ter {$field} para criar pedido no Pagar.me.");
  269. }
  270. }
  271. if (! $address) {
  272. throw new \InvalidArgumentException('Endereco do agendamento nao encontrado para criar pedido no Pagar.me.');
  273. }
  274. $document = $this->digits($client->document);
  275. $phone = $this->buildPhonePayload($user->phone)
  276. ?: $this->buildPhonePayload($options['phone'] ?? null);
  277. $state = $address->state?->code ?? $address->city?->state?->code;
  278. $city = $address->city?->name;
  279. $zipCode = $this->digits($address->zip_code);
  280. $line1 = implode(', ', array_filter([
  281. $address->number ?: 'S/N',
  282. $address->address,
  283. $address->district,
  284. ]));
  285. $requiredFields = [
  286. 'documento' => $document,
  287. 'estado' => $state,
  288. 'cidade' => $city,
  289. 'cep' => $zipCode,
  290. 'endereco' => $line1,
  291. ];
  292. if ($requirePhone) {
  293. $requiredFields['telefone'] = $phone;
  294. }
  295. foreach ($requiredFields as $field => $value) {
  296. if ($value === null || $value === '' || $value === []) {
  297. throw new \InvalidArgumentException("Cliente precisa ter {$field} valido para criar pedido no Pagar.me.");
  298. }
  299. }
  300. $customerAddress = new PagarmeCustomerAddressRequestData(
  301. line1: $line1,
  302. line2: $address->complement ?: $address->instructions,
  303. zipCode: $zipCode,
  304. city: $city,
  305. state: $state,
  306. country: 'BR',
  307. );
  308. $customerPhones = null;
  309. if ($phone) {
  310. $customerPhones = new PagarmeCustomerPhonesRequestData(
  311. mobilePhone: new PagarmeCustomerPhoneData(
  312. countryCode: $phone['country_code'],
  313. areaCode: $phone['area_code'],
  314. number: $phone['number'],
  315. ),
  316. );
  317. }
  318. return new PagarmeCustomerRequestData(
  319. name: $user->name,
  320. email: $user->email,
  321. document: $document,
  322. type: strlen($document) === 14 ? 'company' : 'individual',
  323. documentType: strlen($document) === 14 ? 'CNPJ' : 'CPF',
  324. code: "client-{$client->id}",
  325. address: $customerAddress,
  326. phones: $customerPhones,
  327. );
  328. }
  329. private function buildPhonePayload(?string $phone): ?array
  330. {
  331. $digits = $this->digits($phone);
  332. if (strlen($digits) < 10) {
  333. return null;
  334. }
  335. if (str_starts_with($digits, '55')) {
  336. $digits = substr($digits, 2);
  337. }
  338. return [
  339. 'country_code' => '55',
  340. 'area_code' => substr($digits, 0, 2),
  341. 'number' => substr($digits, 2),
  342. ];
  343. }
  344. private function ensureCustomerPhoneForPayment(Schedule $schedule, array $options = []): void
  345. {
  346. $userPhone = $schedule->client?->user?->phone;
  347. $phone = $this->buildPhonePayload($userPhone)
  348. ?: $this->buildPhonePayload($options['phone'] ?? null);
  349. if ($phone) {
  350. return;
  351. }
  352. throw new \InvalidArgumentException(
  353. 'Voce precisa cadastrar um numero de celular valido no seu perfil para concluir o pagamento.'
  354. );
  355. }
  356. private function digits(?string $value): string
  357. {
  358. return preg_replace('/\D+/', '', (string) $value) ?? '';
  359. }
  360. private function isIncompleteGatewayPayment(Payment $payment): bool
  361. {
  362. return $payment->status === PaymentStatusEnum::PENDING
  363. && empty($payment->gateway_entity_reference)
  364. && empty($payment->gateway_operation_reference)
  365. && empty($payment->gateway_payload);
  366. }
  367. private function resolveCreditCardReference(?ClientPaymentMethod $clientPaymentMethod, array $options): array
  368. {
  369. if (! empty($options['card_id'])) {
  370. return ['card_id' => $options['card_id']];
  371. }
  372. if (! empty($clientPaymentMethod?->gateway_card_id)) {
  373. return ['card_id' => $clientPaymentMethod->gateway_card_id];
  374. }
  375. throw new \InvalidArgumentException('Cartao de pagamento precisa ter gateway_card_id do Pagar.me.');
  376. }
  377. public function syncScheduleStatusAfterPayment(Schedule $schedule, Payment $payment): void
  378. {
  379. if ($payment->status !== PaymentStatusEnum::PAID || $schedule->status === 'paid') {
  380. return;
  381. }
  382. $schedule->update(['status' => 'paid']);
  383. }
  384. }