---
phase: 07-pre-expansion-fixes
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/Modules/Orders/OrdersRepository.php
  - database/migrations/20260313_000048_add_orders_performance_indexes.sql
autonomous: true
---

<objective>
## Goal
Wyeliminować dwa bottlenecki wydajnościowe w module Orders: 4 correlated subqueries na każdy wiersz listy zamówień oraz zapytanie do information_schema przy każdym żądaniu HTTP.

## Purpose
Lista zamówień jest głównym widokiem aplikacji. Przy 50 wierszach na stronę, correlated subqueries oznaczają 200+ dodatkowych zapytań per page load. `canResolveMappedMedia()` uderza w `information_schema` przy każdym żądaniu do OrdersRepository — notoriously slow na MySQL. Oba problemy narastają z liczbą zamówień.

## Output
- `OrdersRepository::buildListSql()` — 4 subqueries zastąpione aggregating LEFT JOIN
- `OrdersRepository::canResolveMappedMedia()` — wynik cachowany w `static` property
- Nowa migracja z brakującymi indeksami dla `orders` table
</objective>

<context>
## Project Context
@.paul/PROJECT.md

## Source Files
@src/Modules/Orders/OrdersRepository.php
</context>

<acceptance_criteria>

## AC-1: Brak correlated subqueries w liście zamówień
```gherkin
Given OrdersRepository::buildListSql() zawiera 4 correlated subqueries (items_count, items_qty, shipments_count, documents_count)
When metoda buduje SQL dla listy zamówień
Then SQL nie zawiera "(SELECT COUNT(*) FROM order_items" ani podobnych correlated subqueries
  AND zwracane kolumny items_count, items_qty, shipments_count, documents_count nadal istnieją
  AND logika transformOrderRow() w pełni działa (te pola nadal trafiają do wyniku)
```

## AC-2: information_schema nie jest odpytywany per-request
```gherkin
Given canResolveMappedMedia() używa instance property $this->supportsMappedMedia jako cache
When OrdersRepository jest instanciowany dwukrotnie w tej samej sesji PHP (np. dwa wywołania repository)
Then information_schema.COLUMNS jest zapytany co najwyżej raz na cykl PHP (static property)
```

## AC-3: Brakujące indeksy dodane migracją
```gherkin
Given tabela orders nie ma indeksów na kolumnach source, external_status_id, ordered_at
When tworzona jest nowa migracja i wykonana przez migrator
Then tabela orders ma indeksy na: source, external_status_id, ordered_at
  AND migracja jest idempotentna (IF NOT EXISTS lub ADD INDEX IF NOT EXISTS lub ALTER IGNORE)
```

</acceptance_criteria>

<tasks>

<task type="auto">
  <name>Task 1: Zamień 4 correlated subqueries na aggregating LEFT JOINs w buildListSql()</name>
  <files>src/Modules/Orders/OrdersRepository.php</files>
  <action>
    W metodzie `buildListSql()` (linie ~135-171) zamień:
    ```sql
    (SELECT COUNT(*) FROM order_items oi WHERE oi.order_id = o.id) AS items_count,
    (SELECT COALESCE(SUM(oi.quantity), 0) FROM order_items oi WHERE oi.order_id = o.id) AS items_qty,
    (SELECT COUNT(*) FROM order_shipments sh WHERE sh.order_id = o.id) AS shipments_count,
    (SELECT COUNT(*) FROM order_documents od WHERE od.order_id = o.id) AS documents_count
    ```
    Na:
    ```sql
    COALESCE(oi_agg.items_count, 0) AS items_count,
    COALESCE(oi_agg.items_qty, 0) AS items_qty,
    COALESCE(sh_agg.shipments_count, 0) AS shipments_count,
    COALESCE(od_agg.documents_count, 0) AS documents_count
    ```
    I dodaj do klauzuli FROM (po istniejących LEFT JOINach, przed WHERE):
    ```sql
    LEFT JOIN (
        SELECT order_id,
               COUNT(*) AS items_count,
               COALESCE(SUM(quantity), 0) AS items_qty
        FROM order_items
        GROUP BY order_id
    ) oi_agg ON oi_agg.order_id = o.id
    LEFT JOIN (
        SELECT order_id, COUNT(*) AS shipments_count
        FROM order_shipments
        GROUP BY order_id
    ) sh_agg ON sh_agg.order_id = o.id
    LEFT JOIN (
        SELECT order_id, COUNT(*) AS documents_count
        FROM order_documents
        GROUP BY order_id
    ) od_agg ON od_agg.order_id = o.id
    ```
    UWAGA: buildListSql() generuje SQL jako string — upewnij się że nowe JOINy są wstawione
    przed `$whereSql` (który jest konkatenowany na końcu z klauzulą WHERE).

    Sprawdź jak wygląda $whereSql — czy zawiera "WHERE" czy tylko "AND ..."?
    Jeśli WHERE jest w $whereSql, nowe JOINy idą bezpośrednio przed nim.

    NIE zmieniaj metody transformOrderRow() — klucze tablicy pozostają te same.
  </action>
  <verify>
    php -l src/Modules/Orders/OrdersRepository.php
    grep -c "SELECT COUNT\|SELECT COALESCE\|subquery" src/Modules/Orders/OrdersRepository.php
    # Powinno zwrócić 0 dla wzorców correlated subquery w buildListSql
  </verify>
  <done>AC-1 satisfied: buildListSql() używa LEFT JOIN zamiast correlated subqueries</done>
</task>

<task type="auto">
  <name>Task 2: Zamień instance property na static w canResolveMappedMedia()</name>
  <files>src/Modules/Orders/OrdersRepository.php</files>
  <action>
    Znajdź deklarację instance property (okolice klasy):
    ```php
    private ?bool $supportsMappedMedia = null;
    ```
    Zmień na:
    ```php
    private static ?bool $supportsMappedMedia = null;
    ```
    W metodzie `canResolveMappedMedia()` zmień referencje z `$this->supportsMappedMedia` na
    `self::$supportsMappedMedia` (we wszystkich miejscach metody — przypisanie i odczyt).

    To jedyna zmiana. Nie modyfikuj logiki zapytania ani żadnej innej metody.
  </action>
  <verify>
    php -l src/Modules/Orders/OrdersRepository.php
    grep -n "supportsMappedMedia" src/Modules/Orders/OrdersRepository.php
    # Powinno pokazać: private static ?bool i self::$supportsMappedMedia
  </verify>
  <done>AC-2 satisfied: canResolveMappedMedia() używa static property</done>
</task>

<task type="auto">
  <name>Task 3: Migracja z brakującymi indeksami dla tabeli orders</name>
  <files>database/migrations/20260313_000048_add_orders_performance_indexes.sql</files>
  <action>
    Stwórz plik `database/migrations/20260313_000048_add_orders_performance_indexes.sql`.

    Przed pisaniem migracji, sprawdź jakie indeksy już istnieją:
    ```bash
    grep -n "INDEX\|KEY " database/migrations/20260302_000018_create_orders_tables_and_schedule.sql
    grep -rn "ADD INDEX\|ADD KEY" database/migrations/ | grep -i "order"
    ```

    Migracja powinna dodać brakujące indeksy:
    ```sql
    -- Indeksy dla typowych filtrów i sortowań na liście zamówień
    ALTER TABLE orders
        ADD INDEX IF NOT EXISTS orders_source_idx (source),
        ADD INDEX IF NOT EXISTS orders_external_status_idx (external_status_id),
        ADD INDEX IF NOT EXISTS orders_ordered_at_idx (ordered_at),
        ADD INDEX IF NOT EXISTS orders_source_status_idx (source, external_status_id);

    -- Indeks dla allegro status mapping lookup (JOIN w buildListSql)
    ALTER TABLE allegro_order_status_mappings
        ADD INDEX IF NOT EXISTS allegro_status_code_idx (allegro_status_code)
    -- tylko jeśli ten indeks nie istnieje (sprawdź migration 000038)
    ```

    WAŻNE: Sprawdź aktualny schemat przed dodaniem indeksu — nie twórz duplikatów.
    Użyj `IF NOT EXISTS` gdzie MySQL to obsługuje (MySQL 8.0+), albo sformułuj jako
    osobne polecenia z IGNORE: `ALTER TABLE orders ADD IGNORE INDEX ...`

    Sprawdź też `database/migrations/20260308_000038_ensure_order_status_mappings_table.sql`
    aby wiedzieć jakie indeksy allegro_order_status_mappings już ma.
  </action>
  <verify>
    php -l database/migrations/20260313_000048_add_orders_performance_indexes.sql 2>/dev/null || echo "SQL file - no php lint needed"
    # Sprawdź czy plik istnieje i nie jest pusty
    cat database/migrations/20260313_000048_add_orders_performance_indexes.sql
  </verify>
  <done>AC-3 satisfied: migracja z indeksami na source, external_status_id, ordered_at istnieje</done>
</task>

</tasks>

<boundaries>

## DO NOT CHANGE
- Logika filtrowania ($whereSql) — tylko zmiana mechanizmu agregacji, nie filtrów
- Metoda transformOrderRow() — zwracane klucze muszą pozostać identyczne
- Inne metody OrdersRepository poza buildListSql() i canResolveMappedMedia()
- Istniejące pliki migracji — tylko NOWY plik 000048

## SCOPE LIMITS
- Tylko optymalizacja zapytania list; nie zmieniamy zapytań detail/find
- Nie dodajemy nowych kolumn ani nie zmieniamy schematu tabel — tylko indeksy
- Nie implementujemy cache'owania na poziomie Redis/Memcached — tylko static property

</boundaries>

<verification>
Przed zamknięciem planu:
- [ ] php -l src/Modules/Orders/OrdersRepository.php — brak błędów
- [ ] grep buildListSql src/Modules/Orders/OrdersRepository.php — brak "SELECT COUNT(*) FROM order_items WHERE oi.order_id"
- [ ] grep "supportsMappedMedia" src/Modules/Orders/OrdersRepository.php — pokazuje "static"
- [ ] Plik migracji 000048 istnieje i jest nieopusty
- [ ] Lista zamówień ładuje się bez błędów PHP
</verification>

<success_criteria>
- 4 correlated subqueries usunięte z buildListSql()
- canResolveMappedMedia() z static property
- Migracja 000048 z indeksami na source, external_status_id, ordered_at
- Zero błędów składniowych PHP
</success_criteria>

<output>
Po zakończeniu utwórz `.paul/phases/07-pre-expansion-fixes/07-01-SUMMARY.md`
</output>
