2026-08-17 14:53:55 +02:00
|
|
|
"""
|
|
|
|
|
Massimport av spillere (`POST /orgs/{id}/players/bulk`, se FEATURE_
|
|
|
|
|
BACKLOG.md "Massimport av spillere til organisasjon/turnering"). Samme
|
|
|
|
|
dedup-mønster som registration.py sin selvregistrering (ADR-017
|
|
|
|
|
Beslutning B), utvidet til alle PlayerCreate-felt.
|
2026-08-18 14:44:42 +02:00
|
|
|
|
|
|
|
|
`match_player_id`/`duplicate_action` (2026-08-18) -- ekte produksjonsbug:
|
|
|
|
|
rader uten e-post har ingen matchingsnøkkel, så reimport av samme CSV
|
|
|
|
|
dublerte dem i stedet for å oppdatere. Frontend oppdager nå slike mulige
|
|
|
|
|
duplikater selv (navnetreff, ingen e-post) og spør organisator eksplisitt
|
|
|
|
|
-- svaret sendes hit som disse to feltene per rad.
|
2026-08-17 14:53:55 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import app.db as app_db
|
2026-08-18 14:44:42 +02:00
|
|
|
from app.routers.players import PlayerBulkRow, PlayerCreate, create_players_bulk
|
2026-08-17 14:53:55 +02:00
|
|
|
|
|
|
|
|
from tests.conftest import create_org, create_player
|
|
|
|
|
|
|
|
|
|
|
2026-08-18 14:44:42 +02:00
|
|
|
def _row(**kwargs) -> PlayerBulkRow:
|
|
|
|
|
match_player_id = kwargs.pop("match_player_id", None)
|
|
|
|
|
duplicate_action = kwargs.pop("duplicate_action", None)
|
|
|
|
|
return PlayerBulkRow(
|
|
|
|
|
data=PlayerCreate(**kwargs), match_player_id=match_player_id, duplicate_action=duplicate_action
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-08-17 14:53:55 +02:00
|
|
|
async def test_bulk_creates_new_players_without_email(pool):
|
|
|
|
|
org_id = await create_org()
|
|
|
|
|
rows = [
|
2026-08-18 14:44:42 +02:00
|
|
|
_row(display_name="Ny Spiller A"),
|
|
|
|
|
_row(display_name="Ny Spiller B", handicap_index=12.5),
|
2026-08-17 14:53:55 +02:00
|
|
|
]
|
|
|
|
|
results = await create_players_bulk(rows, organization_id=org_id)
|
|
|
|
|
assert len(results) == 2
|
|
|
|
|
assert all(r.status == "created" for r in results)
|
|
|
|
|
assert {r.player.display_name for r in results} == {"Ny Spiller A", "Ny Spiller B"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_bulk_matches_existing_player_by_email_and_fills_only_empty_fields(pool):
|
|
|
|
|
org_id = await create_org()
|
|
|
|
|
existing_id = await create_player(org_id, display_name="Eksisterende Spiller")
|
|
|
|
|
# Sett e-post + ett annet felt på forhånd -- importen skal IKKE
|
|
|
|
|
# overskrive country (allerede satt), kun matche på e-posten.
|
|
|
|
|
async with app_db.org_connection(org_id) as conn:
|
|
|
|
|
await conn.execute(
|
|
|
|
|
"UPDATE player SET email = 'match@example.com', country = 'Norge' WHERE id = $1",
|
|
|
|
|
existing_id,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
rows = [
|
2026-08-18 14:44:42 +02:00
|
|
|
_row(
|
2026-08-17 14:53:55 +02:00
|
|
|
display_name="Annet Navn Fra CSV", # skal IKKE overskrive display_name
|
|
|
|
|
email="match@example.com",
|
|
|
|
|
country="Sverige", # skal IKKE overskrive et allerede satt felt
|
|
|
|
|
club="Ny Klubb", # skal FYLLES INN (var tomt før)
|
|
|
|
|
handicap_index=8.0, # skal FYLLES INN (var tomt før)
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
results = await create_players_bulk(rows, organization_id=org_id)
|
|
|
|
|
assert len(results) == 1
|
|
|
|
|
assert results[0].status == "updated"
|
|
|
|
|
p = results[0].player
|
|
|
|
|
assert p.id == existing_id
|
|
|
|
|
assert p.display_name == "Eksisterende Spiller" # uendret
|
|
|
|
|
assert p.country == "Norge" # uendret (var allerede satt)
|
|
|
|
|
assert p.club == "Ny Klubb" # fylt inn (var tomt)
|
|
|
|
|
assert p.handicap_index == 8.0 # fylt inn (var tomt)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_bulk_without_email_never_matches_existing_and_always_creates(pool):
|
2026-08-18 14:44:42 +02:00
|
|
|
"""Uendret standardoppførsel når INGEN duplikat-varsel er besvart
|
|
|
|
|
(match_player_id ikke satt) -- ren e-postløs rad oppretter fortsatt
|
|
|
|
|
alltid en ny rad, akkurat som før denne funksjonen fikk
|
|
|
|
|
duplikat-håndtering."""
|
2026-08-17 14:53:55 +02:00
|
|
|
org_id = await create_org()
|
|
|
|
|
await create_player(org_id, display_name="Har Ikke E-post")
|
|
|
|
|
|
2026-08-18 14:44:42 +02:00
|
|
|
rows = [_row(display_name="Har Ikke E-post")]
|
2026-08-17 14:53:55 +02:00
|
|
|
results = await create_players_bulk(rows, organization_id=org_id)
|
|
|
|
|
assert results[0].status == "created"
|
|
|
|
|
|
|
|
|
|
async with app_db.org_connection(org_id) as conn:
|
|
|
|
|
count = await conn.fetchval(
|
|
|
|
|
"SELECT count(*) FROM player WHERE display_name = 'Har Ikke E-post'"
|
|
|
|
|
)
|
|
|
|
|
assert count == 2 # den opprinnelige + den nye, ingen falsk dedup uten e-post
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_bulk_paid_flag_never_overwritten_on_existing_match(pool):
|
|
|
|
|
org_id = await create_org()
|
|
|
|
|
existing_id = await create_player(org_id, display_name="Betalt Spiller")
|
|
|
|
|
async with app_db.org_connection(org_id) as conn:
|
|
|
|
|
await conn.execute(
|
|
|
|
|
"UPDATE player SET email = 'paid@example.com', paid = true WHERE id = $1", existing_id
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-18 14:44:42 +02:00
|
|
|
rows = [_row(display_name="X", email="paid@example.com", paid=False)]
|
2026-08-17 14:53:55 +02:00
|
|
|
results = await create_players_bulk(rows, organization_id=org_id)
|
|
|
|
|
assert results[0].player.paid is True # uendret, COALESCE mot ikke-nullbar kolonne er en no-op
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_bulk_is_transactional_across_rows(pool):
|
|
|
|
|
"""Selve listen kjøres i org_connection sin egen transaksjon -- ingen
|
|
|
|
|
partial-suksess-forventning her, men bekreft at flere rader i samme
|
|
|
|
|
kall faktisk lander i samme, konsistente organisasjon."""
|
|
|
|
|
org_id = await create_org()
|
2026-08-18 14:44:42 +02:00
|
|
|
rows = [_row(display_name=f"Spiller {i}") for i in range(5)]
|
2026-08-17 14:53:55 +02:00
|
|
|
results = await create_players_bulk(rows, organization_id=org_id)
|
|
|
|
|
assert len(results) == 5
|
|
|
|
|
assert len({r.player.id for r in results}) == 5
|
2026-08-18 14:44:42 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_bulk_duplicate_merge_fills_only_empty_fields_like_email_match(pool):
|
|
|
|
|
"""Organisator svarte "Slå sammen" på et duplikat-varsel -- COALESCE-
|
|
|
|
|
oppførsel, identisk til e-post-matchingen, men rettet mot en
|
|
|
|
|
eksplisitt oppgitt match_player_id i stedet."""
|
|
|
|
|
org_id = await create_org()
|
|
|
|
|
existing_id = await create_player(org_id, display_name="Anders Sperre")
|
|
|
|
|
async with app_db.org_connection(org_id) as conn:
|
|
|
|
|
await conn.execute("UPDATE player SET club = 'Gamle Klubb' WHERE id = $1", existing_id)
|
|
|
|
|
|
|
|
|
|
rows = [
|
|
|
|
|
_row(
|
|
|
|
|
display_name="Anders Sperre",
|
|
|
|
|
club="Ny Klubb", # skal IKKE overskrive (allerede satt)
|
|
|
|
|
handicap_index=14.0, # skal FYLLES INN (var tomt)
|
|
|
|
|
match_player_id=existing_id,
|
|
|
|
|
duplicate_action="merge",
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
results = await create_players_bulk(rows, organization_id=org_id)
|
|
|
|
|
assert results[0].status == "updated"
|
|
|
|
|
assert results[0].player.id == existing_id
|
|
|
|
|
assert results[0].player.club == "Gamle Klubb" # uendret
|
|
|
|
|
assert results[0].player.handicap_index == 14.0 # fylt inn
|
|
|
|
|
|
|
|
|
|
async with app_db.org_connection(org_id) as conn:
|
|
|
|
|
count = await conn.fetchval("SELECT count(*) FROM player WHERE display_name = 'Anders Sperre'")
|
|
|
|
|
assert count == 1 # ingen ny duplikat opprettet
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_bulk_duplicate_overwrite_replaces_already_set_fields(pool):
|
|
|
|
|
"""Organisator svarte "Overskriv" -- eneste sti i endepunktet som
|
|
|
|
|
erstatter et allerede satt felt, og kun for akkurat denne raden."""
|
|
|
|
|
org_id = await create_org()
|
|
|
|
|
existing_id = await create_player(org_id, display_name="Anders Sperre")
|
|
|
|
|
async with app_db.org_connection(org_id) as conn:
|
|
|
|
|
await conn.execute("UPDATE player SET club = 'Gamle Klubb' WHERE id = $1", existing_id)
|
|
|
|
|
|
|
|
|
|
rows = [
|
|
|
|
|
_row(
|
|
|
|
|
display_name="Anders Sperre",
|
|
|
|
|
club="Ny Klubb",
|
|
|
|
|
match_player_id=existing_id,
|
|
|
|
|
duplicate_action="overwrite",
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
results = await create_players_bulk(rows, organization_id=org_id)
|
|
|
|
|
assert results[0].status == "overwritten"
|
|
|
|
|
assert results[0].player.id == existing_id
|
|
|
|
|
assert results[0].player.club == "Ny Klubb" # overskrevet, ikke bevart
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_bulk_duplicate_create_new_still_creates_a_second_row(pool):
|
|
|
|
|
"""Organisator svarte "Importer som ny" (bevisst duplikat) --
|
|
|
|
|
match_player_id kan følge med, men skal IGNORERES når
|
|
|
|
|
duplicate_action er create_new."""
|
|
|
|
|
org_id = await create_org()
|
|
|
|
|
existing_id = await create_player(org_id, display_name="Anders Sperre")
|
|
|
|
|
|
|
|
|
|
rows = [
|
|
|
|
|
_row(
|
|
|
|
|
display_name="Anders Sperre",
|
|
|
|
|
match_player_id=existing_id,
|
|
|
|
|
duplicate_action="create_new",
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
results = await create_players_bulk(rows, organization_id=org_id)
|
|
|
|
|
assert results[0].status == "created"
|
|
|
|
|
assert results[0].player.id != existing_id
|
|
|
|
|
|
|
|
|
|
async with app_db.org_connection(org_id) as conn:
|
|
|
|
|
count = await conn.fetchval("SELECT count(*) FROM player WHERE display_name = 'Anders Sperre'")
|
|
|
|
|
assert count == 2
|