# Raport: rozbicie statystyk zawodnika przy skladzie

Galaz: `statystyki-zawodnika`. Dwa zgloszone bledy naprawione: czyste konto
pokazywane napastnikom oraz brak ujemnych zdarzen w rozbiciu.

## Dowod, ze testy padaly przed implementacja

Przed zmianami cala suita byla zielona: 262 testy, 876 asercji.

Dla kazdej zmiany zachowania napisalem najpierw test i uruchomilem go osobno,
zeby pokazac czerwony wynik przed dotknieciem kodu produkcyjnego:

**Blad 1 — `tests/Unit/Entity/ElementTest.php` (nowy plik):**
```
Error: Call to undefined method App\Entity\Element::earnsCleanSheetPoints()
Tests: 2, Assertions: 0, Errors: 2.
```

**Blad 1 — `tests/Functional/MatchControllerTest.php::testHidesCleanSheetChipFromForwardButShowsItForDefender`:**
```
Failed asserting that 'NA NapastnikowiczARS CS 0' [ASCII](length: 25)
does not contain "CS" [ASCII](length: 2).
Tests: 1, Assertions: 2, Failures: 1.
```

**Blad 2 — `tests/Unit/Fpl/FplApiClientTest.php::testMapsLiveNegativeEventsBreakdown`:**
```
Error: Call to undefined method App\Fpl\Api\Dto\LiveDto::penaltiesMissedFor()
Tests: 1, Assertions: 0, Errors: 1.
```

**Blad 2 — `tests/Integration/EntryGameweekSyncTest.php::testStoresStatsBreakdownOnEveryPick`:**
```
Error: Call to undefined method App\Entity\EntryGameweekPick::getPenaltiesMissed()
Tests: 1, Assertions: 6, Errors: 1.
```

**Blad 2 — `tests/Functional/MatchControllerTest.php::testShowsNegativeEventMarkersAfterPositiveOnesWithNegativeHighlight`:**
```
Failed asserting that 'NA PechowiecARS 1G 0' [ASCII](length: 20)
contains "1PM" [ASCII](length: 3).
Tests: 1, Assertions: 2, Failures: 1.
```

Dopiero po kazdym takim czerwonym wyniku pisalem kod produkcyjny.

## Zmiany plik po pliku

### Blad 1 — czyste konto tylko tam, gdzie daje punkty

- **`src/Entity/Element.php`** — dodana stala `CLEAN_SHEET_ELIGIBLE_TYPES = [1, 2, 3]`
  i metoda `earnsCleanSheetPoints(): bool`, ktora zwraca `true` dla bramkarza,
  obroncy i pomocnika, a `false` dla napastnika i trenera. Regula punktacji FPL
  siedzi teraz w encji, nie w szablonie.
- **`templates/fixture/_chip.html.twig`** — warunek pokazywania znacznika `CS`
  rozszerzony o `pick.element.earnsCleanSheetPoints`. Dane w bazie (`cleanSheets`)
  nadal sie zapisuja bez zmian — zmienia sie wylacznie to, komu je pokazujemy.
- **`tests/Unit/Entity/ElementTest.php`** (nowy) — `earnsCleanSheetPoints()` dla
  wszystkich piecu typow pozycji.
- **`tests/Functional/MatchControllerTest.php`** — nowy test
  `testHidesCleanSheetChipFromForwardButShowsItForDefender`: napastnik i obronca
  z tym samym `cleanSheets = 1`, napastnik nie pokazuje `CS`, obronca pokazuje.

### Blad 2 — ujemne zdarzenia w rozbiciu

- **`src/Fpl/Api/Dto/LiveDto.php`** — cztery nowe pola konstruktora
  (`penaltiesMissed`, `ownGoals`, `yellowCards`, `redCards`, wszystkie domyslnie
  puste tablice) i cztery nowe metody dostepowe (`penaltiesMissedFor`,
  `ownGoalsFor`, `yellowCardsFor`, `redCardsFor`), wzorowane na istniejacym
  `defensiveContributionFor`.
- **`src/Fpl/Api/FplApiClient.php`** — metoda `live()` czyta teraz z `stats`
  odpowiedzi `event/{gw}/live/` rowniez `penalties_missed`, `own_goals`,
  `yellow_cards`, `red_cards` (domyslnie 0, bez logowania ostrzezenia — w
  odroznieniu od `defensive_contribution` te pola nie maja historii niestabilnosci
  kontraktu API, wiec nie potrzebuja tego samego wykrywania).
- **`src/Entity/EntryGameweekPick.php`** — cztery nowe kolumny `smallint`
  (`options: ['default' => 0]`) i odpowiadajace im parametry konstruktora
  (na koncu listy, wszystkie z domyslna wartoscia 0, wiec istniejace wywolania
  konstruktora w kodzie i testach nie wymagaly zmian) oraz gettery
  `getPenaltiesMissed()`, `getOwnGoals()`, `getYellowCards()`, `getRedCards()`.
- **`src/Fpl/Sync/EntryGameweekSync.php`** — metoda `apply()` przekazuje teraz
  cztery nowe wartosci z `LiveDto` przy budowaniu kazdego `EntryGameweekPick`.
- **`templates/fixture/_chip.html.twig`** — cztery nowe znaczniki, w tej
  kolejnosci, zawsze po znacznikach dodajacych punkty:
  - `{{ liczba }}PM`, tytul "Niewykorzystany rzut karny"
  - `{{ liczba }}OG`, tytul "Gol samobojczy"
  - `YC` (bez liczby — w praktyce zawsze 0 albo 1), tytul "Zolta kartka"
  - `RC` (bez liczby), tytul "Czerwona kartka"

  Kazdy ma dodatkowa klase `stat--neg` obok wlasnej klasy (`stat--pm`,
  `stat--og`, `stat--yc`, `stat--rc`).
- **`public/css/app.css`** — nowa zmienna `--neg: #ff8b8b` w `:root` (dokladnie
  ta sama barwa, co dotychczasowa wartosc na sztywno w `.pts--neg`).
  `.pts--neg` przepisane na `color: var(--neg)`, nowa regula
  `.squad-picks .stat--neg { color: var(--neg); }` obejmujaca wszystkie cztery
  nowe znaczniki. Poszerzona kolumna `.squad-picks td.stats` (134px -> 210px)
  i zaktualizowane komentarze opisujace najszersza mozliwa zawartosc oraz
  kolejnosc znacznikow.
- **`migrations/Version20260823150000.php`** (nowy) — dodaje cztery kolumny
  `SMALLINT DEFAULT 0 NOT NULL` do `entry_gameweek_pick`. Sama zmiana `ADD`,
  bez `DROP`/`MODIFY` na istniejacych danych.

### Rozszerzone testy

- **`tests/fixtures/api/live_gw1.json`** — kazdy z czterech zawodnikow (301-304)
  dostal dokladnie jedno nowe ujemne zdarzenie: 301 niewykorzystany karny,
  302 gol samobojczy, 303 (kapitan) zolta kartka, 304 czerwona kartka. Istniejace
  pola i asercje w innych testach korzystajacych z tej atrapy nie ulegly zmianie.
- **`tests/Unit/Fpl/FplApiClientTest.php`** — nowy test
  `testMapsLiveNegativeEventsBreakdown` sprawdza mapowanie wszystkich czterech
  pol dla wlasciwego i niewlasciwego zawodnika oraz wartosc domyslna dla
  nieznanego id.
- **`tests/Integration/EntryGameweekSyncTest.php`** — `testStoresStatsBreakdownOnEveryPick`
  rozszerzony o asercje nowych czterech pol dla wszystkich czterech pozycji
  skladu (zgodnie z nowa atrapa `live_gw1.json`).
- **`tests/Functional/MatchControllerTest.php`** — dwa nowe testy:
  `testHidesCleanSheetChipFromForwardButShowsItForDefender` (blad 1) i
  `testShowsNegativeEventMarkersAfterPositiveOnesWithNegativeHighlight` (blad 2,
  sprawdza tresc znacznikow, ich klasy CSS oraz kolejnosc: regex
  `/G.*PM.*OG.*YC.*RC/s` na tekscie wiersza).

## Kody nowych znacznikow i uzasadnienie

| Zdarzenie | Kod | Liczba z przodu? | Uzasadnienie |
|---|---|---|---|
| `penalties_missed` | `PM` | tak (np. `2PM`) | Skrot od "penalty missed", jednoznaczny, bez znakow diakrytycznych. Zdarzenie moze wystapic wiecej niz raz w meczu, wiec liczba z przodu — zgodnie z konwencja `3G`/`2A`. |
| `own_goals` | `OG` | tak (np. `2OG`) | Powszechnie rozpoznawalny skrot z jezyka angielskiego pilki noznej ("own goal"), stosowany rowniez w polskich serwisach sportowych. Teoretycznie moze wystapic wiecej niz raz. |
| `yellow_cards` | `YC` | nie | FPL zwraca dla tego pola praktycznie zawsze 0 albo 1 (druga zolta karta staje sie czerwona, nie druga zolta) — jak `CS`, ktore rowniez jest z natury zerojedynkowe, wiec bez liczby z przodu. |
| `red_cards` | `RC` | nie | Zawodnik wyrzucony z boiska nie moze dostac drugiej czerwonej kartki w tym samym meczu — wartosc zawsze 0 albo 1, wiec bez liczby, tak jak `YC` i `CS`. |

Kolejnosc na kazdym wierszu: `G A CS DC BP` (dodajace punkty, bez zmian), potem
`PM OG YC RC` (odejmujace punkty) — dokladnie ta kolejnosc, w jakiej te pola
wystepuja w odpowiedzi `event/{gw}/live/` z zadania.

## Migracja: dorobek druzyn kolejki 1 przed i po

Przed uruchomieniem migracji na `fpl` zapisany zostal wynik zapytania:

```sql
SELECT eg.id, eg.fpl_entry_id, eg.gameweek_id, eg.net_points, eg.api_points,
       eg.live_points_sum, eg.captain_points, eg.points_on_bench,
       eg.has_data, eg.is_final
FROM entry_gameweek eg WHERE eg.gameweek_id = 1 ORDER BY eg.fpl_entry_id
```

14 wierszy (14 druzyn z zapisanym snapshotem kolejki 1). Po uruchomieniu
migracji (`doctrine:migrations:migrate --env=dev`) to samo zapytanie zwrocilo
**bajt w bajt identyczny wynik** (`diff` bez roznic) — `net_points`,
`api_points`, `live_points_sum`, `captain_points`, `points_on_bench` niezmienione
dla wszystkich 14 wierszy.

Dodatkowo sprawdzone: `entry_gameweek_pick` mial przed i po migracji ten sam
komplet **210 wierszy** (15 pozycji x 14 druzyn), a cztery nowe kolumny
zsumowaly sie do zera na calej tabeli — migracja wylacznie dodaje kolumny,
nie dotyka zadnego istniejacego wiersza.

Migracja uruchomiona najpierw na `fpl_test` (`--env=test`), potem — po
weryfikacji diffem — na `fpl` (`--env=dev`). Obie bazy sa teraz na wersji
`Version20260823150000`.

## Wynik suity

```
OK (267 tests, 922 assertions)
```

262 testy sprzed zmian + 5 nowych: `ElementTest` (2 testy), rozszerzenie
`testMapsLiveNegativeEventsBreakdown` w `FplApiClientTest`, dwa nowe testy
funkcjonalne w `MatchControllerTest`. `testStoresStatsBreakdownOnEveryPick`
zostal rozszerzony, nie dodany jako nowy plik, wiec nie liczy sie osobno.

Rowniez `SchemaTest::testDatabaseSchemaMatchesMapping` (dziala na `fpl_test`)
przeszedl po migracji — mapowanie encji zgadza sie ze schema bazy.

## Watpliwosci / decyzje do potwierdzenia

- Kody `PM`/`OG`/`YC`/`RC` sa moja propozycja — jesli w zespole istnieje juz
  inna konwencja nazewnictwa tych zdarzen (np. z innego miejsca w aplikacji),
  warto to ujednolicic.
- Szerokosc kolumny `.squad-picks td.stats` zwiekszona z 134px do 210px na
  podstawie szacunku "wszystko na raz" (co w praktyce prawie nigdy sie nie
  zdarzy — np. `own_goals` i `clean_sheets` u tego samego zawodnika w tym samym
  meczu sa wzajemnie mało prawdopodobne). Nie widzialem tego w przegladarce
  (brak zadania wizualnego sprawdzenia), warto rzucic okiem na realny wyglad
  przy pierwszej okazji.
