Skip to main content

Fusió de coreògrafs duplicats

Procediment per detectar i fusionar coreògrafs duplicats al catàleg. Replica el patró de fusió de balls i fusió de cançons.

Atenció: la fusió és irreversible un cop confirmada. Sempre que sigui possible, fes una còpia de seguretat de la base de dades abans d'una sessió intensiva de neteja.

Casos d'ús

  • Mateix coreògraf importat des de diferents fonts (ETL linedance.cat, YouTube, alta manual) amb noms lleugerament diferents (Maggie Gallagher vs M. Gallagher).
  • Solos creats inadvertidament a partir de membres d'un grup (típic després d'imports massius).
  • Variants ortogràfiques (John Doe vs Jhon Doe).

Permisos

Tots els endpoints i pàgines requereixen rol ADMIN.

Detectar duplicats

  1. Vés a /choreographers i fes clic al botó Detectar duplicats (només visible per a admins). També pots accedir directament a /admin/choreographers/duplicates.
  2. Fes clic a Escanejar duplicats. La cerca utilitza dues estratègies:
    • Nom exacte (case-insensitive, dins el mateix isGroup) — confiança 80%.
    • Similitud trigram via pg_trgm (llindar 0.6) — confiança variable calculada a partir de la similitud.
  3. La pantalla mostra grups ordenats per confiança. Cada grup indica:
    • Tipus (Solista o Grup)
    • Conflicte de propietari si dos coreògrafs estan reclamats per usuaris diferents — això bloqueja la fusió fins que un dels owners revoqui la seva reclamació.

Validacions abans de fusionar

El backend rebutja la fusió si es compleix qualsevol d'aquestes condicions:

#CondicióPer què
D1Solo+Grup mesclatsTrenca la semàntica isGroup.
D2Dos CLAIMED amb owners diferentsDecisió no es pot prendre silenciosament; cal revocar primer.
D4Cicle a choreographer_members (un grup conté un altre del set)Provocaria self-membership.
D6Algun candidat té sol·licituds d'ownership pendentsResol-les abans (cancel·la o aprova).
D7Survivor i merged tenen Persons vinculades a Users registrats diferentsConflict d'identitat: dos comptes d'usuari diferents declaren ser la mateixa persona. L'admin ha de resoldre-ho manualment.
D8Hi ha una FK desconeguda cap a people no registrada al codiXarxa de seguretat — evita pèrdua de dades silenciosa quan l'esquema evoluciona.

Executar la fusió (UI)

L'assistent té 3 passos:

Pas 0 — Tria del survivor

Selecciona el coreògraf que sobreviurà. Els altres seran eliminats; les seves dades es traslladaran al survivor.

Cada candidat mostra badges amb informació clau (nombre de balls, membres, grups, links externs, Person, owner).

Pas 1 — Comparar i triar dades

  • Camps escalars (name, country, bio): per defecte preserven els del survivor. Pots triar manualment de quin coreògraf agafar cada camp.
  • Fusió de Person (només si dos solos tenen Person diferents): per defecte manté el del survivor; pots copiar camps específics (email, avatar, etc.) del Person eliminat. Si el Person eliminat té altres rols (TEACHER, DJ…), no s'esborra de la BD; només es desvincula del rol CHOREOGRAPHER.
  • Aliases: marca quins noms dels coreògrafs eliminats vols guardar com a noms històrics del survivor (es registren a resource_name_history).
  • Avisos: la pàgina mostra els enllaços externs que es descartaran per duplicat (UNIQUE constraint a (platform, url)).

Pas 2 — Confirmació

Resum final amb avís d'irreversibilitat. El botó vermell Confirmar fusió executa l'operació.

Què passa al backend

L'execució es fa dins una transacció amb pessimistic lock sobre tots els coreògrafs implicats. Per a cada coreògraf eliminat:

  1. dance_choreographer: les associacions s'mouen al survivor; si el survivor ja té el ball, la fila duplicada s'esborra (el rol del survivor preval).
  2. choreographer_members: les memberships (com a grup i com a membre) s'mouen i es deduplicquen.
  3. resource_external_links: s'mouen al survivor; en cas de col·lisió (platform, url), l'enllaç del survivor preval i el del merged es descarta.
  4. resource_name_history: les entrades s'mouen + s'afegeix una entrada sintètica amb reason = "Fusionat des de #N".
  5. ownership_requests resoltes: s'mouen al survivor (les PENDING ja s'han bloquejat a la validació).
  6. Person merge complet (si survivor i merged tenen Persons diferents):
    • Mou person_roles del Person merged → Person survivor (UNION + dedup per (person_id, role_type))
    • Mou event_people del Person merged → Person survivor (UNION + dedup per (event_id, person_id))
    • Mou users.person_id només si el merged té User i el survivor no
    • Esborra el Person merged. CASCADE neteja els person_roles i event_people que no s'han pogut moure (eren duplicats amb el survivor i s'han descartat per UNIQUE constraint)
  7. listingStatus + owner: si el survivor era INFORMATIVE i un eliminat era CLAIMED, el survivor es promou.
  8. Eliminació: el coreògraf eliminat s'esborra (CASCADE neteja qualsevol resta).
  9. Audit: una entrada nova a choreographer_merge_history registra qui, quan, què, i si algun dels eliminats estava reclamat.

Audit trail

La taula choreographer_merge_history (V95) preserva:

  • survivor_id — coreògraf que va sobreviure.
  • merged_ids[], merged_names[] — coreògrafs eliminats.
  • merged_was_claimed[] — flag per saber si algun era CLAIMED al moment.
  • merge_details (JSONB) — reservat per a detalls de fields chosen, etc.
  • merged_by, merged_at.

Rollback

No hi ha rollback automàtic. Si cal recuperar dades:

  1. Restaura una còpia de seguretat de la BD anterior a la fusió.
  2. Consulta choreographer_merge_history per saber quins IDs es van fusionar.

Mantenir el Person merge segur al llarg del temps

El servei té una whitelist anomenada KNOWN_PEOPLE_FKS amb totes les FKs conegudes que apunten a people:

private static final Set<String> KNOWN_PEOPLE_FKS = Set.of(
"choreographers.person_id",
"users.person_id",
"person_roles.person_id",
"event_people.person_id"
);

Cada executeMerge() consulta information_schema.table_constraints i compara amb aquesta llista. Si troba una FK desconeguda → bloqueja la fusió amb un missatge clar dient què cal actualitzar.

A més, el test ChoreographerMergePeopleFkConsistencyIT verifica al CI que la llista del codi coincideix amb la realitat de la BD. Si algú afegeix una nova FK a people (per exemple class_attendees.person_id), el test falla amb instruccions per actualitzar la whitelist i decidir la semàntica de fusió de la nova taula.

Què fer si el test falla:

  1. Identificar la nova FK i la seva delete_rule
  2. Decidir si:
    • Cal moure-la com person_roles / event_people (UNION + dedup)?
    • O cal bloquejar la fusió quan estigui present?
  3. Afegir-la a KNOWN_PEOPLE_FKS
  4. Implementar la query de mou al ChoreographerRepository
  5. Cridar-la al bloc Person merge de ChoreographerMergeService.executeMerge
  6. Actualitzar EXPECTED_FKS al test

Endpoints API

GET  /api/admin/choreographers/duplicates
POST /api/admin/choreographers/merge/preview { "choreographerIds": [1, 2] }
POST /api/admin/choreographers/merge/execute { ... ChoreographerMergeConfirmRequest ... }

Tots requereixen ROLE_ADMIN.