Bénévole : l'email n'est plus unique (couple, famille)
Plusieurs bénévoles peuvent légitimement partager une même adresse email — l'architecture l'interdisait : l'import ignorait silencieusement le second (« duplicate_email_in_file »), et l'ajout manuel renvoyait un 409. Les deux sont corrigés. - Migration 0009 : retrait de la contrainte d'unicité (edition_id, email) sur `volunteer` (l'index sur `email` seul est conservé). - Import (`app/importing/service.py`) : l'upsert se fait désormais sur (email, nom + prénom), pas sur l'email seul. Un même email revu avec un nom différent dans le fichier crée un second bénévole (nouveau code `import.row.shared_email`, WARNING) au lieu d'être ignoré ; `duplicate_email_in_file` ne se déclenche plus que sur une ligne littéralement répétée (même email ET même nom). - API (`POST`/`PATCH /volunteers`) : retrait du rejet 409 `volunteer.duplicate_email`, devenu sans objet (le mock ne l'a d'ailleurs jamais appliqué à la création manuelle). - Mock frontend (`mock-client.ts`) : même correction de l'upsert par import, pour rester cohérent avec le backend. - CLAUDE.md à jour (modèle de domaine, règles d'import, journal). Contrepartie assumée (documentée) : un email re-importé avec un nom légèrement différent (faute de frappe corrigée) crée désormais un bénévole de plus signalé, plutôt que de renommer silencieusement l'existant — même philosophie que la détection sans email, qui ne fusionne jamais rien automatiquement. Vérifié : 179 tests backend, ruff, alembic check ; typecheck/lint/build/smoke frontend ; reseed de démonstration. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
fcc42c8d43
commit
897a76ca6d
@@ -75,13 +75,13 @@ def test_dry_run_persists_nothing(session, edition):
|
||||
assert _volunteers(session, edition) == []
|
||||
|
||||
|
||||
def test_existing_email_is_updated_not_duplicated(session, edition):
|
||||
def test_existing_email_and_name_is_updated_not_duplicated(session, edition):
|
||||
session.add(
|
||||
Volunteer(
|
||||
edition_id=edition.id,
|
||||
email="alex@ex.fr",
|
||||
first_name="Alexandre",
|
||||
last_name="M.",
|
||||
first_name="Alex",
|
||||
last_name="Martin",
|
||||
phone="0600000000",
|
||||
)
|
||||
)
|
||||
@@ -90,7 +90,7 @@ def test_existing_email_is_updated_not_duplicated(session, edition):
|
||||
report = run_import(
|
||||
session=session,
|
||||
edition=edition,
|
||||
table=_table([["Alex", "Martin", "ALEX@ex.fr", "", ""]]),
|
||||
table=_table([["alex", "MARTIN", "ALEX@ex.fr", "0611111111", ""]]),
|
||||
mapping=_mapping(),
|
||||
)
|
||||
|
||||
@@ -98,18 +98,46 @@ def test_existing_email_is_updated_not_duplicated(session, edition):
|
||||
assert report.rows[0].issues[0].code == "import.row.updated_existing"
|
||||
volunteers = _volunteers(session, edition)
|
||||
assert len(volunteers) == 1
|
||||
assert volunteers[0].first_name == "Alex"
|
||||
assert volunteers[0].last_name == "Martin"
|
||||
assert volunteers[0].phone == "0611111111"
|
||||
|
||||
|
||||
def test_duplicate_email_within_file_is_skipped(session, edition):
|
||||
def test_same_email_different_name_creates_a_second_volunteer(session, edition):
|
||||
# Un même email peut être partagé (couple, famille) : il ne suffit plus à
|
||||
# lui seul à identifier « le » bénévole, il faut aussi le nom — sinon un
|
||||
# second bénévole avec la même adresse était perdu (silencieusement fondu
|
||||
# dans le premier, ou ignoré selon le cas).
|
||||
session.add(
|
||||
Volunteer(
|
||||
edition_id=edition.id,
|
||||
email="famille@ex.fr",
|
||||
first_name="Alex",
|
||||
last_name="Martin",
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
|
||||
report = run_import(
|
||||
session=session,
|
||||
edition=edition,
|
||||
table=_table([["Sam", "Martin", "famille@ex.fr", "", ""]]),
|
||||
mapping=_mapping(),
|
||||
)
|
||||
|
||||
assert (report.created, report.updated, report.skipped) == (1, 0, 0)
|
||||
assert report.rows[0].action == "CREATED"
|
||||
names = {(v.first_name, v.last_name) for v in _volunteers(session, edition)}
|
||||
assert names == {("Alex", "Martin"), ("Sam", "Martin")}
|
||||
|
||||
|
||||
def test_duplicate_row_within_file_is_skipped(session, edition):
|
||||
# Même nom ET même email répétés : une ligne recopiée, pas deux bénévoles.
|
||||
report = run_import(
|
||||
session=session,
|
||||
edition=edition,
|
||||
table=_table(
|
||||
[
|
||||
["Alex", "Martin", "dup@ex.fr", "", ""],
|
||||
["Alex", "Doublon", "dup@ex.fr", "", ""],
|
||||
["alex", "MARTIN", "DUP@ex.fr", "", ""],
|
||||
]
|
||||
),
|
||||
mapping=_mapping(),
|
||||
@@ -121,6 +149,27 @@ def test_duplicate_email_within_file_is_skipped(session, edition):
|
||||
assert skipped.issues[0].params["first_line"] == 2
|
||||
|
||||
|
||||
def test_shared_email_within_file_creates_both_with_a_warning(session, edition):
|
||||
report = run_import(
|
||||
session=session,
|
||||
edition=edition,
|
||||
table=_table(
|
||||
[
|
||||
["Alex", "Martin", "famille@ex.fr", "", ""],
|
||||
["Sam", "Martin", "famille@ex.fr", "", ""],
|
||||
]
|
||||
),
|
||||
mapping=_mapping(),
|
||||
)
|
||||
assert (report.created, report.skipped) == (2, 0)
|
||||
second = report.rows[1]
|
||||
assert second.action == "CREATED"
|
||||
assert second.issues[0].code == "import.row.shared_email"
|
||||
assert second.issues[0].params["first_line"] == 2
|
||||
names = {(v.first_name, v.last_name) for v in _volunteers(session, edition)}
|
||||
assert names == {("Alex", "Martin"), ("Sam", "Martin")}
|
||||
|
||||
|
||||
def test_row_without_email_matching_existing_name_warns_but_creates(
|
||||
session, edition
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user