Skip to content

UserController refactoring & testing - 06/05/2026

  • 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
  • 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à.
  • 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 AccountDeletedRecoverable avec lien signé (URL::signedRoute) valide 25 jours
    • Route GET /api/restore-account/{user_id}/{signature} qui revert le soft delete (deleted_at = null)
    • PurgeDeletedUsers doit vérifier que deleted_at est toujours set au moment du hard delete (sécurité contre race conditions / restore tardif)
  • 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)”
  • 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, …]
  • 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
  • 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
  • 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
  • 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
  • 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

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)
  • 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)