Skip to content

AdminMissionController refactoring & testing - 21/05/2026

  • Il faut centraliser la logique de delete de mission dans un service car utilisé pour deleteMissionAdmin & le closeOwner

2. Tests de chaque route — approche features-first (non-unit)

Section titled “2. Tests de chaque route — approche features-first (non-unit)”

Route : GET /api/admin/get-missions — liste paginee des missions pour le back-office admin.

Auth (a terme : Provider cross-cutting de toutes les routes admin mission)

  • utilisateur non authentifie -> 401
  • utilisateur authentifie avec role !== admin -> 403

Pagination (fixe a 5 missions par page)

  • s’il existe plus de 5 missions -> la response ne renvoie au plus que 5 missions dans data
  • page=2 -> renvoie les missions au-dela des 5 premieres (offset = (page-1)*5)
  • nextpage = page courante + 1 (donc 2 par defaut quand aucun page n’est fourni)

Tri

  • les missions sont triees par created_at decroissant (plus recente en premier)

Filtre status (optionnel)

  • status=open -> ne renvoie que les missions ayant ce status ; les autres status sont exclus
  • aucun status fourni -> renvoie toutes les missions (toutes pages confondues)

Enrichissement owner_details

  • chaque mission renvoyee porte un bloc owner_details avec exactement les cles : [first_name, last_name, business_name, manager_name, profile_photo, address, email, enterprise_id]
  • business_name, manager_name, enterprise_id sont derives de l’EnterpriseProfile rattachee a la mission
  • mission sans enterprise (enterprise_id null) -> business_name et enterprise_id valent null (pas d’erreur)

Compteurs (counts) — agregat global, toutes pages confondues

  • counts agrege le nombre de missions par status sur l’ensemble de la BDD (independant de la pagination)
  • les status matched et in_progress sont fusionnes dans open : counts.open = open + matched + in_progress
  • les cles matched et in_progress ne sont jamais presentes dans counts
  • les autres status (ex: cancel) gardent leur compteur propre

Route : GET /api/admin/get-missions-with-idrole — missions d’un utilisateur donne, resolues differemment selon son role.

Auth

  • utilisateur non authentifie -> 401
  • utilisateur authentifie avec role !== admin -> 403

Validation des inputs

  • user_id manquant -> 422
  • user_id non integer -> 422
  • user_id valide en type mais aucun user en BDD pour cet id -> 422
  • role manquant -> 422
  • role hors de [admin, owner, worker] -> 422
  • status fourni mais pas un tableau -> 422
  • status fourni comme tableau d’elements non-string -> 422

Resolution des missions selon le role

  • role=owner -> renvoie les missions dont owner_id = user_id ; les missions d’autres owners sont exclues
  • role=worker -> renvoie les missions ou le user a un MissionAssignment (resolution via mission_assignments, PAS via owner_id)
  • role=admin -> meme branche que owner : renvoie les missions dont owner_id = user_id

Filtre status (optionnel)

  • status fourni (tableau) -> ne renvoie que les missions dont le status est dans le tableau
  • status absent ou tableau vide -> aucun filtrage par status

Tri

  • missions triees par created_at decroissant

Forme de chaque mission renvoyee

  • chaque element porte exactement les cles : [id, status, created_at, business_name, address, enterprise_id]
  • ne renvoie aucun bloc owner_details ni champ prive de l’owner
  • bug actuel : business_name, address, enterprise_id sont toujours null, meme quand la mission a une enterprise. Le controller fait select('id','status','created_at','owner_id') sans enterprise_id -> la relation belongsTo enterprise ne se resout jamais. Le test pin ce comportement actuel (assertions sur null) : si le select est corrige, le test passera au rouge — signal volontaire pour le mettre a jour, pas une regression. A corriger en section 1.

Route : GET /api/admin/get-assignments — liste des MissionAssignment, filtrable par mission et/ou worker.

Auth

  • utilisateur non authentifie -> 401
  • utilisateur authentifie avec role !== admin -> 403

Validation des inputs (les deux filtres sont optionnels)

  • mission_id non integer -> 422
  • worker_id non integer -> 422
  • mission_id / worker_id integer mais inexistant en BDD -> PAS d’erreur (aucune regle exists), renvoie simplement une liste vide

Filtrage

  • aucun filtre -> renvoie toutes les MissionAssignment
  • mission_id fourni -> ne renvoie que les assignments de cette mission
  • worker_id fourni -> ne renvoie que les assignments de ce worker
  • mission_id + worker_id fournis -> intersection (assignments de cette mission ET de ce worker)

Tri

  • assignments tries par created_at decroissant

Forme de la response

  • chaque assignment expose ses relations worker et mission eager-loaded

Route : POST /api/admin/update-mission-details — mise a jour des details d’une mission.

Auth (boilerplate)

  • utilisateur non authentifie -> 401
  • utilisateur authentifie avec role !== admin -> 403

Validation des inputs (boilerplate)

  • mission_id manquant -> 422
  • mission_id non integer -> 422
  • mission_id integer mais aucune mission en BDD -> 422
  • date au format invalide -> 422
  • start_time pas au format H:i:s -> 422
  • end_time pas au format H:i:s -> 422
  • duration non numeric -> 422
  • rate_per_hour non numeric -> 422
  • description non string -> 422
  • looking_id inexistant en BDD -> 422
  • priorities > 255 caracteres -> 422
  • payload valide (au minimum un mission_id de mission existante) -> 200 (le validator ne sur-rejette pas)

Logique metier / effets observables

  • Quand startTime ou endTime présent et valide appeler missionService->computeDuration pour set les nouvelles valeurs
  • Quand requirement_id est présent, vérifier que c bien un string csv et appeler missionService->syncRequirements
  • Le recalcul duration utilise les valeurs fusionnees : si seul start_time est fourni, end_time reste la valeur existante de la mission (et inversement)
  • Le sync requirements est un remplacement integral : suppression de TOUS les mission_requirements de la mission, puis recreation depuis le CSV requirement_id (priorites depuis le CSV priorities, defaut 0 = obligatoire si priorities absent)
  • Update partiel des champs Mission : date, start_time, end_time, rate_per_hour, description, looking_id ne sont ecrits que s’ils sont presents dans le payload
  • Un champ non fourni dans le payload garde sa valeur d’origine (pas ecrase a null)
  • Quand looking_id change ET la mission est open -> appeler RedisGeoService::moveMissionLooking(mission_id, ancien_looking_id, nouveau_looking_id) pour deplacer la mission de bucket Redis
  • Quand looking_id est fourni mais identique a l’actuel -> aucun move Redis (la comparaison old !== new bloque)
  • Quand la mission n’est PAS open -> aucun move Redis meme si looking_id change (seules les missions ouvertes sont indexees dans Redis)
  • Si moveMissionLooking jette -> l’erreur est catchee + loguee, la requete reussit quand meme (200)
  • Response : renvoie la mission fraiche rechargee avec les relations looking, owner, enterprise, feedbacks
  • Response : la mission porte un bloc owner_details avec exactement les cles [first_name, last_name, business_name, manager_name, profile_photo, address, enterprise_id] — attention, PAS de cle email ici (contrairement a getMissionsAdmin)
  • owner_details.address = adresse de l’enterprise si presente, sinon adresse de l’owner (fallback)
  • owner_details : business_name, manager_name, enterprise_id derives de l’EnterpriseProfile -> null si la mission n’a pas d’enterprise

Route : POST /api/admin/delete-mission — suppression d’une mission.

Etat actuel : le controller fait un forceDelete inline. Version cible (cf 1.1) : soft delete delegue a MissionService::purgeMission.

Auth (boilerplate)

  • utilisateur non authentifie -> 401
  • utilisateur authentifie avec role !== admin -> 403

Validation des inputs (boilerplate)

  • mission_id manquant -> 422
  • mission_id non integer -> 422
  • mission_id integer mais aucune mission en BDD -> PAS d’erreur : la regle est required|integer sans exists, le if ($mission) est skippe silencieusement et la route renvoie 200
  • payload valide (mission_id d’une mission existante) -> 200

Logique metier / effets observables

La logique de delete (cf 1.1) doit etre extraite dans MissionService::purgeMission($mission). Note : AdminUserControllerTest.md (1.2) reference deja un MissionService::cleanMissionBeforeDelete — a unifier sous un seul nom de methode.

Les tests pinnent le comportement observable actuel, sans markTestIncomplete : ils doivent rester verts apres l’extraction de purgeMission (garde anti-regression du refactor).

  • la mission n’est plus accessible apres l’appel (Mission::find renvoie null) — assertion robuste : valide aussi bien le hard delete actuel qu’un futur soft delete
  • les donnees liees a la mission sont supprimees : MissionRequirement, MissionAssignment, MissionWorkerBroadcast
  • mission_id inexistant -> 200 silencieux, aucune donnee touchee

Route : POST /api/admin/cancel-mission — annulation d’une mission par un admin.

Auth

  • utilisateur non authentifie -> 401
  • utilisateur authentifie avec role !== admin -> 403

Validation des inputs

  • mission_id manquant -> 422
  • mission_id non integer -> 422
  • mission_id integer mais aucune mission en BDD -> 422 (regle exists:missions,id)

Le if (!$mission) -> 404 "Mission not found" du controller est du code mort : la regle exists:missions,id garantit deja l’existence avant d’y arriver. A signaler en section 1.

Logique metier / effets observables

  • la mission passe en status = 'cancel'
  • broadcast CancelMission a tous les workers (User role=worker) + broadcast AdminInstantMissionClosed
  • cleanup Redis removeMissionOpen ; si la mission a lat/lng valides -> dispatch RecomputeTensionOnMissionCloseJob
  • echec du cleanup Redis -> catche + logue, la requete reussit quand meme (200)
  • si la mission a une MissionAssignment -> elle passe en status = 'cancel' + ended_at = now()
  • si la mission n’a pas d’assignment -> aucun effet sur les assignments
  • reponse : 200, data vide

Route : POST /api/admin/unmatch-mission — defait le matching d’une mission et la rouvre.

Auth

  • utilisateur non authentifie -> 401
  • utilisateur authentifie avec role !== admin -> 403

Validation des inputs

  • mission_id manquant -> 422
  • mission_id non integer -> 422
  • mission_id integer mais aucune mission en BDD -> 422 (regle exists:missions,id)

Garde metier

  • mission dont le status !== 'matched' -> 422 (“Mission currently not matched. Can only unmatch currently matched missions.”)

Logique metier / effets observables (mission matched)

  • la MissionAssignment de la mission est supprimee
  • la mission repasse en status = 'open'
  • l’ensemble (suppression assignment + repassage open) est dans une transaction DB ; toute exception -> rollback + 500 (“Failed to unmatch mission.”)
  • si la mission a lat/lng valides -> RedisGeoService::addMissionOpen + dispatch RecomputeTensionOnMissionOpenJob
  • dispatch SyncMissionMatching pour relancer le matching sur la mission rouverte
  • echec Redis/matching -> catche + logue, la requete reussit quand meme
  • si un worker etait assigne -> broadcast WorkerStatusChanged (statut UNREADY) + MissionCancelled
  • si la mission a une enterprise -> broadcast AdminInstantMissionOpened
  • echec d’un broadcast -> catche + logue, la requete reussit quand meme
  • reponse : 200 avec la mission rouverte