Module Missions¶
Objectif¶
Le module Missions gere une place de marche interne Solutravo : une societe publie une mission, d'autres societes la consultent, candidatent, echangent et peuvent etre acceptees. Le module couvre aussi la revue administrateur, les filtres, les pieces jointes, la galerie, les notifications email et les regles d'acces selon le plan d'abonnement.
Ce module ne doit pas etre traite comme une simple liste CRUD. Il porte des regles produit sensibles : visibilite publique, validation admin, quotas de candidature, fermeture automatique apres acceptation et isolation entre societes.
Architecture fonctionnelle¶
flowchart LR
Router[Router.php] --> Controller[MissionController]
Controller --> Model[MissionModel]
Model --> AdminNotif[MissionAdminNotificationService]
Model --> AudienceNotif[MissionAudienceNotificationService]
Model --> Messaging[MessagingService / MessagingModel]
Model --> MailQueue[Email queue / Brevo templates]
Model --> DB[(MySQL)]
Controller --> Views[missions.php + partials]
Views --> JS[public/assets/js/missions/*]
Perimetre documente¶
Cette page documente les fichiers listes dans les points d'entree ci-dessous. Avant une correction, ouvrir ces fichiers dans la branche de travail concernee et verifier l'historique git du module si le comportement observe ne correspond pas a la documentation.
Points d'entree¶
Backend¶
app/controllers/MissionController.php: endpoints HTTP, normalisation des filtres, reponses JSON, chargement de la page.app/models/MissionModel.php: regles metier, requetes, permissions, transitions de statut, fichiers, candidatures, notifications.app/services/MissionAdminNotificationService.php: alertes administrateur quand une mission attend validation.app/services/MissionAudienceNotificationService.php: briefs email envoyes aux societes eligibles quand une mission publiee correspond a leur activite.app/services/MessagingService.phpetapp/models/MessagingModel.php: creation de conversation liee a une candidature.
Front¶
app/views/missions.phpapp/views/partials/missions/filter-form.phpapp/views/partials/missions/list-section.phpapp/views/partials/missions/mission-card.phpapp/views/partials/missions/tab-navigation.phpapp/views/partials/modals/mission-application-modal.phpapp/views/partials/modals/mission-applications-drawer.phpapp/views/partials/modals/mission-details-modal.phpapp/views/partials/modals/mission-edit-modal.phpapp/views/partials/modals/mission-publish-modal.phpapp/views/partials/modals/mission-candidates-modal.phpapp/views/partials/modals/mission-gallery-viewer-modal.phpapp/views/partials/modals/mission-share-email-modal.phpapp/views/partials/modals/mission-application-details-modal.phpapp/views/partials/modals/mission-confirm-action-modal.phpapp/views/partials/modals/mission-personal-list-modal.phpapp/views/partials/modals/mission-review-list-modal.phppublic/assets/js/missions/app.jspublic/assets/js/missions/actions.jspublic/assets/js/missions/core.jspublic/assets/js/missions/pickers.jspublic/assets/js/missions/renderers.jspublic/assets/css/missions.css
Routes¶
Les routes sont declarees dans app/core/Router.php.
Pages¶
missions: redirige versmissions/allen conservant la query string.missions/all: catalogue global des missions visibles.missions/personal: missions personnelles de la societe courante.missions/review: file de revue administrateur, uniquement pour les administrateurs.
Actions mission¶
mission/add: creation.mission/update: modification.mission/delete: suppression definitive, reservee aux administrateurs selon le modele.mission/reopen: reouverture d'une mission cloturee.mission/archive: archivage.mission/unarchive: desarchivage.mission/approve: validation admin et publication.mission/view: enregistrement d'une vue.mission/share-email: partage par email.mission/activities/search: recherche d'activites.mission/companies/search: recherche de societes pour filtre/admin.
Actions candidature¶
mission/apply: candidature a une mission.mission/application/update: modification de sa propre candidature.mission/application/delete: suppression de sa propre candidature.mission/application/status: changement de statut par le proprietaire de mission.mission/applications: liste des candidatures recues.mission/application: detail d'une candidature recue.
Permissions, features et plans¶
Le module combine permissions applicatives et features de plan.
Permissions appelees par le controleur ou le modele :
missions:view_allmissions:createmissions:editmissions:apply_to_missionmissions:view_applications
Features de plan appelees par le modele :
FEATURE_CONSULTER_MISSIONFEATURE_POSTULER_MISSION
Plans utilises par les regles de candidature :
GRATUITTPEPMEENTREPRISE
Regle importante : le plan gratuit est limite a une candidature par mois pour les non-admins. Les plans superieurs sont ordonnes par rang pour verifier si une societe peut candidater a une mission exigeant un plan minimum.
Statuts mission¶
Statuts techniques :
draft: brouillon.pending_review: en attente de validation admin.published: visible dans le catalogue.closed: cloturee, notamment apres acceptation d'une candidature.archived: archivee.
Transitions principales :
- creation par admin : publication directe en
published. - creation par non-admin : passage en
pending_review. - modification par admin avec demande de publication :
published. - modification par non-admin d'une mission ouverte : retour en
pending_review. - validation admin :
publishedavecpublished_at,reviewed_by,reviewed_at. - acceptation d'une candidature : mission en
closed. - desarchivage admin :
published. - desarchivage non-admin :
pending_review.
La logique de visibilite depend du scope :
all: missions publiees et cloturees pour les utilisateurs standards ; l'admin voit aussi des etats internes.personal: toutes les missions de la societe courante, y compris brouillon, attente, publiee, cloturee, archivee.review: missionspending_review, reserve admin.
Methodes de transition¶
Les transitions sont centralisees dans MissionModel :
resolveMissionStatusContextOnCreate: admin verspublished, non-admin verspending_review.resolveMissionStatusContextOnUpdate: admin peut publier ; non-admin renvoie en revue sauf mission dejaclosed.resolveMissionStatusContextOnUnarchive: admin republie, non-admin renvoie en revue.approve: publication admin depuispending_review.archive/unarchive/reopen: transitions explicites appelees par le controleur.
Garde-fou important : un changement annonceur sur une mission ouverte retire la mission du flux public. C'est une regle produit de moderation, pas un detail d'interface.
Donnees¶
Tables directement manipulees par le module :
missions: mission principale.mission_activities: relation mission / activites.mission_departements: relation mission / departements.mission_files: images et pieces jointes.mission_applications: candidatures.mission_views: vues journalieres par membre/societe.mission_brief_email_recipients: reservation/idempotence des briefs email.societes,membres,plans,activites,departements: tables referencees.
Scripts SQL actuellement presents :
app/bd/missions-departements.sql: creemission_departements, rendcitynullable, retirepostal_code, ajouteis_nationwide.app/bd/missions-nationwide.sql: ajouteis_nationwide.app/bd/missions-notifications.sql: creemission_brief_email_recipients.app/bd/missions-original-societe.sql: ajouteoriginal_societe_idet backfill depuissociete_id.
Point de reprise important : la fiche historique mentionnait app/bd/missions.sql, mais ce fichier n'est pas present dans le checkout actuel. Avant d'installer le module sur une nouvelle base, retrouver la migration initiale ou reconstruire explicitement le schema de base (missions, mission_activities, mission_files, mission_applications, mission_views) depuis la base de reference.
Etat verifie des migrations¶
Recherche locale effectuee dans app/bd :
- aucune migration initiale
CREATE TABLE missionstrouvee ; - aucune migration initiale
CREATE TABLE mission_applicationstrouvee ; - aucune migration initiale
CREATE TABLE mission_filestrouvee ; - aucune migration initiale
CREATE TABLE mission_viewstrouvee ; - aucune migration initiale
CREATE TABLE mission_activitiestrouvee ; - migration trouvee pour
mission_departements; - migration trouvee pour
mission_brief_email_recipients.
Consequence : le code actuel depend de tables qui existent probablement deja dans les bases de travail, mais le repository ne documente pas totalement une installation neuve. C'est un risque majeur de reprise.
Colonnes attendues par missions¶
Le modele lit ou ecrit notamment :
idsociete_id: proprietaire courant de la mission.original_societe_id: societe d'origine, ajoutee parmissions-original-societe.sql.created_by: membre createur.titledescriptioncityis_nationwidebudget_amountis_budget_on_quotebudget_scopeis_budget_negotiableexecution_deadlineminimum_candidate_plan_idis_urgentstatuspublished_atreviewed_byreviewed_atclosed_atcreated_atupdated_at
Ces colonnes doivent etre confirmees dans la base de reference avant tout deploiement. La migration initiale manquante doit etre reconstituee a partir de la base et du code, pas devinee.
Tables de liaison et d'activite¶
mission_activities
- Relation entre une mission et une activite.
- Utilisee pour les filtres, la recherche d'activites et les briefs audience.
- Le modele supprime puis reinsere les activites lors d'une synchronisation.
mission_departements
- Relation entre une mission et un departement.
- Cle primaire composite
mission_id,departement_id. - Suppression en cascade quand la mission est supprimee.
- Si
is_nationwideest vrai, le modele supprime les departements et n'en reinsere pas.
mission_files
- Stocke
file_type,file_path,original_name,mime_type,file_size,sort_order. - Les chemins physiques sont reconstruits depuis
public/+file_path. - Les suppressions partielles passent par
removed_file_ids.
mission_views
- Enregistre
mission_id,viewer_societe_id,viewer_membre_id,viewed_at. - Le modele evite de compter plusieurs vues le meme jour pour un meme membre.
mission_applications
- Stocke la candidature : mission, societe candidate, membre candidat, message, statut.
- Le modele utilise au moins
mission_id,candidate_societe_id,candidate_membre_id,message,status,created_at. - Les statuts geres par le code sont
new,viewed,accepted,rejected.
mission_brief_email_recipients
- Table d'idempotence des briefs audience.
- Cle unique
mission_id,societe_id. - Colonnes :
email,template_brevo_id,email_queue_id,status,skip_reason,created_at.
Localisation et audience¶
La localisation repose sur :
cityfacultatif ;mission_departementspour les departements cibles ;is_nationwidepour les missions "toute la France".
Quand is_nationwide est actif, les departements ne doivent pas limiter la visibilite. Quand il est inactif, les departements deviennent structurants pour les filtres, les emails de brief et la recherche.
Budget et delais¶
Scopes budgetaires :
supply_and_installation: fourniture + pose.installation_only: pose uniquement.
Delais techniques :
under_10_daysbetween_10_days_and_1_monthbetween_1_and_3_monthsover_3_months
Regle importante : si is_budget_on_quote est actif, le montant fixe, le scope et la negociabilite sont neutralises. Si la mission n'est pas "sur devis", le montant et le scope deviennent obligatoires.
Normalisation budget¶
normalizeMissionBudgetContext applique exactement cette logique :
- si
is_budget_on_quoteest vrai,budget_amountdevientnull,is_budget_on_quotevaut1,budget_scopedevientnulletis_budget_negotiablevaut0; - si
is_budget_on_quoteest faux et que le montant est absent, le modele renvoie une erreur car le forfait est obligatoire sauf attente d'un devis ; - si le montant existe mais que le scope est invalide ou absent, le modele renvoie une erreur demandant de preciser fourniture + pose ou pose uniquement.
Ne pas valider uniquement cote JS : le modele porte deja la regle et doit rester la source de verite.
Fichiers et galerie¶
Les uploads mission utilisent :
- module d'upload :
missions; - taille maximum :
5 242 880octets, soit 5 Mio ; - stockage logique :
uploads/missions/...; - type
imagepour les mimesimage/*, sinonattachment.
Les fichiers sont references en base dans mission_files. La suppression definitive d'une mission recupere les chemins puis supprime les fichiers physiques. Les suppressions partielles de fichiers passent par removed_file_ids.
Avant de modifier ce flux, tester :
- upload multiple ;
- retrait d'un fichier existant ;
- suppression definitive de mission ;
- affichage galerie ;
- piece jointe non image ;
- fichier trop lourd.
Cycle de vie des fichiers¶
syncMissionUploads fait deux choses :
- supprimer les fichiers demandes via
removed_file_ids; - ajouter les nouveaux fichiers via
storeMissionUploads.
storeMissionUploads attend les fichiers sous la cle files. Une integration front qui envoie une autre cle peut donner l'impression que l'upload fonctionne cote formulaire mais ne rien enregistrer en base.
uploadSingleMissionFile classe les fichiers en :
imagesi le mime commence parimage/;attachmentsinon.
Le chemin stocke en base commence par uploads/missions/. Le chemin disque de suppression est reconstruit avec dirname(__DIR__, 2) . '/public/'.
Flux : creation et publication¶
Creation :
- Le controleur lit les champs
societe_id,title,description,city,is_nationwide,department_ids, budget, delai, plan minimum, urgence, statut et activites. - Le modele valide les features, permissions, budget, departements, activites et societe proprietaire.
- Les administrateurs peuvent choisir la societe proprietaire.
- Les non-admins publient pour leur societe active.
- Le statut final depend du role : admin publie directement, non-admin passe en revue.
- Les activites, departements et fichiers sont synchronises.
- Si la mission passe en attente de validation, une notification admin est tentee hors transaction critique.
Piege a eviter : ne pas rendre une mission non revue visible dans all. Une modification par annonceur sur une mission ouverte doit la retirer du flux public jusqu'a validation.
Champs lus par le controleur¶
MissionController::add et MissionController::update collectent :
societe_idtitledescriptioncityis_nationwidedepartment_idsbudget_amountis_budget_on_quotebudget_scopeis_budget_negotiableexecution_deadlineminimum_candidate_plan_idis_urgentstatusactivitiesremoved_file_idsen edition.
Une evolution de formulaire doit donc etre raccordee a ces noms exacts ou au mapping du controleur.
Societe proprietaire¶
La resolution de societe proprietaire est volontairement differente selon le role :
- admin : peut choisir
societe_id, avec validation que la societe existe ; - non-admin : force la societe active ;
- edition par non-admin : conserve le proprietaire courant.
original_societe_id conserve l'origine historique. Ne pas le modifier pour transferer une mission : utiliser societe_id.
Flux : revue administrateur¶
Une mission en pending_review est visible dans le scope review. L'admin peut la valider via mission/approve.
MissionAdminNotificationService gere :
- notification immediate de mission en attente ;
- rappel hebdomadaire des missions en attente ;
- limitation de l'aperçu email aux cinq dernieres missions ;
- destinataires : membres actifs de type
admin, non collaborateurs, avec email valide.
La notification ne doit jamais bloquer la sauvegarde d'une mission. Le modele loggue les echecs et laisse la mission comme source de verite.
Notifications admin¶
MissionAdminNotificationService :
- cherche les membres
type = admin; - exige
statut = actif; - exclut
COALESCE(is_collaborator, 0) = 0; - dedoublonne les emails ;
- envoie au premier admin en destinataire principal et les autres en copie ;
- utilise un lien de revue
/missions/all?missions_modal=review.
Le service n'utilise pas la table mission_brief_email_recipients, reservee aux briefs audience.
Flux : consultation, filtres et vues¶
Les filtres supportes cote controleur incluent :
- recherche texte ;
- dates de publication ;
- urgence ;
- missions personnelles ;
- activites ;
- departements ;
- societes, pour admin uniquement.
registerMissionView enregistre une vue dans mission_views si le membre n'a pas deja vu la mission le meme jour. Cela evite de gonfler artificiellement les compteurs.
Scopes et filtres¶
Le controleur accepte seulement :
allpersonalreview, pour admin.
Filtres normalises :
searchdate_publication_debutdate_publication_finis_urgentmine_onlyactivity_idsdepartment_idssociete_ids, admin uniquement.
Le controleur force actuellement l'appel principal a getPaginatedMissions(..., 'all') pour la page, puis charge les listes modales personal et review a part. Si un futur developpeur modifie les onglets pour charger chaque scope directement, il doit verifier ce comportement et les compteurs.
Flux : candidature¶
Le flux candidature passe par mission/apply.
Regles a respecter :
- permission
missions:apply_to_missionobligatoire ; - feature
FEATURE_POSTULER_MISSIONobligatoire ; - la societe active doit etre valide ;
- impossible de candidater a sa propre mission ;
- impossible de candidater deux fois a la meme mission ;
- impossible de candidater si le plan courant est inferieur au plan minimum de la mission ;
- le plan gratuit non-admin est limite a une candidature par mois ;
- une candidature cree une conversation mission, mais l'echec de messagerie ne doit pas annuler la candidature.
Statuts candidature :
newviewedacceptedrejected
L'acceptation est transactionnelle : le statut de la candidature passe a accepted, la mission est fermee, puis une notification d'acceptation est planifiee. Le modele protege le cas ou une autre candidature serait deja acceptee.
Politique de candidature¶
getApplicationPolicyContext calcule :
- plan courant ;
- si le plan est gratuit ;
- limite mensuelle ;
- nombre utilise ce mois ;
- reste disponible ;
- feature d'upgrade
FEATURE_POSTULER_MISSION.
Le quota gratuit est calcule sur mission_applications avec candidate_societe_id = activeSocieteId et created_at >= premier jour du mois courant. Le code ne filtre pas explicitement les candidatures supprimees dans ce compteur ; verifier le comportement attendu avant de changer la suppression de candidature.
Acceptation et concurrence¶
updateApplicationStatus ouvre une transaction, relit le contexte candidature avec verrou FOR UPDATE, puis appelle getApplicationProcessingGuard.
Garde-fous :
- impossible de traiter manuellement
acceptedourejectedsi la mission est dejaclosed; - accepter une candidature deja acceptee est idempotent ;
- accepter une autre candidature est refuse si une candidature acceptee existe deja ;
closeMissionAfterAcceptedApplicationmetmissions.status = 'closed'etclosed_at = NOW().
La notification d'acceptation est envoyee apres commit. Si elle echoue, la candidature acceptee et la mission cloturee restent la verite.
Flux : gestion des candidatures recues¶
Le proprietaire d'une mission, ou un admin, peut lister et traiter les candidatures. L'acces passe par missions:view_applications et par userCanManageMission.
Regles sensibles :
- une societe ne doit jamais gerer les candidatures d'une mission qui ne lui appartient pas ;
- une mission
closedne doit plus permettre acceptation/refus manuel ; - une seule candidature acceptee par mission ;
- les conversations liees aux candidatures doivent rester accessibles aux participants autorises seulement.
Messagerie¶
Apres creation de candidature, createMissionApplicationConversation tente MessagingService::createForMissionApplication.
Le commentaire du code est important : la candidature reste la source de verite et la messagerie ne doit pas bloquer le depot. En cas d'echec, le modele loggue l'erreur. Une reprise doit donc prevoir un outil de diagnostic ou de recreation de conversation si une candidature existe sans conversation.
app/bd/messaging.sql contient des vues/requetes liees a mission_applications. Le module Missions et la Messagerie sont donc couples par la structure de candidature.
Flux : partage email¶
mission/share-email partage une mission par email.
Protections existantes :
- permission
missions:view_all; - email destinataire obligatoire ;
- rate limit session : 5 partages maximum par fenetre de 15 minutes ;
- template Brevo source
mission_share_email.
Les URLs absolues sont construites via APP_URL, puis APP_BASE_URL, puis l'hote HTTP courant. Ne pas hardcoder de domaine dans ce module.
Rate limit partage¶
allowMissionShareEmailRequest stocke les timestamps en session sous mission_share_email_timestamps.
- fenetre : 900 secondes ;
- maximum : 5 tentatives ;
- le nettoyage garde seulement les timestamps encore dans la fenetre.
Ce rate limit est par session PHP, pas global. Il limite les abus simples mais ne remplace pas une protection serveur globale si le partage devient critique.
Flux : briefs audience¶
MissionAudienceNotificationService envoie des briefs aux societes eligibles quand une mission publiee correspond a leurs activites.
Parametres importants :
- source email :
mission_brief_matched_activity; - variable d'activation :
MISSION_BRIEF_EMAILS_ENABLED; - batch : 50 destinataires ;
- delai entre batches : 300 secondes ;
- limite quotidienne : 3 briefs par societe ;
- table d'idempotence :
mission_brief_email_recipients.
La table d'idempotence empeche de renvoyer le meme brief a la meme societe pour une meme mission. Les statuts possibles dans cette table sont queued, skipped et failed.
Selection et idempotence audience¶
MissionAudienceNotificationService est volontairement opt-in :
- si
MISSION_BRIEF_EMAILS_ENABLEDest absent ou invalide, aucun envoi ; reserveRecipientfait unINSERT IGNOREdansmission_brief_email_recipients;- si la reservation existe deja, le destinataire est ignore ;
- si la limite quotidienne est atteinte, la ligne est reservee avec
status = skippedetskip_reason = daily_limit; - si la mise en file email echoue, la ligne passe en
failed.
La limite quotidienne compte les lignes queued creees depuis UTC_DATE(). Elle ne compte pas les failed ni les skipped.
Integrations internes¶
- Messagerie : conversation creee pour une candidature via
MessagingService::createForMissionApplication. - Emails :
MailerService, file email et templates Brevo. - Plans/features :
getCurrentPlanName,hasFeatureAccess, constantes feature. - Permissions :
PermissionMiddleware. - Societes et membres : ownership, candidature, destinataires.
- Activites et departements : matching, filtres, audience.
Risques et pieges¶
- Ne pas confondre
societe_idetoriginal_societe_id:societe_idrepresente le proprietaire courant,original_societe_idconserve l'origine historique. - Ne pas exposer les missions en attente dans le flux public.
- Ne pas permettre a une societe de candidater a sa propre mission.
- Ne pas autoriser plusieurs candidatures acceptees pour une meme mission.
- Ne pas casser la limite de candidature du plan gratuit.
- Ne pas bloquer la creation/modification d'une mission si l'email admin ou audience echoue.
- Ne pas supprimer physiquement les fichiers sans nettoyer
mission_files, et inversement. - Ne pas reconstruire le schema depuis les seules migrations partielles : la migration initiale manque dans le checkout actuel.
- Ne pas considerer le rate limit email comme global : il est base session.
- Ne pas rendre bloquants les emails ou la messagerie si le code les traite comme secondaires.
- Ne pas modifier les transitions sans verifier les statuts visibles par scope.
- Ne pas oublier que les fichiers physiques peuvent rester sur disque si une suppression base echoue ou inversement.
Tests recommandes avant livraison¶
- Creation mission admin : publication directe.
- Creation mission non-admin :
pending_review. - Modification non-admin d'une mission publiee : retour en revue.
- Validation admin : publication, dates et reviewer renseignes.
- Filtres par activite, departement, urgence, dates et societe admin.
- Mission nationale avec
is_nationwide. - Candidature acceptee : mission fermee.
- Deux candidatures concurrentes : une seule acceptee.
- Candidature plan gratuit : quota mensuel applique.
- Plan minimum requis : blocage si plan insuffisant.
- Candidature a sa propre mission : refusee.
- Upload image et piece jointe.
- Suppression definitive admin avec fichiers associes.
- Partage email : rate limit session.
- Brief audience : idempotence par mission/societe.
Commandes utiles¶
Il n'y a pas encore de test automatise dedie au module Missions dans le checkout actuel. Pour une reprise serieuse, ajouter au minimum des tests PHP de modele sur :
- transitions de statut ;
- candidature et fermeture automatique ;
- quota plan gratuit ;
- filtrage multi-societe ;
- idempotence email audience.
Checklist de reprise¶
- Lire
MissionController.phpetMissionModel.phpensemble. - Verifier la presence du schema initial en base avant de lancer les migrations partielles.
- Tester avec un admin, une societe annonceuse et une societe candidate.
- Tester avec au moins deux societes pour detecter les fuites de donnees.
- Verifier les constantes de feature et les permissions en base.
- Verifier les templates Brevo attendus pour
mission_application_created,mission_application_accepted,mission_share_email,mission_brief_matched_activity. - Verifier que
MISSION_BRIEF_EMAILS_ENABLEDest volontairement active seulement dans les environnements ou l'envoi est souhaite.
Questions ouvertes¶
- Ou se trouve la migration initiale du module Missions pour une installation neuve ?
- Le statut
draftest-il encore cree depuis l'interface ou seulement conserve pour compatibilite ? - Une mission archivee doit-elle rester visible aux candidats ayant deja postule ?
- Le quota gratuit doit-il compter les candidatures supprimees ou seulement les candidatures actives ?
- La fermeture automatique apres acceptation doit-elle notifier les autres candidats non retenus ?