src/Service/ModulrApiService.php line 18

Open in your IDE?
  1. <?php
  2. namespace App\Service;
  3. use Psr\Log\LoggerInterface;
  4. use Symfony\Component\HttpFoundation\File\UploadedFile;
  5. class ModulrApiService
  6. {
  7. private $baseUrl;
  8. private $logger;
  9. private $database;
  10. private $clientId;
  11. private $clientSecret;
  12. private $token; // temporaire
  13. public function __construct($baseUrl, $database, $clientId, $clientSecret, LoggerInterface $logger = null)
  14. {
  15. $this->baseUrl = $baseUrl;//rtrim($baseUrl, '/');
  16. $this->logger = $logger;
  17. $this->database = $database;
  18. $this->clientId = $clientId;
  19. $this->clientSecret = $clientSecret;
  20. }
  21. public function authenticate()
  22. {
  23. $url = $this->baseUrl . 'tokens/users';
  24. $headers = array(
  25. 'Database: ' . $this->database,
  26. 'Accept: application/json',
  27. 'Content-Type: application/x-www-form-urlencoded'
  28. );
  29. $body = http_build_query(array(
  30. 'grant_type' => 'client_credentials',
  31. 'client_id' => $this->clientId,
  32. 'client_secret' => $this->clientSecret
  33. ));
  34. $ch = curl_init($url);
  35. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  36. curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  37. curl_setopt($ch, CURLOPT_POST, true);
  38. curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
  39. $response = curl_exec($ch);
  40. $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  41. if ($status !== 200) {
  42. if ($this->logger) {
  43. $this->logger->error('Modulr auth error: '.$response);
  44. }
  45. return null;
  46. }
  47. $data = json_decode($response, true);
  48. // Selon la réponse exacte...
  49. $this->token = $data['data']['access_token'];
  50. return $this->token;
  51. }
  52. /**
  53. * Appel générique GET sur l'API Modulr.
  54. *
  55. * @param string $path
  56. * @param array $query
  57. *
  58. * @return array|null
  59. */
  60. public function get(string $path, array $query = []): ?array
  61. {
  62. // Si pas de token en mémoire, on s’authentifie
  63. if ($this->token === null) {
  64. $this->authenticate();
  65. }
  66. if ($this->token === null) {
  67. // auth ratée
  68. return null;
  69. }
  70. $url = $this->baseUrl . $path;
  71. if (!empty($query)) {
  72. $url .= '?' . http_build_query($query);
  73. }
  74. $headers = [
  75. 'Authorization: Bearer ' . $this->token,
  76. 'Database: ' . $this->database,
  77. 'Accept: application/json',
  78. ];
  79. $ch = curl_init($url);
  80. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  81. curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  82. $response = curl_exec($ch);
  83. $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  84. if ($response === false) {
  85. if ($this->logger) {
  86. $this->logger->error('Modulr API cURL error: ' . curl_error($ch));
  87. }
  88. curl_close($ch);
  89. return null;
  90. }
  91. curl_close($ch);
  92. if ($status < 200 || $status >= 300) {
  93. if ($this->logger) {
  94. $this->logger->error('Modulr API HTTP ' . $status . ' response: ' . $response);
  95. }
  96. return null;
  97. }
  98. return json_decode($response, true);
  99. }
  100. public function post(string $path, array $body = []): ?array
  101. {
  102. // Auth si besoin
  103. if ($this->token === null) {
  104. $this->authenticate();
  105. }
  106. if ($this->token === null) {
  107. return null;
  108. }
  109. $url = $this->baseUrl . $path;
  110. $jsonBody = json_encode($body);
  111. $headers = [
  112. 'Authorization: Bearer ' . $this->token,
  113. 'Database: ' . $this->database,
  114. 'Accept: application/json',
  115. 'Content-Type: application/json',
  116. ];
  117. $ch = curl_init($url);
  118. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  119. curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  120. curl_setopt($ch, CURLOPT_POST, true);
  121. curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonBody);
  122. $response = curl_exec($ch);
  123. $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  124. $curlErr = curl_error($ch);
  125. curl_close($ch);
  126. if ($response === false) {
  127. $this->logger->error('Modulr API POST cURL error: '.$curlErr);
  128. return null;
  129. }
  130. if ($status < 200 || $status >= 300) {
  131. $this->logger->error('Modulr API POST HTTP '.$status.' response: '.$response);
  132. return null;
  133. }
  134. return json_decode($response, true);
  135. }
  136. /**
  137. * POST multipart/form-data avec un fichier (champ "file") + champs additionnels optionnels.
  138. *
  139. * @param string $path
  140. * @param string|UploadedFile $file Chemin absolu/relatif OU UploadedFile Symfony
  141. * @param array $fields Autres champs multipart si un jour nécessaire (optionnel)
  142. * @param string|null $displayName Nom de fichier à envoyer (optionnel)
  143. *
  144. * @return array|null
  145. */
  146. public function postFile(string $path, $file, array $fields = [], ?string $displayName = null): ?array
  147. {
  148. // Auth si besoin
  149. if ($this->token === null) {
  150. $this->authenticate();
  151. }
  152. if ($this->token === null) {
  153. return null;
  154. }
  155. // Résoudre le fichier
  156. $filePath = null;
  157. $fileName = $displayName;
  158. $mimeType = 'application/octet-stream';
  159. if ($file instanceof \Symfony\Component\HttpFoundation\File\UploadedFile) {
  160. $filePath = $file->getRealPath();
  161. $fileName = $fileName ?: $file->getClientOriginalName();
  162. $mimeType = $file->getMimeType() ?: $mimeType;
  163. } elseif (is_string($file)) {
  164. $filePath = $file;
  165. $fileName = $fileName ?: basename($filePath);
  166. $detected = @mime_content_type($filePath);
  167. if ($detected) {
  168. $mimeType = $detected;
  169. }
  170. } else {
  171. $this->logger?->error('Modulr postFile: type de fichier invalide.');
  172. return null;
  173. }
  174. if (!$filePath || !is_file($filePath) || !is_readable($filePath)) {
  175. $this->logger?->error('Modulr postFile: fichier introuvable ou illisible: ' . (string) $filePath);
  176. return null;
  177. }
  178. $url = $this->baseUrl . $path;
  179. $headers = [
  180. 'Authorization: Bearer ' . $this->token,
  181. 'Database: ' . $this->database,
  182. 'Accept: application/json',
  183. // PAS de Content-Type ici (curl gère le boundary)
  184. ];
  185. $postFields = $fields;
  186. $postFields['file'] = new \CURLFile($filePath, $mimeType, $fileName);
  187. $ch = curl_init($url);
  188. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  189. curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  190. curl_setopt($ch, CURLOPT_POST, true);
  191. curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
  192. $response = curl_exec($ch);
  193. $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  194. $curlErr = curl_error($ch);
  195. curl_close($ch);
  196. if ($response === false) {
  197. $this->logger?->error('Modulr postFile cURL error: ' . $curlErr);
  198. return null;
  199. }
  200. // (Optionnel mais utile) Retry 1 fois si token expiré
  201. if ($status === 401) {
  202. $this->token = null;
  203. $this->authenticate();
  204. if ($this->token) {
  205. return $this->postFile($path, $file, $fields, $displayName);
  206. }
  207. return null;
  208. }
  209. if ($status < 200 || $status >= 300) {
  210. $this->logger?->error('Modulr postFile HTTP ' . $status . ' response: ' . $response);
  211. return null;
  212. }
  213. return json_decode($response, true);
  214. }
  215. public function delete(string $path, array $body = []): ?array
  216. {
  217. // Auth si besoin
  218. if ($this->token === null) {
  219. $this->authenticate();
  220. }
  221. if ($this->token === null) {
  222. return null;
  223. }
  224. $url = $this->baseUrl . $path;
  225. $headers = [
  226. 'Authorization: Bearer ' . $this->token,
  227. 'Database: ' . $this->database,
  228. 'Accept: application/json',
  229. 'Content-Type: application/json',
  230. ];
  231. $ch = curl_init($url);
  232. curl_setopt_array($ch, [
  233. CURLOPT_RETURNTRANSFER => true,
  234. CURLOPT_HTTPHEADER => $headers,
  235. CURLOPT_CUSTOMREQUEST => 'DELETE',
  236. ]);
  237. // ⚠️ Body optionnel (souvent inutile en DELETE, mais autorisé)
  238. if (!empty($body)) {
  239. curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
  240. }
  241. $response = curl_exec($ch);
  242. $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  243. $curlErr = curl_error($ch);
  244. curl_close($ch);
  245. if ($response === false) {
  246. $this->logger->error('Modulr API DELETE cURL error: ' . $curlErr);
  247. return null;
  248. }
  249. if ($status < 200 || $status >= 300) {
  250. $this->logger->error(
  251. sprintf(
  252. 'Modulr API DELETE HTTP %d on %s – response: %s',
  253. $status,
  254. $path,
  255. $response
  256. )
  257. );
  258. return null;
  259. }
  260. // Certaines APIs renvoient une réponse vide sur DELETE
  261. return $response !== '' ? json_decode($response, true) : [];
  262. }
  263. /**
  264. * Upload un document et l'associe à une entité.
  265. * Endpoint: POST /api/1.0/{table}/{id}/documents
  266. * (locale déjà incluse dans baseUrl)
  267. *
  268. * @param string $table
  269. * @param int|string $id
  270. * @param string|UploadedFile $file
  271. * @param string|null $displayName
  272. *
  273. * @return array|null
  274. */
  275. public function addDocument(string $table, $id, $file, ?string $displayName = null): ?array
  276. {
  277. $path = sprintf('%s/%s/documents', trim($table, '/'), $id);
  278. // Si jamais ton baseUrl ne finit pas par /, décommente rtrim dans le constructeur,
  279. // ou assure-toi qu'il y a bien un slash entre baseUrl et path.
  280. return $this->postFile($path, $file, [], $displayName);
  281. }
  282. public function searchDocument(
  283. int $page = 1,
  284. int $perPage = 100,
  285. array $filters = []
  286. ): ?array {
  287. $body = [
  288. 'page' => $page,
  289. 'number_per_page' => $perPage,
  290. 'filters' => $filters,
  291. ];
  292. return $this->post('documents/search', $body);
  293. }
  294. public function findDocumentIdByFilename(string $table, string|int $entityId, string $filename): ?int
  295. {
  296. $filters = [
  297. 'filename' => ['equal' => $filename],
  298. 'creation_date' => ['order_by' => 'DESC'],
  299. ];
  300. $res = $this->searchDocument(1, 100, $filters);
  301. if (!is_array($res)) {
  302. return null;
  303. }
  304. // ✅ Selon l'API, ça peut être top-level ou dans data
  305. $documents = $res['documents']
  306. ?? ($res['data']['documents'] ?? null);
  307. if (!is_array($documents) || count($documents) === 0) {
  308. return null;
  309. }
  310. foreach ($documents as $doc) {
  311. if (!is_array($doc)) {
  312. continue;
  313. }
  314. // ✅ deleted peut être false / 0 / "0" / null
  315. $isDeleted = filter_var($doc['deleted'] ?? false, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
  316. // filter_var renvoie:
  317. // - true / false si reconnaissable
  318. // - null si valeur "bizarre" -> on fallback dessous
  319. if ($isDeleted === null) {
  320. $raw = $doc['deleted'] ?? false;
  321. $isDeleted = in_array($raw, [true, 1, '1', 'true', 'TRUE', 'yes', 'YES'], true);
  322. }
  323. // On prend le doc actif
  324. if ($isDeleted === false) {
  325. $id = $doc['document_id'] ?? null;
  326. return $id !== null ? (int) $id : null;
  327. }
  328. }
  329. return null;
  330. }
  331. public function deleteDocumentById(string $documentId): bool
  332. {
  333. // Endpoint le plus classique
  334. return $this->delete(sprintf('documents/%s', $documentId)) !== null;
  335. }
  336. /**
  337. * Exemple de méthode dédiée : récupérer la liste des véhicules.
  338. */
  339. public function fetchVehicles(array $filters = array())
  340. {
  341. return $this->get('vehicles/search', $filters);
  342. }
  343. /**
  344. * Exemple : récupérer le détail d’un véhicule.
  345. */
  346. public function fetchVehicleById($modulrId)
  347. {
  348. return $this->get('vehicles/'.$modulrId);
  349. }
  350. public function searchVehicles(
  351. int $page = 1,
  352. int $perPage = 100,
  353. array $filters = []
  354. ): ?array {
  355. $body = [
  356. 'page' => $page,
  357. 'number_per_page' => $perPage,
  358. 'filters' => $filters,
  359. ];
  360. return $this->post('vehicles/search', $body);
  361. }
  362. public function searchPolicies(int $page = 1, int $perPage = 100, array $filters = [])
  363. {
  364. $body = [
  365. 'page' => $page,
  366. 'number_per_page' => $perPage,
  367. ];
  368. if (!empty($filters)) {
  369. $body['filters'] = $filters;
  370. }
  371. return $this->post('policies/search', $body);
  372. }
  373. /**
  374. * Récupère le détail d'une police Modulr.
  375. *
  376. * @param int $modulrId
  377. *
  378. * @return array
  379. */
  380. public function getPolicy(int $modulrId): array
  381. {
  382. // à adapter selon ta méthode interne d’appel GET
  383. return $this->get('policies/' . $modulrId);
  384. }
  385. /**
  386. * Récupère le détail d'un client Modulr.
  387. *
  388. * @param int $modulrId
  389. *
  390. * @return array
  391. */
  392. public function getClient(int $modulrId): array
  393. {
  394. // à adapter selon ta méthode interne d’appel GET
  395. return $this->get('clients/' . $modulrId);
  396. }
  397. /**
  398. * Récupère le détail d'une firm Modulr.
  399. *
  400. * @param int $modulrId
  401. *
  402. * @return array
  403. */
  404. public function getFirm(int $modulrId): array
  405. {
  406. // à adapter selon ta méthode interne d’appel GET
  407. return $this->get('firms/' . $modulrId);
  408. }
  409. public function getCompany(int $modulrId): array
  410. {
  411. // à adapter selon ta méthode interne d’appel GET
  412. return $this->get('companies/' . $modulrId);
  413. }
  414. public function getVehicle(int $modulrId): array
  415. {
  416. // à adapter selon ta méthode interne d’appel GET
  417. return $this->get('vehicles/' . $modulrId);
  418. }
  419. public function searchBeneficiaries(int $page = 1, int $perPage = 100, array $filters = [])
  420. {
  421. $body = [
  422. 'page' => $page,
  423. 'number_per_page' => $perPage,
  424. ];
  425. if (!empty($filters)) {
  426. $body['filters'] = $filters;
  427. }
  428. return $this->post('beneficiaries/search', $body);
  429. }
  430. public function getVehicleType(int $modulrId): array
  431. {
  432. // à adapter selon ta méthode interne d’appel GET
  433. return $this->get('vehicles_types/' . $modulrId);
  434. }
  435. public function getClaim(int $modulrId): array
  436. {
  437. return $this->get('claims/'.$modulrId);
  438. }
  439. public function searchClaims(int $page = 1, int $perPage = 100, array $filters = [])
  440. {
  441. $body = [
  442. 'page' => $page,
  443. 'number_per_page' => $perPage,
  444. ];
  445. if (!empty($filters)) {
  446. $body['filters'] = $filters;
  447. }
  448. return $this->post('claims/search', $body);
  449. }
  450. public function getGuarantee(int $modulrId): array
  451. {
  452. return $this->get('guarantees/'.$modulrId);
  453. }
  454. public function searchGuarantees(int $page = 1, int $perPage = 100, array $filters = [])
  455. {
  456. $body = [
  457. 'page' => $page,
  458. 'number_per_page' => $perPage,
  459. ];
  460. if (!empty($filters)) {
  461. $body['filters'] = $filters;
  462. }
  463. return $this->post('guarantees/search', $body);
  464. }
  465. public function getAccessToken($ref)
  466. {
  467. $body = [
  468. 'reference' => $ref,
  469. ];
  470. return $this->post('extranets/clients/get_access', $body);
  471. }
  472. public function buildClientExtranetSsoUrl(string $token, ?string $section = null): string
  473. {
  474. $base = 'https://courtage.modulr.fr/';
  475. $query = [
  476. 'company' => $this->database,
  477. 'token' => $token,
  478. ];
  479. if ($section) {
  480. $query['section'] = $section;
  481. }
  482. return $base . 'fr/scripts/Extranets/Clients/ExtranetClientAutoconnect.php?' . http_build_query($query);
  483. }
  484. public function getCustomValue($modulrId): ?string
  485. {
  486. $result = $this->get('custom_lists_values/'.$modulrId);
  487. if ($result) {
  488. return $result['data']['value'];
  489. }
  490. return null;
  491. }
  492. public function getUser($modulrId): ?array
  493. {
  494. $result = $this->get('users/'.$modulrId);
  495. if ($result) {
  496. return $result['data'];
  497. }
  498. return null;
  499. }
  500. }