<?php
namespace App\Service;
use App\Entity\Claim;
use App\Entity\Customer;
use App\Entity\Firm;
use App\Entity\Guarantee;
use App\Entity\Policy;
use App\Entity\Vehicle;
use App\Entity\VehicleBrand;
use App\Entity\VehicleEnergy;
use App\Entity\VehicleEngine;
use App\Entity\VehicleModel;
use App\Entity\VehicleType;
use App\Service\Modulr\ClaimSyncService;
use Doctrine\ORM\EntityManagerInterface;
class ModulrSyncService
{
/** @var ModulrApiService */
private $modulr;
/** @var EntityManagerInterface */
private $em;
private $customerRepository;
private $firmRepository;
private $policyRepository;
private $vehicleRepository;
private $vehicleTypeRepository;
private $vehicleBrandRepository;
private $vehicleModelRepository;
public function __construct(
ModulrApiService $modulr,
private readonly ClaimSyncService $claimSync,
EntityManagerInterface $em
) {
$this->modulr = $modulr;
$this->em = $em;
$this->customerRepository = $em->getRepository(Customer::class);
$this->firmRepository = $em->getRepository(Firm::class);
$this->policyRepository = $em->getRepository(Policy::class);
$this->vehicleRepository = $em->getRepository(Vehicle::class);
$this->vehicleTypeRepository = $em->getRepository(VehicleType::class);
$this->vehicleBrandRepository = $em->getRepository(VehicleBrand::class);
$this->vehicleModelRepository = $em->getRepository(VehicleModel::class);
}
/**
* Crée ou met à jour une Firm locale à partir d'une firm Modulr.
*
* @param int $firmId
*
* @return Firm
*/
public function syncFirmFromModulr($firmId)
{
$response = $this->modulr->getFirm((int) $firmId);
if (
!$response
|| !isset($response['status'])
|| $response['status'] !== 'success'
|| !isset($response['data'])
|| !is_array($response['data'])
) {
throw new \RuntimeException('Impossible de récupérer la firm Modulr #' . $firmId);
}
$data = $response['data'];
// On cherche par modulrId (string)
$firm = $this->firmRepository->findOneBy(array(
'modulrId' => (string) $data['firm_id'],
));
if (!$firm) {
$firm = new Firm();
$firm->setModulrId((string) $data['firm_id']);
}
// Mapping minimal : name = firm_name
if (isset($data['firm_name'])) {
$firm->setName($data['firm_name']);
}
// Les autres champs (siret, naf, legalNotice, VATNumber...) ne sont pas dans l’API /firms
// -> on les laisse tels quels. Tu pourras les hydrater par un autre flux si besoin.
$this->em->persist($firm);
$this->em->flush();
return $firm;
}
/**
* Crée ou met à jour un Customer local à partir d'un client Modulr.
*
* @param int $clientId
*
* @return Customer
*/
public function syncCustomerFromModulr($clientId)
{
$response = $this->modulr->getClient((int) $clientId);
if (!$response || $response['status'] !== 'success' || !is_array($response['data'])) {
throw new \RuntimeException('Impossible de récupérer le client Modulr #' . $clientId);
}
$c = $response['data'];
$customer = $this->customerRepository->findOneBy(array(
'modulrId' => (string) $c['client_id'],
));
if (!$customer) {
$customer = new Customer();
$customer->setModulrId((string) $c['client_id']);
}
$name = !empty($c['company']) ? $c['company'] :
(!empty($c['business_name']) ? $c['business_name'] : 'Client ' . $c['client_id']);
$customer->setName($name);
$number = !empty($c['reference']) ? $c['reference'] : (string) $c['client_id'];
$customer->setNumber($number);
$customer->setAccountantNumber($number);
$customer->setState(!empty($c['client_status']) ? $c['client_status'] : 'imported');
// autres champs optionnels (legalForm, revenues, siret, ...)
$this->em->persist($customer);
$this->em->flush();
return $customer;
}
/**
* Crée ou met à jour une Policy locale à partir d'une police Modulr.
*
* @param int $policyId
*
* @return Policy
*/
public function syncPolicyFromModulr($policyId)
{
$response = $this->modulr->getPolicy((int) $policyId);
if (!$response || $response['status'] !== 'success' || !is_array($response['data'])) {
throw new \RuntimeException('Impossible de récupérer la police Modulr #' . $policyId);
}
$data = $response['data'];
$policy = $this->policyRepository->findOneBy(array(
'modulrId' => (string) $data['policy_id'],
));
if (!$policy) {
$policy = new Policy();
$policy->setModulrId((string) $data['policy_id']);
}
// Firm et Customer via les méthodes ci-dessus
$firm = $this->syncFirmFromModulr($data['firm_id']);
$customer = $this->syncCustomerFromModulr($data['client_id']);
$policy->setFirm($firm);
$policy->setCustomer($customer);
// number : ref ou policy_id
$number = !empty($data['ref']) ? $data['ref'] : (string) $data['policy_id'];
$policy->setNumber($number);
$policy->setProduct(isset($data['product_type_id']) ? (string) $data['product_type_id'] : '');
$policy->setCompany($this->modulr->getCompany($data['company_id'])['data']['name']);
if (!empty($data['first_effect_date'])) {
$policy->setEffectiveDate(new \DateTime($data['first_effect_date']));
} else {
$policy->setEffectiveDate(new \DateTime());
}
if (!empty($data['creation_date'])) {
$policy->setCreatedAt(new \DateTime($data['creation_date']));
}
if (!empty($data['expiration_date'])) {
$policy->setDeadline(new \DateTime($data['expiration_date']));
} else {
$policy->setDeadline(clone $policy->getEffectiveDate());
}
$policy->setNoticePeriod(0);
$policy->setLastSync(new \DateTime('now'));
$this->em->persist($policy);
$this->em->flush();
return $policy;
}
/**
* Synchronise un véhicule Modulr vers l'entité Vehicle locale.
*
* @param int $modulrId ID du véhicule côté Modulr (vehicles/{id})
* @param ?Policy $policy Police à rattacher au véhicule (obligatoire dans ton entity)
*
* @return Vehicle|null L'entité synchronisée, ou null en cas d'erreur
*/
public function syncVehicle(int $modulrId, Policy $policy = null): ?Vehicle
{
$response = $this->modulr->getVehicle($modulrId);
if (!isset($response['status']) || $response['status'] !== 'success' || empty($response['data'])) {
// ici tu peux logger proprement
// $this->logger?->error('Erreur lors de la récupération du véhicule Modulr', ['id' => $modulrId, 'response' => $response]);
return null;
}
$data = $response['data'];
// On cherche un véhicule existant par modulrId
$vehicle = $this->vehicleRepository->findOneBy([
'modulrId' => $data['vehicle_id'] ?? $modulrId,
]);
$isNew = false;
if (!$vehicle) {
$vehicle = new Vehicle();
$isNew = true;
// Clé Modulr
$vehicle->setModulrId((string) ($data['vehicle_id'] ?? $modulrId));
// Policy obligatoire (nullable=false)
$vehicle->setPolicy($policy);
// Date d’effet par défaut (à adapter si tu as un champ côté API)
$vehicle->setEffectiveDate(new \DateTimeImmutable('now'));
}
if (!empty($data['vehicle_type_id'])) {
$type = $this->syncVehicleType((int) $data['vehicle_type_id']);
$vehicle->setType($type);
}
// Matriculation
if (!empty($data['registration'])) {
$vehicle->setMatriculation($data['registration']);
}
//Marque
if (!empty($data['brand'])) {
$brand = $this->vehicleBrandRepository->findOneBy(['name' => strtoupper($data['brand'])]);
if (!$brand) {
$brand = new VehicleBrand();
}
$brand->setName(strtoupper($data['brand']));
$brand->setDeleted(false);
$this->em->persist($brand);
$vehicle->setBrand($brand);
}
//Modèle
if (!empty($data['model'])) {
$model = $this->vehicleModelRepository->findOneBy(['name' => strtoupper($data['model'])]);
if (!$model) {
$model = new VehicleModel();
}
$model->setName(strtoupper($data['model']));
$model->setDeleted(false);
if (!empty($data['brand']))
$model->setBrand($brand);
$this->em->persist($model);
$vehicle->setModel($model);
}
//Energie
if (!empty($data['power'])) {
$power = $this->em->getRepository(VehicleEnergy::class)->findOneBy(['name' => $data['power']]);
if (!$power) {
$power = new VehicleEnergy();
}
$power->setName(strtoupper($data['power']));
$power->setDeleted(false);
$this->em->persist($power);
$vehicle->setEnergy($power);
}
//Engine
if (!empty($data['fuel'])) {
$fuel = $this->em->getRepository(VehicleEngine::class)->findOneBy(['name' => $data['fuel']]);
if (!$fuel) {
$fuel = new VehicleEngine();
}
$fuel->setName(strtoupper($data['fuel']));
$fuel->setDeleted(false);
$this->em->persist($fuel);
$vehicle->setEngine($fuel);
}
// Label simple : "Brand Model"
$labelParts = [];
if (!empty($data['brand'])) {
$labelParts[] = $data['brand'];
}
if (!empty($data['model'])) {
$labelParts[] = $data['model'];
}
$vehicle->setLabel($labelParts ? implode(' ', $labelParts) : null);
// Valeur déclarée
if (array_key_exists('value', $data)) {
$vehicle->setDeclaredValue($data['value']);
}
// Société de leasing / propriétaire
if (!empty($data['owner_name'])) {
$vehicle->setLeasingCompany($data['owner_name']);
}
// Date de mise en circulation (registration_date)
if (!empty($data['registration_date'])) {
$vehicle->setFirstRelease($this->createDateFromString($data['registration_date']));
}
// Date de mise en service (commissioning_date)
if (!empty($data['commissioning_date'])) {
$vehicle->setReleaseDate($this->createDateFromString($data['commissioning_date']));
}
// Exemple : mapping du type de véhicule si tu as un champ modulrId là-dessus
// if (!empty($data['vehicle_type_id'])) {
// $type = $this->vehicleTypeRepository->findOneBy(['modulrId' => $data['vehicle_type_id']]);
// if ($type) {
// $vehicle->setType($type);
// }
// }
// Exemple : mapping de l’énergie sur un label (fuel)
// if (!empty($data['fuel'])) {
// $energy = $this->vehicleEnergyRepository->findOneBy(['label' => $data['fuel']]);
// if ($energy) {
// $vehicle->setEnergy($energy);
// }
// }
// Dernière sync
$vehicle->setLastSync(new \DateTime('now'));
$this->em->persist($vehicle);
$this->em->flush();
return $vehicle;
}
/**
* Transforme une chaîne "Y-m-d H:i:s" ou "Y-m-d" en \DateTimeImmutable (ou null si vide/invalide).
*/
private function createDateFromString(?string $value): ?\DateTimeImmutable
{
if (!$value) {
return null;
}
try {
// l'API renvoie visiblement "YYYY-MM-DD HH:MM:SS"
return new \DateTimeImmutable($value);
} catch (\Exception $e) {
return null;
}
}
/**
* Synchronise tous les véhicules "fleet_vehicles" d'une police Modulr donnée.
*
* @param int $policyModulrId ID de la police côté Modulr (champ policy_id dans beneficiaries)
* @param Policy $policy Entité Policy locale à rattacher aux véhicules
*
* @return Vehicle[] Tableau des véhicules synchronisés
*/
public function syncFleet(int $policyModulrId, Policy $policy, int $perPage = 100): array
{
$page = 1;
$vehicles = [];
$totalPages = 1;
do {
$response = $this->modulr->searchBeneficiaries(
$page,
$perPage,
[
'policy_id' => ['equal' => $policyModulrId],
'type' => ['equal' => 'fleet_vehicles'],
]
);
if (!isset($response['status']) || $response['status'] !== 'success' || empty($response['data'])) {
// logger si besoin
// $this->logger?->error('Erreur searchBeneficiaries', ['policyModulrId' => $policyModulrId, 'response' => $response]);
break;
}
$data = $response['data'];
$beneficiaries = $data['beneficiaries'] ?? [];
$currentPage = $data['current_page'] ?? $page;
$totalPages = $data['number_of_pages'] ?? 1;
foreach ($beneficiaries as $beneficiary) {
// On s'assure qu'on est bien sur une flotte véhicule + qu'on a un vehicle_id
if (
($beneficiary['type'] ?? null) !== 'fleet_vehicles' ||
empty($beneficiary['vehicle_id'])
) {
continue;
}
$vehicleId = (int) $beneficiary['vehicle_id'];
// Synchronisation du véhicule individuel
$vehicle = $this->syncVehicle($vehicleId, $policy);
if ($vehicle instanceof Vehicle) {
$vehicles[] = $vehicle;
// Si tu veux enrichir avec la plaque qui vient des beneficiaries
if (!empty($beneficiary['vehicle_registration'])) {
$vehicle->setMatriculation($beneficiary['vehicle_registration']);
}
}
}
$page++;
} while ($currentPage < $totalPages);
return $vehicles;
}
/**
* Synchronise un type de véhicule Modulr vers VehicleType local.
*
* @param int $modulrTypeId ID du type côté Modulr (vehicle_type_id)
*
* @return VehicleType
*/
public function syncVehicleType(int $modulrTypeId): VehicleType
{
$response = $this->modulr->getVehicleType($modulrTypeId);
if (!$response || $response['status'] !== 'success' || empty($response['data'])) {
throw new \RuntimeException(sprintf('Impossible de récupérer le type de véhicule Modulr #%d', $modulrTypeId));
}
$data = $response['data'];
// On cherche d'abord par modulrId
$vehicleType = $this->vehicleTypeRepository->findOneBy([
'modulrId' => (int) $data['vehicle_type_id'],
]);
if (!$vehicleType) {
$vehicleType = $this->vehicleTypeRepository->findOneBy([
'name' => $data['name'],
]);
}
if (!$vehicleType) {
$vehicleType = new VehicleType();
$vehicleType->setModulrId((int) $data['vehicle_type_id']);
}
// Mise à jour des champs de base
$vehicleType->setName($data['name'] ?? ''); // "vp", "engin", etc.
$vehicleType->setDeleted(false); // la synchro le réactive au besoin
$this->em->persist($vehicleType);
$this->em->flush();
return $vehicleType;
}
/**
* Synchronise tous les sinistres "claims" d'une police Modulr donnée.
*
* @param int $policyModulrId ID de la police côté Modulr (champ policy_id dans beneficiaries)
* @param Policy $policy Entité Policy locale à rattacher aux véhicules
*
* @return Claim[] Tableau des sinistres synchronisés
*/
public function syncClaims(int $policyModulrId, Policy $policy, int $perPage = 100): array
{
$page = 1;
$claims = [];
$totalPages = 1;
do {
$response = $this->modulr->searchClaims(
$page,
$perPage,
[
'policy_id' => ['equal' => $policyModulrId]
]
);
if (!isset($response['status']) || $response['status'] !== 'success' || empty($response['data'])) {
// logger si besoin
// $this->logger?->error('Erreur searchBeneficiaries', ['policyModulrId' => $policyModulrId, 'response' => $response]);
break;
}
$data = $response['data'];
$claims = $data['claims'] ?? [];
$currentPage = $data['current_page'] ?? $page;
$totalPages = $data['number_of_pages'] ?? 1;
foreach ($claims as $claim) {
$claimId = (int) $claim['claim_id'];
// Synchronisation du véhicule individuel
$claim = $this->claimSync->syncFromModulr($claimId);
}
$page++;
} while ($currentPage < $totalPages);
return $claims;
}
public function syncGuarantee($data): ?Guarantee
{
// Appel API
//$response = $this->modulr->getGuarantee($modulrId);
/*if (!\is_array($response) || ($response['status'] ?? null) !== 'success') {
// ici tu peux logger l'erreur si besoin
return null;
}*/
/*$data = $response['data'] ?? null;
if (!$data) {
return null;
}*/
// On essaie de retrouver un sinistre existant
$guarantee = $this->em->getRepository(Guarantee::class)->findOneBy([
'modulrId' => $data['guarantee_id']
]);
if (!$guarantee) {
$guarantee = new Guarantee();
$guarantee->setModulrId($data['guarantee_id']);
$guarantee->setDeleted(false);
$this->em->persist($guarantee);
}
$guarantee->setName($data['name']);
$guarantee->setLastSync(new \DateTime('now'));
$this->em->flush();
return $guarantee;
}
/**
* Synchronise toutes les "garanties".
*
*
* @return Guarantee[] Tableau des garanties synchronisés
*/
public function syncGuarantees(int $perPage = 100): array
{
$page = 1;
$guarantees = [];
$totalPages = 1;
do {
$response = $this->modulr->searchGuarantees(
$page,
$perPage
);
if (!isset($response['status']) || $response['status'] !== 'success' || empty($response['data'])) {
// logger si besoin
// $this->logger?->error('Erreur searchBeneficiaries', ['policyModulrId' => $policyModulrId, 'response' => $response]);
break;
}
$data = $response['data'];
$guarantees = $data['guarantees'] ?? [];
$currentPage = $data['current_page'] ?? $page;
$totalPages = $data['number_of_pages'] ?? 1;
foreach ($guarantees as $guarantee) {
$guaranteeId = (int) $guarantee['guarantee_id'];
// Synchronisation du véhicule individuel
$guarantee = $this->syncGuarantee($guarantee);
}
$page++;
} while ($currentPage < $totalPages);
return $guarantees;
}
}