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, section 2 re-vérifiée le 2026-08-24 suite à l'arrivée de l'OTP réel (voir OTP, SMS & vérification SBA). 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). Un bouton "Vider les champs" (clearAuthFields) permet de repartir de zéro sans recharger la page.
  2. Code SMS (authStep === 'otp') : code à 6 chiffres, valable 120 secondes côté UI (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.

Mis à jour le 2026-08-24 — l'OTP/SMS est réel, la vérification bancaire l'est aussi désormais (avec un repli mock en cas de panne)

requestOtp/resendOtp/verifyOtp appellent réellement le backend (POST /api/otp/send, POST /api/otp/verify, voir OTP, SMS & vérification SBA) — le code 000000 en dur n'existe plus. Le code à 6 chiffres est généré aléatoirement côté serveur, persisté (table otp_codes, expiration 5 min, usage unique) et envoyé par SMS réel via une passerelle Kannel.

La vérification du numéro de compte auprès de la banque (via un middleware "UWM", pont vers Sopra Banking Amplitude) est également devenue réelle le même jour — backend/src/services/sba.service.js interroge désormais un vrai service bancaire, et l'identité du client (lastName, firstName, ville, dateNaissance, lieuNaissance, email) peut donc venir d'une vraie donnée bancaire. Mais : si ce middleware est indisponible (panne, mauvaise config), le service retombe silencieusement sur un mock à deux comptes de test codés en dur (12345678901, 00000000000) — voir le détail et le piège associé dans OTP, SMS & vérification SBA. Le formulaire ne doit donc toujours pas être présumé fonctionnel en production sans avoir vérifié que le middleware UWM est réellement joignable — un test réussi avec l'un des deux comptes connus ne le prouve pas.

Autre changement notable : dateNaissance/lieuNaissance, pré-remplis par la réponse OTP, sont désormais des champs éditables (auparavant disabled) — le client peut corriger une valeur mal renvoyée par SBA plutôt que d'être bloqué dessus.

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, email, téléphone, n° de compte) : affichés désactivés, avec un badge vert "Vérifié" — ce sont les valeurs renvoyées par 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 éditables : Quartier (obligatoire), Maison, B.P., ainsi que date et lieu de naissance — pré-remplis par la réponse OTP mais éditables depuis le 2026-08-24 (auparavant désactivés comme le reste de l'identité), pour permettre au client de corriger une valeur mal renvoyée par SBA sans être bloqué dessus.

É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é :

  • handleSelectAllDocuments → uncertainDocKeys.add(key) pour chaque fichier assigné.
  • handleSingleDocUpload → uncertainDocKeys.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.value — exactement 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/SMS et la vérification bancaire sont tous deux réels, mais la seconde peut retomber sur un mock sans prévenir (voir section 2) — ne pas présenter ce parcours comme fiable en production sans avoir vérifié que le middleware UWM (backend/src/services/sba.service.js) est réellement joignable ; une démo réussie avec l'un des deux comptes de test ne le garantit pas.
  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.