---
suivi: 1580
date: 2026-09-16
sujet: Import intelligent de parcelles (fichier hétérogène + consigne + tableau de proposition)
chantier: foncier / parcelles / import
type: feature
statut: poussé
hash: c1a4fa58
fichiers:
  - database/migrations/2026_09_16_190000_create_parcelle_import_sessions_table.php
  - app/Models/ParcelleImportSession.php
  - app/Services/Foncier/ParcelleImportPrompt.php
  - app/Services/Foncier/ParcelleImportClaudeClient.php
  - app/Services/Foncier/ParcelleImportFileExtractor.php
  - app/Services/Foncier/ParcelleImportPclSupParser.php
  - app/Services/Foncier/ParcelleImportCommuneResolver.php
  - app/Services/Foncier/ParcelleImportColumnClassifier.php
  - app/Services/Foncier/ParcelleImportAnalysisService.php
  - app/Services/Foncier/ParcelleImportCommitService.php
  - app/Http/Controllers/Foncier/ParcelleImportController.php
  - routes/web.php
  - resources/js/Pages/Foncier/Parcelles/Import.vue
  - resources/js/Pages/Foncier/Prospection/Index.vue
  - tests/Unit/ParcelleImport1580SourceTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - public/build/
  - docs/suivi/SUIVI_1580_import_intelligent_parcelles.md
---

# SUIVI #1580 — Import intelligent de parcelles

## PROMPT ENVOYÉ

Objectif : import INTELLIGENT. On dépose un fichier non formaté, on ajoute un
texte d’explication, l’IA propose un tableau, l’humain ajuste, puis importe.
Réutiliser le chemin Anthropic OCR factures. Cinq pièges du fichier réel
(commune = domicile, indivision, données personnelles, personnes morales,
Pcl sup.). Trois écrans. Rien n’est créé avant validation. Ne pas toucher
à la modale multiparcelle, ni #1567–1574, ni #1578.

## FICHES LUES

- **#1578 LEÇON** — rapprochement d’identité `correspond()` / `trouver()`,
  `force_create`, opposition personne-wide. Impact : l’import réutilise
  `ProprietaireIdentiteRapprochementService` (une collection préchargée,
  pas N×2000). Rattacher par défaut. Opposition en rouge.
- **#1576 LEÇON** — `date_naissance` n’existe pas, choix assumé, faits
  patrimoniaux uniquement. Impact : Année / N naissance écartées, aucun
  champ créé.
- **#1569 / #1568 / #1567 / #1574** — statut déduit, destination À définir,
  responsable = créateur, bâti non posé à l’import. Impact : `createPrepared`
  + `ParcelleStatutService::recalculerIds` ; destination `a_definir` ;
  colonne Bâti affichée, non persistée.
- Aucune fiche n’imposait de stocker le fichier source.

## DROITS RETENUS (avant code)

Même porte que la création de parcelles : permission `foncier.gestion`.

Lecture brute via `PermissionService::userHasPermission` →
`User::m365GroupNames()` (pas `Gate::`, pas `hasAnyM365Group`) :

| Groupe M365 | `foncier.gestion` |
|---|---|
| `GS_HECTARE` | oui |
| `GS_DSI` | oui (catalogue entier) |
| `GS_ADMIN_ERP` | oui (sauf admin.systeme / admin.maintenance) |
| `GS_ENVOL` | non (`!foncier.*`) |

Contrôle : middleware `feature.permission:foncier.gestion` + `abort_unless`
dans `assertGestion`. Un `v-if` seul n’écrit pas. Compte Robin / GS_ADMIN_ERP
faussement positif via `Gate::before` — non utilisé.

## TABLEAU DE SESSION — JUSTIFICATION

Table `parcelle_import_sessions` (uuid, user_id, etape, consigne, payload
JSON, recap JSON, resultat JSON, expires_at 24 h). Pas de colonne fichier.
Le dépôt est lu puis abandonné (tmp PHP). Le JSON de proposition (PII) est
vidé à l’import (`purge` payload + consigne). Sans table, un POST unique
ferait analyser + créer sans écran 2, ou forcerait à renvoyer tout le
fichier. Wizard 3 écrans + lots Anthropic = session serveur nécessaire.

Nom de contrainte court : `pis_user_fk` (limite MySQL 64). Pas de
`DB::transaction()` autour du DDL.

## STRATÉGIE DE DÉCOUPAGE (max_execution_time 300 s, QUEUE_CONNECTION=sync)

- Excel / CSV / texte : parsing PHP (PhpSpreadsheet), colonnes vides
  retirées, envoi TSV à Anthropic par lots de **20 lignes**.
- Timeouts identiques à l’OCR : connect 2 s, total 25 s / appel
  (`DepotFactureClaudeAnalysisService`). 83 lignes ≈ 5 lots ≈ 125 s < 300 s.
- PDF (y compris scanné) : **un** appel document+prompt (même plafond 25 s).
  Si le PDF est trop lourd, coller le tableau en texte.
- Fusion des lots côté PHP, puis filets déterministes (commune consigne,
  Pcl sup., dédoublonnage, existants, rapprochement).

## PROMPT IA (texte intégral envoyé à Claude)

Système :

Tu es un assistant d’import cadastral pour un ERP foncier français.
Tu extrais UNIQUEMENT des faits patrimoniaux. Tu n’inventes aucune valeur.
Tu réponds UNIQUEMENT en JSON valide, sans markdown, sans texte avant ou après.

Utilisateur (source : `ParcelleImportPrompt::user`) :

Lot : {lotLabel}

TEXTE COMPLÉMENTAIRE DE L’UTILISATEUR (prioritaire sur le fichier pour la localisation des parcelles) :
{consigne}

RÈGLES NON NÉGOCIABLES
1. La colonne Commune / CP du fichier est le DOMICILE DU PROPRIÉTAIRE, jamais la commune de la parcelle.
   La commune des parcelles vient UNIQUEMENT du texte complémentaire. Si le texte ne permet pas de la
   déterminer, laisse commune_parcelle_nom vide (ne devine pas).
2. Une même parcelle (même section + numéro) peut occuper plusieurs lignes (indivision). Produis UNE
   parcelle et N propriétaires.
3. N’extrais JAMAIS : année de naissance, date de naissance, nom de naissance, nom de jeune fille.
   Liste ces colonnes dans colonnes_ecartees.
4. Personne morale si civilité ET prénom absents (ex. « SCI AB », « LE BREUIL »). Type proposé, pas lu.
5. La colonne « Pcl sup. » (ou équivalent) contient d’autres parcelles du même propriétaire, en texte
   libre. Extraire CHAQUE parcelle (une cellule peut en contenir plusieurs, séparées par des virgules).
   Extraire numéro, surface, section SI PRÉSENTE. Si la section manque, laisse section vide (ne pas
   inventer). Un fragment illisible va dans fragments_illisibles, jamais deviné.
   Ces parcelles héritent des propriétaires de la ligne d’origine. origine = "pcl_sup".
   Inclus le texte source brut dans pcl_sup_source.

Colonnes reconnues (intitulés variables) : section/sctn, numéro/pcl, surface, zonage/zone, bâti,
pcl sup., civilité, nom, prénom, adresse, cp, commune (domicile), commentaires.
Colonnes vides : les lister dans colonnes_vides, ne pas les extraire.
Toute colonne non reconnue : colonnes_ignorees avec son intitulé d’origine.

Schéma JSON : commune_parcelles_nom, colonnes_ecartees, colonnes_vides,
colonnes_ignorees, parcelles[] (section, numero, surface_m2, zonage,
bati_fichier, origine fichier|pcl_sup, pcl_sup_source, confidence,
proprietaires[]), fragments_illisibles.

Chemin HTTP : `POST https://api.anthropic.com/v1/messages`, clé
`config('services.anthropic.key')`, `anthropic-version: 2023-06-01`,
modèle `claude-haiku-4-5-20251001` (OCR factures). Pas de second client.

## COLONNES RECONNUES

section / sctn, numéro / pcl, surface, zonage / zone, bâti (affichage
seul), pcl sup., titre / civilité (déduction de type, non stockée),
nom, prénom, adresse, cp, commune (domicile propriétaire), commentaires.

Écartées nommément : Année, N naissance (et tout intitulé contenant
« naissance ») — raison : « Année de naissance et nom de naissance :
données personnelles non conservées. »

## RÈGLE D’EXTRACTION « Pcl sup. »

Parser PHP `ParcelleImportPclSupParser` (filet, en plus de l’IA) :
- découpe sur virgule suivie de n°, d’une section+numéro, ou d’un numéro ;
- `n°AR 593 de 1 345 m²` → section AR, numéro 593, surface 1345 ;
- `n°483 de 1 773 m²` → section vide (ligne rouge), numéro 483, surface 1773 ;
- plusieurs parcelles dans une cellule → plusieurs lignes ;
- fragment sans numéro → `illisible`, jamais deviné ;
- toujours orange ; texte source affiché ; propriétaires hérités de la
  ligne mère ; commentaire mère préfixé « Parcelles supplémentaires
  (import) : » + texte source complet.

## MAPPING UI ↔ BACK

Destination : libellé FoncierReferentiels::DESTINATIONS ↔ value postée
(a_definir, support_operation, bouchon, reserve, cession, echange,
retrocession) ↔ colonne parcelles.destination. Défaut À définir.

Type propriétaire : Personne physique / morale / … ↔ physique / morale /
indivision / sci / collectivite / societe_groupe ↔ proprietaires.type.

Choix identité : Rattacher ↔ rattacher ↔ rattachement existant (défaut
si candidat). Créer quand même ↔ creer ↔ force_create #1578.

## SYNTHÈSE

Parcours `/foncier/parcelles/import` (bouton depuis la prospection).
Analyse Anthropic + filets PHP. Tableau éditable. Import via
`ParcelleCreationService::createPrepared` (responsable = importateur).
Statut recalculé. Jamais de modification d’existant. Fichier non stocké.

## DÉPLOIEMENT-TEST

OVH : git pull origin main ; php artisan migrate --force ; php artisan
optimize:clear ; view:clear ; route:clear ; journal:sync.

Tests 1–15 : fichier de référence Robin en production (à faire).
Tests source PHPUnit exécutés localement (parser, colonnes, prompt,
droits, instanciation).

## LEÇON

La commune du fichier est le domicile : l’IA seule ne suffit pas, le PHP
écrase toujours par la commune résolue depuis la consigne (INSEE, jamais
un CP). Pcl sup. doit être parsé en PHP, l’IA rate les cellules tronquées.
Session JSON plutôt que fichier : PII de prospection. Découper à 20 lignes
pour rester sous 25 s × n < 300 s. Ne pas créer de second client Anthropic.
