UserController refactoring & testing - 06/05/2026
1. Dead code et refactor
Section titled “1. Dead code et refactor”1.1 UpdateSkillV2
Section titled “1.1 UpdateSkillV2”- Tout ce qui touche à WorkerProfile->skills est inutile car les skills ont leur propre table d’assignment et utilisent Redis pour le matching. C’est un deadCode
- UpdateSkillV2 et UpdateReadyToWorkV2 ne devraient faire qu’un, le découpage devrait se faire côté service et non controller, 2 requêtes pour une action n’ont aucun sens
1.2 UpdateToken
Section titled “1.2 UpdateToken”- Avec le refactor en multiEnterprise il faudra absolument modifier le test qui impose un refresh de toutes les entreprises en cas de mauvais/pas enterpriseId. On renverra à la place une erreur à ce moment-là.
1.3 DeleteAccount
Section titled “1.3 DeleteAccount”- passer toutes les requêtes en Eloquent
- le système n’est pas bon, il ne supprime pas les assignments, ne broadcast pas les users, ne met pas à jour Redis, et s’il le faisait ne pourrait pas revert Redis en cas d’erreur.
- le design “cancel brutal de toutes les missions ouvertes” n’est pas viable (orphan data, cascades non gérées). À remplacer par : refus de delete tant qu’un engagement actif existe (mission open/matched/in_progress côté owner, assignment pending côté worker). L’user résout ses engagements via les endpoints existants (cancelMission, decline match) puis peut delete proprement.
1.4 Infra restore-account post soft-delete
Section titled “1.4 Infra restore-account post soft-delete”- À implémenter pour soutenir la puce “email + recovery” de 2.8 :
- Mailable
AccountDeletedRecoverableavec lien signé (URL::signedRoute) valide 25 jours - Route
GET /api/restore-account/{user_id}/{signature}qui revert le soft delete (deleted_at = null) PurgeDeletedUsersdoit vérifier quedeleted_atest toujours set au moment du hard delete (sécurité contre race conditions / restore tardif)
- Mailable
- Fenêtre de 25 jours côté user-facing, hard delete à 30 jours côté cron : buffer de 5 jours pour absorber les restores last-minute.
2. Tests de chaque route — approche features-first (non-unit)
Section titled “2. Tests de chaque route — approche features-first (non-unit)”2.1 getUserPublicProfile
Section titled “2.1 getUserPublicProfile”- utilisateur non authentifié -> 401
- doit fournir le champ [user_id: int] (sinon -> 422)
- si aucun utilisateur ne correspond au user_id fourni -> 422
- doit retourner les champs [id, first_name, last_name, profile_photo, rank]
- si la cible est un owner -> rank vaut null
- si la cible est un worker -> rank contient tous les champs du modèle Rank
- ne doit jamais retourner les champs sensibles [email, token, password, …]
2.2 getProfile
Section titled “2.2 getProfile”- utilisateur non authentifié -> 401
- si l’utilisateur est owner -> doit retourner les champs [allUserData, allEnterpriseFields]
- si l’utilisateur est worker -> doit retourner les champs [allUserData, allWorkerProfileFields]
- si le worker a un assignment valide en cours (in progress) -> doit retourner currentMissionId && workerStatus = inProgress
- si le worker a un assignment valide matched -> doit retourner currentMissionId && workerStatus = matched
- si le worker n’a aucun assignment valide et ready_to_work = 1 -> doit retourner workerStatus = ready
- si le worker n’a aucun assignment valide et ready_to_work = 0 -> doit retourner workerStatus = unready
- si l’utilisateur a le flag isBanned -> doit retourner workerStatus = banned && currentMissionId = null
2.3 UpdateReadyToWorkV2
Section titled “2.3 UpdateReadyToWorkV2”- utilisateur non authentifié -> 401
- doit fournir les champs [ready_to_work: int, latitude: float, longitude: float] (sinon -> 422)
- doit mettre à jour WorkerProfile.latitude/longitude avec les valeurs fournies
- doit mettre à jour ready_to_work avec la valeur fournie
- doit positionner ready_to_work_updated_at à now() quand ready_to_work = 1, à null quand ready_to_work = 0
- si ready_to_work = 1 -> doit indexer le worker comme « online » dans Redis pour chacun de ses lookings
- si ready_to_work = 1 -> doit dispatcher FindMissionsForWorker
- si ready_to_work = 1 -> doit mettre à jour DailyStats (last_ready)
- si ready_to_work = 1 -> doit broadcaster WorkerStatusChanged && AdminInstantWorkerOnline
- si ready_to_work = 0 -> doit retirer le worker de l’index « online » Redis pour chacun de ses lookings
- si ready_to_work = 0 -> NE doit PAS dispatcher FindMissionsForWorker ni mettre à jour DailyStats
- si ready_to_work = 0 -> doit broadcaster WorkerStatusChanged && AdminInstantWorkerOffline
2.4 UpdateLocation
Section titled “2.4 UpdateLocation”- utilisateur non authentifié -> 401
- doit être appelée par un worker (sinon -> 403)
- doit fournir les champs [latitude: float, longitude: float] (sinon -> 422)
- doit mettre à jour WorkerProfile.latitude/longitude avec les valeurs fournies
- doit indexer la position du worker dans Redis (via WorkerCacheService)
- doit broadcaster AdminInstantWorkerMoved
- doit dispatcher FindMissionsForWorker
2.5 UpdateSkillV2
Section titled “2.5 UpdateSkillV2”-
utilisateur non authentifié -> 401
-
doit être appelée par un worker (sinon -> 403)
-
doit fournir le champ skills avec la structure attendue (sinon -> 422) :
{"skills": [{ "lookings": 1, "requirement_id": "5,7,12" },{ "lookings": 3, "requirement_id": "15,18" }]} -
doit supprimer tous les WorkerSkillRequirements existants pour l’utilisateur
-
doit créer de nouveaux WorkerSkillRequirements à partir du payload parsé (produit cartésien des lookings × requirement_ids CSV)
-
doit retourner le WorkerProfile
2.6 UpdateToken (device token pour notifications)
Section titled “2.6 UpdateToken (device token pour notifications)”- utilisateur non authentifié -> 401
- doit fournir les champs [device_token: string, enterprise_id?: int] (sinon -> 422)
- si l’utilisateur est worker -> doit mettre à jour WorkerProfile.device_token
- si l’utilisateur est owner avec un enterprise_id valide -> doit mettre à jour device_token sur cette EnterpriseProfile
- si l’utilisateur est owner sans enterprise_id -> doit mettre à jour device_token sur toutes ses EnterpriseProfile (legacy, cf 1.2)
- si l’utilisateur est owner avec un enterprise_id qui ne lui appartient pas -> erreur
- doit retourner l’utilisateur
2.7 UpdateProfileV2
Section titled “2.7 UpdateProfileV2”- utilisateur non authentifié -> 401
- doit fournir les champs [profile_photo?: image jpeg/jpg/png max:10MB, first_name?: string max:50, last_name?: string max:50, address?: string max:255, phone?: string max:30] (sinon -> 422)
- si un champ fourni vaut "" -> traité comme non fourni (silencieusement ignoré, pas de mise à jour)
- si un profile_photo valide est fourni -> doit mettre à jour user.profile_photo et stocker le fichier (utiliser Storage::fake)
- si profile_photo invalide (mauvais mime / taille / ImageService throw) -> 422
- si l’un des champs [first_name, last_name, address, phone] est fourni -> doit le persister en BDD
- si payload vide -> NE doit PAS modifier la BDD
- si l’utilisateur est owner -> doit appeler EnterpriseSyncService::syncFromOwner
- si l’utilisateur N’est PAS owner -> NE doit PAS appeler syncFromOwner
- doit retourner l’utilisateur
2.8 deleteAccount
Section titled “2.8 deleteAccount”Refus de suppression
- utilisateur non authentifié -> 401
- si user == owner avec au moins une mission status IN [open, matched, in_progress] -> 409 (refus, message explicite)
- si user == worker avec au moins un MissionAssignment.status = pending -> 409 (refus, message explicite)
- les missions en waiting_feedback ne bloquent PAS le delete (cron mission:auto-feedback les résoudra)
Cleanup pendant la transaction
- si user == worker -> WorkerProfile.ready_to_work = 0, ready_to_work_updated_at = now(), retirer du Redis online (RedisGeoService::removeWorkerOnline)
- doit supprimer tous les oauth_access_tokens et personal_access_tokens du compte
- doit mettre google_id et apple_id à null, phone à null, et remplacer l’email par “deleted_{$user->id}_” . time() . ‘@neeko.app’
- doit soft delete l’utilisateur (deleted_at set)
Atomicité
- si une erreur survient pendant la transaction -> rollback complet (aucune mutation persistée) -> 500
Effets observables après succès
- doit retourner 200 avec
data: []et un message de succès (pas de données utilisateur leakées) - les anciens tokens du user ne doivent plus permettre d’accéder à l’API (test follow-up : appel /api/get-profile avec l’ancien token -> 401)
- doit envoyer un email à l’ancienne adresse du user expliquant que le compte est récupérable pendant 25 jours (lien signé, cf 1.4)
2.9 banStatus
Section titled “2.9 banStatus”- utilisateur non authentifié -> 401
- si is_banned = true en BDD -> doit retourner data.is_banned = true
- si is_banned = false en BDD -> doit retourner data.is_banned = false
- la response ne doit contenir que le champ is_banned (pas de leak du reste du user)