TshieldService.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. <?php
  2. namespace Services;
  3. use Models\UserModel;
  4. class TshieldService
  5. {
  6. private string $baseUrl;
  7. private string $login;
  8. private string $password;
  9. private string $clientId;
  10. private string $validationIdPf;
  11. private string $validationIdPj;
  12. private int $timeout = 30;
  13. private ?string $token = null;
  14. private UserModel $userModel;
  15. private array $individualFieldMap;
  16. public function __construct()
  17. {
  18. $this->baseUrl = rtrim($_ENV['TSHIELD_BASE_URL'] ?? '', '/');
  19. $this->login = $_ENV['TSHIELD_LOGIN'] ?? '';
  20. $this->password = $_ENV['TSHIELD_PASSWORD'] ?? '';
  21. $this->clientId = $_ENV['TSHIELD_CLIENT'] ?? '';
  22. $this->validationIdPf = $_ENV['TSHIELD_VALIDATION_ID'] ?? '';
  23. $this->validationIdPj = $_ENV['TSHIELD_VALIDATION_ID_CNPJ'] ?? '';
  24. if ($this->baseUrl === ''
  25. || $this->login === ''
  26. || $this->password === ''
  27. || $this->clientId === ''
  28. || $this->validationIdPf === ''
  29. || $this->validationIdPj === ''
  30. ) {
  31. throw new \RuntimeException(
  32. 'Missing TShield configuration. Required envs: TSHIELD_BASE_URL, TSHIELD_LOGIN, ' .
  33. 'TSHIELD_PASSWORD, TSHIELD_CLIENT, TSHIELD_VALIDATION_ID, TSHIELD_VALIDATION_ID_CNPJ.'
  34. );
  35. }
  36. $this->userModel = new UserModel();
  37. $this->individualFieldMap = [
  38. 'name' => (int)($_ENV['TSHIELD_FIELD_NAME_ID'] ?? 16175),
  39. 'document' => (int)($_ENV['TSHIELD_FIELD_DOCUMENT_ID'] ?? 16176),
  40. 'birthdate' => (int)($_ENV['TSHIELD_FIELD_BIRTHDATE_ID'] ?? 16177),
  41. 'phone' => (int)($_ENV['TSHIELD_FIELD_PHONE_ID'] ?? 16178),
  42. 'email' => (int)($_ENV['TSHIELD_FIELD_EMAIL_ID'] ?? 16179),
  43. ];
  44. }
  45. /**
  46. * Cria análise e link de KYC para pessoa física.
  47. *
  48. * @param int $userId Usuário local que receberá o número externo.
  49. * @param array $analysisPayload Payload aceito por POST /api/query.
  50. * @param array $linkPayload Payload adicional para POST /api/query/external/token (por exemplo, redirectUrl).
  51. *
  52. * @return array{
  53. * number: string,
  54. * link: ?string,
  55. * analysis: array<mixed>,
  56. * link_response: array<mixed>
  57. * }
  58. */
  59. public function generateIndividualLink(int $userId, array $analysisPayload, array $linkPayload = []): array
  60. {
  61. $linkPayload = $this->buildIndividualLinkPayload($analysisPayload, $linkPayload);
  62. $linkResponse = $this->createLink($this->validationIdPf, $linkPayload);
  63. $analysisNumber = $linkResponse['number']
  64. ?? ($linkResponse['data']['number'] ?? ($linkResponse['data']['token'] ?? null));
  65. if (!$analysisNumber) {
  66. throw new \RuntimeException(sprintf(
  67. 'Unable to extract analysis number from PF link response: %s',
  68. json_encode($linkResponse, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
  69. ));
  70. }
  71. $this->persistExternalId($userId, (string)$analysisNumber);
  72. $link = $linkResponse['link']
  73. ?? $linkResponse['url']
  74. ?? ($linkResponse['data']['link'] ?? ($linkResponse['data']['url'] ?? null));
  75. return [
  76. 'number' => (string)$analysisNumber,
  77. 'link' => $link,
  78. 'analysis' => $linkPayload,
  79. 'link_response' => $linkResponse,
  80. ];
  81. }
  82. /**
  83. * Cria análise e link de KYC para pessoa jurídica.
  84. *
  85. * @param int $userId
  86. * @param array $analysisPayload Payload aceito por POST /api/query/company.
  87. * @param array $linkPayload
  88. *
  89. * @return array{
  90. * number: string,
  91. * link: ?string,
  92. * analysis: array<mixed>,
  93. * link_response: array<mixed>
  94. * }
  95. */
  96. public function generateCompanyLink(int $userId, array $analysisPayload, array $linkPayload = []): array
  97. {
  98. $analysisPayload = $this->injectDefaults($analysisPayload, $this->validationIdPj);
  99. $analysisNumber = $this->createAnalysis('/api/query/company', $analysisPayload);
  100. $this->persistExternalId($userId, $analysisNumber);
  101. return [
  102. 'number' => $analysisNumber,
  103. 'link' => null,
  104. 'analysis' => $analysisPayload,
  105. 'link_response' => [
  106. 'data' => [
  107. 'client_validation_id' => $this->validationIdPj,
  108. 'number' => $analysisNumber,
  109. ],
  110. ],
  111. ];
  112. }
  113. private function createAnalysis(string $path, array $payload): string
  114. {
  115. $response = $this->request('POST', $path, $payload);
  116. $number = $response['data']['number']
  117. ?? $response['number']
  118. ?? $response['data']['client_validation_id']
  119. ?? $response['client_validation_id']
  120. ?? null;
  121. if (!$number) {
  122. throw new \RuntimeException(sprintf(
  123. 'Unable to extract analysis number from %s response: %s',
  124. $path,
  125. json_encode($response, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
  126. ));
  127. }
  128. return (string)$number;
  129. }
  130. private function createLink(string $validationId, array $payload): array
  131. {
  132. $body = array_merge(['client_validation_id' => $validationId], $payload);
  133. $response = $this->request('POST', '/api/query/external/token', $body);
  134. if (
  135. !isset($response['link']) &&
  136. !isset($response['url']) &&
  137. !isset($response['data']['link']) &&
  138. !isset($response['data']['url'])
  139. ) {
  140. throw new \RuntimeException(sprintf(
  141. 'Invalid link response from TShield: %s',
  142. json_encode($response, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
  143. ));
  144. }
  145. return $response;
  146. }
  147. private function persistExternalId(int $userId, string $externalId): void
  148. {
  149. if (!$this->userModel->updateKycExternalId($userId, $externalId)) {
  150. throw new \RuntimeException('Unable to persist KYC external id for user ' . $userId);
  151. }
  152. }
  153. private function ensureToken(): void
  154. {
  155. if ($this->token !== null) {
  156. return;
  157. }
  158. $loginPayload = [
  159. 'login' => $this->login,
  160. 'password' => $this->password,
  161. 'client' => $this->clientId,
  162. ];
  163. $loginResponse = $this->request('POST', '/api/login', $loginPayload, false);
  164. $token = $loginResponse['token']
  165. ?? ($loginResponse['data']['token'] ?? ($loginResponse['access_token'] ?? null))
  166. ?? ($loginResponse['user']['token'] ?? null);
  167. if ($token === null) {
  168. throw new \RuntimeException('TShield login did not return a token.');
  169. }
  170. $this->token = (string) $token;
  171. }
  172. private function request(string $method, string $path, array $payload = [], bool $requiresAuth = true): array
  173. {
  174. if ($requiresAuth) {
  175. $this->ensureToken();
  176. }
  177. $url = $this->baseUrl . $path;
  178. $ch = curl_init($url);
  179. $headers = ['Content-Type: application/json'];
  180. if ($requiresAuth && $this->token !== null) {
  181. $headers[] = 'Authorization: Bearer ' . $this->token;
  182. }
  183. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
  184. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  185. curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
  186. curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  187. if (!empty($payload)) {
  188. $encoded = json_encode($payload);
  189. if ($encoded === false) {
  190. throw new \RuntimeException('Unable to encode payload to JSON.');
  191. }
  192. curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded);
  193. }
  194. $this->logRequest($method, $url, $payload);
  195. $responseBody = curl_exec($ch);
  196. $curlErrNo = curl_errno($ch);
  197. $curlErr = curl_error($ch);
  198. $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
  199. curl_close($ch);
  200. if ($curlErrNo !== 0) {
  201. throw new \RuntimeException(sprintf('cURL error while calling %s: %s (%d)', $url, $curlErr, $curlErrNo));
  202. }
  203. if ($responseBody === false || $responseBody === '') {
  204. throw new \RuntimeException(sprintf('Empty response from TShield endpoint %s', $url));
  205. }
  206. $decoded = json_decode($responseBody, true);
  207. if ($decoded === null) {
  208. throw new \RuntimeException(sprintf('Invalid JSON response from %s: %s', $url, $responseBody));
  209. }
  210. if ($httpCode >= 400) {
  211. $message = $decoded['message'] ?? $decoded['error'] ?? 'TShield request failed';
  212. throw new \RuntimeException(sprintf(
  213. '%s (HTTP %d) payload=%s',
  214. $message,
  215. $httpCode,
  216. json_encode($decoded, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
  217. ));
  218. }
  219. return $decoded;
  220. }
  221. private function logRequest(string $method, string $url, array $payload): void
  222. {
  223. $encoded = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
  224. error_log(sprintf('[TShield] %s %s payload=%s', $method, $url, $encoded));
  225. }
  226. private function buildIndividualLinkPayload(array $analysisPayload, array $linkPayload): array
  227. {
  228. $payload = $linkPayload;
  229. if (!isset($payload['token_send'])) {
  230. $payload['token_send'] = [];
  231. }
  232. $defaultAdditional = [];
  233. if (isset($analysisPayload['birthdate'])) {
  234. $analysisPayload['birthdate'] = $this->normalizeBirthdate($analysisPayload['birthdate']);
  235. }
  236. $fieldValues = [
  237. 'name' => $analysisPayload['name'] ?? null,
  238. 'document' => $analysisPayload['document'] ?? null,
  239. 'birthdate' => $analysisPayload['birthdate'] ?? null,
  240. 'phone' => $analysisPayload['phone'] ?? null,
  241. 'email' => $analysisPayload['email'] ?? null,
  242. ];
  243. foreach ($fieldValues as $key => $value) {
  244. if (is_array($value)) {
  245. $value = $value['number'] ?? reset($value) ?? null;
  246. }
  247. if (!$value) {
  248. continue;
  249. }
  250. $fieldId = $this->individualFieldMap[$key] ?? null;
  251. if (!$fieldId) {
  252. continue;
  253. }
  254. $defaultAdditional[] = [
  255. 'client_validation_additional_field_id' => $fieldId,
  256. 'value' => $value,
  257. 'file_field_type' => null,
  258. 'values' => [$value],
  259. ];
  260. }
  261. if (!isset($payload['additional_fields']) || !is_array($payload['additional_fields'])) {
  262. $payload['additional_fields'] = $defaultAdditional;
  263. } elseif (!empty($defaultAdditional)) {
  264. $payload['additional_fields'] = array_merge($defaultAdditional, $payload['additional_fields']);
  265. }
  266. if (empty($payload['company_id']) && isset($_ENV['TSHIELD_COMPANY_ID'])) {
  267. $payload['company_id'] = $_ENV['TSHIELD_COMPANY_ID'];
  268. }
  269. return $payload;
  270. }
  271. private function normalizeBirthdate($value): ?string
  272. {
  273. if ($value === null || $value === '') {
  274. return null;
  275. }
  276. if (is_numeric($value)) {
  277. $numeric = (string)$value;
  278. $timestamp = (int)$value;
  279. if ($timestamp > 0) {
  280. if (strlen($numeric) === 8) {
  281. $formatted = \DateTimeImmutable::createFromFormat('Ymd', $numeric);
  282. if ($formatted) {
  283. return $formatted->format('Y-m-d');
  284. }
  285. }
  286. if (strlen($numeric) >= 13) {
  287. $timestamp = (int) floor($timestamp / 1000);
  288. }
  289. if ($timestamp >= 1000000000) {
  290. $dt = (new \DateTimeImmutable())->setTimestamp($timestamp);
  291. return $dt->format('Y-m-d');
  292. }
  293. }
  294. }
  295. if (is_string($value)) {
  296. $value = trim($value);
  297. if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
  298. return $value;
  299. }
  300. if (preg_match('/^\d{8}$/', $value)) {
  301. $formatted = \DateTimeImmutable::createFromFormat('Ymd', $value);
  302. if ($formatted) {
  303. return $formatted->format('Y-m-d');
  304. }
  305. }
  306. foreach (['d/m/Y', 'd-m-Y', 'Y/m/d', 'Y.m.d'] as $fmt) {
  307. $formatted = \DateTimeImmutable::createFromFormat($fmt, $value);
  308. if ($formatted) {
  309. return $formatted->format('Y-m-d');
  310. }
  311. }
  312. }
  313. return null;
  314. }
  315. private function injectDefaults(array $payload, string $validationId): array
  316. {
  317. if (empty($payload['client_validation_id'])) {
  318. $payload['client_validation_id'] = $validationId;
  319. }
  320. return $payload;
  321. }
  322. }