---
titre: Listings plateforme par bien — URL annonce et code flux financier
statut: Phase 0 implémentée (voir migration `Version20260521120000` et [`02-modele-donnees.md`](02-modele-donnees.md))
date: 2026-05-21
date_phase0: 2026-05-21
---

# 06 — Listings plateforme par bien (URL annonce + code paiement)

## 0 · Décisions Phase 0 (Figées au 2026-05-21)

Les arbitrages **opérationnels sont consignés dans** :

- [`docs/evolutions/006-property-platform-listings-phase0-decisions.md`](../evolutions/006-property-platform-listings-phase0-decisions.md)

Résumé exécutif :

| Thème | Décision |
|--------|-----------|
| Entité PHP | **`PropertyPlatformListing`** (table **`property_platform_listing`**) |
| Historique | **Aucune historisation automatique en v1** : plusieurs fiches **actives** par `(bien, plateforme)` permises (ex. plusieurs IDs Booking) ; **édition en place** ; **suppression** si annonce retirée |
| Unicités | **`UNIQUE (organization_id, payout_identifier)`** lorsque l’identifiant de flux est renseigné (plusieurs `NULL` possibles) — **pas** d’unicité `(property_id, rental_platform_id)` |
| Stockage « ID » | **Une valeur canonique** en base après normalisation minimale (**trim**/règle app doc § 7.1); pas double colonne audit en v1 |
| Extraction à l’import | **Regex / pattern paramétrable par `organization`** (défaut documenté équivalent motif `ID.` + chiffres si non renseigné) |
| Permissions | Alignées à l’**[édition `Property` § 6.3](#63-permissions)** (aujourd’hui `ROLE_SUPERADMIN` dans le codebase actuel pour les propriétés) |
| Confidentialité liste code | Affichage **intégral** en UI pour les utilisateurs autorisés sur le listing |

Les sections suivantes ont été harmonisées avec ce socle (« simple » métier sans historisation).

---

## 1. Contexte

Certaines équipes rattachent les écritures bancaires à des **biens immobiliers** via un **identifiant technique** présent dans le libellé des opérations (ex. motif `ID.nnnnnnnn` sur des versements liés à **Booking.com**).

Besoin métier :

- Associer explicitement **un bien** (`property`) à une **présence** sur une **plateforme de location** (`rental_platform`) dans le référentiel global.
- Pour chaque association, renseigner l’**URL publique de l’annonce** et le **code** permettant de **reconnaître** les flux financiers issus de cette plateforme dans les données importées.

**Hypothèse initiale périmétrique** (cas d’usage actuel décrit par le métier) : les codes observés correspondent à Booking ; relation **bijective** intentionnelle sur ce périmètre : **un code = un bien = une annonce Booking**. La conception doit toutefois **tolérer d’autres plateformes** ultérieurement sans refactor majeur du modèle métier.

**Documents liés :**

- Décisions Phase 0 — [006-property-platform-listings-phase0-decisions.md](../evolutions/006-property-platform-listings-phase0-decisions.md)
- Modèle général biens / plateformes / transactions — [02-modele-donnees.md](02-modele-donnees.md).
- Import CSV — [04-import-donnees.md](04-import-donnees.md).
- Liaison organisation ↔ activation plateformes — entité existante `organization_rental_platform` (détail dans [02-modele-donnees.md](02-modele-donnees.md)).

## 2. Objectifs produit

1. **Configurer** une ou plusieurs fiches **`PropertyPlatformListing`** par couple **(bien, plateforme)** avec **URL annonce + identifiant de flux** (un identifiant distinct par annonce lorsqu’il apparaît en banque).
2. **Enrichir automatiquement** les `transaction` créées depuis l’import lorsque le **libellé** (motif défini au niveau **organisation**) permet d’identifier ce code sans ambiguïté — voir aussi priorité CSV § 7.3.
3. **Consultation** : depuis la fiche bien (et facultativement vue organisation), **ouvrir l’URL** annonce (**HTTPS**) dans un nouvel onglet.

## 3. Principes

- **Isolation tenant** : toutes les données de listing sont rattachées à une **organisation** (via `property.organization_id`). Aucune fiche listing sans bien parent appartenant à l’organisation.
- **Référentiel plateforme** : la plateforme reste dans `rental_platform` (globale). L’organisation ne « crée » pas Booking ; elle **référence** l’enregistrement global existant.
- **Complément sans remplacement de `OrganizationRentalPlatform`** : cette entité exprime uniquement « l’organisation **utilise / active** telle plateforme au sens directionnel ». Les **URLs et identifiants de flux vivent dans `property_platform_listing`**, au niveau **bien × plateforme** (plusieurs lignes possibles par couple).
- **Paramétrage extraction inter-organisations** : chaque organisation peut activer différentes plateformes ou recevoir différentes libellures bancaires — le **mécanisme d’extraction** est configuré au **`organization`**, pas sur chaque listing.

## 4. Concept métier : listing plateforme

Une ligne **`PropertyPlatformListing`** représente :

1. Ce bien **expose une annonce** sur cette **plateforme** globale (**URL HTTPS** canonique fonctionnel pour l’humain dans l’UI).
2. Cet **`payout_identifier` stocké canoniquement** sert au moteur d’import à **corrélations** contre ce qui fut extrait depuis le champ libellé importé après application de la regex d’organisation.

Vocabulaire UI possible : « **Annonce** sur {plateforme} », « **Code versement / ID flux** », etc.

## 5. Modèle de données (v1 — sans historisation)

### 5.1 Entité **`PropertyPlatformListing`**

Table suggérée Doctrine : **`property_platform_listing`**

| Champ | Type / contraintes | Obligatoire | Description |
|--------|---------------------|-------------|-------------|
| `id` | BIGINT PK | — | |
| `property_id` | FK → `property`, `ON DELETE CASCADE` | Oui | Bien tenant-scoped |
| `rental_platform_id` | FK → `rental_platform` | Oui | Réf référentiel global |
| `listing_url` | VARCHAR/longueur confort ou TEXT ; **HTTPS** | Oui | URL annonce |
| `payout_identifier` | VARCHAR (**taille défaut 64** indicative) ; **unique par org après normalisation** lorsqu’il est renseigné | Non | Identifiant de flux rapprochant libellés bancaires (`NULL` = pas de liaison import par libellé, la fiche reste pertinente pour URL / couple bien × plateforme) |
| `label` | VARCHAR(255) NULL | Non | Alias affichage humain facultatif |
| `notes` | TEXT NULL | Non | Mémo interne tenant |
| `created_at` / `updated_at` | DATETIME UTC | — | Audits usuels |

**Champs hors v1 (retirés de la conception initiale « historique ») :** ~~`active`~~, ~~`valid_from`~~, ~~`valid_to`~~.

**Remarques :**

- Hydratation facultative **`transaction.property_id`** / **`transaction.rental_platform_id`** après résolution import quand aucune collision mapping CSV défavorable (§ 7.3).

### 5.2 Contraintes d’unicité

1. **Plusieurs lignes** pour le même `(property_id, rental_platform_id)` sont **autorisées** (ex. Booking : annonces désactivées + annonce courante, codes flux différents).
2. **Unicité `payout_identifier` lorsqu’il est renseigné** :

   Une même organisation **ne peut pas enregistrer le même identifiant non vide** après normalisation pour **deux annonces différentes**. Plusieurs lignes peuvent avoir **`NULL`** (pas de rattachement import par libellé pour cette ligne).

   Technique recommandée (au choix implémentation) :

   - matérialiser `organization_id` **sur la ligne listing** (**redondance contrôlée** FK cohérente avec `property.organization_id`), permettant **`UNIQUE(organization_id, payout_identifier)`** (MySQL : plusieurs `NULL` autorisées pour la même colonne dans un unique) **ET** vérif synchro lors écritures **OU**
   - appliquer l’unique en **couche Domain/Validator Doctrine** hors index SQL si contraintes jointure jugées suffisamment couvertes par tests perf import.

Décision infra laissée à la PR migrations ; la **conséquence métier figée**.

## 6. Interfaces utilisateur

### 6.1 Fiche bien (`property`)

Bloc ou onglet **« Listings / plateformes »** :

| Action | Détail MVP |
|---------|-------------|
| Lister | Colonnes : plateforme ; libellé court (facultatif) ; URL **lien ext** ; ID flux (**affiché en clair**) |
| Ajouter | Autant de lignes que nécessaire par couple **(bien, plateforme)** ; chaque identifiant de flux non vide doit rester **unique dans l’organisation** |
| Éditer | Met à jour URL et/ou `payout_identifier` **en conservant unicités** ; si changement radical de code ⇒ **anciennes écritures** restent hors rétro-mapping automatique jusqu’aux prochains flux |
| Supprimer | Quand métier décide que l’annonce ne sert plus (**pas** « désactivation historique » côté plateforme : on garde la ligne tant que les relevés peuvent encore mentionner l’ID) |

Validations UX :

- `listing_url` : schéma **https://** ; validations Symfony standard.
- **`payout_identifier`** : facultatif ; si vide, aucune résolution automatique depuis les libellés à l’import pour cette ligne, uniquité appliquée **uniquement** aux valeurs non nulles.
- `rental_platform` choisissable parmi plateformes **activées** pour l’organisation (`organization_rental_platform.enabled = true`).
- Champ **regex organisation** résidé **hors liste par ligne bien** mais écran/paramètres organisation (§ 7.1 infra).

### 6.2 Vue organisation (toujours **optionnel post-MVP** maquette § plan action)

Synthèse transverse biens ⇄ URLs ⇄ flux IDs pour préparation imports (**non bloquant MVP listing par bien seul** si charge planning serrée).

### 6.3 Permissions

**Identiques à l’édition `Property`** côté code courant (**`ROLE_SUPERADMIN` sur `/admin/**/properties/**`**).  
Évolution doc [03 — rôles & permissions](03-roles-permissions.md) doit **répercuter toute évolution simultanée** sur **`Property`** + **`PropertyPlatformListing`**.

Les **PARTENAIRE** suivent même visibilité qu’existent les biens aujourd’hui (consultation **si elle existe**) — pas de nouveau droit élargissement sans mise à jour explicite spec 03.

### 6.4 Affichage code flux

Pas de masquage partiel MVP : **valeur brute affichée** aux profils pouvant ouvrir l’écran (aligné décision confidentiality Phase 0).

## 7. Import CSV — enrichissement

### 7.1 Extraction depuis libellés + regex **par organisation**

1. Champ configuration **`organization`** (nom technique libre : exemple `listing_payout_extract_regex VARCHAR NULL`) :

   - `NULL`/vide ⇒ **fallback application** comportement équivalent **regex défaut motif `ID.` + suite numérique** (à coder exact + tests fixtures réels corpus LCL utilisateur).
   - Sinon ⇒ **preg** validée admin ; message utilisateur métier lisible (**regex invalide**).

2. Étapes ligne importées :

   a. Produire **`label_normalized_for_listing`** = chaîne brute libellée issue mapping CSV après trim évident comme aujourd’hui.  
   b. **`preg_match_all`** défini org ; capturer groupe identifiant final comparateur.

3. Canonicalisation recherche contre table listing — **stripping léger défini code** (**trim**/unicode spaces) ; **matching ordre préféré : plus long substring first** si plusieurs `payout_identifier` candidats seraient prefixes l’un de l’autre.

### 7.2 Résolution métier ligne

Hypothèses identiques version antérieure spec sauf wording entité **`PropertyPlatformListing`**:

- aucun motif extrait ⇒ skip enrichissement auto listing.
- **Une seule** ligne listing match ⇒ assigner FK **si non bloquée** § 7.3 collisions colonnes CSV.
- **Plusieurs** candidats ⇒ **pas d’affectation automatique** ; tracer log tag **`AMBIGUOUS_PAYOUT_IDENTIFIER`** + valeur extraite brute.

### 7.3 Priorité collision contre mapping CSV

Ordre forcé (**fort > faible**) :

1. (**Plus fort**) valeurs CSV mappées explicites `property` / `rental_platform` **non vides après trim**.
2. (**Plus faible**) enrichissement automatique depuis listings.

Les compteurs rapport import recommandés (inchangés sémantiquement) :

- `enrichment_listing_linked`
- `enrichment_listing_skipped_ambiguous`
- `enrichment_listing_no_match`

Impl log append (`import.log`) pour investigation.

## 8. Hors périmètre (toujours exclu v1 fonctionnel liste)

Liste inchangée : scraping URL, OAuth Booking automatique hors saisie, pas toucher composition **hash SHA256** anti doublons transaction défini [04-import-donnees.md](04-import-donnees.md).

## 9. Artefacts implémentation (checklist mise à jour)

1. Migration + entité Doctrine **`PropertyPlatformListing`** + repos + **validateurs unicité cross-property-org**.
2. Migration/config ajout champ **regex organisation** (**nullable défaut comportement défaut code** strict tests).
3. Fixtures contenant **`rental_platform`** **Booking**.
4. Fiche mise à jour [02-modele-donnees.md](02-modele-donnees.md).
5. [05-interfaces-mvp.md](05-interfaces-mvp.md) rattache nouveaux blocs lorsque UX finalisées.

## 10. Critères d’acceptation (Synthèse — alignés Phase 0)

- [ ] Création listing avec plateforme **activée organisation** uniquement (**HTTPS** enforced).
- [ ] Respect **`UNIQUE (property_id, rental_platform_id)`**.
- [ ] Respect unicité **identifiant flux / organisation**.
- [ ] Import enrichit lignes où regex org + aucune collision CSV forte ; journalise ambigu.
- [ ] Aucune fuite données cross-org dans requêtes listings (toujours `WHERE property.organization_id = :tenant`).

## 11. Changelog

| Date | Auteur | Changement |
|------|--------|------------|
| 2026-05-21 | Spec initiale | Création document 06. |
| 2026-05-21 | Harmonisation Phase 0 | Section 0 : figement décisions métier (`PropertyPlatformListing`, sans historique, regex org, uniq simplifiées, pas masquage code). Liaison fichier [`006-property-platform-listings-phase0-decisions.md`](../evolutions/006-property-platform-listings-phase0-decisions.md). |
