<?php
namespace App\Service;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class ModulrApiService
{
private $baseUrl;
private $logger;
private $database;
private $clientId;
private $clientSecret;
private $token; // temporaire
public function __construct($baseUrl, $database, $clientId, $clientSecret, LoggerInterface $logger = null)
{
$this->baseUrl = $baseUrl;//rtrim($baseUrl, '/');
$this->logger = $logger;
$this->database = $database;
$this->clientId = $clientId;
$this->clientSecret = $clientSecret;
}
public function authenticate()
{
$url = $this->baseUrl . 'tokens/users';
$headers = array(
'Database: ' . $this->database,
'Accept: application/json',
'Content-Type: application/x-www-form-urlencoded'
);
$body = http_build_query(array(
'grant_type' => 'client_credentials',
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret
));
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status !== 200) {
if ($this->logger) {
$this->logger->error('Modulr auth error: '.$response);
}
return null;
}
$data = json_decode($response, true);
// Selon la réponse exacte...
$this->token = $data['data']['access_token'];
return $this->token;
}
/**
* Appel générique GET sur l'API Modulr.
*
* @param string $path
* @param array $query
*
* @return array|null
*/
public function get(string $path, array $query = []): ?array
{
// Si pas de token en mémoire, on s’authentifie
if ($this->token === null) {
$this->authenticate();
}
if ($this->token === null) {
// auth ratée
return null;
}
$url = $this->baseUrl . $path;
if (!empty($query)) {
$url .= '?' . http_build_query($query);
}
$headers = [
'Authorization: Bearer ' . $this->token,
'Database: ' . $this->database,
'Accept: application/json',
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) {
if ($this->logger) {
$this->logger->error('Modulr API cURL error: ' . curl_error($ch));
}
curl_close($ch);
return null;
}
curl_close($ch);
if ($status < 200 || $status >= 300) {
if ($this->logger) {
$this->logger->error('Modulr API HTTP ' . $status . ' response: ' . $response);
}
return null;
}
return json_decode($response, true);
}
public function post(string $path, array $body = []): ?array
{
// Auth si besoin
if ($this->token === null) {
$this->authenticate();
}
if ($this->token === null) {
return null;
}
$url = $this->baseUrl . $path;
$jsonBody = json_encode($body);
$headers = [
'Authorization: Bearer ' . $this->token,
'Database: ' . $this->database,
'Accept: application/json',
'Content-Type: application/json',
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonBody);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);
if ($response === false) {
$this->logger->error('Modulr API POST cURL error: '.$curlErr);
return null;
}
if ($status < 200 || $status >= 300) {
$this->logger->error('Modulr API POST HTTP '.$status.' response: '.$response);
return null;
}
return json_decode($response, true);
}
/**
* POST multipart/form-data avec un fichier (champ "file") + champs additionnels optionnels.
*
* @param string $path
* @param string|UploadedFile $file Chemin absolu/relatif OU UploadedFile Symfony
* @param array $fields Autres champs multipart si un jour nécessaire (optionnel)
* @param string|null $displayName Nom de fichier à envoyer (optionnel)
*
* @return array|null
*/
public function postFile(string $path, $file, array $fields = [], ?string $displayName = null): ?array
{
// Auth si besoin
if ($this->token === null) {
$this->authenticate();
}
if ($this->token === null) {
return null;
}
// Résoudre le fichier
$filePath = null;
$fileName = $displayName;
$mimeType = 'application/octet-stream';
if ($file instanceof \Symfony\Component\HttpFoundation\File\UploadedFile) {
$filePath = $file->getRealPath();
$fileName = $fileName ?: $file->getClientOriginalName();
$mimeType = $file->getMimeType() ?: $mimeType;
} elseif (is_string($file)) {
$filePath = $file;
$fileName = $fileName ?: basename($filePath);
$detected = @mime_content_type($filePath);
if ($detected) {
$mimeType = $detected;
}
} else {
$this->logger?->error('Modulr postFile: type de fichier invalide.');
return null;
}
if (!$filePath || !is_file($filePath) || !is_readable($filePath)) {
$this->logger?->error('Modulr postFile: fichier introuvable ou illisible: ' . (string) $filePath);
return null;
}
$url = $this->baseUrl . $path;
$headers = [
'Authorization: Bearer ' . $this->token,
'Database: ' . $this->database,
'Accept: application/json',
// PAS de Content-Type ici (curl gère le boundary)
];
$postFields = $fields;
$postFields['file'] = new \CURLFile($filePath, $mimeType, $fileName);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);
if ($response === false) {
$this->logger?->error('Modulr postFile cURL error: ' . $curlErr);
return null;
}
// (Optionnel mais utile) Retry 1 fois si token expiré
if ($status === 401) {
$this->token = null;
$this->authenticate();
if ($this->token) {
return $this->postFile($path, $file, $fields, $displayName);
}
return null;
}
if ($status < 200 || $status >= 300) {
$this->logger?->error('Modulr postFile HTTP ' . $status . ' response: ' . $response);
return null;
}
return json_decode($response, true);
}
public function delete(string $path, array $body = []): ?array
{
// Auth si besoin
if ($this->token === null) {
$this->authenticate();
}
if ($this->token === null) {
return null;
}
$url = $this->baseUrl . $path;
$headers = [
'Authorization: Bearer ' . $this->token,
'Database: ' . $this->database,
'Accept: application/json',
'Content-Type: application/json',
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_CUSTOMREQUEST => 'DELETE',
]);
// ⚠️ Body optionnel (souvent inutile en DELETE, mais autorisé)
if (!empty($body)) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);
if ($response === false) {
$this->logger->error('Modulr API DELETE cURL error: ' . $curlErr);
return null;
}
if ($status < 200 || $status >= 300) {
$this->logger->error(
sprintf(
'Modulr API DELETE HTTP %d on %s – response: %s',
$status,
$path,
$response
)
);
return null;
}
// Certaines APIs renvoient une réponse vide sur DELETE
return $response !== '' ? json_decode($response, true) : [];
}
/**
* Upload un document et l'associe à une entité.
* Endpoint: POST /api/1.0/{table}/{id}/documents
* (locale déjà incluse dans baseUrl)
*
* @param string $table
* @param int|string $id
* @param string|UploadedFile $file
* @param string|null $displayName
*
* @return array|null
*/
public function addDocument(string $table, $id, $file, ?string $displayName = null): ?array
{
$path = sprintf('%s/%s/documents', trim($table, '/'), $id);
// Si jamais ton baseUrl ne finit pas par /, décommente rtrim dans le constructeur,
// ou assure-toi qu'il y a bien un slash entre baseUrl et path.
return $this->postFile($path, $file, [], $displayName);
}
public function searchDocument(
int $page = 1,
int $perPage = 100,
array $filters = []
): ?array {
$body = [
'page' => $page,
'number_per_page' => $perPage,
'filters' => $filters,
];
return $this->post('documents/search', $body);
}
public function findDocumentIdByFilename(string $table, string|int $entityId, string $filename): ?int
{
$filters = [
'filename' => ['equal' => $filename],
'creation_date' => ['order_by' => 'DESC'],
];
$res = $this->searchDocument(1, 100, $filters);
if (!is_array($res)) {
return null;
}
// ✅ Selon l'API, ça peut être top-level ou dans data
$documents = $res['documents']
?? ($res['data']['documents'] ?? null);
if (!is_array($documents) || count($documents) === 0) {
return null;
}
foreach ($documents as $doc) {
if (!is_array($doc)) {
continue;
}
// ✅ deleted peut être false / 0 / "0" / null
$isDeleted = filter_var($doc['deleted'] ?? false, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
// filter_var renvoie:
// - true / false si reconnaissable
// - null si valeur "bizarre" -> on fallback dessous
if ($isDeleted === null) {
$raw = $doc['deleted'] ?? false;
$isDeleted = in_array($raw, [true, 1, '1', 'true', 'TRUE', 'yes', 'YES'], true);
}
// On prend le doc actif
if ($isDeleted === false) {
$id = $doc['document_id'] ?? null;
return $id !== null ? (int) $id : null;
}
}
return null;
}
public function deleteDocumentById(string $documentId): bool
{
// Endpoint le plus classique
return $this->delete(sprintf('documents/%s', $documentId)) !== null;
}
/**
* Exemple de méthode dédiée : récupérer la liste des véhicules.
*/
public function fetchVehicles(array $filters = array())
{
return $this->get('vehicles/search', $filters);
}
/**
* Exemple : récupérer le détail d’un véhicule.
*/
public function fetchVehicleById($modulrId)
{
return $this->get('vehicles/'.$modulrId);
}
public function searchVehicles(
int $page = 1,
int $perPage = 100,
array $filters = []
): ?array {
$body = [
'page' => $page,
'number_per_page' => $perPage,
'filters' => $filters,
];
return $this->post('vehicles/search', $body);
}
public function searchPolicies(int $page = 1, int $perPage = 100, array $filters = [])
{
$body = [
'page' => $page,
'number_per_page' => $perPage,
];
if (!empty($filters)) {
$body['filters'] = $filters;
}
return $this->post('policies/search', $body);
}
/**
* Récupère le détail d'une police Modulr.
*
* @param int $modulrId
*
* @return array
*/
public function getPolicy(int $modulrId): array
{
// à adapter selon ta méthode interne d’appel GET
return $this->get('policies/' . $modulrId);
}
/**
* Récupère le détail d'un client Modulr.
*
* @param int $modulrId
*
* @return array
*/
public function getClient(int $modulrId): array
{
// à adapter selon ta méthode interne d’appel GET
return $this->get('clients/' . $modulrId);
}
/**
* Récupère le détail d'une firm Modulr.
*
* @param int $modulrId
*
* @return array
*/
public function getFirm(int $modulrId): array
{
// à adapter selon ta méthode interne d’appel GET
return $this->get('firms/' . $modulrId);
}
public function getCompany(int $modulrId): array
{
// à adapter selon ta méthode interne d’appel GET
return $this->get('companies/' . $modulrId);
}
public function getVehicle(int $modulrId): array
{
// à adapter selon ta méthode interne d’appel GET
return $this->get('vehicles/' . $modulrId);
}
public function searchBeneficiaries(int $page = 1, int $perPage = 100, array $filters = [])
{
$body = [
'page' => $page,
'number_per_page' => $perPage,
];
if (!empty($filters)) {
$body['filters'] = $filters;
}
return $this->post('beneficiaries/search', $body);
}
public function getVehicleType(int $modulrId): array
{
// à adapter selon ta méthode interne d’appel GET
return $this->get('vehicles_types/' . $modulrId);
}
public function getClaim(int $modulrId): array
{
return $this->get('claims/'.$modulrId);
}
public function searchClaims(int $page = 1, int $perPage = 100, array $filters = [])
{
$body = [
'page' => $page,
'number_per_page' => $perPage,
];
if (!empty($filters)) {
$body['filters'] = $filters;
}
return $this->post('claims/search', $body);
}
public function getGuarantee(int $modulrId): array
{
return $this->get('guarantees/'.$modulrId);
}
public function searchGuarantees(int $page = 1, int $perPage = 100, array $filters = [])
{
$body = [
'page' => $page,
'number_per_page' => $perPage,
];
if (!empty($filters)) {
$body['filters'] = $filters;
}
return $this->post('guarantees/search', $body);
}
public function getAccessToken($ref)
{
$body = [
'reference' => $ref,
];
return $this->post('extranets/clients/get_access', $body);
}
public function buildClientExtranetSsoUrl(string $token, ?string $section = null): string
{
$base = 'https://courtage.modulr.fr/';
$query = [
'company' => $this->database,
'token' => $token,
];
if ($section) {
$query['section'] = $section;
}
return $base . 'fr/scripts/Extranets/Clients/ExtranetClientAutoconnect.php?' . http_build_query($query);
}
public function getCustomValue($modulrId): ?string
{
$result = $this->get('custom_lists_values/'.$modulrId);
if ($result) {
return $result['data']['value'];
}
return null;
}
public function getUser($modulrId): ?array
{
$result = $this->get('users/'.$modulrId);
if ($result) {
return $result['data'];
}
return null;
}
}