"""Génération des créneaux d'un poste à partir des créneaux types. Depuis le passage à la création de créneaux à la main (voir `test_shifts_api.py`), un `Position` ne génère plus rien automatiquement à sa création : `generate-shifts` reste l'outil optionnel pour repartir des créneaux types de l'édition.""" import datetime as dt def _positions_url(edition): return f"/editions/{edition.id}/positions" def _create_position(client, edition, name="Bar"): return client.post( _positions_url(edition), json={"name": name, "category": "BAR"} ).json() def _generate(client, edition, position): return client.post(f"{_positions_url(edition)}/{position['id']}/generate-shifts") def test_creating_a_position_generates_no_shifts(client, edition): position = _create_position(client, edition) shifts = client.get( f"/editions/{edition.id}/shifts", params={"position_id": position["id"]} ).json() assert shifts == [] def test_generate_shifts_creates_one_per_day_and_type(client, edition): position = _create_position(client, edition) resp = _generate(client, edition, position) # 4 jours (2026-07-09..12) x 3 créneaux types. assert resp.json()["created"] == 12 shifts = client.get( f"/editions/{edition.id}/shifts", params={"position_id": position["id"]} ).json() assert len(shifts) == 12 assert all(s["min_staff"] == 0 and s["ideal_staff"] == 0 for s in shifts) assert all(s["day_part_id"] is not None for s in shifts) def test_generate_is_idempotent(client, edition): position = _create_position(client, edition) _generate(client, edition, position) resp = _generate(client, edition, position) assert resp.status_code == 200 assert resp.json()["created"] == 0 def test_extending_the_edition_then_generating_adds_the_new_days(client, edition): position = _create_position(client, edition) _generate(client, edition, position) client.patch(f"/editions/{edition.id}", json={"end_date": "2026-07-14"}) resp = _generate(client, edition, position) # 2 jours de plus x 3 créneaux types. assert resp.json()["created"] == 6 shifts = client.get( f"/editions/{edition.id}/shifts", params={"position_id": position["id"]} ).json() assert len(shifts) == 18 def test_adding_a_day_part_then_generating_adds_it_everywhere(client, edition): position = _create_position(client, edition) _generate(client, edition, position) client.post( f"/editions/{edition.id}/day-parts", json={"label": "Nuit", "start_time": "02:00", "end_time": "05:00"}, ) resp = _generate(client, edition, position) assert resp.json()["created"] == 4 # 1 par jour def test_day_part_before_the_day_boundary_rolls_to_the_next_calendar_day(client, edition): """La frontière du jour est le début du créneau type de plus petit `order` (ici MORNING, 06 h — voir CLAUDE.md § 3 et `validation.engine._day_boundary`). Un créneau type qui commence avant cette heure (ex. `Nuit` 02 h–05 h, suite de `Soirée` 19 h–00 h) est donc la fin de la nuit du jour *précédent* : il doit être généré sur le jour suivant, pas sur `day`.""" position = _create_position(client, edition) _generate(client, edition, position) client.post( f"/editions/{edition.id}/day-parts", json={"label": "Nuit", "start_time": "02:00", "end_time": "05:00", "order": 3}, ) _generate(client, edition, position) shifts = client.get( f"/editions/{edition.id}/shifts", params={"position_id": position["id"]} ).json() nuit_shifts = [ s for s in shifts if dt.time.fromisoformat(s["starts_at"][11:19]) == dt.time(2, 0) ] assert len(nuit_shifts) == 4 nuit_dates = sorted(dt.date.fromisoformat(s["starts_at"][:10]) for s in nuit_shifts) # Édition du 9 au 12/07 : la nuit du 9 (Soirée 19 h) se termine le 10 à # 05 h, ..., la nuit du 12 se termine le 13 — jamais le 9 lui-même. assert nuit_dates == [dt.date(2026, 7, d) for d in (10, 11, 12, 13)] def test_generation_does_not_touch_staffing_already_set(client, edition): position = _create_position(client, edition) _generate(client, edition, position) shift = client.get( f"/editions/{edition.id}/shifts", params={"position_id": position["id"]} ).json()[0] client.patch( f"/editions/{edition.id}/shifts/{shift['id']}", json={"min_staff": 3, "ideal_staff": 5}, ) _generate(client, edition, position) again = client.get(f"/editions/{edition.id}/shifts/{shift['id']}").json() assert again["min_staff"] == 3 and again["ideal_staff"] == 5