PagarmeRecipientService.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. <?php
  2. namespace App\Services\Pagarme;
  3. use App\Data\Pagarme\Recipient\BankAccountUpdateRequestData;
  4. use App\Data\Pagarme\Recipient\Parts\Request\AddressData;
  5. use App\Data\Pagarme\Recipient\Parts\Request\AutomaticAnticipationSettingsData;
  6. use App\Data\Pagarme\Recipient\Parts\Request\BankAccountData;
  7. use App\Data\Pagarme\Recipient\Parts\Request\PhoneData;
  8. use App\Data\Pagarme\Recipient\Parts\Request\PhoneNumbersData;
  9. use App\Data\Pagarme\Recipient\Parts\Request\RegisterInformationData;
  10. use App\Data\Pagarme\Recipient\Parts\Request\TransferSettingsData;
  11. use App\Data\Pagarme\Recipient\RecipientRequestData;
  12. use App\Data\Pagarme\Recipient\RecipientResponseData;
  13. use App\Models\Provider;
  14. use App\Models\ProviderBankAccount;
  15. use App\Services\Pagarme\Concerns\FormatsPagarmeData;
  16. use App\Services\Pagarme\Concerns\MocksPagarmeRequests;
  17. use App\Services\Pagarme\Concerns\SendsPagarmeRequests;
  18. class PagarmeRecipientService
  19. {
  20. use FormatsPagarmeData;
  21. use MocksPagarmeRequests;
  22. use SendsPagarmeRequests;
  23. public function createRecipientForProvider(Provider $provider, array $data): string
  24. {
  25. if (! empty($provider->recipient_id)) {
  26. return $provider->recipient_id;
  27. }
  28. $metadata = data_get($data, 'recipient_metadata', []);
  29. $paymentMode = data_get($data, 'recipient_payment_mode');
  30. $recipientCode = $provider->ensureGatewayCode();
  31. $addressParts = $this->extractAddressParts($data);
  32. $registerInformation = new RegisterInformationData(
  33. name: data_get($data, 'recipient_name'),
  34. email: data_get($data, 'recipient_email'),
  35. document: $this->digits(data_get($data, 'recipient_document')),
  36. type: data_get($data, 'recipient_type', 'individual'),
  37. birthdate: $this->formatBirthdate(data_get($data, 'birth_date')),
  38. monthlyIncome: data_get($data, 'monthly_income') !== null ? (int) data_get($data, 'monthly_income') : 1000,
  39. professionalOccupation: data_get($data, 'professional_occupation', 'autonomo'),
  40. phoneNumbers: new PhoneNumbersData(
  41. $this->buildRecipientPhone(data_get($data, 'phone')),
  42. ),
  43. address: new AddressData(
  44. street: data_get($addressParts, 'street'),
  45. complementary: data_get($addressParts, 'complementary'),
  46. streetNumber: data_get($addressParts, 'street_number'),
  47. neighborhood: data_get($addressParts, 'neighborhood'),
  48. city: data_get($data, 'city'),
  49. state: data_get($data, 'state'),
  50. zipCode: $this->digits(data_get($data, 'zip_code')),
  51. referencePoint: data_get($addressParts, 'reference_point'),
  52. ),
  53. );
  54. $defaultBankAccount = $this->buildRecipientBankAccount(
  55. data_get($data, 'recipient_default_bank_account'),
  56. );
  57. $payload = new RecipientRequestData(
  58. code: $recipientCode,
  59. registerInformation: $registerInformation,
  60. defaultBankAccount: $defaultBankAccount,
  61. transferSettings: new TransferSettingsData(
  62. transferEnabled: false,
  63. transferInterval: 'Daily',
  64. transferDay: 0,
  65. ),
  66. automaticAnticipationSettings: new AutomaticAnticipationSettingsData(
  67. enabled: true,
  68. type: '1025', // isso significa que vai ser feita de acordo com o delay (dias)
  69. delay: 1,
  70. ),
  71. );
  72. $bankAccountData = $payload->defaultBankAccount->toArray();
  73. if ($this->shouldMockPagarmeRequest()) {
  74. $recipientId = $this->mockPagarmeId('re', $provider->id);
  75. $provider->forceFill([
  76. 'recipient_id' => $recipientId,
  77. 'recipient_name' => data_get($data, 'recipient_name'),
  78. 'recipient_email' => data_get($data, 'recipient_email'),
  79. 'recipient_description' => data_get($data, 'recipient_description', ''),
  80. 'recipient_document' => data_get($data, 'recipient_document'),
  81. 'recipient_type' => $payload->registerInformation->type,
  82. 'recipient_code' => $recipientCode,
  83. 'recipient_payment_mode' => $paymentMode,
  84. 'recipient_default_bank_account' => $bankAccountData,
  85. 'recipient_transfer_settings' => [
  86. 'transfer_enabled' => false,
  87. 'transfer_interval' => 'daily',
  88. 'transfer_day' => 0,
  89. ],
  90. 'recipient_automatic_anticipation_settings' => [
  91. 'enabled' => true,
  92. 'type' => 'full',
  93. 'delay' => 1,
  94. ],
  95. 'recipient_metadata' => array_merge($metadata, ['mocked' => true]),
  96. ])->save();
  97. return $recipientId;
  98. }
  99. $raw = $this->pagarmeRequest(
  100. method: 'POST',
  101. path: '/recipients',
  102. payload: $payload,
  103. idempotencyKey: $this->idempotencyKey($provider, '', $data),
  104. errorMessage: 'Erro ao criar recebedor no Pagar.me.',
  105. );
  106. $recipientData = RecipientResponseData::fromArray($raw);
  107. $recipientId = $recipientData->requireId();
  108. $provider->forceFill([
  109. 'recipient_id' => $recipientId,
  110. 'recipient_name' => data_get($data, 'recipient_name'),
  111. 'recipient_email' => data_get($data, 'recipient_email'),
  112. 'recipient_description' => data_get($data, 'recipient_description', ''),
  113. 'recipient_document' => data_get($data, 'recipient_document'),
  114. 'recipient_type' => $payload->registerInformation->type,
  115. 'recipient_code' => $recipientCode,
  116. 'recipient_payment_mode' => $paymentMode,
  117. 'recipient_default_bank_account' => $bankAccountData,
  118. 'recipient_transfer_settings' => [
  119. 'transfer_enabled' => false,
  120. 'transfer_interval' => 'daily',
  121. 'transfer_day' => 0,
  122. ],
  123. 'recipient_automatic_anticipation_settings' => [
  124. 'enabled' => true,
  125. 'type' => 'full',
  126. 'delay' => 1,
  127. ],
  128. 'recipient_metadata' => $metadata,
  129. ])->save();
  130. return $recipientId;
  131. }
  132. public function getRecipient(string $recipientId): array
  133. {
  134. if ($this->shouldMockPagarmeRequest()) {
  135. $provider = Provider::query()->where('recipient_id', $recipientId)->first();
  136. return [
  137. 'id' => $recipientId,
  138. 'default_bank_account' => $provider?->recipient_default_bank_account ?? [],
  139. 'mocked' => true,
  140. ];
  141. }
  142. return $this->pagarmeRequest(
  143. method: 'GET',
  144. path: "/recipients/{$recipientId}",
  145. payload: [],
  146. idempotencyKey: "get-recipient-{$recipientId}",
  147. errorMessage: 'Erro ao consultar recebedor no Pagar.me.',
  148. );
  149. }
  150. /**
  151. * O Pagar.me mantem UMA conta bancaria por recebedor. As demais contas do prestador
  152. * existem apenas no nosso banco; aqui so trafega a conta principal.
  153. *
  154. * O recipient_id nao muda: o que e atualizado e o destino das transferencias (saques).
  155. */
  156. public function updateDefaultBankAccount(Provider $provider, ProviderBankAccount $bankAccount): Provider
  157. {
  158. $bankAccountData = $bankAccount->toGatewayBankAccount();
  159. $payload = new BankAccountUpdateRequestData(
  160. holderName: $this->normalizeHolderName(data_get($bankAccountData, 'holder_name')),
  161. holderType: data_get($bankAccountData, 'holder_type'),
  162. holderDocument: $this->digits(data_get($bankAccountData, 'holder_document')),
  163. bank: data_get($bankAccountData, 'bank'),
  164. branchNumber: data_get($bankAccountData, 'branch_number'),
  165. branchCheckDigit: data_get($bankAccountData, 'branch_check_digit'),
  166. accountNumber: data_get($bankAccountData, 'account_number'),
  167. accountCheckDigit: data_get($bankAccountData, 'account_check_digit'),
  168. type: data_get($bankAccountData, 'type'),
  169. );
  170. if ($this->shouldMockPagarmeRequest()) {
  171. $provider->forceFill([
  172. 'recipient_default_bank_account' => $payload->toArray()['bank_account'],
  173. ])->save();
  174. return $provider->fresh();
  175. }
  176. $this->pagarmeRequest(
  177. method: 'PATCH',
  178. path: "/recipients/{$provider->recipient_id}/default-bank-account",
  179. payload: $payload,
  180. idempotencyKey: $this->idempotencyKey($provider, $this->bankAccountIdempotencySuffix($bankAccount)),
  181. errorMessage: 'Erro ao atualizar conta bancaria do recebedor no Pagar.me.',
  182. );
  183. $provider->forceFill([
  184. 'recipient_default_bank_account' => $payload->toArray()['bank_account'],
  185. ])->save();
  186. return $provider->fresh();
  187. }
  188. /**
  189. * A chave precisa ser unica por operacao, e nao pelo conteudo da conta.
  190. *
  191. * Com o hash do payload, alternar a conta principal A -> B -> A reenviava a mesma chave
  192. * da primeira troca: o Pagar.me tratava como repeticao, devolvia a resposta antiga e
  193. * nao aplicava a volta para A - o saque continuaria caindo em B.
  194. *
  195. * Por isso o discriminador e um contador persistido, e nao o timestamp da conta:
  196. * updated_at volta do banco truncado em segundos e repetiria a chave.
  197. */
  198. private function bankAccountIdempotencySuffix(ProviderBankAccount $bankAccount): string
  199. {
  200. return sprintf(
  201. 'default-bank-account-%d-%d',
  202. $bankAccount->id,
  203. $bankAccount->gateway_sync_count,
  204. );
  205. }
  206. //
  207. private function buildRecipientBankAccount(array $data): BankAccountData
  208. {
  209. return new BankAccountData(
  210. holderName: $this->normalizeHolderName(data_get($data, 'holder_name')),
  211. holderType: data_get($data, 'holder_type'),
  212. holderDocument: $this->digits(data_get($data, 'holder_document')),
  213. bank: data_get($data, 'bank'),
  214. branchNumber: data_get($data, 'branch_number'),
  215. branchCheckDigit: data_get($data, 'branch_check_digit'),
  216. accountNumber: data_get($data, 'account_number'),
  217. accountCheckDigit: data_get($data, 'account_check_digit'),
  218. type: data_get($data, 'type'),
  219. );
  220. }
  221. private function buildRecipientPhone(?string $phone): PhoneData
  222. {
  223. $digits = $this->digits($phone);
  224. if (strlen($digits) < 10) {
  225. return new PhoneData(
  226. ddd: '11',
  227. number: '999999999',
  228. type: 'mobile',
  229. );
  230. }
  231. if (str_starts_with($digits, '55')) {
  232. $digits = substr($digits, 2);
  233. }
  234. return new PhoneData(
  235. ddd: substr($digits, 0, 2),
  236. number: substr($digits, 2),
  237. type: 'mobile',
  238. );
  239. }
  240. // evita criacao duplica de recipient
  241. // formato: recipient-{email}-{documento} (tudo minusculo e sem acento)
  242. private function idempotencyKey(Provider $provider, string $suffix = '', array $data = []): string
  243. {
  244. $baseKey = $provider->idempotency_key;
  245. if (empty($baseKey)) {
  246. $provider->loadMissing('user');
  247. $baseKey = $this->pagarmeIdempotencyKey('recipient', [
  248. data_get($data, 'recipient_email', $provider->recipient_email ?? $provider->user?->email ?? "provider-{$provider->id}"),
  249. $this->customerDocument(data_get($data, 'recipient_document', $provider->recipient_document ?? $provider->document))
  250. ?: "document-provider-{$provider->id}",
  251. ]);
  252. $provider->forceFill(['idempotency_key' => $baseKey])->save();
  253. }
  254. return $suffix ? "{$baseKey}-{$suffix}" : $baseKey;
  255. }
  256. }