Skip to main content

Formulaire de demande de prêt scolaire

src/views/public/SchoolLoanView.vue (frontend) — page publique, route /demande-credit-scolaire.

Dernière vérification contre le code

Vérifié ligne à ligne le 2026-08-12. Si ce que tu observes dans l'app diverge de cette page, corrige la page plutôt que de faire confiance à un souvenir plus ancien.

1. Vue d'ensemble

Le parcours a deux phases distinctes :

  1. Portail d'authentification par OTP (avant tout accès au formulaire).
  2. Formulaire en 4 étapes (wizard), avec barre de progression et indicateur "Étape X/4".

À la soumission réussie, le formulaire bascule sur un écran de confirmation et affiche le numéro de ticket via TicketPopup.

Parcours potentiellement masqué

Une constante isUnderReview en tête de <script setup> peut masquer tout le parcours derrière un écran "Ce parcours évolue actuellement" (utilisé pendant la refonte ayant ajouté l'OTP). Actuellement false. À vérifier en premier si le formulaire semble "disparu" en prod.

2. Portail d'authentification OTP (avant l'Étape 1)

Étape ajoutée après la première version du formulaire — le client doit désormais prouver qu'il est bien titulaire du compte avant de pouvoir remplir quoi que ce soit.

  1. Identifiants (authStep === 'credentials') : n° de compte (Code Agence + N° Compte 11 chiffres + Clé RIB 2 chiffres) et téléphone associé (PhoneInput). Le bouton "Recevoir mon code" n'est actif que si ces champs sont valides (isAuthCredentialsValid).
  2. Code SMS (authStep === 'otp') : code à 6 chiffres, valable 120 secondes (OTP_DURATION_SECONDS), avec renvoi possible une fois expiré.

Une fois le code validé (authPassed = true), l'utilisateur accède au formulaire à l'Étape 0.

Intégration backend pas encore branchée

requestOtp/verifyOtp sont actuellement simulés côté frontend uniquement — aucun SMS n'est réellement envoyé. Le code attendu est en dur : 000000, en attendant une future intégration du middleware Sopra Banking Amplitude pour la vérification réelle du couple compte/téléphone et l'envoi du SMS. Le commentaire du code invoquait auparavant à tort "la même convention que le bypass 2FA existant du backoffice" — corrigé le 2026-08-13, ce bypass backoffice n'a jamais existé (voir Authentification & 2FA), sans lien avec ce placeholder OTP qui, lui, reste volontairement en place.

Plus important : une fois l'OTP "validé", l'identité du client (lastName, firstName, ville, dateNaissance, lieuNaissance, email) est actuellement remplie avec des données factices codées en dur (SchoolLoanView.vue, dans verifyOtp), pas récupérée depuis un vrai système bancaire. Tant que cette intégration n'est pas faite, le formulaire ne doit pas être considéré comme fonctionnel en production au-delà de la démonstration du parcours.

3. Les 4 étapes

#TitreContenu
0Vos informationsIdentité + n° de compte affichés en lecture seule ("Vérifié", pré-remplis par l'étape OTP) ; Quartier/Maison/BP restent éditables
1Agence & engagementProfession (+ "Autre"), employeur, date d'embauche, école, agence de traitement, engagement salarial (Oui/Non), champs dynamiques
2Pièces JustificativesUpload des documents requis (voir section 5, la partie la plus complexe du formulaire)
3Montant, Récap & MentionsSimulation (montant/durée/revenus), récapitulatif identité, consentements légaux, bouton d'envoi

La navigation (nextStep/prevStep) est bloquée tant que l'étape courante n'est pas valide (voir section 6).

Étape 0 — Vos informations

Depuis l'ajout du portail OTP, cette étape n'est plus une saisie libre :

  • Identité et n° de compte (nom, prénoms, ville, date/lieu de naissance, email, téléphone, n° de compte) : affichés désactivés, avec un badge vert "Vérifié" — ce sont les valeurs capturées/mockées durant le portail OTP (voir section 2). Un texte prévient le client que ces informations proviennent de son dossier UTB et ne sont pas modifiables ici.
  • Champs encore éditables : Quartier (obligatoire), Maison, B.P.

Étape 1 — Agence & engagement

  • Profession : liste fermée ['Fonctionnaire', 'Employé du privé', 'Autre']. Si 'Autre' est choisi, un champ texte libre (formData.professionAutre) devient obligatoire — c'est cette valeur précisée qui est envoyée au backend comme profession réelle (finalProfession), pas le mot "Autre".
  • Cas particulier "Autre" : deux règles se relâchent, avec une mention explicative affichée au client :
    • Le champ Employeur reste obligatoire mais son placeholder suggère "Indépendant" ou "Moi-même".
    • La vérification "18 ans à la date d'embauche" (dateError) est désactivée — seules "embauche postérieure à la naissance" et "pas dans le futur" restent actives. Le champ affiche alors "Vous pouvez saisir la date de début de votre activité".
  • Engagement salarial (hasSalaryEngagement, true/false/null) : détermine si le client a déjà un prêt/virement domicilié à l'UTB. false (nouveau client) déclenche l'exigence du document "Attestation de virement irrévocable de salaire" à l'étape suivante.
  • Champs dynamiques (requiredFields, chargés depuis /loan-field-requirements/type/scolaire) : configurables côté backoffice, stockés dans dynamicData et envoyés dans additional_data.fields.

Étape 3 — Montant, Récap & Mentions

  • simulation.amount et simulation.income démarrent volontairement vides (null) — le client doit les saisir lui-même. clampAmount borne le montant entre 100 000 et maxAmount (issu de /loan-parameters) au blur uniquement.
  • La mensualité (monthlyPayment) utilise la formule d'amortissement standard (M = P·i/(1-(1+i)^-n)), alignée sur SimulateurCreditView.vue.
  • Le taux d'endettement (debtRatio, calculé côté client sur la seule mensualité du nouveau prêt) bloque l'envoi au-delà de 60 % (:disabled="debtRatio > 60"), avec message invitant à passer par l'agence.
  • Deux consentements obligatoires (consentDataProcessing, consentElectronicSignature) — voir Consentements légaux pour le détail. Il n'existe pas de case "communications marketing" dans le template actuel.

4. Le système de documents (étape 2) — la partie la plus subtile

Le problème de fond

Le client doit fournir plusieurs documents (bulletins de paie, éventuellement une attestation de virement, et des pièces configurables côté backoffice). Deux modes d'upload coexistent, avec un niveau de certitude d'attribution très différent :

  • Mobile : chaque document a sa propre ligne avec son propre bouton "Ajouter"/"Remplacer" (handleSingleDocUpload(key, event)). Le client choisit explicitement à quel document le fichier correspond → attribution certaine.
  • Desktop : un seul bouton "Sélectionner les documents" permet de choisir plusieurs fichiers d'un coup (handleSelectAllDocuments). Impossible de garantir l'ordre de sélection dans la boîte de dialogue native → attribution devinée, pas certaine.

Stockage : uploadedDocs (objet indexé par clé)

const uploadedDocs = ref({}) // { [clé_document]: { name, type, base64, url } }

Chaque document requis a une clé stable (payslip_0, payslip_1, payslip_2, attestation_virement, ou une clé dérivée du libellé via docKey() pour les pièces configurables — ex : "Copie du bulletin de solde"copie_du_bulletin_de_solde). requiredDocItems est un computed recalculé à chaque changement de hasSalaryEngagement, pour rester cohérent si le client revient en arrière modifier sa réponse.

docKey() supprime les accents

docKey() supprime tous les caractères non-ASCII, accents compris (replace(/[^a-z0-9]+/g, '_')). Un libellé comme "Carte nationale d'identité de la personne à prévenir" devient carte_national_d_identit_de_la_personne_pr_venir — les accents sont perdus définitivement à cette étape. Le backoffice compense en réaffichant le vrai libellé configuré plutôt que la clé technique (voir Fiche détail backoffice, resolveDocLabel).

Sélection groupée desktop : additive, pas écrasante

handleSelectAllDocuments assigne les fichiers sélectionnés aux emplacements encore vides (emptyKeys), dans l'ordre — jamais aux positions déjà remplies. Ça permet au client de fournir ses documents en plusieurs fois sans qu'un second passage n'écrase le premier. Le contrôle "trop de fichiers" compare au nombre d'emplacements encore vides à cet instant, pas au total requis.

Attribution incertaine → clé anonyme à l'envoi

uncertainDocKeys (un simple Set, pas un ref — lu/écrit uniquement en dehors du rendu) trace quelles clés viennent d'une sélection groupée (devinée) plutôt que d'un choix individuel confirmé :

  • handleSelectAllDocumentsuncertainDocKeys.add(key) pour chaque fichier assigné.
  • handleSingleDocUploaduncertainDocKeys.delete(key) (choix explicite du client).

À l'envoi (submitForm), un fichier dont la clé est encore dans uncertainDocKeys ne part pas sous son vrai nom, mais sous une clé anonyme a_classer_1, a_classer_2, etc. Un gestionnaire du backoffice attribue ensuite ces documents "à classer" au bon emplacement (voir Fiche détail backoffice, attribution manuelle).

Validation étape 2

isStep2Valid = providedDocsCount.value === requiredDocsCount.valueexactement le bon nombre de documents (pas "au moins"), pour empêcher un envoi partiel de passer inaperçu.

5. Validation — architecture par étape

Chaque étape a son propre computed de validité (isStep0Valid à isStep3Valid), regroupés dans stepValidators. nextStep() bloque la navigation et active showValidationErrors si l'étape courante n'est pas valide. isFormValid (soumission finale) est le ET logique des 4 validateurs.

6. Soumission (submitForm)

Construit un payload POST /api/loan-requests avec :

  • Les colonnes "garanties" en racine (first_name, last_name, phone, email, salaire, amount, duration, charge_mensuel, profession — via finalProfession, adresse, school, nom_employeur, date_naissance, lieu_naissance, id_agence).
  • additional_data : tout le reste (adresse détaillée, n° de compte, champs dynamiques, engagement salarial, taux appliqué, taux d'endettement, consentements horodatés) + les documents :
    • payslip_0/payslip_1/payslip_2 à la racine d'additional_data (uniquement si l'attribution est certaine).
    • documents : objet regroupant les autres pièces (certaines) + les pièces a_classer_N (incertaines).

Le backend (loan.controller.js, createLoanRequest) répartit ensuite ces documents dans la table documents, en unifiant payslips et autres pièces dans le même mécanisme.

7. Dépendances externes

  • PhoneInput.vue — composant réutilisé sur plusieurs formulaires publics.
  • FeedbackModal.vue / TicketPopup.vue — retours utilisateur génériques.
  • Endpoints backend appelés au montage : /loan-document-requirements/type/scolaire, /loan-field-requirements/type/scolaire, /loan-parameters, /agences.

8. Pièges connus pour un futur développeur

  1. L'OTP ne fait rien de réel pour l'instant (voir section 2) — ne pas présenter ce parcours comme prêt pour la production sans avoir vérifié l'état de l'intégration Sopra Banking Amplitude.
  2. isUnderReview peut masquer tout le formulaire — si le parcours "a disparu" du site, vérifier cette constante avant de chercher un bug ailleurs.
  3. Ne pas réintroduire une case "communications marketing" sans vérifier que consent_communications est bien à nouveau branché de bout en bout (checkbox + payload) — une version précédente envoyait un champ toujours à false sans jamais l'avoir demandé au client, ce qui était trompeur.
  4. Ne pas remplacer uploadedDocs par un tableau à plat — c'est précisément ce qui causait le bug historique où revenir en arrière modifier hasSalaryEngagement puis re-uploader écrasait le mauvais document. Le stockage indexé par clé + requiredDocItems recalculé dynamiquement est la correction.
  5. docKey() perd les accents — ne pas s'étonner si un libellé avec accents ressort mangled dans un stored_name ; c'est structurel, pas un bug d'affichage.
  6. uncertainDocKeys n'est pas réactif (Set classique, pas ref) — voulu (il n'a jamais besoin de déclencher un re-rendu), mais s'il faut un jour l'afficher dans l'UI (ex : badge "à confirmer"), il faudra le rendre réactif.