B3CprService.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. <?php
  2. namespace Services;
  3. class B3CprService
  4. {
  5. public function getAccessToken(): string
  6. {
  7. $b3Url = $_ENV['B3_URL'] ?? null;
  8. $grantType = $_ENV['GRANT_TYPE'] ?? 'client_credentials';
  9. $certPass = $_ENV['CERT_PASS'] ?? null;
  10. if (!$b3Url || !$grantType) {
  11. throw new \RuntimeException('Missing required env vars: B3_URL, GRANT_TYPE');
  12. }
  13. $certPath = dirname(__DIR__) . DIRECTORY_SEPARATOR . '319245319-320109623_PROD.cer';
  14. $keyPath = dirname(__DIR__) . DIRECTORY_SEPARATOR . '319245319-320109623_PROD.key';
  15. if (!file_exists($certPath) || !file_exists($keyPath)) {
  16. throw new \RuntimeException('Client certificate or key not found');
  17. }
  18. $postFields = 'grant_type=' . ($grantType);
  19. $ch = curl_init($b3Url);
  20. curl_setopt($ch, CURLOPT_POST, true);
  21. $headers = [
  22. 'Content-Type: application/x-www-form-urlencoded',
  23. 'Accept: application/json',
  24. ];
  25. curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  26. curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
  27. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  28. curl_setopt($ch, CURLOPT_SSLCERT, $certPath);
  29. curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
  30. curl_setopt($ch, CURLOPT_SSLKEY, $keyPath);
  31. curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
  32. if ($certPass) {
  33. curl_setopt($ch, CURLOPT_SSLKEYPASSWD, $certPass);
  34. }
  35. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
  36. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
  37. $responseBody = curl_exec($ch);
  38. $curlErrNo = curl_errno($ch);
  39. $curlErr = curl_error($ch);
  40. $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
  41. curl_close($ch);
  42. if ($curlErrNo !== 0) {
  43. throw new \RuntimeException('cURL error: ' . $curlErr);
  44. }
  45. $decoded = json_decode((string)$responseBody, true);
  46. if (!is_array($decoded)) {
  47. throw new \RuntimeException('Invalid token response (non-JSON), HTTP ' . $httpCode);
  48. }
  49. $token = $decoded['access_token'] ?? null;
  50. if (!is_string($token) || $token === '') {
  51. throw new \RuntimeException('access_token not present in token response');
  52. }
  53. return $token;
  54. }
  55. public function mapToB3(array $cpr): array
  56. {
  57. $collateralTypeCodes = $this->splitSemicolonList($cpr['cpr_collateral_type_code'] ?? null);
  58. $collateralTypeNames = $this->splitSemicolonList($cpr['cpr_collateral_type_name'] ?? null);
  59. $constitutionProcessIndicators = $this->splitSemicolonList($cpr['cpr_constitution_process_indicator'] ?? null);
  60. $otcBondsmanAccountCodes = $this->splitSemicolonList($cpr['cpr_otc_bondsman_account_code'] ?? null);
  61. $collateralsCount = max(
  62. count($collateralTypeCodes),
  63. count($collateralTypeNames),
  64. count($constitutionProcessIndicators),
  65. count($otcBondsmanAccountCodes)
  66. );
  67. if ($collateralsCount <= 0) {
  68. $collateralsCount = 1;
  69. }
  70. $collaterals = [];
  71. for ($i = 0; $i < $collateralsCount; $i++) {
  72. $collaterals[] = [
  73. 'collateralTypeCode' => $collateralTypeCodes[$i] ?? null,
  74. 'collateralTypeName' => $collateralTypeNames[$i] ?? null,
  75. 'constitutionProcessIndicator' => $this->mapIndicator($constitutionProcessIndicators[$i] ?? null),
  76. 'otcBondsmanAccountCode' => $otcBondsmanAccountCodes[$i] ?? null,
  77. ];
  78. }
  79. $issuerNames = $this->splitSemicolonList($cpr['cpr_issuer_name'] ?? null);
  80. $issuerDocumentNumbers = $this->splitSemicolonList($cpr['cpr_issuers_document_number'] ?? null);
  81. $issuerPersonTypes = $this->splitSemicolonList($cpr['cpr_issuers_person_type_acronym'] ?? null);
  82. $issuerLegalNatures = $this->splitSemicolonList($cpr['cpr_issuer_legal_nature_code'] ?? null);
  83. $issuerStates = $this->splitSemicolonList($cpr['cpr_issuers_state_acronym'] ?? null);
  84. $issuerCities = $this->splitSemicolonList($cpr['cpr_issuers_city_name'] ?? null);
  85. $issuersCount = max(
  86. count($issuerNames),
  87. count($issuerDocumentNumbers),
  88. count($issuerPersonTypes),
  89. count($issuerLegalNatures),
  90. count($issuerStates),
  91. count($issuerCities)
  92. );
  93. if ($issuersCount <= 0) {
  94. $issuersCount = 1;
  95. }
  96. $issuers = [];
  97. for ($i = 0; $i < $issuersCount; $i++) {
  98. $issuers[] = [
  99. 'cprIssuerName' => $issuerNames[$i] ?? null,
  100. 'documentNumber' => $issuerDocumentNumbers[$i] ?? null,
  101. 'personTypeAcronym' => $issuerPersonTypes[$i] ?? null,
  102. 'issuerLegalNatureCode' => $issuerLegalNatures[$i] ?? null,
  103. 'stateAcronym' => $issuerStates[$i] ?? null,
  104. 'cityName' => $issuerCities[$i] ?? null,
  105. ];
  106. }
  107. $productionPlaceNames = $this->splitSemicolonList($cpr['cpr_production_place_name'] ?? null);
  108. $propertyRegistrationNumbers = $this->splitSemicolonList($cpr['cpr_property_registration_number'] ?? null);
  109. $notaryNames = $this->splitSemicolonList($cpr['cpr_notary_name'] ?? null);
  110. $totalProductionAreas = $this->splitSemicolonList($cpr['cpr_total_production_area_in_hectares_number'] ?? null);
  111. $totalAreas = $this->splitSemicolonList($cpr['cpr_total_area_in_hectares_number'] ?? null);
  112. $carCodes = $this->splitSemicolonList($cpr['cpr_car_code'] ?? null);
  113. $latitudeCodes = $this->splitSemicolonList($cpr['cpr_latitude_code'] ?? null);
  114. $longitudeCodes = $this->splitSemicolonList($cpr['cpr_longitude_code'] ?? null);
  115. $zipCodes = $this->splitSemicolonList($cpr['cpr_zip_code'] ?? null);
  116. $productionPlacesCount = max(
  117. count($productionPlaceNames),
  118. count($propertyRegistrationNumbers),
  119. count($notaryNames),
  120. count($totalProductionAreas),
  121. count($totalAreas),
  122. count($carCodes),
  123. count($latitudeCodes),
  124. count($longitudeCodes),
  125. count($zipCodes)
  126. );
  127. if ($productionPlacesCount <= 0) {
  128. $productionPlacesCount = 1;
  129. }
  130. $productionPlaces = [];
  131. for ($i = 0; $i < $productionPlacesCount; $i++) {
  132. $productionPlaces[] = [
  133. 'productionPlaceName' => $productionPlaceNames[$i] ?? null,
  134. 'propertyRegistrationNumber' => $propertyRegistrationNumbers[$i] ?? null,
  135. 'notaryName' => $notaryNames[$i] ?? null,
  136. 'totalProductionAreaInHectaresNumber' => $totalProductionAreas[$i] ?? null,
  137. 'totalAreaInHectaresNumber' => $totalAreas[$i] ?? null,
  138. 'carCode' => $carCodes[$i] ?? null,
  139. 'latitudeCode' => $latitudeCodes[$i] ?? null,
  140. 'longitudeCode' => $longitudeCodes[$i] ?? null,
  141. 'zipCode' => $zipCodes[$i] ?? null,
  142. ];
  143. }
  144. $instrument = [
  145. 'cprTypeCode' => $cpr['cpr_type_code'] ?? null,
  146. 'otcRegisterAccountCode' => $cpr['cpr_otc_register_account_code'] ?? null,
  147. 'otcCustodianAccountCode' => $cpr['cpr_otc_custodian_account_code'] ?? null,
  148. 'electronicEmissionIndicator' => $this->mapIndicator($cpr['cpr_electronic_emission_indicator'] ?? null),
  149. 'issueDate' => $this->convertDate($cpr['cpr_issue_date'] ?? null),
  150. 'maturityDate' => $this->convertDate($cpr['cpr_maturity_date'] ?? null),
  151. 'issueQuantity' => $cpr['cpr_issue_quantity'] ?? null,
  152. 'issueValue' => $cpr['cpr_issue_value'] ?? null,
  153. 'issueFinancialValue' => $cpr['cpr_issue_financial_value'] ?? null,
  154. 'profitabilityStartDate' => $this->convertDate($cpr['cpr_profitability_start_date'] ?? null),
  155. 'automaticExpirationIndicator' => $this->mapIndicator($cpr['cpr_automatic_expiration_indicator'] ?? null),
  156. 'collaterals' => $collaterals,
  157. 'products' => [[
  158. 'cprProductName' => $cpr['cpr_product_name'] ?? null,
  159. 'cprProductClassName' => $cpr['cpr_product_class_name'] ?? null,
  160. 'cprProductHarvest' => $cpr['cpr_product_harvest'] ?? null,
  161. 'cprProductDescription' => $cpr['cpr_product_description'] ?? null,
  162. 'cprProductQuantity' => $cpr['cpr_product_quantity'] ?? null,
  163. 'measureUnitName' => $cpr['cpr_measure_unit_name'] ?? null,
  164. 'packagingWayName' => $cpr['cpr_packaging_way_name'] ?? null,
  165. 'cprProductStatusCode' => $cpr['cpr_product_status_code'] ?? null,
  166. 'productionTypeCode' => $cpr['cpr_production_type_code'] ?? null,
  167. ]],
  168. 'deliveryPlace' => [
  169. 'documentDeadlineDaysNumber' => $cpr['cpr_document_deadline_days_number'] ?? null,
  170. 'placeName' => $cpr['cpr_place_name'] ?? null,
  171. 'stateAcronym' => $cpr['cpr_deliveryPlace_state_acronym'] ?? null,
  172. 'cityName' => $cpr['cpr_deliveryPlace_city_name'] ?? null,
  173. ],
  174. 'issuers' => $issuers,
  175. 'scr' => [
  176. 'scrTypeCode' => $cpr['cpr_scr_type_code'] ?? null,
  177. 'finalityCode' => $cpr['cpr_finality_code'] ?? null,
  178. 'contractCode' => $cpr['cpr_contract_code'] ?? null,
  179. ],
  180. 'creditor' => [
  181. 'creditorName' => $cpr['cpr_creditor_name'] ?? null,
  182. 'documentNumber' => $cpr['cpr_creditor_document_number'] ?? null,
  183. ],
  184. 'productionPlaces' => $productionPlaces,
  185. ];
  186. return ['data' => ['instrument' => $instrument]];
  187. }
  188. private function splitSemicolonList($value): array
  189. {
  190. if (is_array($value)) {
  191. $items = array_map(static fn($v) => trim((string)$v), array_values($value));
  192. return array_values(array_filter($items, static fn($v) => $v !== ''));
  193. }
  194. if (!is_string($value)) {
  195. return [];
  196. }
  197. $trimmed = trim($value);
  198. if ($trimmed === '') {
  199. return [];
  200. }
  201. $parts = preg_split('/\s*;\s*/', $trimmed) ?: [];
  202. $parts = array_map(static fn($v) => trim((string)$v), $parts);
  203. return array_values(array_filter($parts, static fn($v) => $v !== ''));
  204. }
  205. private function convertDate(?string $date): ?string
  206. {
  207. if (!is_string($date) || trim($date) === '') {
  208. return null;
  209. }
  210. $t = trim($date);
  211. if (mb_strtoupper($t, 'UTF-8') === 'NA') {
  212. return null;
  213. }
  214. $dt = \DateTime::createFromFormat('Y-m-d', $t);
  215. if ($dt instanceof \DateTime) {
  216. return $dt->format('d/m/Y');
  217. }
  218. $alt = \DateTime::createFromFormat('d/m/Y', $t);
  219. if ($alt instanceof \DateTime) {
  220. return $t;
  221. }
  222. return $t;
  223. }
  224. private function mapIndicator($value): ?string
  225. {
  226. if (!is_string($value)) {
  227. return null;
  228. }
  229. $v = mb_strtoupper(trim($value), 'UTF-8');
  230. if ($v === 'SIM' || $v === 'S') {
  231. return 'S';
  232. }
  233. if ($v === 'N' || $v === 'NAO' || $v === 'NÃO') {
  234. return 'N';
  235. }
  236. if ($v === 'NA') {
  237. return null;
  238. }
  239. return $value;
  240. }
  241. public function sanitize(array $payload): array
  242. {
  243. return $this->removeNulls($payload);
  244. }
  245. private function removeNulls($value)
  246. {
  247. if (is_array($value)) {
  248. $isAssoc = $this->isAssoc($value);
  249. $result = [];
  250. foreach ($value as $k => $v) {
  251. if (is_string($v) && mb_strtoupper(trim($v), 'UTF-8') === 'NA') {
  252. $v = null;
  253. }
  254. $v = $this->removeNulls($v);
  255. if (is_array($v) && count($v) === 0) {
  256. continue;
  257. }
  258. $result[$k] = $v;
  259. }
  260. if (!$isAssoc) {
  261. $result = array_values($result);
  262. }
  263. return $result;
  264. }
  265. if (is_string($value) && mb_strtoupper(trim($value), 'UTF-8') === 'NA') {
  266. return null;
  267. }
  268. return $value;
  269. }
  270. private function isAssoc(array $arr): bool
  271. {
  272. return array_keys($arr) !== range(0, count($arr) - 1);
  273. }
  274. public function postCpr(string $accessToken, array $payload): array
  275. {
  276. $url = $_ENV['B3_URL_CPR'] ?? null;
  277. if (!$url) {
  278. throw new \RuntimeException('B3_URL_CPR not configured');
  279. }
  280. $json = json_encode($this->sanitize($payload), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
  281. if ($json === false) {
  282. throw new \RuntimeException('Failed to encode payload');
  283. }
  284. $certPath = dirname(__DIR__) . DIRECTORY_SEPARATOR . '319245319-320109623_PROD.cer';
  285. $keyPath = dirname(__DIR__) . DIRECTORY_SEPARATOR . '319245319-320109623_PROD.key';
  286. $certPass = $_ENV['CERT_PASS'] ?? null;
  287. if (!file_exists($certPath) || !file_exists($keyPath)) {
  288. throw new \RuntimeException('Client certificate or key not found');
  289. }
  290. $ch = curl_init($url);
  291. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
  292. $headers = [
  293. 'Accept: application/json',
  294. 'Content-Type: application/json',
  295. 'Authorization: Bearer ' . $accessToken,
  296. ];
  297. curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  298. curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
  299. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  300. curl_setopt($ch, CURLOPT_SSLCERT, $certPath);
  301. curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
  302. curl_setopt($ch, CURLOPT_SSLKEY, $keyPath);
  303. curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
  304. if ($certPass) {
  305. curl_setopt($ch, CURLOPT_SSLKEYPASSWD, $certPass);
  306. }
  307. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
  308. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
  309. $responseBody = curl_exec($ch);
  310. $curlErrNo = curl_errno($ch);
  311. $curlErr = curl_error($ch);
  312. $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
  313. curl_close($ch);
  314. if ($curlErrNo !== 0) {
  315. return ['error' => $curlErr, 'status' => 0];
  316. }
  317. $decoded = json_decode((string)$responseBody, true);
  318. if ($decoded === null) {
  319. return ['raw' => $responseBody, 'status' => $httpCode ?: 502];
  320. }
  321. return ['json' => $decoded, 'status' => $httpCode ?: 200];
  322. }
  323. }