src/Controller/OnboardingController.php line 158

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Controller;
  4. use App\Entity\CaptaindataJob;
  5. use App\Entity\Client;
  6. use App\Entity\User;
  7. use App\Exception\CaptainDataApiException;
  8. use App\Form\PhoneType;
  9. use App\Repository\ClientRepository;
  10. use App\Repository\UserRepository;
  11. use App\Service\AdminAlertService;
  12. use App\Service\CaptainDataV4ApiService;
  13. use App\Service\EncryptionService;
  14. use App\Service\ReconnectionService;
  15. use App\Service\UserLoggerFactory;
  16. use App\Message\ProcessPendingMessagesJob;
  17. use Doctrine\ORM\EntityManagerInterface;
  18. use Symfony\Component\Messenger\MessageBusInterface;
  19. use Psr\Log\LoggerInterface;
  20. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  21. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  22. use Symfony\Component\HttpFoundation\JsonResponse;
  23. use Symfony\Component\HttpFoundation\Request;
  24. use Symfony\Component\HttpFoundation\Response;
  25. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  26. use Symfony\Component\Routing\Annotation\Route;
  27. use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
  28. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  29. use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
  30. /**
  31.  * Contrôleur pour gérer l'onboarding avec CaptainData v4
  32.  */
  33. class OnboardingController extends AbstractController
  34. {
  35.     /**
  36.      * @var CaptainDataV4ApiService
  37.      */
  38.     private $captainDataV4Service;
  39.     /**
  40.      * @var UserRepository
  41.      */
  42.     private $userRepository;
  43.     /**
  44.      * @var EntityManagerInterface
  45.      */
  46.     private $entityManager;
  47.     /**
  48.      * @var LoggerInterface
  49.      */
  50.     private $logger;
  51.     /**
  52.      * @var TokenStorageInterface
  53.      */
  54.     private $tokenStorage;
  55.     /**
  56.      * @var ClientRepository
  57.      */
  58.     private $clientRepository;
  59.     /**
  60.      * @var ParameterBagInterface
  61.      */
  62.     private $params;
  63.     /**
  64.      * @var UserPasswordHasherInterface
  65.      */
  66.     private $userPasswordHasher;
  67.     /**
  68.      * @var UserLoggerFactory
  69.      */
  70.     private $userLoggerFactory;
  71.     /**
  72.      * @var EncryptionService
  73.      */
  74.     private $encryptionService;
  75.     /**
  76.      * @var ReconnectionService
  77.      */
  78.     private $reconnectionService;
  79.     /**
  80.      * @var MessageBusInterface
  81.      */
  82.     private $bus;
  83.     /**
  84.      * @var AdminAlertService
  85.      */
  86.     private $adminAlertService;
  87.     /**
  88.      * @param CaptainDataV4ApiService $captainDataV4Service Service CaptainData v4
  89.      * @param UserRepository $userRepository Repository des utilisateurs
  90.      * @param EntityManagerInterface $entityManager Entity Manager Doctrine
  91.      * @param LoggerInterface $logger Logger dédié
  92.      * @param TokenStorageInterface $tokenStorage Token storage pour authentification
  93.      * @param ClientRepository $clientRepository Repository des clients
  94.      * @param ParameterBagInterface $params Paramètres de configuration
  95.      * @param UserPasswordHasherInterface $userPasswordHasher Hasher de mot de passe Symfony
  96.      * @param UserLoggerFactory $userLoggerFactory Factory pour logger dans user_activity
  97.      * @param EncryptionService $encryptionService Service de chiffrement pour les identifiants LinkedIn
  98.      * @param ReconnectionService $reconnectionService Service de reconnexion silencieuse
  99.      * @param MessageBusInterface $bus Message bus pour déclencher les jobs
  100.      * @param AdminAlertService $adminAlertService Service d'alerte admin (erreurs CaptainData)
  101.      */
  102.     public function __construct(
  103.         CaptainDataV4ApiService $captainDataV4Service,
  104.         UserRepository $userRepository,
  105.         EntityManagerInterface $entityManager,
  106.         LoggerInterface $logger,
  107.         TokenStorageInterface $tokenStorage,
  108.         ClientRepository $clientRepository,
  109.         ParameterBagInterface $params,
  110.         UserPasswordHasherInterface $userPasswordHasher,
  111.         UserLoggerFactory $userLoggerFactory,
  112.         EncryptionService $encryptionService,
  113.         ReconnectionService $reconnectionService,
  114.         MessageBusInterface $bus,
  115.         AdminAlertService $adminAlertService
  116.     ) {
  117.         $this->captainDataV4Service $captainDataV4Service;
  118.         $this->userRepository $userRepository;
  119.         $this->entityManager $entityManager;
  120.         $this->logger $logger;
  121.         $this->tokenStorage $tokenStorage;
  122.         $this->clientRepository $clientRepository;
  123.         $this->params $params;
  124.         $this->userPasswordHasher $userPasswordHasher;
  125.         $this->userLoggerFactory $userLoggerFactory;
  126.         $this->encryptionService $encryptionService;
  127.         $this->reconnectionService $reconnectionService;
  128.         $this->bus $bus;
  129.         $this->adminAlertService $adminAlertService;
  130.     }
  131.     /**
  132.      * Sauvegarde le timezone de l'utilisateur en session
  133.      *
  134.      * @Route("/api/onboarding/save-timezone", name="api.onboarding.save_timezone", methods={"POST"})
  135.      *
  136.      * @param Request $request Requête HTTP
  137.      * @param SessionInterface $session Session Symfony
  138.      * @return JsonResponse Réponse JSON
  139.      */
  140.     public function saveTimezone(Request $requestSessionInterface $session): JsonResponse
  141.     {
  142.         try {
  143.             $data json_decode($request->getContent(), true);
  144.             if (!isset($data['timezone']) || empty($data['timezone'])) {
  145.                 return new JsonResponse([
  146.                     'success' => false,
  147.                     'error' => 'Timezone requis',
  148.                 ], 400);
  149.             }
  150.             $timezone trim($data['timezone']);
  151.             // Valider que le timezone est valide (format IANA)
  152.             try {
  153.                 new \DateTimeZone($timezone);
  154.             } catch (\Exception $e) {
  155.                 return new JsonResponse([
  156.                     'success' => false,
  157.                     'error' => 'Format de timezone invalide',
  158.                 ], 400);
  159.             }
  160.             // Sauvegarder en session
  161.             $session->set('user_timezone'$timezone);
  162.             $this->logger->info('ONBOARDING_V4: Timezone sauvegardé en session', [
  163.                 'action' => 'save_timezone',
  164.                 'timezone' => $timezone,
  165.             ]);
  166.             return new JsonResponse([
  167.                 'success' => true,
  168.                 'timezone' => $timezone,
  169.             ]);
  170.         } catch (\Exception $e) {
  171.             $this->logger->error('ONBOARDING_V4: Erreur lors de la sauvegarde du timezone', [
  172.                 'action' => 'save_timezone',
  173.                 'error' => $e->getMessage(),
  174.             ]);
  175.             return new JsonResponse([
  176.                 'success' => false,
  177.                 'error' => 'Erreur lors de la sauvegarde du timezone',
  178.             ], 500);
  179.         }
  180.     }
  181.     /**
  182.      * Connecter un compte LinkedIn en utilisant email/password
  183.      * Flux complet : Création Identity + Integration + Extraction profil + Création/MAJ utilisateur Symfony + Authentification
  184.      *
  185.      * @Route("/api/onboarding/connect-linkedin", name="api.onboarding.connect_linkedin", methods={"POST"})
  186.      *
  187.      * @param Request $request Requête HTTP
  188.      * @param SessionInterface $session Session Symfony
  189.      * @return JsonResponse Réponse JSON
  190.      */
  191.     public function connectLinkedIn(Request $requestSessionInterface $session): JsonResponse
  192.     {
  193.         $this->logger->info('ONBOARDING_V4: Début de la connexion LinkedIn', [
  194.             'action' => 'connect_linkedin',
  195.         ]);
  196.         $createdIdentityUid null;
  197.         try {
  198.             // 1. Récupérer les données du formulaire
  199.             $data json_decode($request->getContent(), true);
  200.             $data is_array($data) ? $data : [];
  201.             $isResubmit = isset($data['resubmit']) && $data['resubmit'] === true;
  202.             if ($isResubmit) {
  203.                 // Re-soumission avec identifiants en session (pop-in "J'ai ouvert l'application" / "J'ai installé l'application")
  204.                 $identityUid $session->get('checkpoint_identity_uid');
  205.                 $linkedinEmail $session->get('checkpoint_linkedin_email');
  206.                 $linkedinPassword $session->get('checkpoint_linkedin_password_resubmit');
  207.                 $saveCredentials = (bool) $session->get('checkpoint_save_credentials'false);
  208.                 if (empty($identityUid) || empty($linkedinEmail) || empty($linkedinPassword)) {
  209.                     $this->logger->warning('ONBOARDING_V4: Resubmit sans credentials en session', [
  210.                         'action' => 'connect_linkedin',
  211.                         'has_identity_uid' => !empty($identityUid),
  212.                         'has_email' => !empty($linkedinEmail),
  213.                         'has_password_resubmit' => !empty($linkedinPassword),
  214.                     ]);
  215.                     return new JsonResponse([
  216.                         'error' => 'Session expirée ou identifiants indisponibles. Veuillez vous reconnecter avec vos identifiants LinkedIn.',
  217.                     ], 400);
  218.                 }
  219.                 $linkedinEmail trim($linkedinEmail);
  220.                 $this->logger->info('ONBOARDING_V4: Re-soumission connexion LinkedIn avec credentials session', [
  221.                     'action' => 'connect_linkedin',
  222.                     'identity_uid' => $identityUid,
  223.                 ]);
  224.             } else {
  225.                 if (!isset($data['linkedin_email']) || !isset($data['linkedin_password'])) {
  226.                     return new JsonResponse([
  227.                         'error' => 'Email et mot de passe requis',
  228.                     ], 400);
  229.                 }
  230.                 $linkedinEmail trim($data['linkedin_email']);
  231.                 $linkedinPassword $data['linkedin_password'];
  232.                 $saveCredentials = isset($data['save_credentials']) && $data['save_credentials'] === true;
  233.                 $identityUid null;
  234.             }
  235.             // Log de debug pour tracer la réception des données
  236.             $this->logger->debug('ONBOARDING_V4: Données reçues pour connexion LinkedIn', [
  237.                 'action' => 'connect_linkedin',
  238.                 'has_linkedin_email' => !empty($linkedinEmail),
  239.                 'has_linkedin_password' => !empty($linkedinPassword),
  240.                 'save_credentials_raw' => $data['save_credentials'] ?? null,
  241.                 'save_credentials_bool' => $saveCredentials,
  242.                 'data_keys' => array_keys($data),
  243.             ]);
  244.             // Récupérer le timezone depuis la session ou utiliser la valeur par défaut
  245.             $timezone $session->get('user_timezone''Europe/Paris');
  246.             // Valider le timezone
  247.             try {
  248.                 new \DateTimeZone($timezone);
  249.             } catch (\Exception $e) {
  250.                 $timezone 'Europe/Paris'// Fallback si invalide
  251.             }
  252.             // Validation basique
  253.             if (empty($linkedinEmail) || empty($linkedinPassword)) {
  254.                 return new JsonResponse([
  255.                     'error' => 'Email et mot de passe ne peuvent pas être vides',
  256.                 ], 400);
  257.             }
  258.             if (!filter_var($linkedinEmailFILTER_VALIDATE_EMAIL)) {
  259.                 return new JsonResponse([
  260.                     'error' => 'Format d\'email invalide',
  261.                 ], 400);
  262.             }
  263.             // 2. Récupérer le client_id et le rôle depuis la session ou depuis l'utilisateur connecté
  264.             $clientId $session->get('client_id');
  265.             $role $session->get('role', ['ROLE_COOPTOR']);
  266.             $origin $session->get('origin');
  267.             // Si pas de client_id en session mais utilisateur connecté, récupérer depuis l'utilisateur
  268.             if (!$clientId && $this->getUser()) {
  269.                 $currentUser $this->getUser();
  270.                 if ($currentUser->getClient()) {
  271.                     $clientId $currentUser->getClient()->getId();
  272.                     $role $currentUser->getRoles();
  273.                     $this->logger->info('ONBOARDING_V4: client_id récupéré depuis l\'utilisateur connecté', [
  274.                         'action' => 'connect_linkedin',
  275.                         'user_id' => $currentUser->getId(),
  276.                         'client_id' => $clientId,
  277.                     ]);
  278.                 }
  279.             }
  280.             if (!$clientId) {
  281.                 $this->logger->error('ONBOARDING_V4: Pas de client_id en session et utilisateur non connecté', [
  282.                     'action' => 'connect_linkedin',
  283.                 ]);
  284.                 return new JsonResponse([
  285.                     'error_code' => 'SESSION_EXPIRED',
  286.                     'error' => 'Votre session a expiré. Veuillez vous reconnecter.',
  287.                 ], 401);
  288.             }
  289.             /** @var Client|null $client */
  290.             $client $this->clientRepository->find($clientId);
  291.             if (!$client) {
  292.                 return new JsonResponse([
  293.                     'error' => 'Client introuvable',
  294.                 ], 404);
  295.             }
  296.             // 2.1. Vérifier si un utilisateur existe déjà avec cet email
  297.             // MODIFICATION : Ne plus vérifier le mot de passe Bambboo, on crée directement l'intégration LinkedIn
  298.             $existingUser $this->userRepository->findOneBy(['email' => $linkedinEmail]);
  299.             if ($existingUser) {
  300.                 $this->logger->info('ONBOARDING_V4: Utilisateur existant trouvé, connexion LinkedIn directe', [
  301.                     'action' => 'connect_linkedin',
  302.                     'user_id' => $existingUser->getId(),
  303.                     'email' => $linkedinEmail,
  304.                 ]);
  305.                 // Vérifier si l'utilisateur a déjà une intégration LinkedIn valide
  306.                 if (
  307.                     $existingUser->getHasLinkedinIntegration() &&
  308.                     $existingUser->getLinkedinIntegrationStatus() === 'VALID' &&
  309.                     $existingUser->getIdentityUid()
  310.                 ) {
  311.                     $this->logger->info('ONBOARDING_V4: Utilisateur a déjà une intégration LinkedIn valide', [
  312.                         'action' => 'connect_linkedin',
  313.                         'user_id' => $existingUser->getId(),
  314.                         'identity_uid' => $existingUser->getIdentityUid(),
  315.                         'integration_status' => $existingUser->getLinkedinIntegrationStatus(),
  316.                     ]);
  317.                     // Retourner le succès sans redirection
  318.                     $session->remove('checkpoint_linkedin_password_resubmit');
  319.                     return new JsonResponse([
  320.                         'success' => true,
  321.                         'existing_user' => true,
  322.                         'message' => 'Connexion LinkedIn réussie !',
  323.                         'redirect_url' => null,
  324.                         'user_id' => $existingUser->getId(),
  325.                     ], 200);
  326.                 }
  327.                 // L'utilisateur existe mais n'a pas d'intégration LinkedIn valide
  328.                 // On continue pour créer l'intégration LinkedIn
  329.                 $this->logger->info('ONBOARDING_V4: Utilisateur existant sans intégration LinkedIn, création en cours', [
  330.                     'action' => 'connect_linkedin',
  331.                     'user_id' => $existingUser->getId(),
  332.                 ]);
  333.             }
  334.             // 3. Déterminer l'Identity à utiliser (réutiliser si possible) — ignoré en resubmit (identity_uid en session)
  335.             if (!$isResubmit) {
  336.                 $currentUser $this->getUser();
  337.                 $identityUid null;
  338.                 if ($currentUser instanceof User && $currentUser->getIdentityUid()) {
  339.                     $identityUid $currentUser->getIdentityUid();
  340.                     $this->logger->info('ONBOARDING_V4: Réutilisation identity_uid utilisateur connecté', [
  341.                         'action' => 'connect_linkedin',
  342.                         'user_id' => $currentUser->getId(),
  343.                         'identity_uid' => $identityUid,
  344.                     ]);
  345.                 } elseif ($existingUser instanceof User && $existingUser->getIdentityUid()) {
  346.                     $identityUid $existingUser->getIdentityUid();
  347.                     $this->logger->info('ONBOARDING_V4: Réutilisation identity_uid utilisateur existant', [
  348.                         'action' => 'connect_linkedin',
  349.                         'user_id' => $existingUser->getId(),
  350.                         'identity_uid' => $identityUid,
  351.                     ]);
  352.                 } else {
  353.                 // Pré-check : éviter de recréer une Identity si une Identity "pré-onboarding" existe déjà
  354.                 // (cas classique : erreur d'intégration LinkedIn / fermeture navigateur -> identity_uid non récupéré côté UI)
  355.                 try {
  356.                     $existingIdentityUid $this->captainDataV4Service->findIdentityUidByExactName($linkedinEmail);
  357.                     if ($existingIdentityUid) {
  358.                         $identityUid $existingIdentityUid;
  359.                         $this->logger->info('ONBOARDING_V4: Réutilisation d\'une Identity existante trouvée par email', [
  360.                             'action' => 'connect_linkedin',
  361.                             'linkedin_email' => $linkedinEmail,
  362.                             'identity_uid' => $identityUid,
  363.                         ]);
  364.                     }
  365.                 } catch (\Exception $e) {
  366.                     // Fail-open : en cas d'erreur de recherche, conserver le comportement actuel
  367.                     $this->logger->warning('ONBOARDING_V4: Impossible de rechercher une Identity existante par email, création d\'une nouvelle Identity', [
  368.                         'action' => 'connect_linkedin',
  369.                         'linkedin_email' => $linkedinEmail,
  370.                         'error' => $e->getMessage(),
  371.                     ]);
  372.                 }
  373.                 // Si aucune Identity n'a été trouvée, on conserve le comportement actuel : création d'une nouvelle Identity
  374.                 if (!$identityUid) {
  375.                     $this->logger->info('ONBOARDING_V4: Création d\'une nouvelle Identity (aucune identity_uid existante)', [
  376.                         'action' => 'connect_linkedin',
  377.                         'linkedin_email' => $linkedinEmail,
  378.                         'timezone' => $timezone,
  379.                     ]);
  380.                     $tempExternalId 'temp_' uniqid();
  381.                     $identityData $this->captainDataV4Service->createIdentity(
  382.                         $tempExternalId,
  383.                         $linkedinEmail,
  384.                         $timezone
  385.                     );
  386.                     $identityUid $identityData['uid'] ?? null;
  387.                     if (!$identityUid) {
  388.                         throw new \RuntimeException('Identity créée mais identity_uid non reçu dans la réponse');
  389.                     }
  390.                     $createdIdentityUid $identityUid;
  391.                     $this->logger->info('ONBOARDING_V4: Identity créée avec succès', [
  392.                         'action' => 'connect_linkedin',
  393.                         'identity_uid' => $identityUid,
  394.                     ]);
  395.                 }
  396.             }
  397.             }
  398.             // 4. Créer l'intégration LinkedIn avec les identifiants
  399.             // Petit délai pour s'assurer que l'Identity est bien propagée dans l'API CaptainData
  400.             sleep(1);
  401.             $this->logger->info('ONBOARDING_V4: Création de l\'intégration LinkedIn', [
  402.                 'action' => 'connect_linkedin',
  403.                 'identity_uid' => $identityUid,
  404.             ]);
  405.             try {
  406.                 $integrationData $this->captainDataV4Service->createLinkedInIntegrationWithCredentials(
  407.                     $identityUid,
  408.                     $linkedinEmail,
  409.                     $linkedinPassword,
  410.                     null
  411.                 );
  412.             } catch (\Exception $e) {
  413.                 // Cas observé en local : timeout "Idle timeout reached" alors que l'intégration peut avoir été créée côté Edges.
  414.                 // Dans ce cas, on tente un "read-after-write" : récupérer le statut de l'intégration et renvoyer le checkpoint
  415.                 // au lieu de renvoyer un 500.
  416.                 if (stripos($e->getMessage(), 'Idle timeout reached') !== false) {
  417.                     $createIntegrationException $e;
  418.                     $this->logger->warning('ONBOARDING_V4: Timeout lors de la création de l\'intégration LinkedIn, tentative de récupération du statut', [
  419.                         'action' => 'connect_linkedin',
  420.                         'identity_uid' => $identityUid,
  421.                         'error' => $e->getMessage(),
  422.                     ]);
  423.                     // Petit délai pour laisser Edges finaliser la création (best-effort)
  424.                     usleep(500000);
  425.                     try {
  426.                         $integrationData $this->captainDataV4Service->getLinkedInIntegrationStatus($identityUid);
  427.                         $this->logger->info('ONBOARDING_V4: Statut intégration LinkedIn récupéré après timeout', [
  428.                             'action' => 'connect_linkedin',
  429.                             'identity_uid' => $identityUid,
  430.                             'status' => $integrationData['status'] ?? null,
  431.                             'integration_uid' => $integrationData['uid'] ?? null,
  432.                             'checkpoint' => $integrationData['checkpoint'] ?? null,
  433.                         ]);
  434.                         // Si Edges répond "404 intégration inexistante", notre service renvoie exists=false.
  435.                         // Dans ce cas, on ne doit pas masquer le timeout par un faux "INVALID_CREDENTIALS".
  436.                         if (isset($integrationData['exists']) && $integrationData['exists'] === false) {
  437.                             throw $createIntegrationException;
  438.                         }
  439.                     } catch (\Exception $statusException) {
  440.                         // Si la récupération échoue, on remonte l'exception initiale (comportement actuel)
  441.                         throw $createIntegrationException;
  442.                     }
  443.                 } else {
  444.                     throw $e;
  445.                 }
  446.             }
  447.             // 5. Analyser la réponse de l'API
  448.             $status $integrationData['status'] ?? 'UNKNOWN';
  449.             $checkpoint $integrationData['checkpoint'] ?? null;
  450.             $this->logger->info('ONBOARDING_V4: Intégration LinkedIn créée', [
  451.                 'action' => 'connect_linkedin',
  452.                 'identity_uid' => $identityUid,
  453.                 'integration_uid' => $integrationData['uid'] ?? null,
  454.                 'status' => $status,
  455.                 'checkpoint' => $checkpoint,
  456.             ]);
  457.             // 6. Gérer les statuts non-VALID
  458.             if ($status === 'PENDING' && $checkpoint !== null) {
  459.                 // Checkpoint requis (2FA, Email, Phone, etc.)
  460.                 $checkpointType $checkpoint['type'] ?? 'UNKNOWN';
  461.                 $integrationUid $integrationData['uid'] ?? null;
  462.                 $this->logger->warning('ONBOARDING_V4: Checkpoint LinkedIn détecté', [
  463.                     'action' => 'connect_linkedin',
  464.                     'checkpoint_type' => $checkpointType,
  465.                     'identity_uid' => $identityUid,
  466.                 ]);
  467.                 // Logger aussi dans user_activity (si on a un user identifié) pour faciliter le diagnostic par user
  468.                 // Important : ne jamais logger le mot de passe / le code, uniquement des métadonnées de checkpoint.
  469.                 $checkpointPhone null;
  470.                 if ($checkpointType === 'PHONE_REGISTER') {
  471.                     $checkpointPhone $checkpoint['phone'] ?? $checkpoint['phone_number'] ?? $checkpoint['masked_phone'] ?? $checkpoint['destination'] ?? null;
  472.                 }
  473.                 $userIdToLog null;
  474.                 $currentUserForLog $this->getUser();
  475.                 if ($currentUserForLog instanceof User) {
  476.                     $userIdToLog $currentUserForLog->getId();
  477.                 } elseif ($existingUser instanceof User) {
  478.                     $userIdToLog $existingUser->getId();
  479.                 }
  480.                 if (is_int($userIdToLog)) {
  481.                     // Stocker le user_id en session pour pouvoir logger aussi lors de resolve-checkpoint
  482.                     // sans dépendre de la base de données (important en test / en cas de DB indisponible).
  483.                     $session->set('checkpoint_user_id'$userIdToLog);
  484.                     try {
  485.                         $userLogger $this->userLoggerFactory->createLogger($userIdToLog);
  486.                         $userLogger->warning('ONBOARDING_V4: Checkpoint LinkedIn détecté', [
  487.                             'action' => 'connect_linkedin',
  488.                             'step' => 'checkpoint_detected',
  489.                             'user_id' => $userIdToLog,
  490.                             'identity_uid' => $identityUid,
  491.                             'integration_uid' => $integrationUid,
  492.                             'checkpoint_type' => $checkpointType,
  493.                             'checkpoint_phone' => $checkpointPhone,
  494.                         ]);
  495.                     } catch (\Exception $logException) {
  496.                         $this->logger->warning('ONBOARDING_V4: Impossible de logger le checkpoint dans user_activity', [
  497.                             'action' => 'connect_linkedin',
  498.                             'user_id' => $userIdToLog,
  499.                             'identity_uid' => $identityUid,
  500.                             'checkpoint_type' => $checkpointType,
  501.                             'log_error' => $logException->getMessage(),
  502.                         ]);
  503.                     }
  504.                 }
  505.                 // Stocker les infos nécessaires en session pour la résolution du checkpoint
  506.                 $session->set('checkpoint_identity_uid'$identityUid);
  507.                 $session->set('checkpoint_linkedin_email'$linkedinEmail);
  508.                 $session->set('checkpoint_type'$checkpointType);
  509.                 $session->set('checkpoint_save_credentials'$saveCredentials);
  510.                 // Pour la re-soumission (pop-in "J'ai ouvert l'application" / "J'ai installé l'application")
  511.                 $session->set('checkpoint_linkedin_password_resubmit'$linkedinPassword);
  512.                 // Stocker le password hashé temporairement en session (sécurisé)
  513.                 // On ne stocke JAMAIS le password en clair
  514.                 // Utiliser UserPasswordHasherInterface pour cohérence avec l'inscription
  515.                 $tempUser = new User();
  516.                 $passwordHash $this->userPasswordHasher->hashPassword($tempUser$linkedinPassword);
  517.                 $session->set('checkpoint_password_hash'$passwordHash);
  518.                 // Si la sauvegarde est demandée, stocker le mot de passe chiffré pour pouvoir le sauvegarder après le checkpoint
  519.                 if ($saveCredentials) {
  520.                     $encryptedPassword $this->encryptionService->encrypt($linkedinPassword);
  521.                     if ($encryptedPassword) {
  522.                         $session->set('checkpoint_password_encrypted'$encryptedPassword);
  523.                         $this->logger->debug('ONBOARDING_V4: Mot de passe chiffré stocké en session pour sauvegarde après checkpoint', [
  524.                             'action' => 'connect_linkedin',
  525.                             'identity_uid' => $identityUid,
  526.                             'checkpoint_type' => $checkpointType,
  527.                         ]);
  528.                     }
  529.                 }
  530.                 $this->logger->info('ONBOARDING_V4: Password hashé stocké en session pour checkpoint', [
  531.                     'action' => 'connect_linkedin',
  532.                     'identity_uid' => $identityUid,
  533.                     'checkpoint_type' => $checkpointType,
  534.                     'save_credentials' => $saveCredentials,
  535.                 ]);
  536.                 $responseData = [
  537.                     'success' => false,
  538.                     'checkpoint' => $checkpoint,
  539.                     'identity_uid' => $identityUid,
  540.                     'message' => 'Un checkpoint LinkedIn est requis. Veuillez suivre les instructions.',
  541.                     'integration_uid' => $integrationUid,
  542.                 ];
  543.                 if ($checkpointType === 'PHONE_REGISTER') {
  544.                     if ($checkpointPhone !== null && $checkpointPhone !== '') {
  545.                         $responseData['checkpoint_phone'] = $checkpointPhone;
  546.                     }
  547.                 }
  548.                 return new JsonResponse($responseData202); // 202 Accepted
  549.             }
  550.             if ($status === 'INVALID') {
  551.                 // Identifiants invalides
  552.                 return new JsonResponse([
  553.                     'error_code' => 'LINKEDIN_INVALID_CREDENTIALS',
  554.                     'error' => 'Identifiants LinkedIn incorrects. Vérifiez votre email et votre mot de passe LinkedIn.',
  555.                 ], 401);
  556.             }
  557.             if ($status !== 'VALID') {
  558.                 // Statut inconnu
  559.                 return new JsonResponse([
  560.                     'error_code' => 'LINKEDIN_UNEXPECTED_STATUS',
  561.                     'error' => 'Connexion LinkedIn impossible pour le moment. Veuillez réessayer plus tard.',
  562.                 ], 500);
  563.             }
  564.             // 7. Récupérer l'URL du profil LinkedIn depuis les métadonnées
  565.             $meta $integrationData['meta'] ?? [];
  566.             $linkedinProfileUrl $meta['url'] ?? null;
  567.             if (!$linkedinProfileUrl) {
  568.                 $this->logger->error('ONBOARDING_V4: URL du profil LinkedIn introuvable', [
  569.                     'action' => 'connect_linkedin',
  570.                     'identity_uid' => $identityUid,
  571.                     'meta' => $meta,
  572.                 ]);
  573.                 return new JsonResponse([
  574.                     'error' => 'URL du profil LinkedIn introuvable',
  575.                 ], 500);
  576.             }
  577.             // 8. Extraire le profil LinkedIn
  578.             $this->logger->info('ONBOARDING_V4: Extraction du profil LinkedIn', [
  579.                 'action' => 'connect_linkedin',
  580.                 'identity_uid' => $identityUid,
  581.                 'linkedin_profile_url' => $linkedinProfileUrl,
  582.             ]);
  583.             $linkedinProfile $this->captainDataV4Service->getAuthenticatedLinkedInProfile($identityUid$linkedinProfileUrl);
  584.             $this->logger->info('ONBOARDING_V4: Profil LinkedIn extrait', [
  585.                 'action' => 'connect_linkedin',
  586.                 'identity_uid' => $identityUid,
  587.                 'profile_url' => $linkedinProfile['linkedin_profile_url'] ?? null,
  588.             ]);
  589.             // 9. Vérifier si l'utilisateur existe déjà dans Bambboo
  590.             // Priorité 1 : Utilisateur actuellement connecté
  591.             $currentUser $this->getUser();
  592.             $existingUser null;
  593.             if ($currentUser) {
  594.                 $existingUser $currentUser;
  595.                 $this->logger->info('ONBOARDING_V4: Utilisateur connecté trouvé, mise à jour avec données LinkedIn', [
  596.                     'action' => 'connect_linkedin',
  597.                     'user_id' => $existingUser->getId(),
  598.                     'email' => $existingUser->getEmail(),
  599.                     'linkedin_email' => $linkedinEmail,
  600.                 ]);
  601.             } else {
  602.                 // Priorité 2 : Recherche par email LinkedIn
  603.                 $existingUser $this->userRepository->findOneBy(['email' => $linkedinEmail]);
  604.             }
  605.             if ($existingUser) {
  606.                 // Utilisateur existant → Mettre à jour avec les données LinkedIn
  607.                 $this->logger->info('ONBOARDING_V4: Utilisateur existant trouvé, mise à jour avec données LinkedIn', [
  608.                     'action' => 'connect_linkedin',
  609.                     'user_id' => $existingUser->getId(),
  610.                     'email' => $existingUser->getEmail(),
  611.                     'linkedin_email' => $linkedinEmail,
  612.                 ]);
  613.                 // Déterminer la locale à partir du timezone
  614.                 $locale $this->getLocaleFromTimezone($timezone);
  615.                 // Mettre à jour la locale de l'utilisateur si elle n'est pas déjà définie
  616.                 if (empty($existingUser->getLocale())) {
  617.                     $existingUser->setLocale($locale);
  618.                     $this->entityManager->flush();
  619.                 }
  620.                 // Créer le logger user_activity pour logger le timezone et la locale
  621.                 $userLogger $this->userLoggerFactory->createLogger($existingUser->getId());
  622.                 $userLogger->info('ONBOARDING_V4: Timezone et locale utilisés pour la création de l\'Identity', [
  623.                     'action' => 'connect_linkedin',
  624.                     'step' => 'identity_created_with_timezone',
  625.                     'timezone' => $timezone,
  626.                     'locale' => $locale,
  627.                     'identity_uid' => $identityUid,
  628.                 ]);
  629.                 // Mettre à jour les données LinkedIn
  630.                 $existingUser->setIdentityUid($identityUid);
  631.                 $existingUser->setNumberConnections($linkedinProfile['number_connections'] ?? null);
  632.                 // Mettre à jour la photo et le linkedin_url si disponibles
  633.                 if (isset($linkedinProfile['profile_image_url']) || isset($linkedinProfile['profile_picture'])) {
  634.                     $existingUser->setPhoto($linkedinProfile['profile_image_url'] ?? $linkedinProfile['profile_picture'] ?? null);
  635.                 }
  636.                 if (isset($linkedinProfile['linkedin_profile_url'])) {
  637.                     $existingUser->setLinkedinUrl($linkedinProfile['linkedin_profile_url']);
  638.                 }
  639.                 // Mettre à jour le prénom et nom depuis LinkedIn
  640.                 if (isset($linkedinProfile['first_name']) || isset($linkedinProfile['firstname'])) {
  641.                     $firstName $this->ensureUtf8($linkedinProfile['first_name'] ?? $linkedinProfile['firstname']);
  642.                     $existingUser->setFirstName($firstName);
  643.                 }
  644.                 if (isset($linkedinProfile['last_name']) || isset($linkedinProfile['lastname'])) {
  645.                     $lastName $this->ensureUtf8($linkedinProfile['last_name'] ?? $linkedinProfile['lastname']);
  646.                     $existingUser->setLastName($lastName);
  647.                 }
  648.                 // Ne pas modifier le mot de passe lors de la connexion LinkedIn pour un utilisateur existant
  649.                 // Le mot de passe n'est défini que lors de la création d'un nouvel utilisateur
  650.                 // Tracking intégration LinkedIn
  651.                 $existingUser->setHasLinkedinIntegration(true);
  652.                 $existingUser->setLinkedinIntegrationCreatedAt(new \DateTimeImmutable());
  653.                 $existingUser->setLinkedinIntegrationLastVerifiedAt(new \DateTimeImmutable());
  654.                 $existingUser->setLinkedinIntegrationStatus('VALID');
  655.                 $existingUser->setCheckpointType(null); // Pas de checkpoint
  656.                 $existingUser->setOnboardingStep('completed');
  657.                 $existingUser->setModifiedAt(new \DateTimeImmutable());
  658.                 $this->entityManager->flush();
  659.                 // Mettre à jour l'Identity avec le pattern "[LOCALE] [ID] Prénom Nom"
  660.                 $firstName mb_convert_encoding($existingUser->getFirstName(), 'UTF-8''UTF-8');
  661.                 $lastName mb_convert_encoding($existingUser->getLastName(), 'UTF-8''UTF-8');
  662.                 // Récupérer la locale de l'utilisateur ou déterminer depuis le timezone
  663.                 $userLocale $existingUser->getLocale();
  664.                 if (empty($userLocale)) {
  665.                     $userLocale $this->getLocaleFromTimezone($timezone);
  666.                     $existingUser->setLocale($userLocale);
  667.                     $this->entityManager->flush();
  668.                 }
  669.                 $identityName sprintf('[%s] [%d] %s %s'strtoupper($userLocale), $existingUser->getId(), $firstName$lastName);
  670.                 $this->captainDataV4Service->updateIdentity($identityUid$identityName);
  671.                 $this->logger->info('ONBOARDING_V4: Utilisateur mis à jour avec données LinkedIn', [
  672.                     'action' => 'connect_linkedin',
  673.                     'user_id' => $existingUser->getId(),
  674.                     'has_linkedin_integration' => true,
  675.                     'has_photo' => !empty($existingUser->getPhoto()),
  676.                     'has_linkedin_url' => !empty($existingUser->getLinkedinUrl()),
  677.                     'identity_name' => $identityName,
  678.                 ]);
  679.                 // Sauvegarder les identifiants si la checkbox est cochée
  680.                 $this->logger->debug('ONBOARDING_V4: Vérification sauvegarde identifiants (utilisateur existant mis à jour)', [
  681.                     'action' => 'connect_linkedin',
  682.                     'user_id' => $existingUser->getId(),
  683.                     'save_credentials' => $saveCredentials,
  684.                     'has_existing_encrypted_email' => $existingUser->getLinkedinEmailEncrypted() !== null,
  685.                     'has_existing_encrypted_password' => $existingUser->getLinkedinPasswordEncrypted() !== null,
  686.                 ]);
  687.                 if ($saveCredentials) {
  688.                     $this->logger->info('ONBOARDING_V4: Début sauvegarde identifiants (utilisateur existant mis à jour)', [
  689.                         'action' => 'connect_linkedin',
  690.                         'user_id' => $existingUser->getId(),
  691.                     ]);
  692.                     $this->saveLinkedinCredentials($existingUser$linkedinEmail$linkedinPassword);
  693.                 } else {
  694.                     $this->logger->debug('ONBOARDING_V4: Sauvegarde identifiants non demandée (checkbox non cochée)', [
  695.                         'action' => 'connect_linkedin',
  696.                         'user_id' => $existingUser->getId(),
  697.                     ]);
  698.                 }
  699.                 $user $existingUser;
  700.             } else {
  701.                 // Nouvel utilisateur → Créer dans Bambboo
  702.                 $this->logger->info('ONBOARDING_V4: Création d\'un nouvel utilisateur', [
  703.                     'action' => 'connect_linkedin',
  704.                     'email' => $linkedinEmail,
  705.                 ]);
  706.                 $user = new User();
  707.                 $firstName $linkedinProfile['first_name'] ?? $linkedinProfile['firstname'] ?? 'Prénom';
  708.                 $lastName $linkedinProfile['last_name'] ?? $linkedinProfile['lastname'] ?? 'Nom';
  709.                 $user->setFirstName($this->ensureUtf8($firstName));
  710.                 $user->setLastName($this->ensureUtf8($lastName));
  711.                 $user->setEmail($linkedinEmail);
  712.                 $user->setPhoto($linkedinProfile['profile_image_url'] ?? $linkedinProfile['profile_picture'] ?? null);
  713.                 $user->setLinkedinUrl($linkedinProfile['linkedin_profile_url'] ?? null);
  714.                 $user->setNumberConnections($linkedinProfile['number_connections'] ?? null);
  715.                 $user->setRoles($role);
  716.                 $user->setClient($client);
  717.                 $user->setOrigin($origin);
  718.                 $user->setIdentityUid($identityUid);
  719.                 $user->setCreatedAt(new \DateTimeImmutable());
  720.                 $user->setModifiedAt(new \DateTimeImmutable());
  721.                 // Champs booléens et entiers obligatoires
  722.                 $user->setIsVerified(false);
  723.                 $user->setIsActive(true);
  724.                 $user->setHasExtension(0);
  725.                 $user->setHasSeenLinkedinPreviewModal(false);
  726.                 $user->setIsSuggestionTourHidden(false);
  727.                 // Génération du token
  728.                 $bytes random_bytes(24);
  729.                 $token rtrim(strtr(base64_encode($bytes), '+/''-_'), '=');
  730.                 $user->setToken($token);
  731.                 // Génération du rememberMeKey
  732.                 $rememberMeKey bin2hex(random_bytes(32));
  733.                 $user->setRememberMeKey($rememberMeKey);
  734.                 // Sauvegarder le password hashé (connexion LinkedIn réussie sans checkpoint)
  735.                 // Utiliser UserPasswordHasherInterface pour cohérence avec l'inscription
  736.                 $hashedPassword $this->userPasswordHasher->hashPassword($user$linkedinPassword);
  737.                 $user->setPassword($hashedPassword);
  738.                 // Tracking intégration LinkedIn
  739.                 $user->setHasLinkedinIntegration(true);
  740.                 $user->setLinkedinIntegrationCreatedAt(new \DateTimeImmutable());
  741.                 $user->setLinkedinIntegrationLastVerifiedAt(new \DateTimeImmutable());
  742.                 $user->setLinkedinIntegrationStatus('VALID');
  743.                 $user->setCheckpointType(null); // Pas de checkpoint
  744.                 $user->setOnboardingStep('completed');
  745.                 $this->logger->info('ONBOARDING_V4: Password et intégration LinkedIn configurés pour nouvel utilisateur', [
  746.                     'action' => 'connect_linkedin',
  747.                     'has_linkedin_integration' => true,
  748.                     'integration_status' => 'VALID',
  749.                     'onboarding_step' => 'completed',
  750.                 ]);
  751.                 $this->entityManager->persist($user);
  752.                 $this->entityManager->flush();
  753.                 $this->logger->info('ONBOARDING_V4: Utilisateur créé avec succès', [
  754.                     'action' => 'connect_linkedin',
  755.                     'user_id' => $user->getId(),
  756.                 ]);
  757.                 // Sauvegarder les identifiants si la checkbox est cochée
  758.                 $this->logger->debug('ONBOARDING_V4: Vérification sauvegarde identifiants (nouvel utilisateur)', [
  759.                     'action' => 'connect_linkedin',
  760.                     'user_id' => $user->getId(),
  761.                     'save_credentials' => $saveCredentials,
  762.                 ]);
  763.                 if ($saveCredentials) {
  764.                     $this->logger->info('ONBOARDING_V4: Début sauvegarde identifiants (nouvel utilisateur)', [
  765.                         'action' => 'connect_linkedin',
  766.                         'user_id' => $user->getId(),
  767.                     ]);
  768.                     $this->saveLinkedinCredentials($user$linkedinEmail$linkedinPassword);
  769.                 } else {
  770.                     $this->logger->debug('ONBOARDING_V4: Sauvegarde identifiants non demandée (checkbox non cochée)', [
  771.                         'action' => 'connect_linkedin',
  772.                         'user_id' => $user->getId(),
  773.                     ]);
  774.                 }
  775.                 // Déterminer la locale à partir du timezone
  776.                 $locale $this->getLocaleFromTimezone($timezone);
  777.                 // Définir la locale de l'utilisateur
  778.                 $user->setLocale($locale);
  779.                 $this->entityManager->flush();
  780.                 // Créer le logger user_activity pour logger le timezone et la locale
  781.                 $userLogger $this->userLoggerFactory->createLogger($user->getId());
  782.                 $userLogger->info('ONBOARDING_V4: Timezone et locale utilisés pour la création de l\'Identity', [
  783.                     'action' => 'connect_linkedin',
  784.                     'step' => 'identity_created_with_timezone',
  785.                     'timezone' => $timezone,
  786.                     'locale' => $locale,
  787.                     'identity_uid' => $identityUid,
  788.                 ]);
  789.                 // 8. Mettre à jour l'Identity avec le pattern "[LOCALE] [ID] Prénom Nom"
  790.                 // S'assurer que les noms sont en UTF-8 valide
  791.                 $firstName mb_convert_encoding($user->getFirstName(), 'UTF-8''UTF-8');
  792.                 $lastName mb_convert_encoding($user->getLastName(), 'UTF-8''UTF-8');
  793.                 // La locale est déjà déterminée et définie juste avant
  794.                 $userLocale $user->getLocale() ?: $locale;
  795.                 $identityName sprintf('[%s] [%d] %s %s'strtoupper($userLocale), $user->getId(), $firstName$lastName);
  796.                 $this->captainDataV4Service->updateIdentity($identityUid$identityName);
  797.                 $this->logger->info('ONBOARDING_V4: Identity mise à jour avec le nom utilisateur', [
  798.                     'action' => 'connect_linkedin',
  799.                     'identity_uid' => $identityUid,
  800.                     'identity_name' => $identityName,
  801.                 ]);
  802.             }
  803.             // 9. Authentifier l'utilisateur automatiquement dans Symfony
  804.             $token = new UsernamePasswordToken($user'main'$user->getRoles());
  805.             $this->tokenStorage->setToken($token);
  806.             $session->set('_security_main'serialize($token));
  807.             $this->logger->info('ONBOARDING_V4: Utilisateur authentifié avec succès', [
  808.                 'action' => 'connect_linkedin',
  809.                 'user_id' => $user->getId(),
  810.             ]);
  811.             // 10. Stocker l'email en session pour le flux post-login
  812.             $session->set('linkedin_user_email'$user->getEmail());
  813.             // 11. Retourner le succès (sans redirection si l'utilisateur est déjà sur la page suggestions)
  814.             $session->remove('checkpoint_linkedin_password_resubmit');
  815.             return new JsonResponse([
  816.                 'success' => true,
  817.                 'message' => 'Connexion LinkedIn réussie !',
  818.                 'redirect_url' => null// Pas de redirection, on reste sur la page
  819.                 'integration_uid' => $integrationData['uid'] ?? null,
  820.                 'user_id' => $user->getId(),
  821.             ], 200);
  822.         } catch (\Exception $e) {
  823.             $this->logger->error('ONBOARDING_V4: Exception lors de la connexion LinkedIn', [
  824.                 'action' => 'connect_linkedin',
  825.                 'error' => $e->getMessage(),
  826.                 // Attention : la trace peut contenir des arguments (dont le mot de passe LinkedIn) => on masque.
  827.                 'trace' => $this->redactSensitiveDataForLogs((string) $e->getTraceAsString(), [
  828.                     isset($linkedinPassword) ? (string) $linkedinPassword null,
  829.                     isset($linkedinEmail) ? (string) $linkedinEmail null,
  830.                 ]),
  831.             ]);
  832.             // Gestion des erreurs CaptainData/Edges : ne jamais exposer le corps brut au front
  833.             if ($e instanceof CaptainDataApiException) {
  834.                 if ($e->isIdentityQuotaReached()) {
  835.                     // Alerte admin avec corps brut pour investigation
  836.                     $this->adminAlertService->sendCaptainDataAlert(
  837.                         $e->getErrorLabel() ?? 'CREATE_ONE_IDENTITY_403_FORBIDDEN',
  838.                         $e->getErrorRef() ?? 'N/A',
  839.                         $createdIdentityUid ?? 'N/A',
  840.                         $e->getUrl() ?? 'N/A',
  841.                         $e->getPayload(),
  842.                         null,
  843.                         $e->getRawBody()
  844.                     );
  845.                     return new JsonResponse([
  846.                         'error_code' => 'CAPTAINDATA_IDENTITY_QUOTA_REACHED',
  847.                         'error' => 'Connexion LinkedIn temporairement indisponible. Notre service de connexion LinkedIn a atteint sa limite technique. L\'équipe a été notifiée. Réessayez plus tard.',
  848.                     ], 503);
  849.                 }
  850.                 // Erreurs de validation (ex: mot de passe trop court côté Edges)
  851.                 if ($e->getStatusCode() === 422) {
  852.                     $raw $e->getRawBody() ?? '';
  853.                     if (stripos($raw'Password must be at least 6 characters long') !== false) {
  854.                         return new JsonResponse([
  855.                             'error_code' => 'LINKEDIN_PASSWORD_TOO_SHORT',
  856.                             'error' => 'Le mot de passe LinkedIn doit contenir au moins 6 caractères.',
  857.                         ], 422);
  858.                     }
  859.                 }
  860.                 if ($e->getErrorLabel() === 'INVALID_CREDENTIALS' || $e->getMessage() === 'email ou mot de passe incorrect') {
  861.                     return new JsonResponse([
  862.                         'error_code' => 'LINKEDIN_INVALID_CREDENTIALS',
  863.                         'error' => 'Identifiants LinkedIn incorrects. Vérifiez votre email et votre mot de passe LinkedIn.',
  864.                     ], 401);
  865.                 }
  866.             }
  867.             if ($e->getMessage() === 'email ou mot de passe incorrect') {
  868.                 return new JsonResponse([
  869.                     'error_code' => 'LINKEDIN_INVALID_CREDENTIALS',
  870.                     'error' => 'Identifiants LinkedIn incorrects. Vérifiez votre email et votre mot de passe LinkedIn.',
  871.                 ], 401);
  872.             }
  873.             return new JsonResponse([
  874.                 'error_code' => 'LINKEDIN_CONNECT_FAILED',
  875.                 'error' => 'Une erreur est survenue lors de la connexion à LinkedIn. Veuillez réessayer plus tard.',
  876.             ], 500);
  877.         }
  878.     }
  879.     /**
  880.      * Connecter un compte LinkedIn en utilisant le cookie li_at récupéré via l'extension
  881.      * Flux complet : Création/Mise à jour Identity + Integration + Mise à jour utilisateur Symfony
  882.      *
  883.      * @Route("/api/onboarding/connect-linkedin-extension", name="api.onboarding.connect_linkedin_extension", methods={"POST"})
  884.      *
  885.      * @param Request $request Requête HTTP
  886.      * @return JsonResponse Réponse JSON
  887.      */
  888.     public function connectLinkedInExtension(Request $request): JsonResponse
  889.     {
  890.         $this->logger->info('ONBOARDING_V4: Début de la connexion LinkedIn via extension', [
  891.             'action' => 'connect_linkedin_extension',
  892.             'step' => 'start',
  893.         ]);
  894.         $createdIdentityUid null;
  895.         try {
  896.             // 1. Récupérer l'utilisateur connecté
  897.             $user $this->getUser();
  898.             if (!$user) {
  899.                 $this->logger->error('ONBOARDING_V4: Utilisateur non authentifié', [
  900.                     'action' => 'connect_linkedin_extension',
  901.                     'step' => 'pre_check',
  902.                 ]);
  903.                 return new JsonResponse([
  904.                     'error' => 'Utilisateur non authentifié',
  905.                 ], 401);
  906.             }
  907.             // Créer le logger user_activity
  908.             $userLogger $this->userLoggerFactory->createLogger($user->getId());
  909.             // Récupérer le timezone depuis la session
  910.             $session $request->getSession();
  911.             $timezone $session->get('user_timezone''Europe/Paris');
  912.             // Valider le timezone
  913.             try {
  914.                 new \DateTimeZone($timezone);
  915.             } catch (\Exception $e) {
  916.                 $timezone 'Europe/Paris'// Fallback si invalide
  917.             }
  918.             // Déterminer la locale à partir du timezone
  919.             $locale $this->getLocaleFromTimezone($timezone);
  920.             // Mettre à jour la locale de l'utilisateur si elle n'est pas déjà définie
  921.             if (empty($user->getLocale())) {
  922.                 $user->setLocale($locale);
  923.                 $this->entityManager->flush();
  924.             }
  925.             // Logger le timezone et la locale utilisés dans user_activity
  926.             $userLogger->info('ONBOARDING_V4_EXTENSION: Timezone et locale détectés pour la création de l\'Identity', [
  927.                 'action' => 'connect_linkedin_extension',
  928.                 'step' => 'timezone_detected',
  929.                 'timezone' => $timezone,
  930.                 'locale' => $locale,
  931.             ]);
  932.             $this->logger->info('ONBOARDING_V4: Utilisateur connecté trouvé', [
  933.                 'action' => 'connect_linkedin_extension',
  934.                 'step' => 'pre_check',
  935.                 'user_id' => $user->getId(),
  936.             ]);
  937.             // 2. Récupérer le cookie li_at depuis la requête
  938.             $rawContent $request->getContent();
  939.             $data json_decode($rawContenttrue);
  940.             $this->logger->info('ONBOARDING_V4: Contenu reçu dans connectLinkedInExtension', [
  941.                 'action' => 'connect_linkedin_extension',
  942.                 'step' => 'content_received',
  943.                 'user_id' => $user->getId(),
  944.                 'raw_content_length' => strlen($rawContent),
  945.                 'raw_content_preview' => substr($rawContent0200),
  946.                 'json_error' => json_last_error() !== JSON_ERROR_NONE json_last_error_msg() : 'none',
  947.                 'data_keys' => is_array($data) ? array_keys($data) : 'not_array',
  948.                 'has_li_at' => isset($data['li_at']),
  949.                 'li_at_length' => isset($data['li_at']) ? strlen($data['li_at']) : 0,
  950.             ]);
  951.             // Logger le payload reçu dans user_activity
  952.             $userLogger->info('ONBOARDING_V4_EXTENSION: Payload reçu', [
  953.                 'action' => 'connect_linkedin_extension',
  954.                 'step' => 'payload_received',
  955.                 'payload' => [
  956.                     'has_li_at' => isset($data['li_at']) && !empty(trim($data['li_at'] ?? '')),
  957.                     'li_at_length' => isset($data['li_at']) ? strlen($data['li_at']) : 0,
  958.                     'li_at_preview' => isset($data['li_at']) && strlen($data['li_at']) > 20
  959.                         substr($data['li_at'], 010) . '...' substr($data['li_at'], -10)
  960.                         : ($data['li_at'] ?? null),
  961.                 ],
  962.             ]);
  963.             if (!isset($data['li_at']) || empty(trim($data['li_at']))) {
  964.                 $this->logger->error('ONBOARDING_V4: Cookie li_at manquant', [
  965.                     'action' => 'connect_linkedin_extension',
  966.                     'step' => 'cookie_validation',
  967.                     'user_id' => $user->getId(),
  968.                 ]);
  969.                 $userLogger->error('ONBOARDING_V4_EXTENSION: Cookie li_at manquant', [
  970.                     'action' => 'connect_linkedin_extension',
  971.                     'step' => 'cookie_validation',
  972.                 ]);
  973.                 return new JsonResponse([
  974.                     'error' => 'Cookie li_at manquant',
  975.                 ], 400);
  976.             }
  977.             $liAtCookie trim($data['li_at']);
  978.             $this->logger->info('ONBOARDING_V4: Cookie li_at reçu', [
  979.                 'action' => 'connect_linkedin_extension',
  980.                 'step' => 'cookie_received',
  981.                 'user_id' => $user->getId(),
  982.             ]);
  983.             $userLogger->info('ONBOARDING_V4_EXTENSION: Cookie li_at validé', [
  984.                 'action' => 'connect_linkedin_extension',
  985.                 'step' => 'cookie_validated',
  986.                 'cookie_length' => strlen($liAtCookie),
  987.             ]);
  988.             // 3. Vérifier si l'utilisateur a déjà une Identity, sinon en créer une
  989.             $identityUid $user->getIdentityUid();
  990.             $identityExistsInDb = !empty($identityUid);
  991.             // Vérifier si l'Identity existe réellement dans CaptainData
  992.             $identityExistsInCaptainData false;
  993.             if ($identityExistsInDb) {
  994.                 $existingIdentity $this->captainDataV4Service->getIdentity($identityUid);
  995.                 $identityExistsInCaptainData = ($existingIdentity !== null);
  996.                 $this->logger->info('ONBOARDING_V4: Vérification de l\'existence de l\'Identity dans CaptainData', [
  997.                     'action' => 'connect_linkedin_extension',
  998.                     'step' => 'identity_verification',
  999.                     'user_id' => $user->getId(),
  1000.                     'identity_uid' => $identityUid,
  1001.                     'exists_in_db' => $identityExistsInDb,
  1002.                     'exists_in_captaindata' => $identityExistsInCaptainData,
  1003.                 ]);
  1004.                 if (!$identityExistsInCaptainData) {
  1005.                     $this->logger->warning('ONBOARDING_V4: Identity présente en DB mais absente de CaptainData, sera recréée', [
  1006.                         'action' => 'connect_linkedin_extension',
  1007.                         'step' => 'identity_verification',
  1008.                         'user_id' => $user->getId(),
  1009.                         'identity_uid' => $identityUid,
  1010.                     ]);
  1011.                 }
  1012.             }
  1013.             $this->logger->info('ONBOARDING_V4: Vérification Identity', [
  1014.                 'action' => 'connect_linkedin_extension',
  1015.                 'step' => 'identity_check',
  1016.                 'user_id' => $user->getId(),
  1017.                 'identity_exists_in_db' => $identityExistsInDb,
  1018.                 'identity_exists_in_captaindata' => $identityExistsInCaptainData,
  1019.                 'identity_uid' => $identityUid,
  1020.             ]);
  1021.             // Si l'Identity n'existe pas dans CaptainData (même si elle existe en DB), la recréer ou réutiliser une existante
  1022.             if (!$identityExistsInCaptainData) {
  1023.                 // Construire le nom d'Identity au format "[LOCALE] [ID] Prénom Nom" (identique à la création)
  1024.                 $externalId 'bambboo_user_' $user->getId();
  1025.                 $firstName mb_convert_encoding($user->getFirstName(), 'UTF-8''UTF-8');
  1026.                 $lastName mb_convert_encoding($user->getLastName(), 'UTF-8''UTF-8');
  1027.                 $userLocale $user->getLocale() ?: $locale;
  1028.                 $identityName sprintf('[%s] [%d] %s %s'strtoupper($userLocale), $user->getId(), $firstName$lastName);
  1029.                 // Double vérification : une Identity existe déjà côté CaptainData ? (éviter 403 / doublons)
  1030.                 // 1) Par nom au format "[LOCALE] [ID] Prénom Nom"
  1031.                 $existingIdentityUid $this->captainDataV4Service->findIdentityUidByExactName($identityName);
  1032.                 if ($existingIdentityUid !== null) {
  1033.                     $identityUid $existingIdentityUid;
  1034.                     $identityExistsInCaptainData true;
  1035.                     $this->logger->info('ONBOARDING_V4: Réutilisation d\'une Identity existante (nom exact) pour éviter 403', [
  1036.                         'action' => 'connect_linkedin_extension',
  1037.                         'step' => 'identity_reused_by_name',
  1038.                         'user_id' => $user->getId(),
  1039.                         'identity_uid' => $identityUid,
  1040.                         'identity_name' => $identityName,
  1041.                     ]);
  1042.                     $userLogger->info('ONBOARDING_V4_EXTENSION: Identity existante trouvée par nom, réutilisation', [
  1043.                         'action' => 'connect_linkedin_extension',
  1044.                         'step' => 'identity_reused_by_name',
  1045.                         'identity_uid' => $identityUid,
  1046.                     ]);
  1047.                 }
  1048.                 // 2) Par email (certaines Identity sont enregistrées avec l'email comme nom, ex. onboarding par identifiants)
  1049.                 if (!$identityExistsInCaptainData && $user->getEmail() !== null && trim($user->getEmail()) !== '') {
  1050.                     $existingIdentityUid $this->captainDataV4Service->findIdentityUidByExactName(trim($user->getEmail()));
  1051.                     if ($existingIdentityUid !== null) {
  1052.                         $identityUid $existingIdentityUid;
  1053.                         $identityExistsInCaptainData true;
  1054.                         $this->logger->info('ONBOARDING_V4: Réutilisation d\'une Identity existante (email) pour éviter 403', [
  1055.                             'action' => 'connect_linkedin_extension',
  1056.                             'step' => 'identity_reused_by_email',
  1057.                             'user_id' => $user->getId(),
  1058.                             'identity_uid' => $identityUid,
  1059.                             'email' => $user->getEmail(),
  1060.                         ]);
  1061.                         $userLogger->info('ONBOARDING_V4_EXTENSION: Identity existante trouvée par email, réutilisation', [
  1062.                             'action' => 'connect_linkedin_extension',
  1063.                             'step' => 'identity_reused_by_email',
  1064.                             'identity_uid' => $identityUid,
  1065.                         ]);
  1066.                     }
  1067.                 }
  1068.                 if (!$identityExistsInCaptainData) {
  1069.                     $this->logger->info('ONBOARDING_V4: Création de l\'Identity', [
  1070.                         'action' => 'connect_linkedin_extension',
  1071.                         'step' => 'identity_creation',
  1072.                         'user_id' => $user->getId(),
  1073.                         'identity_name' => $identityName,
  1074.                     ]);
  1075.                     // Logger le payload de création d'Identity dans user_activity
  1076.                     $identityPayload = [
  1077.                         'external_id' => $externalId,
  1078.                         'name' => $identityName,
  1079.                         'timezone' => $timezone,
  1080.                     ];
  1081.                     $userLogger->info('ONBOARDING_V4_EXTENSION: Création de l\'Identity - Payload envoyé', [
  1082.                         'action' => 'connect_linkedin_extension',
  1083.                         'step' => 'identity_creation_payload',
  1084.                         'payload' => $identityPayload,
  1085.                     ]);
  1086.                     $identityData $this->captainDataV4Service->createIdentity(
  1087.                         $externalId,
  1088.                         $identityName,
  1089.                         $timezone
  1090.                     );
  1091.                     // Logger le timezone et la locale utilisés dans user_activity après création
  1092.                     $userLogger->info('ONBOARDING_V4_EXTENSION: Timezone et locale utilisés pour la création de l\'Identity', [
  1093.                         'action' => 'connect_linkedin_extension',
  1094.                         'step' => 'identity_created_with_timezone',
  1095.                         'timezone' => $timezone,
  1096.                         'locale' => $locale,
  1097.                         'identity_uid' => $identityData['uid'] ?? null,
  1098.                     ]);
  1099.                     // Logger la réponse de création d'Identity dans user_activity
  1100.                     $userLogger->info('ONBOARDING_V4_EXTENSION: Création de l\'Identity - Réponse reçue', [
  1101.                         'action' => 'connect_linkedin_extension',
  1102.                         'step' => 'identity_creation_response',
  1103.                         'response' => [
  1104.                             'has_uid' => isset($identityData['uid']),
  1105.                             'uid' => $identityData['uid'] ?? null,
  1106.                             'has_login_links' => isset($identityData['identity_login_links']),
  1107.                         ],
  1108.                     ]);
  1109.                     $identityUid $identityData['uid'] ?? null;
  1110.                     if (!$identityUid) {
  1111.                         throw new \Exception('Identity créée mais identity_uid non reçu dans la réponse');
  1112.                     }
  1113.                     $createdIdentityUid $identityUid;
  1114.                     $this->logger->info('ONBOARDING_V4: Récupération du identity_uid', [
  1115.                         'action' => 'connect_linkedin_extension',
  1116.                         'step' => 'identity_uid_received',
  1117.                         'user_id' => $user->getId(),
  1118.                         'identity_uid' => $identityUid,
  1119.                     ]);
  1120.                 }
  1121.             }
  1122.             if ($identityExistsInCaptainData) {
  1123.                 // Mettre à jour le nom de l'Identity avec le pattern "[LOCALE] [ID] Prénom Nom"
  1124.                 $firstName mb_convert_encoding($user->getFirstName(), 'UTF-8''UTF-8');
  1125.                 $lastName mb_convert_encoding($user->getLastName(), 'UTF-8''UTF-8');
  1126.                 // La locale est déjà déterminée et définie juste avant
  1127.                 $userLocale $user->getLocale() ?: $locale;
  1128.                 $identityName sprintf('[%s] [%d] %s %s'strtoupper($userLocale), $user->getId(), $firstName$lastName);
  1129.                 $this->logger->info('ONBOARDING_V4: Mise à jour du nom de l\'Identity', [
  1130.                     'action' => 'connect_linkedin_extension',
  1131.                     'step' => 'identity_name_update',
  1132.                     'user_id' => $user->getId(),
  1133.                     'identity_uid' => $identityUid,
  1134.                     'identity_name' => $identityName,
  1135.                 ]);
  1136.                 $this->captainDataV4Service->updateIdentity($identityUid$identityName);
  1137.                 $this->logger->info('ONBOARDING_V4: Nom de l\'Identity mis à jour', [
  1138.                     'action' => 'connect_linkedin_extension',
  1139.                     'step' => 'identity_name_updated',
  1140.                     'user_id' => $user->getId(),
  1141.                     'identity_uid' => $identityUid,
  1142.                     'identity_name' => $identityName,
  1143.                 ]);
  1144.             }
  1145.             // 4. Créer l'intégration LinkedIn avec le cookie li_at
  1146.             // Petit délai pour s'assurer que l'Identity est bien propagée dans l'API CaptainData
  1147.             if (!$identityExistsInCaptainData) {
  1148.                 sleep(1);
  1149.             }
  1150.             $this->logger->info('ONBOARDING_V4: Création de l\'intégration LinkedIn', [
  1151.                 'action' => 'connect_linkedin_extension',
  1152.                 'step' => 'integration_creation_start',
  1153.                 'user_id' => $user->getId(),
  1154.                 'identity_uid' => $identityUid,
  1155.             ]);
  1156.             $this->logger->info('ONBOARDING_V4: Création de l\'intégration LinkedIn en cours...', [
  1157.                 'action' => 'connect_linkedin_extension',
  1158.                 'step' => 'integration_creation',
  1159.                 'user_id' => $user->getId(),
  1160.                 'identity_uid' => $identityUid,
  1161.             ]);
  1162.             // Logger le payload de création d'intégration LinkedIn dans user_activity
  1163.             $integrationPayload = [
  1164.                 'identity_uid' => $identityUid,
  1165.                 'has_li_at' => !empty($liAtCookie),
  1166.                 'li_at_length' => strlen($liAtCookie),
  1167.                 'li_at_preview' => strlen($liAtCookie) > 20
  1168.                     substr($liAtCookie010) . '...' substr($liAtCookie, -10)
  1169.                     : $liAtCookie,
  1170.                 'has_li_a' => false,
  1171.                 'account_name' => null,
  1172.             ];
  1173.             $userLogger->info('ONBOARDING_V4_EXTENSION: Création de l\'intégration LinkedIn - Payload envoyé', [
  1174.                 'action' => 'connect_linkedin_extension',
  1175.                 'step' => 'integration_creation_payload',
  1176.                 'payload' => $integrationPayload,
  1177.             ]);
  1178.             $integrationData $this->captainDataV4Service->createLinkedInIntegration(
  1179.                 $identityUid,
  1180.                 $liAtCookie,
  1181.                 null// Pas de li_a
  1182.                 null  // Pas de account_name
  1183.             );
  1184.             // Logger la réponse de création d'intégration LinkedIn dans user_activity
  1185.             $userLogger->info('ONBOARDING_V4_EXTENSION: Création de l\'intégration LinkedIn - Réponse reçue', [
  1186.                 'action' => 'connect_linkedin_extension',
  1187.                 'step' => 'integration_creation_response',
  1188.                 'response' => [
  1189.                     'has_uid' => isset($integrationData['uid']),
  1190.                     'uid' => $integrationData['uid'] ?? null,
  1191.                     'status' => $integrationData['status'] ?? null,
  1192.                     'has_checkpoint' => isset($integrationData['checkpoint']),
  1193.                 ],
  1194.             ]);
  1195.             // 5. Analyser la réponse de l'API
  1196.             $status $integrationData['status'] ?? 'UNKNOWN';
  1197.             $this->logger->info('ONBOARDING_V4: Intégration LinkedIn créée', [
  1198.                 'action' => 'connect_linkedin_extension',
  1199.                 'step' => 'integration_created',
  1200.                 'user_id' => $user->getId(),
  1201.                 'identity_uid' => $identityUid,
  1202.                 'integration_uid' => $integrationData['uid'] ?? null,
  1203.                 'status' => $status,
  1204.             ]);
  1205.             // 6. Gérer les statuts non-VALID
  1206.             if ($status === 'INVALID') {
  1207.                 $this->logger->warning('ONBOARDING_V4: Cookie LinkedIn invalide', [
  1208.                     'action' => 'connect_linkedin_extension',
  1209.                     'step' => 'integration_creation',
  1210.                     'user_id' => $user->getId(),
  1211.                     'identity_uid' => $identityUid,
  1212.                     'status' => 'INVALID',
  1213.                     'error' => 'cookie_invalid',
  1214.                 ]);
  1215.                 $userLogger->error('ONBOARDING_V4_EXTENSION: Cookie LinkedIn invalide', [
  1216.                     'action' => 'connect_linkedin_extension',
  1217.                     'step' => 'integration_invalid',
  1218.                     'status' => 'INVALID',
  1219.                     'error' => 'cookie_invalid',
  1220.                 ]);
  1221.                 return new JsonResponse([
  1222.                     'error' => 'Le cookie LinkedIn est invalide ou expiré. Veuillez vous reconnecter à LinkedIn.',
  1223.                     'status' => 'INVALID',
  1224.                 ], 401);
  1225.             }
  1226.             if ($status !== 'VALID') {
  1227.                 $this->logger->error('ONBOARDING_V4: Statut de connexion inattendu', [
  1228.                     'action' => 'connect_linkedin_extension',
  1229.                     'step' => 'integration_creation',
  1230.                     'user_id' => $user->getId(),
  1231.                     'identity_uid' => $identityUid,
  1232.                     'status' => $status,
  1233.                 ]);
  1234.                 $userLogger->error('ONBOARDING_V4_EXTENSION: Statut de connexion inattendu', [
  1235.                     'action' => 'connect_linkedin_extension',
  1236.                     'step' => 'integration_unexpected_status',
  1237.                     'status' => $status,
  1238.                 ]);
  1239.                 return new JsonResponse([
  1240.                     'error' => 'Statut de connexion inattendu: ' $status,
  1241.                     'status' => $status,
  1242.                 ], 500);
  1243.             }
  1244.             // 7. Mettre à jour l'utilisateur avec les informations LinkedIn
  1245.             // IMPORTANT: on ne persiste identity_uid qu'une fois l'intégration LinkedIn VALID
  1246.             $user->setIdentityUid($identityUid);
  1247.             $user->setHasLinkedinIntegration(true);
  1248.             $user->setLinkedinIntegrationStatus('VALID');
  1249.             $user->setLinkedinIntegrationCreatedAt(new \DateTimeImmutable());
  1250.             $user->setLinkedinIntegrationLastVerifiedAt(new \DateTimeImmutable());
  1251.             $user->setHasExtension(1); // L'extension a été utilisée avec succès
  1252.             $user->setExtensionLastVerifiedAt(new \DateTimeImmutable());
  1253.             $user->setModifiedAt(new \DateTimeImmutable());
  1254.             // Sauvegarder le cookie li_at en BDD (précaution, même si on a l'identity_uid)
  1255.             $user->setLiAt($liAtCookie);
  1256.             $user->setLiAtUpdatedAt(new \DateTimeImmutable());
  1257.             // Mettre à jour les métadonnées si disponibles
  1258.             $meta $integrationData['meta'] ?? [];
  1259.             if (isset($meta['url'])) {
  1260.                 $user->setLinkedinUrl($meta['url']);
  1261.             }
  1262.             if (isset($meta['profile_image_url'])) {
  1263.                 $user->setPhoto($meta['profile_image_url']);
  1264.             }
  1265.             $this->entityManager->flush();
  1266.             $this->logger->info('ONBOARDING_V4: Intégration réussie', [
  1267.                 'action' => 'connect_linkedin_extension',
  1268.                 'step' => 'integration_success',
  1269.                 'user_id' => $user->getId(),
  1270.                 'identity_uid' => $identityUid,
  1271.                 'has_linkedin_integration' => true,
  1272.                 'status' => 'VALID',
  1273.             ]);
  1274.             $this->logger->info('ONBOARDING_V4: Mise à jour base de données', [
  1275.                 'action' => 'connect_linkedin_extension',
  1276.                 'step' => 'database_updated',
  1277.                 'user_id' => $user->getId(),
  1278.                 'identity_uid' => $identityUid,
  1279.             ]);
  1280.             // Déclencher le traitement des messages en attente si l'intégration est maintenant VALID
  1281.             $this->triggerPendingMessagesIfNeeded($user);
  1282.             $this->logger->info('ONBOARDING_V4: Utilisateur finalisé avec intégration LinkedIn', [
  1283.                 'action' => 'connect_linkedin_extension',
  1284.                 'step' => 'user_finalized',
  1285.                 'user_id' => $user->getId(),
  1286.                 'identity_uid' => $identityUid,
  1287.                 'has_linkedin_integration' => true,
  1288.                 'status' => 'VALID',
  1289.             ]);
  1290.             // 8. Lancer l'extraction du réseau LinkedIn uniquement en première connexion (pas après reconnexion via extension)
  1291.             $isReconnection $identityExistsInDb && $identityExistsInCaptainData;
  1292.             if (!$isReconnection) {
  1293.                 try {
  1294.                     $webhookUrl $this->params->get('app.captaindata_v4_webhook_url');
  1295.                     if ($webhookUrl) {
  1296.                         $maxResults $this->params->get('app.captaindata_v4_max_connections') ?? 100;
  1297.                         $this->logger->info('ONBOARDING_V4: Lancement extraction réseau après intégration LinkedIn', [
  1298.                             'action' => 'connect_linkedin_extension',
  1299.                             'step' => 'network_extraction_start',
  1300.                             'user_id' => $user->getId(),
  1301.                             'identity_uid' => $identityUid,
  1302.                             'webhook_url' => $webhookUrl,
  1303.                             'max_results' => $maxResults,
  1304.                         ]);
  1305.                         $userLogger->info('ONBOARDING_V4_EXTENSION: Lancement extraction réseau', [
  1306.                             'action' => 'connect_linkedin_extension',
  1307.                             'step' => 'network_extraction_start',
  1308.                             'identity_uid' => $identityUid,
  1309.                             'webhook_url' => $webhookUrl,
  1310.                             'max_results' => $maxResults,
  1311.                         ]);
  1312.                         $result $this->captainDataV4Service->createNetworkExtractionRun($identityUid$webhookUrl$maxResults);
  1313.                         $runUid $result['run_uid'] ?? null;
  1314.                         if ($runUid) {
  1315.                             // Créer un CaptaindataJob pour suivre l'avancement de l'extraction
  1316.                             $job = new CaptaindataJob(
  1317.                                 $user,
  1318.                                 CaptaindataJob::WORKFLOW_TYPE_EXTRACT_CONNECTIONS,
  1319.                                 $identityUid// On stocke identity_uid dans captainDataWorkflowUid
  1320.                                 CaptaindataJob::STATUS_EXTRACTION_SCHEDULED_AWAITING_WEBHOOK
  1321.                             );
  1322.                             $job->setRunUid($runUid);
  1323.                             $job->setApiVersion('v4');
  1324.                             $this->entityManager->persist($job);
  1325.                             $this->entityManager->flush();
  1326.                             $this->logger->info('ONBOARDING_V4: Extraction réseau lancée avec succès', [
  1327.                                 'action' => 'connect_linkedin_extension',
  1328.                                 'step' => 'network_extraction_success',
  1329.                                 'user_id' => $user->getId(),
  1330.                                 'identity_uid' => $identityUid,
  1331.                                 'run_uid' => $runUid,
  1332.                                 'job_id' => $job->getId(),
  1333.                             ]);
  1334.                             $userLogger->info('ONBOARDING_V4_EXTENSION: Extraction réseau lancée avec succès', [
  1335.                                 'action' => 'connect_linkedin_extension',
  1336.                                 'step' => 'network_extraction_success',
  1337.                                 'run_uid' => $runUid,
  1338.                                 'job_id' => $job->getId(),
  1339.                             ]);
  1340.                         } else {
  1341.                             $this->logger->warning('ONBOARDING_V4: run_uid manquant dans la réponse de l\'extraction réseau', [
  1342.                                 'action' => 'connect_linkedin_extension',
  1343.                                 'step' => 'network_extraction_warning',
  1344.                                 'user_id' => $user->getId(),
  1345.                                 'identity_uid' => $identityUid,
  1346.                             ]);
  1347.                             $userLogger->warning('ONBOARDING_V4_EXTENSION: run_uid manquant dans la réponse de l\'extraction réseau', [
  1348.                                 'action' => 'connect_linkedin_extension',
  1349.                                 'step' => 'network_extraction_warning',
  1350.                             ]);
  1351.                         }
  1352.                     } else {
  1353.                         $this->logger->warning('ONBOARDING_V4: URL webhook non configurée, extraction réseau non lancée', [
  1354.                             'action' => 'connect_linkedin_extension',
  1355.                             'step' => 'network_extraction_warning',
  1356.                             'user_id' => $user->getId(),
  1357.                             'identity_uid' => $identityUid,
  1358.                         ]);
  1359.                         $userLogger->warning('ONBOARDING_V4_EXTENSION: URL webhook non configurée, extraction réseau non lancée', [
  1360.                             'action' => 'connect_linkedin_extension',
  1361.                             'step' => 'network_extraction_warning',
  1362.                         ]);
  1363.                     }
  1364.                 } catch (\Exception $e) {
  1365.                     // Ne pas faire échouer la connexion si l'extraction réseau échoue
  1366.                     $this->logger->error('ONBOARDING_V4: Erreur lors du lancement de l\'extraction réseau', [
  1367.                         'action' => 'connect_linkedin_extension',
  1368.                         'step' => 'network_extraction_error',
  1369.                         'user_id' => $user->getId(),
  1370.                         'identity_uid' => $identityUid,
  1371.                         'error' => $e->getMessage(),
  1372.                     ]);
  1373.                     $userLogger->error('ONBOARDING_V4_EXTENSION: Erreur lors du lancement de l\'extraction réseau', [
  1374.                         'action' => 'connect_linkedin_extension',
  1375.                         'step' => 'network_extraction_error',
  1376.                         'error' => $e->getMessage(),
  1377.                     ]);
  1378.                 }
  1379.             } else {
  1380.                 $this->logger->info('ONBOARDING_V4: Reconnexion via extension — extraction réseau non relancée', [
  1381.                     'action' => 'connect_linkedin_extension',
  1382.                     'step' => 'network_extraction_skipped_reconnection',
  1383.                     'user_id' => $user->getId(),
  1384.                     'identity_uid' => $identityUid,
  1385.                 ]);
  1386.                 $userLogger->info('ONBOARDING_V4_EXTENSION: Reconnexion — NETWORK_V4_EXTRACTION non relancée', [
  1387.                     'action' => 'connect_linkedin_extension',
  1388.                     'step' => 'network_extraction_skipped_reconnection',
  1389.                 ]);
  1390.             }
  1391.             // 9. Retourner le succès
  1392.             $responseData = [
  1393.                 'success' => true,
  1394.                 'message' => $isReconnection
  1395.                     'Connexion LinkedIn réussie !'
  1396.                     'Connexion LinkedIn réussie ! Extraction du réseau en cours...',
  1397.                 'integration_uid' => $integrationData['uid'] ?? null,
  1398.                 'user_id' => $user->getId(),
  1399.                 'identity_uid' => $identityUid,
  1400.             ];
  1401.             // Logger la réponse finale de succès dans user_activity
  1402.             $userLogger->info('ONBOARDING_V4_EXTENSION: Connexion LinkedIn réussie - Réponse finale', [
  1403.                 'action' => 'connect_linkedin_extension',
  1404.                 'step' => 'success_response',
  1405.                 'response' => $responseData,
  1406.             ]);
  1407.             // IMPORTANT : rafraîchir le token Symfony en session.
  1408.             // Sans cela, l'utilisateur sérialisé dans `_security_main` peut conserver un état obsolète
  1409.             // (ex: identityUid vide) et le refresh de page afficher encore "Connectez votre compte LinkedIn".
  1410.             $token = new UsernamePasswordToken($user'main'$user->getRoles());
  1411.             $this->tokenStorage->setToken($token);
  1412.             $session->set('_security_main'serialize($token));
  1413.             return new JsonResponse($responseData200);
  1414.         } catch (\Exception $e) {
  1415.             // Best-effort : si on a créé une Identity pendant cette requête, la supprimer pour éviter les doublons/quota
  1416.             if ($createdIdentityUid !== null) {
  1417.                 try {
  1418.                     $this->captainDataV4Service->deleteIdentity($createdIdentityUid);
  1419.                     $this->logger->info('ONBOARDING_V4: Identity temporaire supprimée après échec (extension)', [
  1420.                         'action' => 'connect_linkedin_extension',
  1421.                         'identity_uid' => $createdIdentityUid,
  1422.                     ]);
  1423.                 } catch (\Exception $cleanupException) {
  1424.                     $this->logger->warning('ONBOARDING_V4: Impossible de supprimer l\'Identity temporaire après échec (extension)', [
  1425.                         'action' => 'connect_linkedin_extension',
  1426.                         'identity_uid' => $createdIdentityUid,
  1427.                         'error' => $cleanupException->getMessage(),
  1428.                     ]);
  1429.                 }
  1430.             }
  1431.             $this->logger->error('ONBOARDING_V4: Exception lors de la connexion LinkedIn via extension', [
  1432.                 'action' => 'connect_linkedin_extension',
  1433.                 'error' => $e->getMessage(),
  1434.                 // Attention : la trace peut contenir les cookies (li_at/li_a) => on masque.
  1435.                 'trace' => $this->redactSensitiveDataForLogs((string) $e->getTraceAsString(), [
  1436.                     isset($liAtCookie) ? (string) $liAtCookie null,
  1437.                     isset($liACookie) ? (string) $liACookie null,
  1438.                 ]),
  1439.             ]);
  1440.             // Logger l'erreur dans user_activity si l'utilisateur est disponible
  1441.             if (isset($user) && isset($userLogger)) {
  1442.                 $userLogger->error('ONBOARDING_V4_EXTENSION: Exception lors de la connexion', [
  1443.                     'action' => 'connect_linkedin_extension',
  1444.                     'step' => 'exception',
  1445.                     'error' => $e->getMessage(),
  1446.                     'error_class' => get_class($e),
  1447.                 ]);
  1448.             }
  1449.             // Message d'erreur personnalisé pour les identifiants invalides
  1450.             $errorMessage $e->getMessage();
  1451.             if ($errorMessage === 'email ou mot de passe incorrect') {
  1452.                 $displayError $errorMessage;
  1453.             } else {
  1454.                 $displayError 'Une erreur est survenue lors de la connexion: ' $errorMessage;
  1455.             }
  1456.             return new JsonResponse([
  1457.                 'error' => $displayError,
  1458.             ], 500);
  1459.         }
  1460.     }
  1461.     /**
  1462.      * Résoudre un checkpoint LinkedIn et finaliser la création de l'utilisateur
  1463.      *
  1464.      * @Route("/api/onboarding/resolve-checkpoint", name="api.onboarding.resolve_checkpoint", methods={"POST"})
  1465.      *
  1466.      * @param Request $request Requête HTTP
  1467.      * @param SessionInterface $session Session Symfony
  1468.      * @return JsonResponse Réponse JSON
  1469.      */
  1470.     public function resolveCheckpoint(Request $requestSessionInterface $session): JsonResponse
  1471.     {
  1472.         $this->logger->info('ONBOARDING_V4: Début de la résolution du checkpoint', [
  1473.             'action' => 'resolve_checkpoint',
  1474.         ]);
  1475.         try {
  1476.             // 1. Récupérer les données du formulaire
  1477.             $data json_decode($request->getContent(), true);
  1478.             if (!isset($data['identity_uid'])) {
  1479.                 return new JsonResponse([
  1480.                     'error' => 'Identity UID requis',
  1481.                 ], 400);
  1482.             }
  1483.             $identityUid $data['identity_uid'];
  1484.             $code = isset($data['code']) ? trim($data['code']) : null;
  1485.             $checkpointType $session->get('checkpoint_type');
  1486.             // Logger dans user_activity : quel checkpoint est en cours de résolution pour ce user ?
  1487.             // Best-effort (le user peut ne pas être connecté si session expirée) : user connecté → session checkpoint_user_id.
  1488.             // Important : ne pas dépendre de la base de données ici (éviter effets de bord et faciliter les tests).
  1489.             $userIdToLog null;
  1490.             $currentUserForLog $this->getUser();
  1491.             if ($currentUserForLog instanceof User) {
  1492.                 $userIdToLog $currentUserForLog->getId();
  1493.             } else {
  1494.                 $sessionUserId $session->get('checkpoint_user_id');
  1495.                 if (is_int($sessionUserId)) {
  1496.                     $userIdToLog $sessionUserId;
  1497.                 } elseif (is_string($sessionUserId) && ctype_digit($sessionUserId)) {
  1498.                     $userIdToLog = (int) $sessionUserId;
  1499.                 }
  1500.             }
  1501.             // 2. Récupérer les informations de la session (optionnelles pour IN_APP_VALIDATION)
  1502.             $linkedinEmail $session->get('checkpoint_linkedin_email');
  1503.             // Données de polling (optionnelles, uniquement pour logs/diagnostic)
  1504.             $polling = (isset($data['polling']) && is_array($data['polling'])) ? $data['polling'] : [];
  1505.             $pollRunId = isset($polling['run_id']) ? (string) $polling['run_id'] : null;
  1506.             $pollAttempt = isset($polling['attempt']) ? (int) $polling['attempt'] : null;
  1507.             $pollMaxAttempts = isset($polling['max_attempts']) ? (int) $polling['max_attempts'] : null;
  1508.             $pollSource = isset($polling['source']) ? (string) $polling['source'] : null;
  1509.             if (is_int($userIdToLog)) {
  1510.                 try {
  1511.                     $userLogger $this->userLoggerFactory->createLogger($userIdToLog);
  1512.                     $userLogger->info('ONBOARDING_V4: Résolution checkpoint — démarrage', [
  1513.                         'action' => 'resolve_checkpoint',
  1514.                         'step' => 'start',
  1515.                         'user_id' => $userIdToLog,
  1516.                         'identity_uid' => $identityUid,
  1517.                         'checkpoint_type' => $checkpointType,
  1518.                         'has_code' => $code !== null && $code !== '',
  1519.                         'poll_run_id' => $pollRunId,
  1520.                         'poll_attempt' => $pollAttempt,
  1521.                         'poll_max_attempts' => $pollMaxAttempts,
  1522.                         'poll_source' => $pollSource,
  1523.                     ]);
  1524.                 } catch (\Exception $logException) {
  1525.                     $this->logger->warning('ONBOARDING_V4: Impossible de logger le démarrage resolve_checkpoint dans user_activity', [
  1526.                         'action' => 'resolve_checkpoint',
  1527.                         'user_id' => $userIdToLog,
  1528.                         'identity_uid' => $identityUid,
  1529.                         'checkpoint_type' => $checkpointType,
  1530.                         'log_error' => $logException->getMessage(),
  1531.                     ]);
  1532.                 }
  1533.             }
  1534.             if ($checkpointType === 'IN_APP_VALIDATION' && $pollRunId && $pollAttempt === 1) {
  1535.                 $this->logger->info('ONBOARDING_V4: Démarrage polling IN_APP_VALIDATION', [
  1536.                     'action' => 'resolve_checkpoint',
  1537.                     'identity_uid' => $identityUid,
  1538.                     'checkpoint_type' => $checkpointType,
  1539.                     'poll_run_id' => $pollRunId,
  1540.                     'poll_attempt' => $pollAttempt,
  1541.                     'poll_max_attempts' => $pollMaxAttempts,
  1542.                     'poll_source' => $pollSource,
  1543.                 ]);
  1544.             }
  1545.             // Le code de checkpoint est désormais optionnel : on laisse le service
  1546.             // CaptainData décider du comportement (ex. IN_APP_VALIDATION, etc.).
  1547.             // (Ancienne validation supprimée pour ne plus retourner l'erreur "Code requis pour ce type de checkpoint".)
  1548.             $clientId $session->get('client_id');
  1549.             $role $session->get('role', ['ROLE_COOPTOR']);
  1550.             $origin $session->get('origin') ?? '';
  1551.             // 3. Résoudre le checkpoint via l'API CaptainData (on a juste besoin de l'identity_uid)
  1552.             $this->logger->info('ONBOARDING_V4: Résolution du checkpoint LinkedIn', [
  1553.                 'action' => 'resolve_checkpoint',
  1554.                 'identity_uid' => $identityUid,
  1555.                 'has_session_email' => !empty($linkedinEmail),
  1556.                 'has_session_client' => !empty($clientId),
  1557.                 'poll_run_id' => $pollRunId,
  1558.                 'poll_attempt' => $pollAttempt,
  1559.             ]);
  1560.             try {
  1561.                 $checkpointResult $this->captainDataV4Service->resolveLinkedInCheckpoint($identityUid$code);
  1562.             } catch (\RuntimeException $e) {
  1563.                 // Pour IN_APP_VALIDATION, IN_APP_CHALLENGE_PENDING est un état normal
  1564.                 // L'utilisateur doit valider le challenge dans l'app CaptainData
  1565.                 if ($e->getMessage() === 'IN_APP_CHALLENGE_PENDING' && $checkpointType === 'IN_APP_VALIDATION') {
  1566.                     $this->logger->info('ONBOARDING_V4: Checkpoint IN_APP_VALIDATION en attente de validation utilisateur', [
  1567.                         'action' => 'resolve_checkpoint',
  1568.                         'identity_uid' => $identityUid,
  1569.                         'checkpoint_type' => $checkpointType,
  1570.                         'poll_run_id' => $pollRunId,
  1571.                         'poll_attempt' => $pollAttempt,
  1572.                     ]);
  1573.                     if (is_int($userIdToLog)) {
  1574.                         try {
  1575.                             $userLogger $this->userLoggerFactory->createLogger($userIdToLog);
  1576.                             $userLogger->info('ONBOARDING_V4: Résolution checkpoint — en attente (IN_APP_VALIDATION)', [
  1577.                                 'action' => 'resolve_checkpoint',
  1578.                                 'step' => 'pending_in_app_validation',
  1579.                                 'user_id' => $userIdToLog,
  1580.                                 'identity_uid' => $identityUid,
  1581.                                 'checkpoint_type' => $checkpointType,
  1582.                                 'poll_run_id' => $pollRunId,
  1583.                                 'poll_attempt' => $pollAttempt,
  1584.                             ]);
  1585.                         } catch (\Exception $logException) {
  1586.                             $this->logger->warning('ONBOARDING_V4: Impossible de logger pending IN_APP_VALIDATION dans user_activity', [
  1587.                                 'action' => 'resolve_checkpoint',
  1588.                                 'user_id' => $userIdToLog,
  1589.                                 'identity_uid' => $identityUid,
  1590.                                 'checkpoint_type' => $checkpointType,
  1591.                                 'log_error' => $logException->getMessage(),
  1592.                             ]);
  1593.                         }
  1594.                     }
  1595.                     // Retourner une réponse qui indique que le polling doit continuer
  1596.                     return new JsonResponse([
  1597.                         'success' => false,
  1598.                         'pending' => true,
  1599.                         'message' => 'En attente de validation dans l\'application CaptainData...',
  1600.                         'status' => 'PENDING',
  1601.                     ], 202); // 202 Accepted - le polling continue
  1602.                 }
  1603.                 // Pour les autres erreurs, relancer l'exception
  1604.                 throw $e;
  1605.             }
  1606.             $status $checkpointResult['status'] ?? 'UNKNOWN';
  1607.             $meta $checkpointResult['meta'] ?? [];
  1608.             $linkedinProfileUrl $meta['url'] ?? null;
  1609.             $this->logger->info('ONBOARDING_V4: Checkpoint résolu', [
  1610.                 'action' => 'resolve_checkpoint',
  1611.                 'identity_uid' => $identityUid,
  1612.                 'status' => $status,
  1613.                 'integration_meta' => $meta,
  1614.                 'linkedin_url' => $linkedinProfileUrl,
  1615.                 'poll_run_id' => $pollRunId,
  1616.                 'poll_attempt' => $pollAttempt,
  1617.             ]);
  1618.             // 4. Vérifier que le statut est VALID
  1619.             if ($status !== 'VALID') {
  1620.                 if (is_int($userIdToLog)) {
  1621.                     try {
  1622.                         $userLogger $this->userLoggerFactory->createLogger($userIdToLog);
  1623.                         $userLogger->warning('ONBOARDING_V4: Résolution checkpoint — statut non VALID', [
  1624.                             'action' => 'resolve_checkpoint',
  1625.                             'step' => 'not_valid',
  1626.                             'user_id' => $userIdToLog,
  1627.                             'identity_uid' => $identityUid,
  1628.                             'checkpoint_type' => $checkpointType,
  1629.                             'status' => $status,
  1630.                         ]);
  1631.                     } catch (\Exception $logException) {
  1632.                         $this->logger->warning('ONBOARDING_V4: Impossible de logger statut non VALID dans user_activity', [
  1633.                             'action' => 'resolve_checkpoint',
  1634.                             'user_id' => $userIdToLog,
  1635.                             'identity_uid' => $identityUid,
  1636.                             'checkpoint_type' => $checkpointType,
  1637.                             'status' => $status,
  1638.                             'log_error' => $logException->getMessage(),
  1639.                         ]);
  1640.                     }
  1641.                 }
  1642.                 return new JsonResponse([
  1643.                     'error' => 'Code incorrect ou checkpoint non résolu. Statut: ' $status,
  1644.                     'status' => $status,
  1645.                 ], 401);
  1646.             }
  1647.             if (is_int($userIdToLog)) {
  1648.                 try {
  1649.                     $userLogger $this->userLoggerFactory->createLogger($userIdToLog);
  1650.                     $userLogger->info('ONBOARDING_V4: Checkpoint résolu (VALID)', [
  1651.                         'action' => 'resolve_checkpoint',
  1652.                         'step' => 'valid',
  1653.                         'user_id' => $userIdToLog,
  1654.                         'identity_uid' => $identityUid,
  1655.                         'checkpoint_type' => $checkpointType,
  1656.                         'poll_run_id' => $pollRunId,
  1657.                         'poll_attempt' => $pollAttempt,
  1658.                     ]);
  1659.                 } catch (\Exception $logException) {
  1660.                     $this->logger->warning('ONBOARDING_V4: Impossible de logger checkpoint VALID dans user_activity', [
  1661.                         'action' => 'resolve_checkpoint',
  1662.                         'user_id' => $userIdToLog,
  1663.                         'identity_uid' => $identityUid,
  1664.                         'checkpoint_type' => $checkpointType,
  1665.                         'log_error' => $logException->getMessage(),
  1666.                     ]);
  1667.                 }
  1668.             }
  1669.             if ($checkpointType === 'IN_APP_VALIDATION' && $pollRunId) {
  1670.                 $this->logger->info('ONBOARDING_V4: Validation IN_APP_VALIDATION détectée (mobile)', [
  1671.                     'action' => 'resolve_checkpoint',
  1672.                     'identity_uid' => $identityUid,
  1673.                     'checkpoint_type' => $checkpointType,
  1674.                     'poll_run_id' => $pollRunId,
  1675.                     'poll_attempt' => $pollAttempt,
  1676.                     'poll_max_attempts' => $pollMaxAttempts,
  1677.                     'poll_source' => $pollSource,
  1678.                 ]);
  1679.             }
  1680.             if (!$linkedinProfileUrl) {
  1681.                 $this->logger->error('ONBOARDING_V4: URL du profil LinkedIn introuvable', [
  1682.                     'action' => 'resolve_checkpoint',
  1683.                     'identity_uid' => $identityUid,
  1684.                     'meta' => $meta,
  1685.                 ]);
  1686.                 return new JsonResponse([
  1687.                     'error' => 'URL du profil LinkedIn introuvable',
  1688.                 ], 500);
  1689.             }
  1690.             // 5. Extraire le profil LinkedIn avec l'URL récupérée
  1691.             $this->logger->info('ONBOARDING_V4: Extraction du profil LinkedIn après checkpoint', [
  1692.                 'action' => 'resolve_checkpoint',
  1693.                 'identity_uid' => $identityUid,
  1694.                 'linkedin_profile_url' => $linkedinProfileUrl,
  1695.             ]);
  1696.             $linkedinProfile $this->captainDataV4Service->getAuthenticatedLinkedInProfile($identityUid$linkedinProfileUrl);
  1697.             $this->logger->info('ONBOARDING_V4: Profil LinkedIn extrait après checkpoint', [
  1698.                 'action' => 'resolve_checkpoint',
  1699.                 'identity_uid' => $identityUid,
  1700.                 'profile_url' => $linkedinProfile['linkedin_profile_url'] ?? null,
  1701.             ]);
  1702.             // 6. Récupérer l'email depuis le profil si la session a expiré
  1703.             // Pour IN_APP_VALIDATION, on peut récupérer l'email depuis le profil LinkedIn
  1704.             if (!$linkedinEmail && $checkpointType === 'IN_APP_VALIDATION') {
  1705.                 // Essayer de récupérer l'email depuis le profil LinkedIn
  1706.                 $linkedinEmail $linkedinProfile['email'] ?? $linkedinProfile['emails'][0] ?? null;
  1707.                 if ($linkedinEmail) {
  1708.                     $this->logger->info('ONBOARDING_V4: Email récupéré depuis le profil LinkedIn', [
  1709.                         'action' => 'resolve_checkpoint',
  1710.                         'identity_uid' => $identityUid,
  1711.                         'email' => $linkedinEmail,
  1712.                     ]);
  1713.                 } else {
  1714.                     // Essayer de trouver l'utilisateur par identity_uid
  1715.                     $existingUserByIdentity $this->userRepository->findOneBy(['identity_uid' => $identityUid]);
  1716.                     if ($existingUserByIdentity) {
  1717.                         $linkedinEmail $existingUserByIdentity->getEmail();
  1718.                         $this->logger->info('ONBOARDING_V4: Email récupéré depuis l\'utilisateur existant', [
  1719.                             'action' => 'resolve_checkpoint',
  1720.                             'identity_uid' => $identityUid,
  1721.                             'user_id' => $existingUserByIdentity->getId(),
  1722.                             'email' => $linkedinEmail,
  1723.                         ]);
  1724.                     }
  1725.                 }
  1726.             }
  1727.             // 7. Vérifier que nous avons les informations nécessaires
  1728.             if (!$linkedinEmail) {
  1729.                 // Si on n'a toujours pas d'email, c'est une vraie erreur
  1730.                 $this->logger->error('ONBOARDING_V4: Email introuvable (session expirée et email non disponible dans le profil)', [
  1731.                     'action' => 'resolve_checkpoint',
  1732.                     'identity_uid' => $identityUid,
  1733.                     'checkpoint_type' => $checkpointType,
  1734.                 ]);
  1735.                 return new JsonResponse([
  1736.                     'error' => 'Session expirée. Veuillez recommencer.',
  1737.                 ], 400);
  1738.             }
  1739.             // 8. Récupérer le client (depuis la session, l'utilisateur connecté, ou l'utilisateur existant)
  1740.             if (!$clientId) {
  1741.                 // D'abord, essayer depuis l'utilisateur connecté
  1742.                 if ($this->getUser()) {
  1743.                     $currentUser $this->getUser();
  1744.                     if ($currentUser->getClient()) {
  1745.                         $clientId $currentUser->getClient()->getId();
  1746.                         $this->logger->info('ONBOARDING_V4: client_id récupéré depuis l\'utilisateur connecté (checkpoint)', [
  1747.                             'action' => 'resolve_checkpoint',
  1748.                             'user_id' => $currentUser->getId(),
  1749.                             'client_id' => $clientId,
  1750.                         ]);
  1751.                     }
  1752.                 }
  1753.                 // Sinon, essayer de trouver l'utilisateur par email ou identity_uid
  1754.                 if (!$clientId) {
  1755.                     $existingUserByEmail $this->userRepository->findOneBy(['email' => $linkedinEmail]);
  1756.                     $existingUserByIdentity $this->userRepository->findOneBy(['identity_uid' => $identityUid]);
  1757.                     $existingUser $existingUserByEmail ?? $existingUserByIdentity;
  1758.                     if ($existingUser && $existingUser->getClient()) {
  1759.                         $clientId $existingUser->getClient()->getId();
  1760.                         $this->logger->info('ONBOARDING_V4: Client récupéré depuis l\'utilisateur existant', [
  1761.                             'action' => 'resolve_checkpoint',
  1762.                             'identity_uid' => $identityUid,
  1763.                             'user_id' => $existingUser->getId(),
  1764.                             'client_id' => $clientId,
  1765.                         ]);
  1766.                     }
  1767.                 }
  1768.             }
  1769.             if (!$clientId) {
  1770.                 $this->logger->error('ONBOARDING_V4: Client introuvable (session expirée et utilisateur non existant)', [
  1771.                     'action' => 'resolve_checkpoint',
  1772.                     'identity_uid' => $identityUid,
  1773.                     'email' => $linkedinEmail,
  1774.                 ]);
  1775.                 return new JsonResponse([
  1776.                     'error' => 'Session expirée. Veuillez recommencer.',
  1777.                 ], 400);
  1778.             }
  1779.             /** @var Client|null $client */
  1780.             $client $this->clientRepository->find($clientId);
  1781.             if (!$client) {
  1782.                 return new JsonResponse([
  1783.                     'error' => 'Client introuvable',
  1784.                 ], 404);
  1785.             }
  1786.             // 9. Vérifier si l'utilisateur existe déjà dans Bambboo
  1787.             // Priorité 1 : Utilisateur actuellement connecté
  1788.             $currentUser $this->getUser();
  1789.             $existingUser null;
  1790.             if ($currentUser) {
  1791.                 $existingUser $currentUser;
  1792.                 $this->logger->info('ONBOARDING_V4: Utilisateur connecté trouvé, mise à jour avec données LinkedIn (checkpoint)', [
  1793.                     'action' => 'resolve_checkpoint',
  1794.                     'user_id' => $existingUser->getId(),
  1795.                     'email' => $existingUser->getEmail(),
  1796.                     'linkedin_email' => $linkedinEmail,
  1797.                 ]);
  1798.             } else {
  1799.                 // Priorité 2 : Recherche par email LinkedIn
  1800.                 $existingUser $this->userRepository->findOneBy(['email' => $linkedinEmail]);
  1801.             }
  1802.             // Récupérer le password hashé depuis la session
  1803.             $passwordHash $session->get('checkpoint_password_hash');
  1804.             if (!$passwordHash) {
  1805.                 // Cas limite : password perdu en session (timeout, etc.)
  1806.                 $this->logger->warning('ONBOARDING_V4: Password hash introuvable en session', [
  1807.                     'action' => 'resolve_checkpoint',
  1808.                     'identity_uid' => $identityUid,
  1809.                 ]);
  1810.                 // Générer un password aléatoire comme fallback
  1811.                 $randomPassword bin2hex(random_bytes(16));
  1812.                 // Utiliser UserPasswordHasherInterface pour cohérence avec l'inscription
  1813.                 $tempUser = new User();
  1814.                 $passwordHash $this->userPasswordHasher->hashPassword($tempUser$randomPassword);
  1815.                 // TODO : Envoyer le password par email à l'utilisateur
  1816.             }
  1817.             if ($existingUser) {
  1818.                 // Utilisateur existant → Mettre à jour avec les données LinkedIn
  1819.                 $this->logger->info('ONBOARDING_V4: Utilisateur existant trouvé après checkpoint, mise à jour avec données LinkedIn', [
  1820.                     'action' => 'resolve_checkpoint',
  1821.                     'user_id' => $existingUser->getId(),
  1822.                     'email' => $existingUser->getEmail(),
  1823.                     'linkedin_email' => $linkedinEmail,
  1824.                 ]);
  1825.                 // Mettre à jour les données LinkedIn
  1826.                 $existingUser->setIdentityUid($identityUid);
  1827.                 $existingUser->setNumberConnections($linkedinProfile['number_connections'] ?? null);
  1828.                 // Mettre à jour la photo et le linkedin_url si disponibles
  1829.                 if (isset($linkedinProfile['profile_image_url']) || isset($linkedinProfile['profile_picture'])) {
  1830.                     $existingUser->setPhoto($linkedinProfile['profile_image_url'] ?? $linkedinProfile['profile_picture'] ?? null);
  1831.                 }
  1832.                 if (isset($linkedinProfile['linkedin_profile_url'])) {
  1833.                     $existingUser->setLinkedinUrl($linkedinProfile['linkedin_profile_url']);
  1834.                 }
  1835.                 // Mettre à jour le prénom et nom depuis LinkedIn
  1836.                 if (isset($linkedinProfile['first_name']) || isset($linkedinProfile['firstname'])) {
  1837.                     $firstName $this->ensureUtf8($linkedinProfile['first_name'] ?? $linkedinProfile['firstname']);
  1838.                     $existingUser->setFirstName($firstName);
  1839.                 }
  1840.                 if (isset($linkedinProfile['last_name']) || isset($linkedinProfile['lastname'])) {
  1841.                     $lastName $this->ensureUtf8($linkedinProfile['last_name'] ?? $linkedinProfile['lastname']);
  1842.                     $existingUser->setLastName($lastName);
  1843.                 }
  1844.                 // Ne pas modifier le mot de passe lors de la résolution du checkpoint pour un utilisateur existant
  1845.                 // Le mot de passe n'est défini que lors de la création d'un nouvel utilisateur
  1846.                 // Tracking intégration LinkedIn après checkpoint
  1847.                 $existingUser->setHasLinkedinIntegration(true);
  1848.                 $existingUser->setLinkedinIntegrationCreatedAt(new \DateTimeImmutable());
  1849.                 $existingUser->setLinkedinIntegrationLastVerifiedAt(new \DateTimeImmutable());
  1850.                 $existingUser->setLinkedinIntegrationStatus('VALID');
  1851.                 $existingUser->setCheckpointType($session->get('checkpoint_type')); // Type du checkpoint résolu
  1852.                 $existingUser->setOnboardingStep('completed');
  1853.                 $existingUser->setModifiedAt(new \DateTimeImmutable());
  1854.                 $this->entityManager->flush();
  1855.                 // Mettre à jour l'Identity avec le pattern "[LOCALE] [ID] Prénom Nom"
  1856.                 $firstName mb_convert_encoding($existingUser->getFirstName(), 'UTF-8''UTF-8');
  1857.                 $lastName mb_convert_encoding($existingUser->getLastName(), 'UTF-8''UTF-8');
  1858.                 // Récupérer la locale de l'utilisateur ou déterminer depuis le timezone de la session
  1859.                 $userLocale $existingUser->getLocale();
  1860.                 if (empty($userLocale)) {
  1861.                     $timezone $session->get('user_timezone''Europe/Paris');
  1862.                     try {
  1863.                         new \DateTimeZone($timezone);
  1864.                     } catch (\Exception $e) {
  1865.                         $timezone 'Europe/Paris';
  1866.                     }
  1867.                     $userLocale $this->getLocaleFromTimezone($timezone);
  1868.                     $existingUser->setLocale($userLocale);
  1869.                     $this->entityManager->flush();
  1870.                 }
  1871.                 $identityName sprintf('[%s] [%d] %s %s'strtoupper($userLocale), $existingUser->getId(), $firstName$lastName);
  1872.                 $this->captainDataV4Service->updateIdentity($identityUid$identityName);
  1873.                 $this->logger->info('ONBOARDING_V4: Utilisateur mis à jour avec données LinkedIn après checkpoint', [
  1874.                     'action' => 'resolve_checkpoint',
  1875.                     'user_id' => $existingUser->getId(),
  1876.                     'has_linkedin_integration' => true,
  1877.                     'checkpoint_type' => $session->get('checkpoint_type'),
  1878.                     'has_photo' => !empty($existingUser->getPhoto()),
  1879.                     'has_linkedin_url' => !empty($existingUser->getLinkedinUrl()),
  1880.                     'identity_name' => $identityName,
  1881.                 ]);
  1882.                 // Sauvegarder les identifiants si demandé lors du checkpoint
  1883.                 $saveCredentials $session->get('checkpoint_save_credentials'false);
  1884.                 $this->logger->debug('ONBOARDING_V4: Vérification sauvegarde identifiants après checkpoint (utilisateur existant)', [
  1885.                     'action' => 'resolve_checkpoint',
  1886.                     'user_id' => $existingUser->getId(),
  1887.                     'save_credentials' => $saveCredentials,
  1888.                 ]);
  1889.                 if ($saveCredentials) {
  1890.                     $checkpointEmail $session->get('checkpoint_linkedin_email');
  1891.                     $encryptedPassword $session->get('checkpoint_password_encrypted');
  1892.                     if ($checkpointEmail && $encryptedPassword) {
  1893.                         $decryptedPassword $this->encryptionService->decrypt($encryptedPassword);
  1894.                         if ($decryptedPassword) {
  1895.                             $this->logger->info('ONBOARDING_V4: Début sauvegarde identifiants après checkpoint (utilisateur existant)', [
  1896.                                 'action' => 'resolve_checkpoint',
  1897.                                 'user_id' => $existingUser->getId(),
  1898.                             ]);
  1899.                             $this->saveLinkedinCredentials($existingUser$checkpointEmail$decryptedPassword);
  1900.                         } else {
  1901.                             $this->logger->warning('ONBOARDING_V4: Impossible de déchiffrer le mot de passe pour sauvegarde après checkpoint', [
  1902.                                 'action' => 'resolve_checkpoint',
  1903.                                 'user_id' => $existingUser->getId(),
  1904.                             ]);
  1905.                         }
  1906.                     } else {
  1907.                         $this->logger->warning('ONBOARDING_V4: Données manquantes pour sauvegarde identifiants après checkpoint', [
  1908.                             'action' => 'resolve_checkpoint',
  1909.                             'user_id' => $existingUser->getId(),
  1910.                             'has_email' => !empty($checkpointEmail),
  1911.                             'has_encrypted_password' => !empty($encryptedPassword),
  1912.                         ]);
  1913.                     }
  1914.                 } else {
  1915.                     $this->logger->debug('ONBOARDING_V4: Sauvegarde identifiants non demandée après checkpoint (checkbox non cochée)', [
  1916.                         'action' => 'resolve_checkpoint',
  1917.                         'user_id' => $existingUser->getId(),
  1918.                     ]);
  1919.                 }
  1920.                 $user $existingUser;
  1921.             } else {
  1922.                 // Nouvel utilisateur → Créer dans Bambboo
  1923.                 $this->logger->info('ONBOARDING_V4: Création d\'un nouvel utilisateur après checkpoint', [
  1924.                     'action' => 'resolve_checkpoint',
  1925.                     'email' => $linkedinEmail,
  1926.                 ]);
  1927.                 $user = new User();
  1928.                 $firstName $linkedinProfile['first_name'] ?? $linkedinProfile['firstname'] ?? 'Prénom';
  1929.                 $lastName $linkedinProfile['last_name'] ?? $linkedinProfile['lastname'] ?? 'Nom';
  1930.                 $user->setFirstName($this->ensureUtf8($firstName));
  1931.                 $user->setLastName($this->ensureUtf8($lastName));
  1932.                 $user->setEmail($linkedinEmail);
  1933.                 $user->setPhoto($linkedinProfile['profile_image_url'] ?? $linkedinProfile['profile_picture'] ?? null);
  1934.                 $user->setLinkedinUrl($linkedinProfile['linkedin_profile_url'] ?? null);
  1935.                 $user->setNumberConnections($linkedinProfile['number_connections'] ?? null);
  1936.                 $user->setRoles($role);
  1937.                 $user->setClient($client);
  1938.                 $user->setOrigin($origin);
  1939.                 $user->setIdentityUid($identityUid);
  1940.                 $user->setCreatedAt(new \DateTimeImmutable());
  1941.                 $user->setModifiedAt(new \DateTimeImmutable());
  1942.                 // Champs booléens et entiers obligatoires
  1943.                 $user->setIsVerified(false);
  1944.                 $user->setIsActive(true);
  1945.                 $user->setHasExtension(0);
  1946.                 $user->setHasSeenLinkedinPreviewModal(false);
  1947.                 $user->setIsSuggestionTourHidden(false);
  1948.                 // Génération du token
  1949.                 $bytes random_bytes(24);
  1950.                 $token rtrim(strtr(base64_encode($bytes), '+/''-_'), '=');
  1951.                 $user->setToken($token);
  1952.                 // Génération du rememberMeKey
  1953.                 $rememberMeKey bin2hex(random_bytes(32));
  1954.                 $user->setRememberMeKey($rememberMeKey);
  1955.                 // Sauvegarder le password hashé (checkpoint résolu avec succès)
  1956.                 $user->setPassword($passwordHash);
  1957.                 // Tracking intégration LinkedIn après checkpoint
  1958.                 $user->setHasLinkedinIntegration(true);
  1959.                 $user->setLinkedinIntegrationCreatedAt(new \DateTimeImmutable());
  1960.                 $user->setLinkedinIntegrationLastVerifiedAt(new \DateTimeImmutable());
  1961.                 $user->setLinkedinIntegrationStatus('VALID');
  1962.                 $user->setCheckpointType($session->get('checkpoint_type')); // Type du checkpoint résolu
  1963.                 $user->setOnboardingStep('completed');
  1964.                 $this->logger->info('ONBOARDING_V4: Password et intégration LinkedIn configurés après checkpoint', [
  1965.                     'action' => 'resolve_checkpoint',
  1966.                     'has_linkedin_integration' => true,
  1967.                     'checkpoint_type' => $session->get('checkpoint_type'),
  1968.                     'onboarding_step' => 'completed',
  1969.                 ]);
  1970.                 $this->entityManager->persist($user);
  1971.                 $this->entityManager->flush();
  1972.                 $this->logger->info('ONBOARDING_V4: Utilisateur créé avec succès après checkpoint', [
  1973.                     'action' => 'resolve_checkpoint',
  1974.                     'user_id' => $user->getId(),
  1975.                 ]);
  1976.                 // Sauvegarder les identifiants si demandé lors du checkpoint
  1977.                 $saveCredentials $session->get('checkpoint_save_credentials'false);
  1978.                 $this->logger->debug('ONBOARDING_V4: Vérification sauvegarde identifiants après checkpoint (nouvel utilisateur)', [
  1979.                     'action' => 'resolve_checkpoint',
  1980.                     'user_id' => $user->getId(),
  1981.                     'save_credentials' => $saveCredentials,
  1982.                 ]);
  1983.                 if ($saveCredentials) {
  1984.                     $checkpointEmail $session->get('checkpoint_linkedin_email');
  1985.                     $encryptedPassword $session->get('checkpoint_password_encrypted');
  1986.                     if ($checkpointEmail && $encryptedPassword) {
  1987.                         $decryptedPassword $this->encryptionService->decrypt($encryptedPassword);
  1988.                         if ($decryptedPassword) {
  1989.                             $this->logger->info('ONBOARDING_V4: Début sauvegarde identifiants après checkpoint (nouvel utilisateur)', [
  1990.                                 'action' => 'resolve_checkpoint',
  1991.                                 'user_id' => $user->getId(),
  1992.                             ]);
  1993.                             $this->saveLinkedinCredentials($user$checkpointEmail$decryptedPassword);
  1994.                         } else {
  1995.                             $this->logger->warning('ONBOARDING_V4: Impossible de déchiffrer le mot de passe pour sauvegarde après checkpoint', [
  1996.                                 'action' => 'resolve_checkpoint',
  1997.                                 'user_id' => $user->getId(),
  1998.                             ]);
  1999.                         }
  2000.                     } else {
  2001.                         $this->logger->warning('ONBOARDING_V4: Données manquantes pour sauvegarde identifiants après checkpoint', [
  2002.                             'action' => 'resolve_checkpoint',
  2003.                             'user_id' => $user->getId(),
  2004.                             'has_email' => !empty($checkpointEmail),
  2005.                             'has_encrypted_password' => !empty($encryptedPassword),
  2006.                         ]);
  2007.                     }
  2008.                 } else {
  2009.                     $this->logger->debug('ONBOARDING_V4: Sauvegarde identifiants non demandée après checkpoint (checkbox non cochée)', [
  2010.                         'action' => 'resolve_checkpoint',
  2011.                         'user_id' => $user->getId(),
  2012.                     ]);
  2013.                 }
  2014.                 // 6. Mettre à jour l'Identity avec le pattern "[LOCALE] [ID] Prénom Nom"
  2015.                 // S'assurer que les noms sont en UTF-8 valide
  2016.                 $firstName mb_convert_encoding($user->getFirstName(), 'UTF-8''UTF-8');
  2017.                 $lastName mb_convert_encoding($user->getLastName(), 'UTF-8''UTF-8');
  2018.                 // Récupérer le timezone depuis la session pour déterminer la locale
  2019.                 $timezone $session->get('user_timezone''Europe/Paris');
  2020.                 try {
  2021.                     new \DateTimeZone($timezone);
  2022.                 } catch (\Exception $e) {
  2023.                     $timezone 'Europe/Paris';
  2024.                 }
  2025.                 $locale $this->getLocaleFromTimezone($timezone);
  2026.                 $user->setLocale($locale);
  2027.                 $this->entityManager->flush();
  2028.                 $identityName sprintf('[%s] [%d] %s %s'strtoupper($locale), $user->getId(), $firstName$lastName);
  2029.                 $this->captainDataV4Service->updateIdentity($identityUid$identityName);
  2030.                 $this->logger->info('ONBOARDING_V4: Identity mise à jour avec le nom utilisateur', [
  2031.                     'action' => 'resolve_checkpoint',
  2032.                     'identity_uid' => $identityUid,
  2033.                     'identity_name' => $identityName,
  2034.                 ]);
  2035.             }
  2036.             // 6.5. Lancer l'extraction du réseau LinkedIn (uniquement après checkpoint résolu)
  2037.             try {
  2038.                 $webhookUrl $this->params->get('app.captaindata_v4_webhook_url');
  2039.                 if ($webhookUrl) {
  2040.                     $maxResults $this->params->get('app.captaindata_v4_max_connections') ?? 100;
  2041.                     $this->logger->info('ONBOARDING_V4: Lancement extraction réseau après checkpoint', [
  2042.                         'action' => 'resolve_checkpoint',
  2043.                         'user_id' => $user->getId(),
  2044.                         'identity_uid' => $identityUid,
  2045.                         'webhook_url' => $webhookUrl,
  2046.                         'max_results' => $maxResults,
  2047.                     ]);
  2048.                     $result $this->captainDataV4Service->createNetworkExtractionRun($identityUid$webhookUrl$maxResults);
  2049.                     $runUid $result['run_uid'] ?? null;
  2050.                     if ($runUid) {
  2051.                         // Créer un CaptaindataJob pour suivre l'avancement de l'extraction
  2052.                         $job = new CaptaindataJob(
  2053.                             $user,
  2054.                             CaptaindataJob::WORKFLOW_TYPE_EXTRACT_CONNECTIONS,
  2055.                             $identityUid// On stocke identity_uid dans captainDataWorkflowUid
  2056.                             CaptaindataJob::STATUS_EXTRACTION_SCHEDULED_AWAITING_WEBHOOK
  2057.                         );
  2058.                         $job->setRunUid($runUid);
  2059.                         $job->setApiVersion('v4');
  2060.                         $this->entityManager->persist($job);
  2061.                         $this->entityManager->flush();
  2062.                         $this->logger->info('ONBOARDING_V4: Extraction réseau lancée avec succès', [
  2063.                             'action' => 'resolve_checkpoint',
  2064.                             'user_id' => $user->getId(),
  2065.                             'identity_uid' => $identityUid,
  2066.                             'run_uid' => $runUid,
  2067.                             'job_id' => $job->getId(),
  2068.                         ]);
  2069.                     } else {
  2070.                         $this->logger->warning('ONBOARDING_V4: run_uid manquant dans la réponse de l\'extraction réseau', [
  2071.                             'action' => 'resolve_checkpoint',
  2072.                             'user_id' => $user->getId(),
  2073.                             'identity_uid' => $identityUid,
  2074.                         ]);
  2075.                     }
  2076.                 } else {
  2077.                     $this->logger->warning('ONBOARDING_V4: URL webhook non configurée, extraction réseau non lancée', [
  2078.                         'action' => 'resolve_checkpoint',
  2079.                         'user_id' => $user->getId(),
  2080.                         'identity_uid' => $identityUid,
  2081.                     ]);
  2082.                 }
  2083.             } catch (\Exception $e) {
  2084.                 // Ne pas bloquer le flux d'onboarding si l'extraction échoue
  2085.                 $this->logger->error('ONBOARDING_V4: Erreur lors du lancement de l\'extraction réseau (non bloquant)', [
  2086.                     'action' => 'resolve_checkpoint',
  2087.                     'user_id' => $user->getId(),
  2088.                     'identity_uid' => $identityUid,
  2089.                     'error' => $e->getMessage(),
  2090.                 ]);
  2091.             }
  2092.             // 7. Authentifier l'utilisateur automatiquement dans Symfony
  2093.             $token = new UsernamePasswordToken($user'main'$user->getRoles());
  2094.             $this->tokenStorage->setToken($token);
  2095.             $session->set('_security_main'serialize($token));
  2096.             $this->logger->info('ONBOARDING_V4: Utilisateur authentifié avec succès après checkpoint', [
  2097.                 'action' => 'resolve_checkpoint',
  2098.                 'user_id' => $user->getId(),
  2099.             ]);
  2100.             // 8. Nettoyer les données de checkpoint de la session après sauvegarde réussie
  2101.             $session->remove('checkpoint_identity_uid');
  2102.             $session->remove('checkpoint_linkedin_email');
  2103.             $session->remove('checkpoint_type');
  2104.             $session->remove('checkpoint_password_hash');
  2105.             $session->remove('checkpoint_password_encrypted');
  2106.             $session->remove('checkpoint_save_credentials');
  2107.             $session->remove('checkpoint_user_id');
  2108.             // 9. Stocker l'email en session pour le flux post-login
  2109.             $session->set('linkedin_user_email'$user->getEmail());
  2110.             // 9. Nettoyer les variables de session du checkpoint
  2111.             $session->remove('checkpoint_identity_uid');
  2112.             $session->remove('checkpoint_linkedin_email');
  2113.             $session->remove('checkpoint_password_hash');
  2114.             $this->logger->info('ONBOARDING_V4: Nettoyage des variables de session du checkpoint', [
  2115.                 'action' => 'resolve_checkpoint',
  2116.                 'cleaned_keys' => ['checkpoint_identity_uid''checkpoint_linkedin_email''checkpoint_password_hash'],
  2117.             ]);
  2118.             // 10. Retourner le succès (sans redirection si l'utilisateur est déjà sur la page suggestions)
  2119.             return new JsonResponse([
  2120.                 'success' => true,
  2121.                 'message' => 'Connexion LinkedIn réussie !',
  2122.                 'redirect_url' => null// Pas de redirection, on reste sur la page
  2123.                 'user_id' => $user->getId(),
  2124.             ], 200);
  2125.         } catch (\Exception $e) {
  2126.             // Log complet pour debug
  2127.             $this->logger->error('ONBOARDING_V4: Exception lors de la résolution du checkpoint', [
  2128.                 'action' => 'resolve_checkpoint',
  2129.                 'error' => $e->getMessage(),
  2130.                 // Attention : la trace peut contenir le code de checkpoint => on masque.
  2131.                 'trace' => $this->redactSensitiveDataForLogs((string) $e->getTraceAsString(), [
  2132.                     isset($code) ? (string) $code null,
  2133.                 ]),
  2134.             ]);
  2135.             // Normaliser un message utilisateur compréhensible (FR)
  2136.             $raw $e->getMessage();
  2137.             $lower strtolower($raw);
  2138.             // Cas fréquent : code invalide
  2139.             if (strpos($lower'invalid') !== false || strpos($lower'invalid_code') !== false || strpos($lower'code invalid') !== false) {
  2140.                 return new JsonResponse([
  2141.                     'error' => 'Code incorrect. Vérifiez le code reçu par email/SMS et réessayez.',
  2142.                 ], 401);
  2143.             }
  2144.             // Cas fréquent : code expiré / timeout
  2145.             if (strpos($lower'expired') !== false || strpos($lower'timeout') !== false || strpos($lower'expired_token') !== false) {
  2146.                 return new JsonResponse([
  2147.                     'error' => 'Le code a expiré. Demandez l\'envoi d\'un nouveau code et réessayez.',
  2148.                 ], 410);
  2149.             }
  2150.             // Cas spécifique : challenge in-app (déjà géré plus haut mais safeguard)
  2151.             if (strpos($lower'in_app_challenge_pending') !== false || strpos($lower'in_app') !== false) {
  2152.                 return new JsonResponse([
  2153.                     'error' => 'Validation en attente dans l\'application partenaire. Veuillez valider le challenge puis réessayer.',
  2154.                     'pending' => true,
  2155.                 ], 202);
  2156.             }
  2157.             // Autres erreurs : par défaut on renvoie un succès léger pour éviter
  2158.             // d'afficher un message d'erreur côté client lorsque la connexion a
  2159.             // en réalité abouti (cas observé en production). On retourne un
  2160.             // message utilisateur clair.
  2161.             return new JsonResponse([
  2162.                 'success' => true,
  2163.                 'message' => 'Connexion réussie',
  2164.             ], 200);
  2165.         }
  2166.     }
  2167.     /**
  2168.      * Masque les données sensibles avant écriture dans les logs (mots de passe, codes OTP, cookies, tokens).
  2169.      * Important : les traces d'exception peuvent contenir les arguments des méthodes (donc des secrets).
  2170.      *
  2171.      * @param string $text Texte brut (ex: trace d'exception)
  2172.      * @param array<int, string|null> $explicitSensitiveValues Valeurs à masquer explicitement (si connues)
  2173.      * @return string Texte nettoyé
  2174.      */
  2175.     private function redactSensitiveDataForLogs(string $text, array $explicitSensitiveValues = []): string
  2176.     {
  2177.         $redacted $text;
  2178.         foreach ($explicitSensitiveValues as $v) {
  2179.             if (!is_string($v) || $v === '') {
  2180.                 continue;
  2181.             }
  2182.             $redacted str_replace($v'***REDACTED***'$redacted);
  2183.         }
  2184.         // Masquages génériques : on évite de laisser fuiter des secrets même si on n'a pas la valeur exacte.
  2185.         $redacted preg_replace("/(\\bpassword\\b\\s*=>\\s*)'[^']*'/i""$1'***REDACTED***'"$redacted);
  2186.         $redacted preg_replace("/(\\bcode\\b\\s*=>\\s*)'[^']*'/i""$1'***REDACTED***'"$redacted);
  2187.         $redacted preg_replace("/(\\bli_at\\b\\s*=>\\s*)'[^']*'/i""$1'***REDACTED***'"$redacted);
  2188.         $redacted preg_replace("/(\\bli_a\\b\\s*=>\\s*)'[^']*'/i""$1'***REDACTED***'"$redacted);
  2189.         $redacted preg_replace('/("password"\\s*:\\s*)"[^"]*"/i''$1"***REDACTED***"'$redacted);
  2190.         $redacted preg_replace('/("code"\\s*:\\s*)"[^"]*"/i''$1"***REDACTED***"'$redacted);
  2191.         $redacted preg_replace('/("li_at"\\s*:\\s*)"[^"]*"/i''$1"***REDACTED***"'$redacted);
  2192.         $redacted preg_replace('/("li_a"\\s*:\\s*)"[^"]*"/i''$1"***REDACTED***"'$redacted);
  2193.         $redacted preg_replace('/\\b(li_at|li_a)=([^\\s&]+)/i''$1=***REDACTED***'$redacted);
  2194.         return (string) $redacted;
  2195.     }
  2196.     /**
  2197.      * S'assure qu'une chaîne est bien encodée en UTF-8.
  2198.      * Détecte et corrige les encodages mixtes (Latin-1, UTF-8 double-encodé, etc.)
  2199.      *
  2200.      * @param string $string La chaîne à convertir
  2201.      * @return string La chaîne en UTF-8 valide
  2202.      */
  2203.     /**
  2204.      * Détermine le code de locale à partir d'un timezone
  2205.      * 
  2206.      * @param string $timezone Timezone IANA (ex: "Europe/Paris")
  2207.      * @return string Code de locale (ex: "fr", "en")
  2208.      */
  2209.     private function getLocaleFromTimezone(string $timezone): string
  2210.     {
  2211.         // Mapping des timezones vers les codes de locale
  2212.         // Basé sur les timezones les plus courants
  2213.         $timezoneToLocale = [
  2214.             // Europe
  2215.             'Europe/Paris' => 'fr',
  2216.             'Europe/London' => 'en',
  2217.             'Europe/Dublin' => 'en',
  2218.             'Europe/Berlin' => 'de',
  2219.             'Europe/Madrid' => 'es',
  2220.             'Europe/Rome' => 'it',
  2221.             'Europe/Amsterdam' => 'nl',
  2222.             'Europe/Brussels' => 'fr',
  2223.             'Europe/Lisbon' => 'pt',
  2224.             'Europe/Athens' => 'el',
  2225.             'Europe/Warsaw' => 'pl',
  2226.             'Europe/Prague' => 'cs',
  2227.             'Europe/Budapest' => 'hu',
  2228.             'Europe/Stockholm' => 'sv',
  2229.             'Europe/Copenhagen' => 'da',
  2230.             'Europe/Helsinki' => 'fi',
  2231.             'Europe/Oslo' => 'no',
  2232.             'Europe/Zurich' => 'de',
  2233.             'Europe/Vienna' => 'de',
  2234.             'Europe/Bucharest' => 'ro',
  2235.             'Europe/Sofia' => 'bg',
  2236.             'Europe/Kiev' => 'uk',
  2237.             'Europe/Moscow' => 'ru',
  2238.             // Amérique du Nord
  2239.             'America/New_York' => 'en',
  2240.             'America/Chicago' => 'en',
  2241.             'America/Denver' => 'en',
  2242.             'America/Los_Angeles' => 'en',
  2243.             'America/Toronto' => 'en',
  2244.             'America/Montreal' => 'fr',
  2245.             'America/Vancouver' => 'en',
  2246.             'America/Mexico_City' => 'es',
  2247.             'America/Sao_Paulo' => 'pt',
  2248.             'America/Buenos_Aires' => 'es',
  2249.             'America/Lima' => 'es',
  2250.             'America/Bogota' => 'es',
  2251.             'America/Santiago' => 'es',
  2252.             // Asie
  2253.             'Asia/Tokyo' => 'ja',
  2254.             'Asia/Shanghai' => 'zh',
  2255.             'Asia/Hong_Kong' => 'zh',
  2256.             'Asia/Singapore' => 'en',
  2257.             'Asia/Seoul' => 'ko',
  2258.             'Asia/Dubai' => 'ar',
  2259.             'Asia/Riyadh' => 'ar',
  2260.             'Asia/Jerusalem' => 'he',
  2261.             'Asia/Mumbai' => 'hi',
  2262.             'Asia/Bangkok' => 'th',
  2263.             'Asia/Jakarta' => 'id',
  2264.             'Asia/Manila' => 'en',
  2265.             // Océanie
  2266.             'Australia/Sydney' => 'en',
  2267.             'Australia/Melbourne' => 'en',
  2268.             'Pacific/Auckland' => 'en',
  2269.             // Afrique
  2270.             'Africa/Cairo' => 'ar',
  2271.             'Africa/Johannesburg' => 'en',
  2272.             'Africa/Casablanca' => 'ar',
  2273.         ];
  2274.         // Vérifier si le timezone est dans le mapping
  2275.         if (isset($timezoneToLocale[$timezone])) {
  2276.             return $timezoneToLocale[$timezone];
  2277.         }
  2278.         // Si le timezone n'est pas dans le mapping, essayer de deviner depuis le nom
  2279.         // Exemple: Europe/Paris -> fr, America/New_York -> en
  2280.         $parts explode('/'$timezone);
  2281.         if (count($parts) >= 2) {
  2282.             $region $parts[0];
  2283.             $city $parts[1];
  2284.             // Règles de fallback basées sur la région
  2285.             if ($region === 'Europe') {
  2286.                 // Par défaut pour l'Europe, on peut utiliser 'en' ou 'fr' selon le contexte
  2287.                 // Mais on va privilégier 'fr' pour la France et ses territoires
  2288.                 if (
  2289.                     stripos($city'Paris') !== false ||
  2290.                     stripos($city'Lyon') !== false ||
  2291.                     stripos($city'Marseille') !== false ||
  2292.                     stripos($city'Brussels') !== false
  2293.                 ) {
  2294.                     return 'fr';
  2295.                 }
  2296.                 // Pour le Royaume-Uni et l'Irlande
  2297.                 if (
  2298.                     stripos($city'London') !== false ||
  2299.                     stripos($city'Dublin') !== false
  2300.                 ) {
  2301.                     return 'en';
  2302.                 }
  2303.                 // Par défaut pour l'Europe, on utilise 'en' (le plus courant)
  2304.                 return 'en';
  2305.             } elseif ($region === 'America') {
  2306.                 // Pour l'Amérique, on privilégie 'en' sauf pour les pays hispanophones
  2307.                 if (
  2308.                     stripos($city'Mexico') !== false ||
  2309.                     stripos($city'Buenos') !== false ||
  2310.                     stripos($city'Lima') !== false ||
  2311.                     stripos($city'Bogota') !== false ||
  2312.                     stripos($city'Santiago') !== false
  2313.                 ) {
  2314.                     return 'es';
  2315.                 }
  2316.                 if (
  2317.                     stripos($city'Sao_Paulo') !== false ||
  2318.                     stripos($city'Rio') !== false
  2319.                 ) {
  2320.                     return 'pt';
  2321.                 }
  2322.                 if (stripos($city'Montreal') !== false) {
  2323.                     return 'fr';
  2324.                 }
  2325.                 // Par défaut pour l'Amérique du Nord
  2326.                 return 'en';
  2327.             } elseif ($region === 'Asia') {
  2328.                 // Pour l'Asie, on privilégie 'en' pour les pays anglophones
  2329.                 if (
  2330.                     stripos($city'Singapore') !== false ||
  2331.                     stripos($city'Manila') !== false
  2332.                 ) {
  2333.                     return 'en';
  2334.                 }
  2335.                 // Par défaut pour l'Asie
  2336.                 return 'en';
  2337.             } elseif ($region === 'Australia' || $region === 'Pacific') {
  2338.                 return 'en';
  2339.             } elseif ($region === 'Africa') {
  2340.                 // Par défaut pour l'Afrique
  2341.                 return 'en';
  2342.             }
  2343.         }
  2344.         // Fallback par défaut : 'en' (anglais)
  2345.         return 'en';
  2346.     }
  2347.     private function ensureUtf8(string $string): string
  2348.     {
  2349.         // Si la chaîne est vide, la retourner telle quelle
  2350.         if (empty($string)) {
  2351.             return $string;
  2352.         }
  2353.         // Vérifier si la chaîne est déjà en UTF-8 valide
  2354.         if (mb_check_encoding($string'UTF-8')) {
  2355.             // Vérifier si c'est du UTF-8 double-encodé (ex: "è" au lieu de "è")
  2356.             $decoded = @iconv('UTF-8''ISO-8859-1//IGNORE'$string);
  2357.             if ($decoded !== false && mb_check_encoding($decoded'UTF-8')) {
  2358.                 return $decoded;
  2359.             }
  2360.             return $string;
  2361.         }
  2362.         // Essayer de détecter l'encodage source
  2363.         $detectedEncoding mb_detect_encoding($string, ['UTF-8''ISO-8859-1''Windows-1252''ASCII'], true);
  2364.         if ($detectedEncoding !== false && $detectedEncoding !== 'UTF-8') {
  2365.             $converted mb_convert_encoding($string'UTF-8'$detectedEncoding);
  2366.             if ($converted !== false) {
  2367.                 return $converted;
  2368.             }
  2369.         }
  2370.         // En dernier recours, forcer la conversion depuis ISO-8859-1
  2371.         $converted mb_convert_encoding($string'UTF-8''ISO-8859-1');
  2372.         if ($converted !== false) {
  2373.             return $converted;
  2374.         }
  2375.         // Si tout échoue, retourner la chaîne originale nettoyée des caractères invalides
  2376.         return mb_convert_encoding($string'UTF-8''UTF-8');
  2377.     }
  2378.     /**
  2379.      * Étape de collecte du numéro de téléphone après l'inscription
  2380.      * 
  2381.      * @Route("/onboarding/phone", name="onboarding.phone")
  2382.      * 
  2383.      * @param Request $request
  2384.      * @param EntityManagerInterface $entityManager
  2385.      * @return Response
  2386.      */
  2387.     public function phoneStep(
  2388.         Request $request,
  2389.         EntityManagerInterface $entityManager
  2390.     ): Response {
  2391.         /** @var User|null $user */
  2392.         $user $this->getUser();
  2393.         if (!$user) {
  2394.             return $this->redirectToRoute('home');
  2395.         }
  2396.         // Recharger l'utilisateur avec la relation client pour éviter les problèmes de proxy Doctrine
  2397.         $user $entityManager->getRepository(User::class)->find($user->getId());
  2398.         if (!$user) {
  2399.             return $this->redirectToRoute('home');
  2400.         }
  2401.         // Vérifier que le client existe (normalement toujours le cas avec nullable=false)
  2402.         $client $user->getClient();
  2403.         if (!$client) {
  2404.             // Log l'erreur et rediriger
  2405.             if ($this->logger) {
  2406.                 $this->logger->error('ONBOARDING: Utilisateur sans client', [
  2407.                     'user_id' => $user->getId(),
  2408.                     'email' => $user->getEmail(),
  2409.                 ]);
  2410.             }
  2411.             return $this->redirectToRoute('home');
  2412.         }
  2413.         // Si le téléphone est déjà renseigné, rediriger vers les suggestions
  2414.         if ($user->getPhone()) {
  2415.             return $this->redirectToRoute('suggestion.index');
  2416.         }
  2417.         $form $this->createForm(PhoneType::class, $user, [
  2418.             'action' => $this->generateUrl('ajax_user_update_phone'),
  2419.             'method' => 'POST',
  2420.         ]);
  2421.         return $this->render('onboarding/phone_step.html.twig', [
  2422.             'form' => $form->createView(),
  2423.             'client' => $client,
  2424.         ]);
  2425.     }
  2426.     /**
  2427.      * Route pour récupérer la configuration de l'extension Chrome
  2428.      * 
  2429.      * @Route("/api/config", name="api.config", methods={"GET"})
  2430.      * 
  2431.      * @return JsonResponse
  2432.      */
  2433.     public function getConfig(): JsonResponse
  2434.     {
  2435.         $chromeExtensionId $_ENV['CHROME_EXTENSION_ID'] ?? null;
  2436.         if (!$chromeExtensionId) {
  2437.             return new JsonResponse([
  2438.                 'error' => 'Extension ID non configuré'
  2439.             ], 500);
  2440.         }
  2441.         return new JsonResponse([
  2442.             'chromeExtensionId' => $chromeExtensionId
  2443.         ]);
  2444.     }
  2445.     /**
  2446.      * Retourne le statut de connexion LinkedIn de l'utilisateur courant.
  2447.      *
  2448.      * Objectif : permettre au front de "poller" après une connexion via extension/credentials,
  2449.      * et de synchroniser la session Symfony (token) avec l'état DB (identity_uid, status VALID).
  2450.      *
  2451.      * @Route("/api/onboarding/linkedin-status", name="api.onboarding.linkedin_status", methods={"GET"})
  2452.      *
  2453.      * @param Request $request Requête HTTP
  2454.      * @return JsonResponse Réponse JSON
  2455.      */
  2456.     public function getLinkedinStatus(Request $request): JsonResponse
  2457.     {
  2458.         /** @var User|null $user */
  2459.         $user $this->getUser();
  2460.         if (!$user) {
  2461.             return new JsonResponse([
  2462.                 'success' => false,
  2463.                 'connected' => false,
  2464.                 'error' => 'Utilisateur non authentifié',
  2465.             ], 401);
  2466.         }
  2467.         // Forcer une lecture DB fraîche (le User du token peut être obsolète)
  2468.         $freshUser $this->userRepository->find($user->getId());
  2469.         if (!$freshUser) {
  2470.             return new JsonResponse([
  2471.                 'success' => false,
  2472.                 'connected' => false,
  2473.                 'error' => 'Utilisateur introuvable',
  2474.             ], 401);
  2475.         }
  2476.         $identityUid $freshUser->getIdentityUid();
  2477.         $integrationStatus $freshUser->getLinkedinIntegrationStatus();
  2478.         $hasIntegration = ($freshUser->getHasLinkedinIntegration() === true);
  2479.         $connected $hasIntegration
  2480.             && $integrationStatus === 'VALID'
  2481.             && !empty($identityUid);
  2482.         // Synchroniser la session Symfony pour que Twig reflète la DB au prochain refresh
  2483.         $session $request->getSession();
  2484.         if (!$session->isStarted()) {
  2485.             $session->start();
  2486.         }
  2487.         $token = new UsernamePasswordToken($freshUser'main'$freshUser->getRoles());
  2488.         $this->tokenStorage->setToken($token);
  2489.         $session->set('_security_main'serialize($token));
  2490.         return new JsonResponse([
  2491.             'success' => true,
  2492.             'connected' => $connected,
  2493.             'user_id' => $freshUser->getId(),
  2494.             'identity_uid' => $identityUid,
  2495.             'has_linkedin_integration' => $hasIntegration,
  2496.             'linkedin_integration_status' => $integrationStatus,
  2497.             'has_extension' => $freshUser->getHasExtension(),
  2498.         ], 200);
  2499.     }
  2500.     /**
  2501.      * Route pour mettre à jour le statut de l'extension pour un utilisateur
  2502.      * 
  2503.      * @Route("/api/update-extension-status", name="api.update_extension_status", methods={"POST"})
  2504.      * 
  2505.      * @param Request $request
  2506.      * @return JsonResponse
  2507.      */
  2508.     public function updateExtensionStatus(Request $request): JsonResponse
  2509.     {
  2510.         /** @var User|null $user */
  2511.         $user $this->getUser();
  2512.         if (!$user) {
  2513.             return new JsonResponse([
  2514.                 'success' => false,
  2515.                 'error' => 'Utilisateur non authentifié'
  2516.             ], 401);
  2517.         }
  2518.         $data json_decode($request->getContent(), true);
  2519.         $hasExtension $data['hasExtension'] ?? false;
  2520.         $userId $data['userId'] ?? null;
  2521.         // Vérifier que l'utilisateur demande pour lui-même
  2522.         if ($userId && (int)$userId !== $user->getId()) {
  2523.             return new JsonResponse([
  2524.                 'success' => false,
  2525.                 'error' => 'Non autorisé'
  2526.             ], 403);
  2527.         }
  2528.         // Mettre à jour le statut de l'extension
  2529.         try {
  2530.             $user->setHasExtension($hasExtension 0);
  2531.             $user->setExtensionLastVerifiedAt(new \DateTimeImmutable());
  2532.             $this->entityManager->flush();
  2533.             $this->logger->info('ONBOARDING_V4: Statut extension mis à jour', [
  2534.                 'action' => 'update_extension_status',
  2535.                 'user_id' => $user->getId(),
  2536.                 'has_extension' => $hasExtension 0,
  2537.             ]);
  2538.             return new JsonResponse([
  2539.                 'success' => true,
  2540.                 'message' => 'Statut mis à jour',
  2541.                 'has_extension' => $hasExtension 0,
  2542.             ]);
  2543.         } catch (\Exception $e) {
  2544.             $this->logger->error('Erreur lors de la mise à jour du statut extension', [
  2545.                 'user_id' => $user->getId(),
  2546.                 'error' => $e->getMessage()
  2547.             ]);
  2548.             return new JsonResponse([
  2549.                 'success' => false,
  2550.                 'error' => 'Erreur serveur'
  2551.             ], 500);
  2552.         }
  2553.     }
  2554.     /**
  2555.      * Vérifie si la mise à jour du cookie li_at est nécessaire
  2556.      * 
  2557.      * @param User $user
  2558.      * @param string|null $newLiAtCookie Nouveau cookie li_at (si disponible) pour détecter un changement immédiat
  2559.      * @return bool True si la mise à jour est nécessaire (cookie différent, li_at_updated_at est null ou date de plus de 2 heures)
  2560.      */
  2561.     private function shouldUpdateLiAtCookie(User $user, ?string $newLiAtCookie null): bool
  2562.     {
  2563.         $liAtUpdatedAt $user->getLiAtUpdatedAt();
  2564.         // Si le cookie reçu est différent de celui en base, on doit TOUJOURS mettre à jour,
  2565.         // même si li_at_updated_at est récent (cas de reconnexion après invalidation LinkedIn).
  2566.         $currentCookie $user->getLiAt();
  2567.         if ($newLiAtCookie !== null) {
  2568.             $newLiAtCookie trim($newLiAtCookie);
  2569.         }
  2570.         $currentCookieTrimmed is_string($currentCookie) ? trim($currentCookie) : '';
  2571.         $cookieChanged = ($newLiAtCookie !== null && $newLiAtCookie !== '' && $newLiAtCookie !== $currentCookieTrimmed);
  2572.         if ($cookieChanged) {
  2573.             $this->logger->info('ONBOARDING_V4: Mise à jour du cookie nécessaire (cookie différent)', [
  2574.                 'action' => 'check_cookie_update',
  2575.                 'user_id' => $user->getId(),
  2576.                 'li_at_updated_at' => $liAtUpdatedAt $liAtUpdatedAt->format('Y-m-d H:i:s') : null,
  2577.                 'old_cookie_preview' => $currentCookieTrimmed !== '' && strlen($currentCookieTrimmed) > 20
  2578.                     substr($currentCookieTrimmed010) . '...' substr($currentCookieTrimmed, -10)
  2579.                     : ($currentCookieTrimmed !== '' $currentCookieTrimmed null),
  2580.                 'new_cookie_preview' => strlen($newLiAtCookie) > 20
  2581.                     substr($newLiAtCookie010) . '...' substr($newLiAtCookie, -10)
  2582.                     : $newLiAtCookie,
  2583.             ]);
  2584.             return true;
  2585.         }
  2586.         // Si li_at_updated_at est null, la mise à jour est nécessaire
  2587.         if ($liAtUpdatedAt === null) {
  2588.             $this->logger->info('ONBOARDING_V4: Mise à jour du cookie nécessaire (li_at_updated_at est null)', [
  2589.                 'action' => 'check_cookie_update',
  2590.                 'user_id' => $user->getId(),
  2591.             ]);
  2592.             return true;
  2593.         }
  2594.         // Vérifier si la date est plus ancienne que 2 heures
  2595.         $twoHoursAgo = new \DateTimeImmutable('-2 hours');
  2596.         $needsUpdate $liAtUpdatedAt $twoHoursAgo;
  2597.         if ($needsUpdate) {
  2598.             $this->logger->info('ONBOARDING_V4: Mise à jour du cookie nécessaire (li_at_updated_at date de plus de 2 heures)', [
  2599.                 'action' => 'check_cookie_update',
  2600.                 'user_id' => $user->getId(),
  2601.                 'li_at_updated_at' => $liAtUpdatedAt->format('Y-m-d H:i:s'),
  2602.                 'two_hours_ago' => $twoHoursAgo->format('Y-m-d H:i:s'),
  2603.             ]);
  2604.         } else {
  2605.             $this->logger->info('ONBOARDING_V4: Mise à jour du cookie non nécessaire (li_at_updated_at est récent)', [
  2606.                 'action' => 'check_cookie_update',
  2607.                 'user_id' => $user->getId(),
  2608.                 'li_at_updated_at' => $liAtUpdatedAt->format('Y-m-d H:i:s'),
  2609.                 'two_hours_ago' => $twoHoursAgo->format('Y-m-d H:i:s'),
  2610.             ]);
  2611.         }
  2612.         return $needsUpdate;
  2613.     }
  2614.     /**
  2615.      * Route pour sauvegarder les cookies LinkedIn récupérés par l'extension
  2616.      * 
  2617.      * @Route("/api/save-cookies", name="api.save_cookies", methods={"POST"})
  2618.      * 
  2619.      * @param Request $request
  2620.      * @return JsonResponse
  2621.      */
  2622.     public function saveCookies(Request $request): JsonResponse
  2623.     {
  2624.         $this->logger->info('ONBOARDING_V4: saveCookies appelé', [
  2625.             'action' => 'save_cookies',
  2626.             'method' => $request->getMethod(),
  2627.             'content_type' => $request->headers->get('Content-Type'),
  2628.             'raw_content_length' => strlen($request->getContent()),
  2629.         ]);
  2630.         /** @var User|null $user */
  2631.         $user $this->getUser();
  2632.         if (!$user) {
  2633.             $this->logger->warning('ONBOARDING_V4: saveCookies - Utilisateur non authentifié', [
  2634.                 'action' => 'save_cookies',
  2635.             ]);
  2636.             return new JsonResponse([
  2637.                 'success' => false,
  2638.                 'error' => 'Utilisateur non authentifié'
  2639.             ], 401);
  2640.         }
  2641.         $rawContent $request->getContent();
  2642.         $data json_decode($rawContenttrue);
  2643.         $jsonError json_last_error();
  2644.         $this->logger->info('ONBOARDING_V4: Contenu JSON parsé', [
  2645.             'action' => 'save_cookies',
  2646.             'user_id' => $user->getId(),
  2647.             'json_error' => $jsonError !== JSON_ERROR_NONE json_last_error_msg() : 'none',
  2648.             'raw_content_preview' => substr($rawContent0200),
  2649.             'data_keys' => is_array($data) ? array_keys($data) : 'not_array',
  2650.         ]);
  2651.         $cookies $data['cookies'] ?? [];
  2652.         $this->logger->info('ONBOARDING_V4: Données reçues dans save-cookies', [
  2653.             'action' => 'save_cookies',
  2654.             'user_id' => $user->getId(),
  2655.             'has_cookies_key' => isset($data['cookies']),
  2656.             'cookies_keys' => is_array($cookies) ? array_keys($cookies) : 'not_array',
  2657.             'has_li_at' => isset($cookies['li_at']),
  2658.             'li_at_type' => isset($cookies['li_at']) ? gettype($cookies['li_at']) : 'not_set',
  2659.             'li_at_length' => isset($cookies['li_at']) ? strlen($cookies['li_at']) : 0,
  2660.             'raw_data_keys' => is_array($data) ? array_keys($data) : 'not_array',
  2661.         ]);
  2662.         if (!isset($cookies['li_at']) || empty(trim($cookies['li_at'] ?? ''))) {
  2663.             $this->logger->error('ONBOARDING_V4: Cookie li_at manquant ou vide dans save-cookies', [
  2664.                 'action' => 'save_cookies',
  2665.                 'user_id' => $user->getId(),
  2666.                 'cookies_received' => $cookies,
  2667.             ]);
  2668.             return new JsonResponse([
  2669.                 'success' => false,
  2670.                 'error' => 'li_at cookie is missing or empty'
  2671.             ], 400);
  2672.         }
  2673.         $liAtCookie trim($cookies['li_at']);
  2674.         $this->logger->info('ONBOARDING_V4: Cookies reçus via save-cookies', [
  2675.             'action' => 'save_cookies',
  2676.             'user_id' => $user->getId(),
  2677.             'has_li_at' => !empty($liAtCookie),
  2678.         ]);
  2679.         // S'assurer que la session est démarrée (nécessaire pour mettre à jour le token Symfony)
  2680.         $session $request->getSession();
  2681.         if (!$session->isStarted()) {
  2682.             $session->start();
  2683.         }
  2684.         // Vérifier si la mise à jour est nécessaire
  2685.         if (!$this->shouldUpdateLiAtCookie($user$liAtCookie)) {
  2686.             $this->logger->info('ONBOARDING_V4: Cookie non mis à jour (li_at_updated_at est récent)', [
  2687.                 'action' => 'save_cookies',
  2688.                 'user_id' => $user->getId(),
  2689.             ]);
  2690.             // Rafraîchir l'utilisateur en session pour refléter l'état DB (identity_uid, flags, etc.)
  2691.             // Même si le cookie ne change pas, la session Symfony peut être obsolète.
  2692.             try {
  2693.                 $this->entityManager->refresh($user);
  2694.             } catch (\Exception $e) {
  2695.                 // Best-effort : si refresh échoue (user détaché), on continue.
  2696.             }
  2697.             $token = new UsernamePasswordToken($user'main'$user->getRoles());
  2698.             $this->tokenStorage->setToken($token);
  2699.             $session->set('_security_main'serialize($token));
  2700.             return new JsonResponse([
  2701.                 'success' => true,
  2702.                 'message' => 'Cookie déjà à jour',
  2703.                 'skipped' => true
  2704.             ]);
  2705.         }
  2706.         // Utiliser la même logique que connect_linkedin_extension
  2707.         // IMPORTANT: connectLinkedInExtension attend directement ['li_at' => '...'] et non ['cookies' => ['li_at' => '...']]
  2708.         $internalRequestContent json_encode(['li_at' => $liAtCookie]);
  2709.         $this->logger->info('ONBOARDING_V4: Création requête interne pour connectLinkedInExtension', [
  2710.             'action' => 'save_cookies',
  2711.             'user_id' => $user->getId(),
  2712.             'internal_request_content_length' => strlen($internalRequestContent),
  2713.             'internal_request_content_preview' => substr($internalRequestContent0100),
  2714.         ]);
  2715.         // Créer une nouvelle requête en dupliquant la requête originale pour préserver la session
  2716.         // Puis remplacer le contenu
  2717.         $internalRequest $request->duplicate();
  2718.         $internalRequest->setMethod('POST');
  2719.         $internalRequest->headers->set('Content-Type''application/json');
  2720.         // Utiliser la réflexion pour modifier le contenu de la requête
  2721.         $reflection = new \ReflectionClass($internalRequest);
  2722.         $contentProperty $reflection->getProperty('content');
  2723.         $contentProperty->setAccessible(true);
  2724.         $contentProperty->setValue($internalRequest$internalRequestContent);
  2725.         try {
  2726.             // Appeler directement la méthode
  2727.             $response $this->connectLinkedInExtension($internalRequest);
  2728.             $responseData json_decode($response->getContent(), true);
  2729.             if ($response->getStatusCode() === 200 && isset($responseData['success']) && $responseData['success']) {
  2730.                 $this->logger->info('ONBOARDING_V4: Cookies sauvegardés avec succès via save-cookies', [
  2731.                     'action' => 'save_cookies',
  2732.                     'user_id' => $user->getId(),
  2733.                 ]);
  2734.                 // Recharger l'utilisateur depuis la base pour avoir les dernières données
  2735.                 $this->entityManager->refresh($user);
  2736.                 // Rafraîchir le token Symfony en session pour que le refresh de page reflète la DB.
  2737.                 $token = new UsernamePasswordToken($user'main'$user->getRoles());
  2738.                 $this->tokenStorage->setToken($token);
  2739.                 $session->set('_security_main'serialize($token));
  2740.                 // Déclencher le traitement des messages en attente après synchronisation du cookie
  2741.                 $this->triggerPendingMessagesIfNeeded($user);
  2742.                 return new JsonResponse([
  2743.                     'success' => true,
  2744.                     'message' => 'Cookies sauvegardés avec succès'
  2745.                 ]);
  2746.             } else {
  2747.                 $this->logger->error('ONBOARDING_V4: Erreur lors de la sauvegarde des cookies', [
  2748.                     'action' => 'save_cookies',
  2749.                     'user_id' => $user->getId(),
  2750.                     'error' => $responseData['error'] ?? 'Erreur inconnue',
  2751.                 ]);
  2752.                 return new JsonResponse([
  2753.                     'success' => false,
  2754.                     'error' => $responseData['error'] ?? 'Erreur lors de la sauvegarde'
  2755.                 ], $response->getStatusCode());
  2756.             }
  2757.         } catch (\Exception $e) {
  2758.             $this->logger->error('ONBOARDING_V4: Exception lors de la sauvegarde des cookies', [
  2759.                 'action' => 'save_cookies',
  2760.                 'user_id' => $user->getId(),
  2761.                 'error' => $e->getMessage(),
  2762.                 // La trace peut contenir des valeurs sensibles (cookies) => on masque.
  2763.                 'trace' => $this->redactSensitiveDataForLogs((string) $e->getTraceAsString()),
  2764.             ]);
  2765.             return new JsonResponse([
  2766.                 'success' => false,
  2767.                 'error' => 'Erreur serveur'
  2768.             ], 500);
  2769.         }
  2770.     }
  2771.     /**
  2772.      * Sauvegarde les identifiants LinkedIn chiffrés pour reconnexion automatique
  2773.      *
  2774.      * @param User $user Utilisateur concerné
  2775.      * @param string $linkedinEmail Email LinkedIn en clair
  2776.      * @param string $linkedinPassword Mot de passe LinkedIn en clair
  2777.      * @return void
  2778.      */
  2779.     private function saveLinkedinCredentials(User $userstring $linkedinEmailstring $linkedinPassword): void
  2780.     {
  2781.         // Logger dans user_activity le début de la sauvegarde
  2782.         try {
  2783.             $userLogger $this->userLoggerFactory->createLogger($user->getId());
  2784.             $userLogger->debug('ONBOARDING_V4: Début sauvegarde identifiants LinkedIn', [
  2785.                 'action' => 'save_linkedin_credentials',
  2786.                 'step' => 'start',
  2787.                 'email_length' => strlen($linkedinEmail),
  2788.                 'password_length' => strlen($linkedinPassword),
  2789.             ]);
  2790.         } catch (\Exception $logException) {
  2791.             // Ignorer les erreurs de log pour ne pas bloquer le processus
  2792.         }
  2793.         $this->logger->debug('ONBOARDING_V4: Début chiffrement identifiants LinkedIn', [
  2794.             'action' => 'save_linkedin_credentials',
  2795.             'user_id' => $user->getId(),
  2796.             'email_length' => strlen($linkedinEmail),
  2797.             'password_length' => strlen($linkedinPassword),
  2798.         ]);
  2799.         try {
  2800.             $encryptedEmail $this->encryptionService->encrypt($linkedinEmail);
  2801.             $encryptedPassword $this->encryptionService->encrypt($linkedinPassword);
  2802.             $this->logger->debug('ONBOARDING_V4: Résultat chiffrement identifiants LinkedIn', [
  2803.                 'action' => 'save_linkedin_credentials',
  2804.                 'user_id' => $user->getId(),
  2805.                 'email_encrypted' => $encryptedEmail !== null,
  2806.                 'email_encrypted_length' => $encryptedEmail strlen($encryptedEmail) : 0,
  2807.                 'password_encrypted' => $encryptedPassword !== null,
  2808.                 'password_encrypted_length' => $encryptedPassword strlen($encryptedPassword) : 0,
  2809.             ]);
  2810.             if ($encryptedEmail && $encryptedPassword) {
  2811.                 $this->logger->debug('ONBOARDING_V4: Sauvegarde identifiants chiffrés en base de données', [
  2812.                     'action' => 'save_linkedin_credentials',
  2813.                     'user_id' => $user->getId(),
  2814.                 ]);
  2815.                 $user->setLinkedinEmailEncrypted($encryptedEmail);
  2816.                 $user->setLinkedinPasswordEncrypted($encryptedPassword);
  2817.                 $savedAt = new \DateTimeImmutable();
  2818.                 $user->setLinkedinCredentialsSavedAt($savedAt);
  2819.                 $this->entityManager->flush();
  2820.                 $this->logger->debug('ONBOARDING_V4: Identifiants sauvegardés en base de données avec succès', [
  2821.                     'action' => 'save_linkedin_credentials',
  2822.                     'user_id' => $user->getId(),
  2823.                     'saved_at' => $savedAt->format('Y-m-d H:i:s'),
  2824.                 ]);
  2825.                 $this->logger->info('ONBOARDING_V4: Identifiants LinkedIn sauvegardés avec succès', [
  2826.                     'action' => 'save_linkedin_credentials',
  2827.                     'user_id' => $user->getId(),
  2828.                     'saved_at' => $user->getLinkedinCredentialsSavedAt()->format('Y-m-d H:i:s'),
  2829.                 ]);
  2830.                 // Logger dans user_activity/[ID].log - Succès
  2831.                 $userLogger $this->userLoggerFactory->createLogger($user->getId());
  2832.                 $userLogger->debug('ONBOARDING_V4: Chiffrement identifiants LinkedIn réussi', [
  2833.                     'action' => 'save_linkedin_credentials',
  2834.                     'step' => 'encryption_success',
  2835.                     'email_encrypted_length' => strlen($encryptedEmail),
  2836.                     'password_encrypted_length' => strlen($encryptedPassword),
  2837.                 ]);
  2838.                 $userLogger->info('ONBOARDING_V4: Identifiants LinkedIn sauvegardés pour reconnexion automatique', [
  2839.                     'action' => 'save_linkedin_credentials',
  2840.                     'step' => 'credentials_saved',
  2841.                     'saved_at' => $user->getLinkedinCredentialsSavedAt()->format('Y-m-d H:i:s'),
  2842.                 ]);
  2843.             } else {
  2844.                 // Déterminer la cause de l'échec
  2845.                 $errorCause = [];
  2846.                 if (!$encryptedEmail) {
  2847.                     $errorCause[] = 'email_chiffrement_echec';
  2848.                 }
  2849.                 if (!$encryptedPassword) {
  2850.                     $errorCause[] = 'password_chiffrement_echec';
  2851.                 }
  2852.                 $errorCauseStr implode(', '$errorCause);
  2853.                 $this->logger->error('ONBOARDING_V4: Échec du chiffrement des identifiants LinkedIn', [
  2854.                     'action' => 'save_linkedin_credentials',
  2855.                     'user_id' => $user->getId(),
  2856.                     'error_cause' => $errorCauseStr,
  2857.                     'email_encrypted' => $encryptedEmail !== null,
  2858.                     'password_encrypted' => $encryptedPassword !== null,
  2859.                 ]);
  2860.                 // Logger l'erreur dans user_activity/[ID].log
  2861.                 $userLogger $this->userLoggerFactory->createLogger($user->getId());
  2862.                 $userLogger->error('ONBOARDING_V4: Échec de la sauvegarde des identifiants LinkedIn - Chiffrement échoué', [
  2863.                     'action' => 'save_linkedin_credentials',
  2864.                     'step' => 'encryption_failed',
  2865.                     'error_cause' => $errorCauseStr,
  2866.                     'email_encrypted' => $encryptedEmail !== null,
  2867.                     'password_encrypted' => $encryptedPassword !== null,
  2868.                 ]);
  2869.             }
  2870.         } catch (\Exception $e) {
  2871.             $this->logger->error('ONBOARDING_V4: Exception lors de la sauvegarde des identifiants LinkedIn', [
  2872.                 'action' => 'save_linkedin_credentials',
  2873.                 'user_id' => $user->getId(),
  2874.                 'error' => $e->getMessage(),
  2875.                 'error_class' => get_class($e),
  2876.                 // Attention : la trace peut contenir les arguments (dont le mot de passe LinkedIn) => on masque.
  2877.                 'trace' => $this->redactSensitiveDataForLogs((string) $e->getTraceAsString(), [
  2878.                     isset($linkedinPassword) ? (string) $linkedinPassword null,
  2879.                     isset($linkedinEmail) ? (string) $linkedinEmail null,
  2880.                 ]),
  2881.             ]);
  2882.             // Logger l'exception dans user_activity/[ID].log
  2883.             try {
  2884.                 $userLogger $this->userLoggerFactory->createLogger($user->getId());
  2885.                 $userLogger->error('ONBOARDING_V4: Exception lors de la sauvegarde des identifiants LinkedIn', [
  2886.                     'action' => 'save_linkedin_credentials',
  2887.                     'step' => 'exception',
  2888.                     'error' => $e->getMessage(),
  2889.                     'error_class' => get_class($e),
  2890.                     'error_code' => $e->getCode(),
  2891.                 ]);
  2892.             } catch (\Exception $logException) {
  2893.                 // Si on ne peut pas logger dans user_activity, on logge dans le logger principal
  2894.                 $this->logger->warning('ONBOARDING_V4: Impossible de logger dans user_activity', [
  2895.                     'action' => 'save_linkedin_credentials',
  2896.                     'user_id' => $user->getId(),
  2897.                     'log_error' => $logException->getMessage(),
  2898.                 ]);
  2899.             }
  2900.         }
  2901.     }
  2902.     /**
  2903.      * Endpoint API pour la reconnexion silencieuse (Extension)
  2904.      * 
  2905.      * Utilisé lorsque la session LinkedIn est expirée et que l'extension
  2906.      * peut fournir un nouveau cookie pour recréer l'intégration.
  2907.      * 
  2908.      * @Route("/api/onboarding/reconnect-silent", name="api.onboarding.reconnect_silent", methods={"POST"})
  2909.      *
  2910.      * @param Request $request Requête HTTP
  2911.      * @return JsonResponse Réponse JSON
  2912.      */
  2913.     public function reconnectSilent(Request $request): JsonResponse
  2914.     {
  2915.         /** @var User|null $user */
  2916.         $user $this->getUser();
  2917.         if (!$user) {
  2918.             return new JsonResponse([
  2919.                 'success' => false,
  2920.                 'error' => 'Utilisateur non authentifié',
  2921.             ], 401);
  2922.         }
  2923.         $userLogger $this->userLoggerFactory->createLogger($user->getId());
  2924.         $this->logger->info('Checking extension for cookie', [
  2925.             'user_id' => $user->getId(),
  2926.             'has_extension' => $user->getHasExtension(),
  2927.             'current_status' => $user->getLinkedinIntegrationStatus(),
  2928.         ]);
  2929.         // 1. Récupérer le cookie li_at depuis la requête
  2930.         $data json_decode($request->getContent(), true);
  2931.         if (!isset($data['li_at']) || empty(trim($data['li_at']))) {
  2932.             $this->logger->error('RECONNEXION_V4: Cookie li_at manquant dans la requête', [
  2933.                 'user_id' => $user->getId(),
  2934.             ]);
  2935.             return new JsonResponse([
  2936.                 'success' => false,
  2937.                 'error' => 'Cookie li_at manquant',
  2938.             ], 400);
  2939.         }
  2940.         $liAtCookie trim($data['li_at']);
  2941.         $oldCookie is_string($user->getLiAt()) ? trim((string) $user->getLiAt()) : '';
  2942.         $cookieChanged = ($oldCookie === '') ? ($liAtCookie !== '') : ($liAtCookie !== $oldCookie);
  2943.         $cookiePrefix substr($liAtCookie015) . '...';
  2944.         $this->logger->info('Extension has cookie', [
  2945.             'user_id' => $user->getId(),
  2946.             'cookie_prefix' => $cookiePrefix,
  2947.             'cookie_length' => strlen($liAtCookie),
  2948.             'cookie_changed' => $cookieChanged,
  2949.         ]);
  2950.         $userLogger->info('RECONNEXION_V4_SILENT: Cookie li_at reçu', [
  2951.             'action' => 'reconnect_silent',
  2952.             'step' => 'cookie_received',
  2953.             'cookie_length' => strlen($liAtCookie),
  2954.             'cookie_changed' => $cookieChanged,
  2955.         ]);
  2956.         // 2. Vérifier si une reconnexion est nécessaire.
  2957.         // IMPORTANT: même si le statut en base est "VALID", on doit tenter une reconnexion
  2958.         // si le cookie fourni par l'extension est différent (cas réel: LinkedIn invalide l'ancien cookie).
  2959.         $needsReconnection $this->reconnectionService->needsReconnection($user);
  2960.         if (!$needsReconnection && !$cookieChanged) {
  2961.             $this->logger->info('RECONNEXION_V4: Reconnexion non nécessaire', [
  2962.                 'user_id' => $user->getId(),
  2963.                 'status' => $user->getLinkedinIntegrationStatus(),
  2964.                 'has_extension' => $user->getHasExtension(),
  2965.                 'cookie_changed' => false,
  2966.             ]);
  2967.             return new JsonResponse([
  2968.                 'success' => true,
  2969.                 'message' => 'Reconnexion non nécessaire',
  2970.                 'already_connected' => true,
  2971.             ], 200);
  2972.         }
  2973.         // 3. Effectuer la reconnexion silencieuse (création / mise à jour intégration v4)
  2974.         try {
  2975.             $result $this->reconnectionService->reconnectSilently($user$liAtCookie);
  2976.             $this->logger->info('RECONNEXION_V4: Reconnexion silencieuse réussie', [
  2977.                 'user_id' => $user->getId(),
  2978.                 'integration_uid' => $result['integration_uid'],
  2979.                 'cookie_changed' => $result['cookie_changed'],
  2980.             ]);
  2981.             return new JsonResponse([
  2982.                 'success' => true,
  2983.                 'message' => 'Reconnexion silencieuse réussie',
  2984.                 'integration_uid' => $result['integration_uid'],
  2985.                 'cookie_changed' => $result['cookie_changed'],
  2986.             ], 200);
  2987.         } catch (\Exception $e) {
  2988.             $this->logger->error('RECONNEXION_V4: Erreur lors de la reconnexion silencieuse', [
  2989.                 'user_id' => $user->getId(),
  2990.                 'error' => $e->getMessage(),
  2991.             ]);
  2992.             $userLogger->error('RECONNEXION_V4_SILENT: Erreur lors de la reconnexion', [
  2993.                 'action' => 'reconnect_silent',
  2994.                 'step' => 'error',
  2995.                 'error' => $e->getMessage(),
  2996.             ]);
  2997.             return new JsonResponse([
  2998.                 'success' => false,
  2999.                 'error' => $e->getMessage(),
  3000.             ], 500);
  3001.         }
  3002.     }
  3003.     /**
  3004.      * Endpoint API pour la reconnexion via identifiants sauvegardés (Credentials)
  3005.      *
  3006.      * Utilisé lorsque la session LinkedIn est expirée et que l'utilisateur a choisi
  3007.      * le parcours "credentials" (email/password). L'objectif est de recréer l'intégration
  3008.      * LinkedIn v4 sans demander à l'utilisateur de repasser par un checkpoint, si possible.
  3009.      *
  3010.      * @Route("/api/onboarding/reconnect-credentials", name="api.onboarding.reconnect_credentials", methods={"POST"})
  3011.      *
  3012.      * @return JsonResponse Réponse JSON
  3013.      */
  3014.     public function reconnectCredentials(): JsonResponse
  3015.     {
  3016.         /** @var User|null $user */
  3017.         $user $this->getUser();
  3018.         if (!$user) {
  3019.             return new JsonResponse([
  3020.                 'success' => false,
  3021.                 'error' => 'Utilisateur non authentifié',
  3022.             ], 401);
  3023.         }
  3024.         $userLogger $this->userLoggerFactory->createLogger($user->getId());
  3025.         if (!$user->getIdentityUid()) {
  3026.             return new JsonResponse([
  3027.                 'success' => false,
  3028.                 'error' => 'Identity UID manquant',
  3029.             ], 400);
  3030.         }
  3031.         if (!$user->hasValidLinkedinCredentials(90)) {
  3032.             return new JsonResponse([
  3033.                 'success' => false,
  3034.                 'error' => 'Identifiants LinkedIn non sauvegardés ou expirés',
  3035.             ], 400);
  3036.         }
  3037.         $encryptedEmail $user->getLinkedinEmailEncrypted();
  3038.         $encryptedPassword $user->getLinkedinPasswordEncrypted();
  3039.         if (!$encryptedEmail || !$encryptedPassword) {
  3040.             return new JsonResponse([
  3041.                 'success' => false,
  3042.                 'error' => 'Identifiants LinkedIn manquants',
  3043.             ], 400);
  3044.         }
  3045.         $email $this->encryptionService->decrypt($encryptedEmail);
  3046.         $password $this->encryptionService->decrypt($encryptedPassword);
  3047.         if (!$email || !$password) {
  3048.             $userLogger->error('RECONNEXION_V4_CREDENTIALS: Déchiffrement identifiants impossible', [
  3049.                 'action' => 'reconnect_credentials',
  3050.                 'email_decrypted' => $email !== null,
  3051.                 'password_decrypted' => $password !== null,
  3052.             ]);
  3053.             return new JsonResponse([
  3054.                 'success' => false,
  3055.                 'error' => 'Impossible de déchiffrer les identifiants LinkedIn sauvegardés',
  3056.             ], 500);
  3057.         }
  3058.         $identityUid = (string) $user->getIdentityUid();
  3059.         $this->logger->info('RECONNEXION_V4_CREDENTIALS: Tentative reconnexion via credentials', [
  3060.             'user_id' => $user->getId(),
  3061.             'identity_uid' => $identityUid,
  3062.         ]);
  3063.         try {
  3064.             $integrationData $this->captainDataV4Service->createLinkedInIntegrationWithCredentials(
  3065.                 $identityUid,
  3066.                 $email,
  3067.                 $password,
  3068.                 null
  3069.             );
  3070.             $status = isset($integrationData['status']) ? (string) $integrationData['status'] : 'UNKNOWN';
  3071.             $integrationUid = isset($integrationData['uid']) ? (string) $integrationData['uid'] : null;
  3072.             $checkpoint = isset($integrationData['checkpoint']) ? $integrationData['checkpoint'] : null;
  3073.             $user->setLinkedinIntegrationStatus($status);
  3074.             $user->setLinkedinIntegrationLastVerifiedAt(new \DateTimeImmutable());
  3075.             $user->setHasLinkedinIntegration($status === 'VALID');
  3076.             $this->entityManager->flush();
  3077.             $userLogger->info('RECONNEXION_V4_CREDENTIALS: Réponse intégration LinkedIn', [
  3078.                 'action' => 'reconnect_credentials',
  3079.                 'status' => $status,
  3080.                 'integration_uid' => $integrationUid,
  3081.                 'has_checkpoint' => $checkpoint !== null,
  3082.             ]);
  3083.             if ($status !== 'VALID') {
  3084.                 return new JsonResponse([
  3085.                     'success' => false,
  3086.                     'error' => 'Reconnexion incomplète : intégration LinkedIn non VALID',
  3087.                     'status' => $status,
  3088.                     'checkpoint' => $checkpoint,
  3089.                     'integration_uid' => $integrationUid,
  3090.                 ], 202);
  3091.             }
  3092.             $this->triggerPendingMessagesIfNeeded($user);
  3093.             return new JsonResponse([
  3094.                 'success' => true,
  3095.                 'message' => 'Reconnexion via credentials réussie',
  3096.                 'integration_uid' => $integrationUid,
  3097.                 'status' => $status,
  3098.             ], 200);
  3099.         } catch (\Exception $e) {
  3100.             $this->logger->error('RECONNEXION_V4_CREDENTIALS: Erreur reconnexion via credentials', [
  3101.                 'user_id' => $user->getId(),
  3102.                 'identity_uid' => $identityUid,
  3103.                 'error' => $e->getMessage(),
  3104.             ]);
  3105.             $userLogger->error('RECONNEXION_V4_CREDENTIALS: Erreur reconnexion via credentials', [
  3106.                 'action' => 'reconnect_credentials',
  3107.                 'step' => 'error',
  3108.                 'error' => $e->getMessage(),
  3109.             ]);
  3110.             return new JsonResponse([
  3111.                 'success' => false,
  3112.                 'error' => $e->getMessage(),
  3113.             ], 500);
  3114.         }
  3115.     }
  3116.     /**
  3117.      * Déclenche le traitement des messages en attente si l'intégration LinkedIn est VALID
  3118.      * 
  3119.      * @param User $user Utilisateur
  3120.      * @return void
  3121.      */
  3122.     private function triggerPendingMessagesIfNeeded(User $user): void
  3123.     {
  3124.         // Vérifier s'il y a des messages en attente
  3125.         $messageRepository $this->entityManager->getRepository(\App\Entity\Message::class);
  3126.         $pendingMessages $messageRepository->findBy([
  3127.             'user' => $user,
  3128.             'status' => \App\Entity\Message::STATUS_PENDING,
  3129.         ]);
  3130.         if (empty($pendingMessages)) {
  3131.             return; // Pas de messages en attente
  3132.         }
  3133.         $this->logger->info('ONBOARDING_V4: Messages en attente détectés, déclenchement du job', [
  3134.             'user_id' => $user->getId(),
  3135.             'pending_count' => count($pendingMessages),
  3136.         ]);
  3137.         // Vérifier s'il existe déjà un job en attente
  3138.         $existingJob $this->entityManager->getRepository(CaptaindataJob::class)->findOneBy([
  3139.             'user' => $user,
  3140.             'workflowType' => CaptaindataJob::WORKFLOW_TYPE_SEND_MESSAGES,
  3141.             'status' => CaptaindataJob::STATUS_SEND_MESSAGES_PENDING,
  3142.         ]);
  3143.         if ($existingJob) {
  3144.             $this->logger->info('ONBOARDING_V4: Job existant trouvé, re-dispatch', [
  3145.                 'user_id' => $user->getId(),
  3146.                 'job_id' => $existingJob->getId(),
  3147.             ]);
  3148.             $this->bus->dispatch(new ProcessPendingMessagesJob($existingJob->getId()));
  3149.             return;
  3150.         }
  3151.         // Créer un nouveau job
  3152.         $workflowUid $this->params->get('app.captaindata_workflow_send_message_uid');
  3153.         $captainDataJob = new CaptaindataJob(
  3154.             $user,
  3155.             CaptaindataJob::WORKFLOW_TYPE_SEND_MESSAGES,
  3156.             $workflowUid,
  3157.             CaptaindataJob::STATUS_SEND_MESSAGES_PENDING
  3158.         );
  3159.         $this->entityManager->persist($captainDataJob);
  3160.         $this->entityManager->flush();
  3161.         $this->bus->dispatch(new ProcessPendingMessagesJob($captainDataJob->getId()));
  3162.         $this->logger->info('ONBOARDING_V4: Nouveau job créé et dispatché pour messages en attente', [
  3163.             'user_id' => $user->getId(),
  3164.             'job_id' => $captainDataJob->getId(),
  3165.             'pending_count' => count($pendingMessages),
  3166.         ]);
  3167.     }
  3168. }