Manuel de l'app. À LIRE avant toute intervention, à METTRE À JOUR après chaque modif (CHANGELOG en bas).
Créée le 2026-08-22. Statut : EN LIGNE (https://contrats.panelbay.com), 9 formules FR (5 actuelles 2026-09 + 4 anciennes/legacy), relié à l'onboarding et au dashboard ops RP.
ops-resident.panelbay.com/contrats, contrats.js) ou automatiquement à la soumission du formulaire client (rp-onboarding -> aios-automations).make-migration/SPEC-04-onboarding-contrat.md./root/workspace/apps/rp-contractscd backend && uvicorn app:app --host 127.0.0.1 --port 8365rp-contracts.service (systemctl restart rp-contracts)contrats.panelbay.com.backend/app.py — API FastAPI (création contrat, page de signature, signature, PDF). Deux régimes cohabitent (voir §5/§7) : FAMILLE (multi-signataires, formules actuelles) et LEGACY (mono-signataire, anciennes formules), routés automatiquement via _resolve_token().backend/pricing.py — grille tarifaire + dégressif famille des 5 formules actuelles (ajouté 2026-09-02, seule source de vérité pour ces montants, voir §7).backend/render.py — rendu HTML -> PDF (wkhtmltopdf). Blocs mono-signataire historiques (signature_block_html, audit_block_html) + blocs multi-signataires (parties_block_html, signers_table_html, signature_blocks_html, audit_block_html_multi, family_html, family_pdf), SHA-256.backend/db.py — SQLite data/contracts.db (tables contracts + contract_signers, voir §4).templates/<pack>-<lang>.html — contrats. Formules actuelles : placeholders {{...}} + zones {{PARTIES_BLOCK}}, {{SIGNERS_TABLE}}, {{SIGNATURE_BLOCKS}}, {{AUDIT_BLOCK}} (multi-signataires). Anciennes formules : {{SIGNATURE_BLOCK}} (singulier), {{AUDIT_BLOCK}} (mono-signataire, inchangé).frontend/sign.html — page de signature (pad canvas + consentement), générique pour les deux régimes ; affiche un bandeau "contrat famille" + statut des autres signataires quand pertinent.frontend/intake.html — page de saisie d'identité pour un dossier "tout neuf" (mode famille, voir §5bis) : N blocs de formulaire (un par personne prévue), envoi unique qui déclenche N emails de signature individuels.data/pdf/ — PDF générés (aperçu + signés).data/contracts.db : table contracts (id, sign_token[1], pack, language, template, client_name/email, dossier_token, placeholders JSON, status draft/sent/partially_signed/signed/void, person_count, discount_pct, family_total, family_currency, audit legacy : signed_at/ip/ua/typed_name/signature_png, doc_sha256, pdf_path).sign_token a 3 usages selon le statut : contrat LEGACY = lien direct mono-signataire ; contrat FAMILLE draft (identités pas encore connues) = jeton d'INTAKE (/intake/<token>) ; contrat FAMILLE sent/signed = vide (''), chaque signataire a alors son propre lien (voir contract_signers).contract_signers (ajoutée 2026-09-02) : un contrat FAMILLE a une ligne par signataire — id, contract_id (FK), sign_token (UNIQUE, lien perso de CE signataire), name, email, placeholders JSON (identité propre : dob, passeport, adresse...), share_amount (part personnelle), status pending/signed, signed_at/signer_ip/signer_ua/signature_png/typed_name, created_at. Pour un contrat créé en mode "dossier tout neuf" (draft), les N lignes existent dès la création mais avec name/email/placeholders VIDES jusqu'à la soumission de l'intake (voir §5bis).contract_signers pour un contract_id donné. Pas de colonne "mode" séparée : SELECT ... WHERE contract_id=? vide = legacy.POST /api/contracts (auth token) — crée un contrat. Trois formes de body :signers, formules actuelles) : {pack, language, currency, placeholders:{dossier_name, dossier_token, group_members_list, group_deposit_paid,...}, signers:[{name, email, dob, country_of_birth, passport_number, passport_country, address, country_of_residence, phone, share_amount?}], family_total_override?, send_email}. Le total dégressif est calculé côté serveur (pricing.py) sauf si family_total_override est fourni ; la part de chaque signataire est répartie à parts égales sauf share_amount explicite. Renvoie {id, family:true, person_count, discount_pct, family_total, currency, signers:[{name,email,sign_url,emailed}]} — un lien de signature PAR personne.draft:true, formules actuelles uniquement) : {pack, language, currency, draft:true, person_count, placeholders:{dossier_name, group_deposit_paid,...}}. Aucune identité requise : le total dégressif est déjà calculable (person_count connu). Renvoie {id, family:true, draft:true, person_count, family_total, discount_pct, currency, intake_url} — UN SEUL lien d'intake (pas de signers). Voir §5bis. Ajouté 2026-09-02 (3).{pack, language, client_name, client_email, placeholders:{...}} (comportement 2026-08-22 inchangé). Renvoie {id, sign_url}.GET /api/pricing (auth token) — ?pack=¤cy=&person_count= → grille + dégressif calculés (pricing.py). 404 pour une ancienne formule (pas de grille automatique, montants toujours saisis à la main côté dashboard). Ajouté 2026-09-02.GET /api/contracts (auth token) — liste les contrats + signer_rows/signed_count (progression de signature famille, 0/0 = legacy), pour la vue admin du dashboard ops RP.GET /api/packs (auth token) — formules disponibles {current:[...], legacy:[...]}, chaque entrée avec family:true/false, pour alimenter le sélecteur du dashboard.GET /sign/{token} — page de signature (publique, lien unique). {token} = un contract_signers.sign_token (famille) OU un contracts.sign_token (legacy), résolu automatiquement.GET /api/contracts/{token}/view — HTML complet du contrat (tous les signataires + leur statut si famille) pour l'iframe.GET /api/contracts/{token}/meta — métadonnées ; famille : {family:true, person_count, signed_count, you:{name,status}} ; legacy : {family:false, ...}.POST /api/sign/{token} — signe. Famille : ne marque QUE ce signataire ; contrat passe signed seulement quand TOUS ont signé (sinon partially_signed), réponse {all_signed, remaining_signers:[...]}. Legacy : comportement 2026-08-22 inchangé ({accept:true, typed_name, signature_png(dataURL)}).GET /api/contracts/{id}/pdf (auth token) — télécharge le PDF (signé si tous ont signé / si dispo, sinon aperçu reflétant l'état actuel des signatures).GET /health.Cas : Younes crée un dossier famille dont les membres n'existent NULLE PART encore (ni Airtable, ni ce service) — typiquement un tout nouveau client dont il ne veut pas gérer la saisie lui-même. Il fixe juste le nom du dossier et le nombre de personnes (POST /api/contracts avec draft:true, voir §5). Le service crée immédiatement N lignes contract_signers VIDES (identité à blanc) et UN SEUL jeton d'intake public (stocké dans contracts.sign_token, statut draft).
GET /intake/{token} (public, pas d'auth) — page HTML (frontend/intake.html) : N blocs de formulaire (un par personne prévue), champs identité (nom, email, téléphone, date de naissance, pays de naissance, passeport + pays, adresse, pays de résidence). 404 si le jeton n'existe pas ou si l'intake a déjà été soumis (status != 'draft').GET /api/contracts/intake/{token}/meta (public) — {dossier_name, pack_title, person_count, language} pour pré-remplir l'en-tête de la page.POST /api/contracts/intake/{token} (public) — body {people:[{name, email, phone, dob, country_of_birth, passport_number, passport_country, address, country_of_residence}, ...]}, exactement person_count entrées, chacune avec un nom non vide. Remplit les N contract_signers (dans l'ordre de création), passe contracts.status à sent, puis envoie à CHAQUE personne son propre email + lien /sign/{token} (réutilise _send_invite_email, best-effort). Après cet appel, le lien d'intake est définitivement fermé (le contrat n'est plus draft) : impossible de le re-remplir, seule une intervention manuelle en base permettrait de rouvrir (non prévu)._resolve_token() (utilisé par /sign/{token}, /view, /meta, POST /api/sign/{token}) rejette explicitement un contracts.sign_token dont le pack est une formule actuelle (FAMILY_PACKS) — un jeton d'intake ne peut donc jamais être utilisé comme lien de signature par erreur, même avant soumission.FAMILY_PACKS), pas pour les anciennes (mono-signataire, pas de notion de dossier "tout neuf" à ce niveau).CONTRACTS_API_TOKEN dans /root/aios/.env (repli sur AUTOMATIONS_TOKEN si absent).CONTRACTS_BASE_URL (défaut https://contrats.panelbay.com) pour construire les liens de signature.{{PARTIES_BLOCK}}/{{SIGNERS_TABLE}}/{{SIGNATURE_BLOCKS}}/{{AUDIT_BLOCK}} construites côté serveur à partir de contract_signers (identité + montant + statut PROPRES à chaque signataire ne sont plus des placeholders plats, voir render.py).TEMPLATES/TITLES/CURRENT_PACKS/FAMILY_PACKS dans backend/app.py) :residence-temporaire, residence-fiscalite, residence-integrale, residence-permanente, investor-pass) : reflètent le site public resident-paraguay.fr/tarifs.html au 2026-09-02 (prix : Temporaire 1890€, +Fiscalité 2490€, Permanente +1990€ EN OPTION sur un parcours déjà engagé, Investor Pass 3000€ + investissement dès 70k USD) plus residence-integrale, ajoutée le 2026-09-03 (Temporaire+Fiscalité+Permanente en UN SEUL contrat signé dès le départ, 4150€ = 2450 + 1700 au tarif bundle au lieu de 1950 en avenant séparé, voir §7bis et CHANGELOG). Présentées en avant-plan du sélecteur de Younes (dashboard ops RP, /contrats). FR uniquement pour l'instant (pack-residence-temporaire-fr.html, pack-residence-fiscalite-fr.html, pack-residence-integrale-fr.html, pack-residence-permanente-fr.html, pack-investor-pass-fr.html). EN/ES restent à construire (repli FR en attendant). Mode FAMILLE : un seul document peut avoir plusieurs signataires (les adultes d'un même dossier), chacun avec son propre lien /sign/<token> ; le contrat n'est signed que lorsque TOUS ont signé (sinon partially_signed). Montant dégressif famille automatique (pricing.py, §7bis), sauf Investor Pass (honoraires fixes par personne, pas de remise). Chaque signataire n'est tenu que de sa propre part personnelle (clause explicite dans les 5 templates, pas de solidarité de paiement par défaut).temporaire, fiscal, permanent, investor, 12 templates x 3 langues) : CONSERVÉES TELLES QUELLES, accessibles via le bouton "+" du sélecteur, pour des cas particuliers. Mode LEGACY inchangé : 1 contrat = 1 personne = 1 lien de signature. Ne pas les supprimer ni les renommer, ne pas leur ajouter le mode famille sans décision séparée.permanent est un parcours COMPLET (temporaire + cédula + RUC + permanente empaqueté, prix pricing.js 3700-6900€) ; la formule residence-permanente (actuelle) est un AVENANT qui vient s'ajouter à un contrat Temporaire ou Fiscalité déjà signé (prix site : +1990€, ne redécrit pas les étapes déjà couvertes) ; et residence-integrale (actuelle, ajoutée 2026-09-03) empaquette les 3 étapes (temporaire+fiscalité+permanente) dans UN SEUL contrat signé dès le départ à 4150€, avec remise bundle sur la partie permanente (1700€ au lieu de 1950€). Ne pas les confondre en cas de doute sur le pack à choisir : residence-permanente = le client a DÉJÀ un contrat en cours et veut juste ajouter la permanente plus tard ; residence-integrale = le client veut tout signer d'un coup dès le départ.pack-temporaire-fr.html/pack-fiscal-fr.html/pack-permanent-fr.html/pack-investor-fr.html, Article 1/2/3/4/5/12 réécrits pour le mode multi-signataires le 2026-09-02 ; pack-residence-integrale-fr.html composé le 2026-09-03 en fusionnant les Articles 3 de pack-residence-fiscalite-fr.html et pack-residence-permanente-fr.html) : ils ne passent PAS par tools/build_templates.py (ce générateur ne connaît que les 4 anciens packs x 3 langues, JOBS codé en dur) et n'ont pas de source .md dans make-migration/contract-sources/. Toute future modification de ces 5 templates se fait en éditant directement le HTML dans templates/ (attention à préserver {{PARTIES_BLOCK}}/{{SIGNERS_TABLE}}/{{SIGNATURE_BLOCKS}}/{{AUDIT_BLOCK}}, ne pas revenir à {{SIGNATURE_BLOCK}} singulier).pack-residence-temporaire-fr (formule actuelle par défaut, remplace l'ancien repli pack-temporaire-fr). Une langue inconnue (ex. de) tombe sur FR.tools/build_templates.py (legacy uniquement) est multilingue : tables financières, ligne de référence dossier et zone de signature sont localisées par langue (dict LANG). Les sources EN/ES respectent les mêmes tokens de structure ([pdf_page_break], [table], [signer_field_checkbox], {{placeholders}}, numérotation ### 5.1) que le FR ; aucun em-dash dans EN/ES.backend/pricing.py, ajouté 2026-09-02)GET /api/pricing). Ne pas dupliquer cette grille ailleurs.FAMILY_DISCOUNT_PACKS = les 4 formules Résidence, PAS Investor Pass) : 0% seul, -5% à 2 personnes, -10% à partir de 3 (règle du site, "Tarif dégressif appliqué automatiquement"). S'applique au prix de base de residence-integrale (4150) comme aux 3 autres ; se cumule avec la remise bundle déjà intégrée dans ce prix de base (les deux remises sont de nature différente : bundle = tout prendre d'un coup en 1 contrat, famille = plusieurs personnes sur le même dossier).EUR_TO_USD = 1.08 (même approximation que l'ancien rp-onboarding/backend/pricing.js, pas un taux du jour live). Toujours affiché comme MODIFIABLE côté dashboard avant envoi (family_total_override), jamais imposé silencieusement.family_pricing() renvoie None), montants toujours saisis à la main comme avant.contract_signers.is_minor (INTEGER, migration idempotente db.py) : calculé depuis dob via _is_minor() (app.py, moins de 18 ans à la date du jour ; dob absente/illisible -> False, jamais de dispense sans date explicite)._create_family_contract, dob connue tout de suite) OU à la soumission de l'intake (intake_submit, dob saisie par la personne elle-même à ce moment-là). Pas de nouveau champ formulaire : dob existait déjà des deux côtés.{{PARTIES_BLOCK}} l'annonce "dispensé de signature, représenté par ses parents/tuteurs") mais n'a ni lien de signature généré, ni email envoyé, ni zone de signature active ({{SIGNATURE_BLOCKS}} affiche une simple mention). all_signed/remaining_signers/GET .../meta (required_count, minors_count, signed_count) l'excluent du calcul : le contrat passe signed dès que tous les signataires NON mineurs ont signé.POST /api/sign/{token} répond {"ok":true,"minor":true} sans rien modifier.sign.html/intake.html mis à jour pour afficher "X mineur(s), dispensé(s) de signature" et le ratio de signatures sur le nombre REQUIS (pas le total de personnes).GET /api/contracts/{cid}/links (auth token) : renvoie les liens de signature de chaque signataire (ou le lien d'intake si le contrat est encore draft), pour que Younes les retransmette à la main si le client a fermé sa page avant de les noter.GET /api/contrats/:id/links dans rp-dashboard/contrats.js. Bouton "Voir liens" sur chaque ligne de l'onglet "Contrats" (public/contrats.html) : ouvre une ligne dépliée avec chaque lien cliquable + bouton Copier (mineurs affichés sans lien, juste la mention).frontend/intake.html : les liens de la page de fin d'intake sont maintenant de vrais <a href> cliquables (avant : texte brut à copier-coller).SERVICE_PACKS = ["permis", "domiciliation", "comptabilite", "comptabilite-mensuelle"] (app.py) : mode LEGACY (1 contrat = 1 personne, montant saisi à la main comme les anciennes formules, pas de grille dégressif famille). comptabilite-mensuelle (ajouté 2026-09-07) = suivi comptable/fiscal mensuel d'un RUC déjà ouvert, prix fixe 330 000 ₲/mois écrit en dur dans le template, sans engagement de durée (résiliable 30 j avant fin de mois) ; distinct du comptabilite annuel (ouverture RUC + suivi 12 mois) qui reste disponible. 3 nouveaux templates FR : pack-permis-fr.html (obtention/échange du permis de conduire paraguayen), pack-domiciliation-fr.html (adresse légale, 12 mois renouvelable par tacite reconduction), pack-comptabilite-fr.html (ouverture RUC + suivi comptable/déclarations, contenu repris des pages comptabilite-ruc.html/domiciliation.html/permis-de-conduire.html du site public au 2026-09).GET /api/packs renvoie une 3e clé services (en plus de current/legacy), affichée dans le dashboard sous "Services complémentaires" (entre les formules actuelles et le bouton "+ anciennes formules"). Uniquement dans le flux "dossier existant" (pas dans "nouveau dossier", réservé aux formules famille avec calcul dégressif).pricing.py (comme les anciennes formules) : Younes saisit le montant à la main dans le dashboard. Repères tarifaires du site (informatifs, non codés en dur) : RUC 300€ à l'ouverture, suivi comptable 390€/an, domiciliation 790€/an.rp-dashboard/public/contrats.html : constante JS PACK_STYLE (icône + couleur par pack), appliquée en bordure colorée + icône sur les cartes de sélection (packCardHtml()) et en badge coloré dans la colonne "Formule" de l'onglet Contrats (packBadge()), pour distinguer visuellement les types de contrat à mesure qu'ils se multiplient. Palette propre à cet outil interne (pas le design system RP), toujours sans doré/or.DELETE /api/contracts/{cid} (auth token) : supprime la ligne contracts, ses lignes contract_signers liées, et les PDF preview/signed sur disque. Irréversible, pas de corbeille.DELETE /api/contrats/:id dans rp-dashboard/contrats.js. Bouton rouge "Supprimer" à côté de "Voir liens" dans l'onglet Contrats du dashboard (public/contrats.html), avec confirmation JS obligatoire (nom du client affiché dans le message) avant l'appel.sign.html/meta mais ne reçoit pas de relance automatique par email. À ajouter si besoin.rp-onboarding -> aios-automations, soumission du formulaire) crée toujours 1 contrat LEGACY par adulte (anciennes formules) : PAS branché sur le mode famille des formules actuelles. Décision délibérée le 2026-09-02 pour ne pas risquer le pipeline client live sans y être invité explicitement ; à reprendre si on veut que le funnel auto génère aussi un contrat famille.Idioma (singleSelect) accepte maintenant fr/en/es (option "es" créée) et rp_onboarding.py trace les dossiers ES. Onboarding trilingue de bout en bout.backend/pricing.py BASE_PRICE_EUR["residence-integrale"] = 4150 (+ docstring). La remise bundle passe donc de 210€ à 250€ (vs souscription séparée 2450+1950=4400) : note de transparence du template client pack-residence-integrale-fr.html mise à jour (210 → 250 EUR). Doc §7 et §7bis alignées. Le dégressif famille et les autres prix (Temporaire 1850, Fiscalité 2450, Permanente 1950, Investor Pass 3000) sont inchangés. Les montants figés dans les contrats déjà signés ne sont pas touchés. Testé : self-check pricing.py (integrale 4150 / -5% / -10%). Service redémarré.frontend/intake.html : à la réponse du POST /api/contracts/intake/{token}, si un seul signataire signable est renvoyé (signers filtré sur non-mineur + sign_url), on fait location.href = sign_url (accès direct à /sign/<token>). Le récap listant les liens n'est conservé que pour un intake multi-personnes (plusieurs liens à distribuer). Le passeport saisi à l'intake est déjà pré-rempli sur la page de signature (voir 2026-09-08 numéro passeport), donc pas de double saisie. Aucun restart (intake.html relu à chaque requête).frontend/intake.html : les 3 champs passent à required:true dans FIELDS (astérisque * sur le label) et la validation avant envoi liste tous les champs obligatoires manquants par personne (« Personne i : champ(s) obligatoire(s) manquant(s) : ... »). Backend POST /api/contracts/intake/{token} (app.py) : validation renforcée, rejette 400 avec message clair si nom / date de naissance / n° de passeport / pays du passeport manque pour une personne (défense côté serveur, pas seulement l'UI). S'applique à chaque personne du dossier (y compris en intake multi-personnes). Testé : draft 1 pers → intake sans passport_number = 400 « Personne 1 : numéro de passeport obligatoire » ; avec les 4 champs = 200 (liens de signature générés) ; contrat de test supprimé. intake.html relu à chaque requête (pas de restart nécessaire pour le front), mais app.py a changé donc service redémarré, /health 200. NB : les champs Téléphone, Pays de naissance, Adresse, Pays de résidence restent facultatifs ; si un jour des mineurs sans passeport doivent être saisis, prévoir une exemption (non demandé).render.py : {{#multi}}...{{/multi}} (gardé si ≥ 2 signataires) et {{#solo}}...{{/solo}} (gardé si 1 seul), pilotés par family_html(..., multi=len(signers)>1) (_apply_conditionals). Les 5 templates Résidence FR (pack-residence-*-fr.html, pack-investor-pass-fr.html) ont été adaptés : en solo disparaissent ou sont simplifiés : sous-titre « CONTRAT FAMILLE / PLUSIEURS SIGNATAIRES », intro Article 1 (« la personne ci-dessous, dénommée le Client » au lieu de « les Clients »), paragraphe Article 2 « un seul document couvrant tout le groupe », lignes du tableau 5.1 « Nombre de Clients » + « prix par personne » + « remise » (ne reste que Montant total / Acompte / Solde), titres « de la famille » et « (famille) », toute la section 5.2 (remplacée par « 5.2 Montant dû par le Client », une phrase), phrase Article 9 (« tous les Clients... ayant signé » → « Le Client signe »), clause Article 12 (« parts personnelles des autres Clients »). Aussi : parties_block_html n'affiche plus « (contact principal) » quand il n'y a qu'un signataire. À 2 personnes ou plus, tout le libellé famille est conservé à l'identique. Migration appliquée par script (backups .bak supprimés après vérif). Testé : rendu solo + multi (2 pers) des 5 formules → en solo aucun marqueur famille ni balise {{#...}} résiduelle et présence de « Montant dû par le Client » ; en multi tous les marqueurs présents ; spot-check visuel Article 1/5 solo OK (residence-temporaire : 1850 / acompte 500 / solde 1350). Service redémarré, /health 200. NB : templates lus à chaque rendu (pas de cache), mais render.py a changé donc restart. EN/ES : templates pas encore construits (repli FR), à adapter pareil quand ils existeront.backend/render.py (render_html) : quand discount_pct vaut 0 (ce qui correspond exactement à un seul signataire, grille dégressive pricing.py), la ligne <tr> « Remise famille appliquée » est retirée du HTML avant substitution des placeholders (regex ciblée, s'applique aux 5 templates Résidence sans les éditer un par un ; no-op sur les templates legacy/services qui n'ont pas cette ligne). À 2 personnes ou plus, la ligne reste avec la vraie remise. Testé : contrat residence-temporaire 1 signataire → discount_pct=0, « Remise famille » absent de la vue ; 2 signataires → discount_pct=5, ligne présente ; contrats de test supprimés. Service redémarré, /health 200.Passeport n° (seule info d'identité imprimée avec le nom, voir 2026-09-07 (3)), mais pour un contrat créé à partir de signataires connus (vente RP → dossier sans passeport en base), il restait vide, jamais saisi. Seul l'intake (dossier tout neuf) le collectait. Fix : la page /sign/{token} (frontend/sign.html) demande maintenant le numéro de passeport avant de débloquer la signature (champ requis, à côté du nom ; note « figurera sur votre contrat, vérifiez-le »). Comme chaque signataire a son propre lien, chacun saisit SON passeport (cas multi-personnes couvert nativement). Backend (backend/app.py) : (1) GET /api/contracts/{token}/meta renvoie le passeport déjà connu (you.passport_number en famille depuis contract_signers.placeholders, passport_number en legacy depuis client_passport_number) pour pré-remplir le champ (le signataire vérifie au lieu de retaper). (2) POST /api/sign/{token} accepte passport_number, le persiste dans les placeholders AVANT le rendu (famille : contract_signers.placeholders du signataire ; legacy : contracts.placeholders.client_passport_number) → le PDF signé et la vue HTML l'affichent. Requis sauf pour un mineur (dispensé de signature). Testé bout en bout (contrat famille residence-temporaire créé sans passeport → meta vide, absent de la vue → signature avec AB123456 → présent dans la vue ; signature sans passeport → 400 « numéro de passeport requis » ; 2 contrats de test supprimés). Service redémarré, /health 200. NB : sign.html est relu à chaque requête (sign_page), le changement front seul n'aurait pas eu besoin de restart, mais app.py a changé.backend/pricing.py (BASE_PRICE_EUR). Intégrale maintenue à 4190€ (choix explicite de Matt) : la remise bundle passe donc mécaniquement de 290€ à 210€ (vs 2450+1950=4400 en souscription séparée) ; docstring de pricing.py + note de transparence du template pack-residence-integrale-fr.html (2 490/1 990/290 → 2 450/1 950/210) mis à jour en cohérence. Investor Pass 3000€ et dégressif famille inchangés. Autres services (RUC, compta, domiciliation, permis) inchangés. Les montants figés dans les contrats déjà signés ne sont pas touchés. Testé : self-check pricing.py (1850/2450/1950/4190/3000), aucun prix en dur résiduel dans les templates.parties_block_html (render.py, couvre les 5 packs famille) + bloc partybox inline des 4 services (permis, domiciliation, comptabilite, comptabilite-mensuel). (2) Résidence+Fiscalité et Intégrale : ouverture du compte bancaire ajoutée à l'étape du second voyage (Article 2 fiscalité + clauses délais 3.5/3.7) ; déclarations RUC désormais "jusqu'à l'expiration de la carte de résidence temporaire" (au lieu de "pendant les 2 ans") ; mention IRP retirée (exclusion société conservée) ; certificat de résidence fiscale "sur demande" ; en-tête Cédula = "(sur place)" (retrait de "après obtention de la résidence"). (3) Pouvoir notarial : retrait de "peut être révoqué à tout moment par écrit" sur les 4 packs. Testé : rendu des 4 packs + service, 8 points vérifiés, aucun placeholder orphelin, service redémarré.pack-comptabilite-mensuel-fr.html (suivi comptable/fiscal mensuel d'un RUC déjà ouvert, prix fixe 330 000 guaraníes/mois, sans engagement, résiliable avec préavis 30 j, périmètre RUC aligné sur le retour de Younes : déclarations obligatoires de base + factures de l'activité + IRP possible selon situation, hors société), enregistré dans TEMPLATES/TITLES/SERVICE_PACKS (clé comptabilite-mensuelle). Apparaît automatiquement dans le sélecteur via GET /api/packs. Testé en réel : rendu OK (prix présent, aucun placeholder orphelin), py_compile OK, service redémarré, /api/packs liste bien les 4 services dont comptabilite-mensuelle. Voir aussi 2026-09-07 (contrats Résidence) ci-dessous.{{group_balance_due}} calculé dans render.py, = total famille - acompte). Grille de prix inchangée (déjà correcte). Testé : rendu des 4 packs, solde net juste (Fiscalité 2490-500=1990), aucun placeholder orphelin. NB : le site public promet encore "un seul voyage" et doit être corrigé séparément.DELETE /api/contracts/{cid} (supprime contracts + contract_signers + PDF disque), proxy rp-dashboard, bouton "Supprimer" (rouge, confirmation obligatoire avec nom du client) à côté de "Voir liens" dans l'onglet Contrats (voir §7septies). Testé en réel (script Python + TestClient) : contrat créé puis supprimé, DELETE sans token -> 401, avec token -> 200 et disparition de GET /api/contracts, nouvelle tentative -> 404. py_compile/node --check OK, les deux services redémarrés.SERVICE_PACKS (mode LEGACY, montant à la main, voir §7quinquies), 3 templates FR écrits sur le modèle des anciennes formules (structure/articles identiques, contenu adapté à partir des pages service du site public), GET /api/packs gagne une clé services. Dashboard : nouvelle section "Services complémentaires" dans l'onglet "Nouvelle invitation" (flux dossier existant uniquement), PACK_STYLE (icône + couleur par pack) appliqué aux cartes de sélection et à la colonne "Formule" de la liste des contrats (voir §7sexies). Testé de bout en bout en réel (TestClient) : GET /api/packs liste bien les 3 services, un contrat créé pour chacun (send_email:false) → HTML vérifié (aucun placeholder {{...}} orphelin, titre et articles présents) ; contrats de test supprimés après vérification. py_compile OK sur app.py, node --check OK sur le script inline de contrats.html, les deux services (rp-contracts, rp-dashboard) redémarrés, /api/packs reconfirmé en direct après redémarrage.frontend/intake.html : les liens de signature affichés après soumission sont maintenant de vrais <a href> cliquables (avant : texte brut) ; (2) nouvel endpoint GET /api/contracts/{cid}/links + proxy rp-dashboard + bouton "Voir liens" sur chaque contrat de l'onglet "Contrats" (ligne dépliable, liens cliquables + bouton Copier, gère aussi le cas draft en renvoyant le lien d'intake) pour que Younes retrouve les liens si un client a fermé sa page sans les noter (voir §7quater) ; (3) un signataire mineur (moins de 18 ans, calculé depuis dob à la création ou à l'intake, colonne contract_signers.is_minor) reste partie au contrat mais n'a plus besoin de signer lui-même : pas de lien généré/envoyé pour lui, exclu du calcul all_signed/remaining_signers, mention "dispensé de signature, représenté par ses parents/tuteurs" dans le PDF (voir §7ter). Testé de bout en bout en réel (TestClient) : contrat famille 1 adulte + 1 enfant (dob 2015), création -> l'enfant apparaît avec minor:true sans sign_url, GET .../links l'exclut du lien, signature du seul adulte -> all_signed:true, statut signed, PDF final généré, GET .../meta renvoie required_count:1/minors_count:1/signed_count:1 ; HTML du contrat vérifié (aucun placeholder orphelin, mention mineur présente dans {{PARTIES_BLOCK}}). Contrat de test supprimé après vérification. py_compile OK sur les 4 modules backend, node --check OK sur contrats.js, les deux services (rp-contracts, rp-dashboard) redémarrés.UNIQUE constraint failed: contracts.sign_token) pour TOUT contrat famille direct (dossier déjà connu, formule quelconque), pas seulement residence-integrale. Signalé par Matt en essayant de créer le contrat de Serge Dechosal (formule Résidence Intégrale). Cause réelle, présente depuis le 2026-09-02 (2) : _create_family_contract() insérait une chaîne vide fixe ('') dans contracts.sign_token pour TOUT contrat famille "envoyé directement" (dossier déjà dans Airtable, signers connus dès la création) — ce champ est UNIQUE NOT NULL en base, et n'est de toute façon jamais utilisé comme vrai lien pour ce type de contrat (chaque signataire a le sien dans contract_signers). Le tout premier contrat de ce type créé (2026-09-02, "Alice QA") a donc pris la valeur '', et TOUT contrat famille direct suivant, pour n'importe quelle formule, était condamné à échouer sur ce même conflit. Ça n'avait jamais été revu depuis faute d'un 2e contrat famille direct réel créé entre-temps (les tests suivants du 09-02 et 09-03 passaient par le mode draft/intake, qui génère lui un vrai jeton unique). Fix : sign_token reçoit désormais un placeholder unique généré (f"family-{cid}") au lieu de ''. Commentaire de schéma (db.py) mis à jour. Testé : 2 contrats famille directs créés à la suite sans collision (residence-integrale puis residence-fiscalite, tous deux ok:true), HTML des deux vérifié (aucun placeholder orphelin, montants corrects) ; py_compile OK, service redémarré. Pas d'impact sur les contrats déjà existants (la ligne historique "Alice QA" garde son sign_token='', _resolve_token() ne la sert jamais comme lien réel de toute façon). Younes/Matt peuvent réessayer "Générer le contrat" normalement à partir de maintenant.residence-integrale (Temporaire + Fiscalité + Permanente en UN SEUL contrat, 4190€/pers). Demande Matt : la Permanente n'est aujourd'hui qu'un avenant "extra" ajouté à un parcours déjà engagé ; il voulait une formule qui empile directement Résidence+Fiscalité (2490) et Permanente en un seul contrat signé dès le départ. Calcul clarifié avec Matt (le total annoncé, 4190€, ne correspondait pas à la somme brute 2490+1990=4480€) : la remise vient d'un tarif bundle sur la partie Permanente (1700€ au lieu de 1990€ quand elle est prise d'un coup avec le reste), soit 2490+1700=4190€. backend/pricing.py : BASE_PRICE_EUR["residence-integrale"] = 4190, ajouté à FAMILY_DISCOUNT_PACKS (dégressif famille -5%/-10% s'applique aussi à ce prix de base, en plus de la remise bundle déjà incluse dedans). backend/app.py : TEMPLATES/TITLES/CURRENT_PACKS étendus (nouvel ordre : temporaire, fiscalite, integrale, permanente, investor-pass). Nouveau template templates/pack-residence-integrale-fr.html composé à la main en fusionnant les Articles 3 (prestations) de pack-residence-fiscalite-fr.html (préalable, résidence temporaire, Cédula, RUC+certificat fiscal) et pack-residence-permanente-fr.html (condition des 2 ans, passage au statut permanent, mise à jour Cédula), avec un Article 5 réécrit pour un montant unique (pas de mention "s'ajoute au contrat initial" puisque tout est dans le même contrat dès le départ) et une note de transparence sur la remise bundle. Aucun changement côté rp-dashboard (contrats.js/contrats.html) : le sélecteur de formules et le calcul dégressif sont 100% dynamiques (GET /api/packs/GET /api/pricing), la 5e formule apparaît automatiquement. Testé de bout en bout en réel : GET /api/packs liste bien les 5 formules, GET /api/pricing?pack=residence-integrale&person_count=2 renvoie 4190 base / -5% / 3980 par pers. / 7960 total, contrat draft créé (2 personnes, dossier "Test QA Integrale") → intake rempli → HTML du contrat vérifié (titre présent, 12 articles, montants 4190/3980/7960 corrects, aucun placeholder {{...}} orphelin) ; py_compile OK sur app.py/pricing.py/render.py, service redémarré. Ligne de test restante dans contracts.db (garde-fou anti-suppression, sans risque, à retirer à la main si besoin, même limitation que les lignes de test précédentes)._create_draft_family_contract() : crée le contrat + N contract_signers VIDES d'un coup (le prix dégressif est déjà calculable, person_count connu dès la création), stocke un jeton d'intake dans contracts.sign_token, statut draft. Nouvelles routes publiques GET /intake/{token} (page HTML), GET /api/contracts/intake/{token}/meta, POST /api/contracts/intake/{token} (remplit les N signataires en un seul envoi, ferme l'intake, déclenche N emails de signature individuels). Nouveau fichier frontend/intake.html. _resolve_token() durci pour qu'un jeton d'intake ne puisse jamais être interprété comme un jeton de signature legacy (rejette tout contracts.sign_token dont le pack est une formule actuelle). Voir §5bis pour le détail. Testé de bout en bout en réel : dossier "Famille QA Draft" créé avec person_count:2 (total dégressif -5% déjà affiché à la création), intake rempli pour 2 personnes (Claire QA, David QA) → 2 liens de signature générés automatiquement, intake fermé (404 après soumission), vue du contrat sans placeholder orphelin, signature des 2 -> partially_signed puis signed. PDF de test supprimé ; ligne de test restante dans contracts.db (garde-fou anti-suppression, voir CHANGELOG 2026-09-02 (2)).contract_signers (1 ligne par signataire, son propre sign_token, sa part share_amount, son statut pending/signed indépendant) ; contracts gagne person_count/discount_pct/family_total/family_currency, status gagne la valeur partially_signed. Nouveau backend/pricing.py (seule source de vérité des prix + dégressif famille, voir §7bis). render.py : nouveaux blocs multi-signataires (parties_block_html, signers_table_html, signature_blocks_html, audit_block_html_multi, family_html, family_pdf), blocs mono-signataire historiques conservés intacts. Les 4 templates actuels réécrits (Article 1 : plusieurs .partybox via {{PARTIES_BLOCK}}, désignation "les Clients" ; Article 5 : total famille + remise + tableau des parts via {{SIGNERS_TABLE}} + clause "chaque Client n'est tenu que de sa propre part, pas de solidarité" ; Article 12 : {{SIGNATURE_BLOCKS}} pluriel). app.py : POST /api/contracts détecte signers:[...] dans le body (famille) vs l'ancien shape client_name/placeholders (legacy, comportement 100% inchangé) ; nouveau GET /api/pricing ; _resolve_token() route chaque appel (/sign/{token}, /view, /meta, POST /api/sign/{token}, /pdf) vers le bon régime automatiquement selon que le token appartient à contract_signers ou contracts ; GET /api/contracts (liste) renvoie signer_rows/signed_count via JOIN. sign.html : bandeau "contrat famille, X/Y ont signé, vous signez pour <nom>", gère le cas "vous avez déjà signé" sans re-proposer le formulaire, message de fin adapté ("en attente de : ...") si tout le monde n'a pas encore signé. Décision volontaire : le funnel client automatique (rp-onboarding->aios-automations) n'est PAS branché sur ce nouveau mode, il continue de créer un contrat legacy par adulte comme avant (voir §8, "reste à faire"). Testé de bout en bout en réel : contrat famille 2 signataires (Résidence + Fiscalité, dégressif -5% vérifié à 2 pers. et -10% à 3 pers. via /api/pricing), vue HTML sans placeholder orphelin, signature d'Alice -> partially_signed (1/2), signature de Bob -> signed (2/2) + PDF final généré (SHA-256 vérifié) ; contrat legacy (ancienne formule) créé et signé en parallèle pour confirmer l'absence de régression (chemin _sign_legacy inchangé). PDF de test supprimés ; 2 lignes de test restent dans contracts.db (Test QA Famille/Test QA Legacy, garde-fou anti-suppression bloque le nettoyage automatique, sans risque, à retirer à la main si besoin).tools/build_templates.py (sources make-migration/contract-sources -> templates HTML charte RP). 4 contrats FR générés : temporaire, fiscal, permanent, investor (investor = BROUILLON à valider avocat). Emails ajoutés : invite (lien de signature) à la création + PDF signé après signature, expéditeur resident.paraguay. Service systemd rp-contracts.service (port 8365). Exposé https://contrats.panelbay.com (Cloudflare + SSL). Ajouté au portail (onglet RP). Relié à l'onboarding : aios-automations/rp_onboarding.py appelle ce service par défaut (CONTRACT_BACKEND=internal ; eSignatures = fallback). Testé bout en bout en mode test (Airtable + contrat interne). RESTE : templates EN/ES, UI portail Younes (choix pack) + upsell, repointer MAKE_WEBHOOK_URL, test réel puis couper Make #4.make-migration/contract-sources/pack-<pack>-{en,es}.md), tokens de structure et placeholders préservés à l'identique, zéro em-dash, Article 9 = plateforme sécurisée RP + journal d'audit. tools/build_templates.py rendu multilingue (JOBS 12 entrées, dict LANG pour tables financières / réf. dossier / zone de signature ; détection de la ligne de référence par présence des deux placeholders ; mot-clé d'article configurable fr/en=Article, es=Artículo). backend/app.py : map TEMPLATES étendue avec en/es par pack. Service redémarré. Testé en live (create send_email=false + view) : fiscal EN -> pack-fiscal-en, fiscal ES -> pack-fiscal-es, placeholders remplis, langue inconnue -> repli FR ; contrats de test supprimés. Pas de validation avocat demandée pour cette étape.pack-residence-permanente-fr.html restructuré en AVENANT (ne redécrit pas les étapes déjà couvertes par le contrat initial). backend/app.py : TEMPLATES/TITLES étendus, nouveau CURRENT_PACKS, repli par défaut passé de pack-temporaire-fr à pack-residence-temporaire-fr. Ajout GET /api/packs (formules dispo pour le sélecteur) et GET /api/contracts (liste + statut, vue admin). Consommé par le nouveau module contrats.js de rp-dashboard (voir son MANUAL). Testé : contrat de test créé (pack residence-permanente, send_email:false), HTML rendu vérifié (titre présent, aucun placeholder {{...}} non résolu, montant bien injecté), ligne supprimée de contracts.db après vérification. RESTE : templates EN/ES de ces 4 formules, validation avocat (Turbaux), corriger rp-onboarding/backend/pricing.js (grille interne encore périmée par rapport au site, voir son MANUAL).