PaymentConfirmController.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. <?php
  2. namespace Controllers;
  3. use Libs\ResponseLib;
  4. use Models\CommodityModel;
  5. use Models\CprModel;
  6. use Models\PaymentModel;
  7. use Psr\Http\Message\ServerRequestInterface;
  8. use Services\B3CprService;
  9. use Services\TokenCreateService;
  10. class PaymentConfirmController
  11. {
  12. private PaymentModel $paymentModel;
  13. private CprModel $cprModel;
  14. private B3CprService $b3Service;
  15. private CommodityModel $commodityModel;
  16. private TokenCreateService $tokenCreateService;
  17. private \PDO $pdo;
  18. public function __construct()
  19. {
  20. if (!isset($GLOBALS['pdo']) || !$GLOBALS['pdo'] instanceof \PDO) {
  21. throw new \RuntimeException('Global PDO connection not initialized');
  22. }
  23. $this->pdo = $GLOBALS['pdo'];
  24. $this->paymentModel = new PaymentModel();
  25. $this->cprModel = new CprModel();
  26. $this->b3Service = new B3CprService();
  27. $this->commodityModel = new CommodityModel();
  28. $this->tokenCreateService = new TokenCreateService();
  29. }
  30. public function __invoke(ServerRequestInterface $request)
  31. {
  32. $body = json_decode((string)$request->getBody(), true) ?? [];
  33. $paymentId = isset($body['payment_id']) ? (int)$body['payment_id'] : 0;
  34. if ($paymentId <= 0) {
  35. return ResponseLib::sendFail('payment_id inválido', [], 'E_VALIDATE')->withStatus(400);
  36. }
  37. $payment = $this->paymentModel->findById($paymentId);
  38. if (!$payment) {
  39. return ResponseLib::sendFail('Pagamento não encontrado', [], 'E_NOT_FOUND')->withStatus(404);
  40. }
  41. $statusId = (int)($payment['status_id'] ?? 0);
  42. if ($statusId === 0) {
  43. return ResponseLib::sendFail('Pagamento ainda não confirmado', ['payment_id' => $paymentId], 'E_PAYMENT_PENDING')->withStatus(409);
  44. }
  45. if ($statusId !== 1) {
  46. return ResponseLib::sendFail('Pagamento em status inválido', ['status_id' => $statusId], 'E_PAYMENT_STATUS')->withStatus(409);
  47. }
  48. $cpr = $this->cprModel->findByPaymentId($paymentId);
  49. if (!$cpr) {
  50. return ResponseLib::sendFail('Nenhuma CPR vinculada ao pagamento', [], 'E_CPR_NOT_FOUND')->withStatus(404);
  51. }
  52. try {
  53. $payload = $this->b3Service->mapToB3($cpr);
  54. } catch (\Throwable $e) {
  55. return ResponseLib::sendFail('Falha ao montar payload para a B3: ' . $e->getMessage(), [], 'E_B3_MAP')->withStatus(500);
  56. }
  57. try {
  58. $token = $this->resolveB3Token($request, $body);
  59. } catch (\Throwable $e) {
  60. return ResponseLib::sendFail('Falha ao obter token de acesso da B3: ' . $e->getMessage(), [], 'E_B3_TOKEN')->withStatus(502);
  61. }
  62. try {
  63. $result = $this->b3Service->postCpr($token, $payload);
  64. } catch (\Throwable $e) {
  65. return ResponseLib::sendFail('Falha ao enviar CPR à B3: ' . $e->getMessage(), [], 'E_EXTERNAL')->withStatus(502);
  66. }
  67. if (isset($result['error'])) {
  68. return ResponseLib::sendFail('cURL error during B3 CPR request', ['error' => $result['error']], 'E_EXTERNAL')->withStatus(502);
  69. }
  70. $tickerSymbol = $result['json']['data']['tickerSymbol'] ?? null;
  71. try {
  72. $tokenResult = $this->createTokenFromCpr($cpr);
  73. } catch (\Throwable $e) {
  74. return ResponseLib::sendFail(
  75. 'Falha ao gerar token: ' . $e->getMessage(),
  76. [],
  77. 'E_TOKEN_CREATE'
  78. )->withStatus(500);
  79. }
  80. try {
  81. $this->cprModel->updateTokenId((int)$cpr['cpr_id'], (int)$tokenResult['token_id']);
  82. if ($tickerSymbol !== null) {
  83. $this->cprModel->updateTicker((int)$cpr['cpr_id'], (string)$tickerSymbol);
  84. }
  85. } catch (\Throwable $e) {
  86. return ResponseLib::sendFail(
  87. 'Falha ao atualizar CPR: ' . $e->getMessage(),
  88. [],
  89. 'E_CPR_UPDATE'
  90. )->withStatus(500);
  91. }
  92. return ResponseLib::sendOk([
  93. 'message' => 'CPR enviada e token criado com sucesso',
  94. 'payment_id' => $paymentId,
  95. 'b3_response' => $result['json'] ?? ($result['raw'] ?? null),
  96. 'token_id' => $tokenResult['token_id'],
  97. 'token_external_id' => $tokenResult['token_external_id'],
  98. 'tx_hash' => $tokenResult['tx_hash'],
  99. ], 'S_CPR_SENT');
  100. }
  101. private function resolveB3Token(ServerRequestInterface $request, array $body): string
  102. {
  103. $token = $body['b3_access_token'] ?? ($body['access_token'] ?? null);
  104. if (!$token) {
  105. $b3Auth = $request->getHeaderLine('X-B3-Authorization') ?: '';
  106. if (stripos($b3Auth, 'Bearer ') === 0) {
  107. $token = trim(substr($b3Auth, 7));
  108. }
  109. }
  110. if (!$token) {
  111. $token = $request->getHeaderLine('X-B3-Access-Token') ?: null;
  112. }
  113. if (!$token) {
  114. $token = $this->b3Service->getAccessToken();
  115. }
  116. return $token;
  117. }
  118. private function createTokenFromCpr(array $cpr): array
  119. {
  120. $inputs = $this->prepareTokenInputs($cpr);
  121. return $this->tokenCreateService->createToken(
  122. $inputs['token_commodities_amount'],
  123. $inputs['token_commodities_value'],
  124. $inputs['token_uf'],
  125. $inputs['token_city'],
  126. $inputs['token_content'],
  127. $inputs['token_flag'],
  128. $inputs['wallet_id'],
  129. $inputs['chain_id'],
  130. $inputs['commodities_id'],
  131. $inputs['cpr_id'],
  132. $inputs['user_id']
  133. );
  134. }
  135. /**
  136. * @return array{
  137. * token_commodities_amount:int,
  138. * token_commodities_value:int,
  139. * token_uf:string,
  140. * token_city:string,
  141. * token_content:string,
  142. * token_flag:string,
  143. * wallet_id:int,
  144. * chain_id:int,
  145. * commodities_id:int,
  146. * cpr_id:int,
  147. * user_id:int
  148. * }
  149. */
  150. private function prepareTokenInputs(array $cpr): array
  151. {
  152. $cprId = (int)($cpr['cpr_id'] ?? 0);
  153. if ($cprId <= 0) {
  154. throw new \InvalidArgumentException('CPR sem identificador válido.');
  155. }
  156. $userId = (int)($cpr['user_id'] ?? 0);
  157. if ($userId <= 0) {
  158. throw new \InvalidArgumentException('CPR sem usuário associado.');
  159. }
  160. $companyId = 1;
  161. $wallet = $this->findWalletByCompanyId($companyId);
  162. $commoditiesName = $this->requireStringField($cpr, ['cpr_product_name'], 'cpr_product_name');
  163. $commoditiesId = $this->resolveCommodityId($commoditiesName);
  164. $tokenCommoditiesAmount = $this->requireNumericField(
  165. $cpr,
  166. ['cpr_product_quantity', 'cpr_issue_quantity'],
  167. 'quantidade do produto'
  168. );
  169. $tokenCommoditiesValue = $this->requireNumericField(
  170. $cpr,
  171. ['cpr_issue_value', 'cpr_issue_financial_value'],
  172. 'valor do produto'
  173. );
  174. $tokenUf = $this->requireStringField(
  175. $cpr,
  176. ['cpr_deliveryPlace_state_acronym', 'cpr_issuers_state_acronym'],
  177. 'UF'
  178. );
  179. $tokenCity = $this->requireStringField(
  180. $cpr,
  181. ['cpr_deliveryPlace_city_name', 'cpr_issuers_city_name'],
  182. 'cidade'
  183. );
  184. return [
  185. 'token_commodities_amount' => $tokenCommoditiesAmount,
  186. 'token_commodities_value' => $tokenCommoditiesValue,
  187. 'token_uf' => $tokenUf,
  188. 'token_city' => $tokenCity,
  189. 'token_content' => (string)$cprId,
  190. 'token_flag' => '',
  191. 'wallet_id' => $wallet['wallet_id'],
  192. 'chain_id' => $wallet['chain_id'],
  193. 'commodities_id' => $commoditiesId,
  194. 'cpr_id' => $cprId,
  195. 'user_id' => $userId,
  196. ];
  197. }
  198. private function findWalletByCompanyId(int $companyId): array
  199. {
  200. $stmt = $this->pdo->prepare(
  201. 'SELECT wallet_id, chain_id
  202. FROM "wallet"
  203. WHERE company_id = :company_id
  204. ORDER BY wallet_id ASC
  205. LIMIT 1'
  206. );
  207. $stmt->execute(['company_id' => $companyId]);
  208. $wallet = $stmt->fetch(\PDO::FETCH_ASSOC);
  209. if (!$wallet) {
  210. throw new \RuntimeException('Nenhuma carteira encontrada para a empresa informada.');
  211. }
  212. return [
  213. 'wallet_id' => (int)$wallet['wallet_id'],
  214. 'chain_id' => (int)$wallet['chain_id'],
  215. ];
  216. }
  217. private function resolveCommodityId(string $name): int
  218. {
  219. $commodityId = $this->commodityModel->getIdByName($name);
  220. if ($commodityId === null) {
  221. throw new \RuntimeException('Commodity não encontrada para o produto: ' . $name);
  222. }
  223. return $commodityId;
  224. }
  225. private function requireStringField(array $cpr, array $candidates, string $label): string
  226. {
  227. foreach ($candidates as $field) {
  228. if (!array_key_exists($field, $cpr)) {
  229. continue;
  230. }
  231. $value = $this->normalizeStringValue($cpr[$field]);
  232. if ($value !== '') {
  233. return $value;
  234. }
  235. }
  236. throw new \InvalidArgumentException("Campo {$label} ausente ou inválido na CPR.");
  237. }
  238. private function requireNumericField(array $cpr, array $candidates, string $label): int
  239. {
  240. foreach ($candidates as $field) {
  241. if (!array_key_exists($field, $cpr)) {
  242. continue;
  243. }
  244. $value = $this->normalizeNumericValue($cpr[$field]);
  245. if ($value !== null) {
  246. return $value;
  247. }
  248. }
  249. throw new \InvalidArgumentException("Campo {$label} ausente ou inválido na CPR.");
  250. }
  251. private function normalizeStringValue($value): string
  252. {
  253. if (is_array($value)) {
  254. $value = reset($value);
  255. }
  256. if (!is_scalar($value)) {
  257. return '';
  258. }
  259. $stringValue = trim((string)$value);
  260. if ($stringValue === '') {
  261. return '';
  262. }
  263. $parts = preg_split('/\s*;\s*/', $stringValue) ?: [];
  264. $first = $parts[0] ?? $stringValue;
  265. return trim((string)$first);
  266. }
  267. private function normalizeNumericValue($value): ?int
  268. {
  269. if (is_array($value)) {
  270. $value = reset($value);
  271. }
  272. if (is_string($value)) {
  273. $value = str_replace([' ', ','], ['', '.'], $value);
  274. }
  275. if (is_numeric($value)) {
  276. return (int)round((float)$value);
  277. }
  278. return null;
  279. }
  280. }