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 GallaghervsM. Gallagher). - Solos creats inadvertidament a partir de membres d'un grup (típic després d'imports massius).
- Variants ortogràfiques (
John DoevsJhon Doe).
Permisos
Tots els endpoints i pàgines requereixen rol ADMIN.
Detectar duplicats
- Vés a
/choreographersi fes clic al botó Detectar duplicats (només visible per a admins). També pots accedir directament a/admin/choreographers/duplicates. - 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.
- Nom exacte (case-insensitive, dins el mateix
- 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è |
|---|---|---|
| D1 | Solo+Grup mesclats | Trenca la semàntica isGroup. |
| D2 | Dos CLAIMED amb owners diferents | Decisió no es pot prendre silenciosament; cal revocar primer. |
| D4 | Cicle a choreographer_members (un grup conté un altre del set) | Provocaria self-membership. |
| D6 | Algun candidat té sol·licituds d'ownership pendents | Resol-les abans (cancel·la o aprova). |
| D7 | Survivor i merged tenen Persons vinculades a Users registrats diferents | Conflict d'identitat: dos comptes d'usuari diferents declaren ser la mateixa persona. L'admin ha de resoldre-ho manualment. |
| D8 | Hi ha una FK desconeguda cap a people no registrada al codi | Xarxa 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:
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).choreographer_members: les memberships (com a grup i com a membre) s'mouen i es deduplicquen.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.resource_name_history: les entrades s'mouen + s'afegeix una entrada sintètica ambreason = "Fusionat des de #N".ownership_requestsresoltes: s'mouen al survivor (lesPENDINGja s'han bloquejat a la validació).- Person merge complet (si survivor i merged tenen Persons diferents):
- Mou
person_rolesdel Person merged → Person survivor (UNION + dedup per(person_id, role_type)) - Mou
event_peopledel Person merged → Person survivor (UNION + dedup per(event_id, person_id)) - Mou
users.person_idnomés si el merged té User i el survivor no - Esborra el Person merged. CASCADE neteja els
person_rolesievent_peopleque no s'han pogut moure (eren duplicats amb el survivor i s'han descartat per UNIQUE constraint)
- Mou
listingStatus+owner: si el survivor eraINFORMATIVEi un eliminat eraCLAIMED, el survivor es promou.- Eliminació: el coreògraf eliminat s'esborra (CASCADE neteja qualsevol resta).
- Audit: una entrada nova a
choreographer_merge_historyregistra 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:
- Restaura una còpia de seguretat de la BD anterior a la fusió.
- Consulta
choreographer_merge_historyper 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:
- Identificar la nova FK i la seva
delete_rule - Decidir si:
- Cal moure-la com
person_roles/event_people(UNION + dedup)? - O cal bloquejar la fusió quan estigui present?
- Cal moure-la com
- Afegir-la a
KNOWN_PEOPLE_FKS - Implementar la query de mou al
ChoreographerRepository - Cridar-la al bloc Person merge de
ChoreographerMergeService.executeMerge - Actualitzar
EXPECTED_FKSal 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.