---
phase: 09-apilo-email-fix
plan: 01
type: execute
wave: 1
depends_on: []
files_modified: [cron.php, autoload/Domain/CronJob/CronJobRepository.php, autoload/Domain/CronJob/CronJobType.php]
autonomous: true
---

<objective>
## Goal
1. Wzbogacić email notyfikacji o trwałym błędzie Apilo o czytelne dane zamówienia (numer, klient, kwota)
2. Zamówienia Apilo (send_order, sync_payment, sync_status) muszą być ponawiane w nieskończoność co 30 minut
3. Email o błędzie nadal wysyłany (jako ostrzeżenie), ale job wraca do pending zamiast permanent failure
4. Po udanym wysłaniu zamówienia — czyścimy powiązane failed/pending joby

## Purpose
Administrator dostaje email bez informacji o którym zamówieniu chodzi. Dodatkowo, po 10 próbach zamówienie przestaje być synchronizowane — to niedopuszczalne, bo zamówienie musi trafić do Apilo.

## Output
- Zmodyfikowany `cron.php` — lepsza treść emaila + czyszczenie jobów po sukcesie
- Zmodyfikowany `CronJobRepository` — obsługa infinite retry
- Zmodyfikowany `CronJobType` — stała backoffu 30min dla Apilo
</objective>

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

## Prior Work
@.paul/phases/08-apilo-orders-fix/08-01-SUMMARY.md

## Source Files
@cron.php (linie 198-529 — handler APILO_SEND_ORDER, linie 763-781 — email notification)
@autoload/Domain/CronJob/CronJobRepository.php (markFailed — linie 131-156)
@autoload/Domain/CronJob/CronJobType.php (stałe backoff)
</context>

<acceptance_criteria>

## AC-1: Email zawiera czytelne dane zamówienia
```gherkin
Given trwale nieudane zadanie Apilo z payload zawierającym order_id
When system wysyła email notyfikacji
Then email zawiera: numer zamówienia, dane klienta, datę zamówienia, kwotę
  And temat emaila zawiera numery zamówień
```

## AC-2: Brak order_id w payload nie powoduje błędu
```gherkin
Given trwale nieudane zadanie Apilo bez order_id w payload (np. apilo_token_keepalive)
When system wysyła email notyfikacji
Then email wyświetla dane job-a bez sekcji zamówienia, bez błędów
```

## AC-3: Joby zamówień Apilo ponawiają się w nieskończoność co 30 minut
```gherkin
Given job typu apilo_send_order, apilo_sync_payment lub apilo_sync_status
When job osiąga max_attempts
Then job NIE jest oznaczany jako failed
  And job wraca do pending ze scheduled_at = now + 30 minut
  And email ostrzegawczy jest wysyłany (z informacją że job dalej jest ponawiany)
```

## AC-4: Inne joby Apilo (token, product sync) nadal mają limit prób
```gherkin
Given job typu apilo_token_keepalive lub apilo_product_sync
When job osiąga max_attempts
Then job jest oznaczany jako failed (zachowanie bez zmian)
```

## AC-5: Po udanym wysłaniu zamówienia czyszczone są powiązane failed joby
```gherkin
Given zamówienie wysłane pomyślnie do Apilo (apilo_order_id ustawiony)
When handler APILO_SEND_ORDER kończy się sukcesem
Then powiązane joby apilo_sync_payment i apilo_sync_status ze statusem failed
  zostają usunięte lub anulowane (żeby nie zaśmiecały kolejki)
```

</acceptance_criteria>

<tasks>

<task type="auto">
  <name>Task 1: Infinite retry dla order-related Apilo jobów</name>
  <files>autoload/Domain/CronJob/CronJobType.php, autoload/Domain/CronJob/CronJobRepository.php</files>
  <action>
    **CronJobType.php:**
    1. Dodać stałą `APILO_ORDER_BACKOFF_SECONDS = 1800` (30 minut)
    2. Dodać statyczną metodę `isOrderRelatedApiloJob($jobType)` zwracającą true dla:
       - APILO_SEND_ORDER, APILO_SYNC_PAYMENT, APILO_SYNC_STATUS

    **CronJobRepository::markFailed():**
    3. Przed sprawdzeniem `$attempts >= $maxAttempts`:
       - Pobrać `job_type` z bazy (dodać do selecta w linia 133)
       - Jeśli `CronJobType::isOrderRelatedApiloJob($jobType)`:
         - ZAWSZE wracaj do pending (nigdy failed)
         - Użyj stałego backoffu `APILO_ORDER_BACKOFF_SECONDS` zamiast exponential
         - Ustaw `last_error` jak normalnie
       - Dla pozostałych jobów — logika bez zmian

    UWAGA: Nie zmieniaj sygnatury markFailed() — dodaj job_type do wewnętrznego selecta
  </action>
  <verify>
    1. Przeczytaj kod i zweryfikuj że:
       - isOrderRelatedApiloJob zwraca true tylko dla 3 typów
       - markFailed nigdy nie ustawia status=failed dla tych typów
       - Inne joby zachowują się jak dotychczas
    2. Uruchom: ./test.ps1 tests/Unit/Domain/CronJob/
  </verify>
  <done>AC-3, AC-4 satisfied: Order joby retry w nieskończoność, inne bez zmian</done>
</task>

<task type="auto">
  <name>Task 2: Lepszy email + ostrzeżenie zamiast trwałego błędu + czyszczenie po sukcesie</name>
  <files>cron.php</files>
  <action>
    **Email notification (linie ~763-781):**
    1. Zmienić query o failed joby — RÓWNIEŻ szukać order-related jobów w statusie pending z dużą liczbą prób (np. attempts >= 10), żeby wysyłać ostrzeżenie
    2. Dla każdego job-a: sparsować payload (json_decode jeśli string), wyciągnąć order_id
    3. Jeśli order_id istnieje — pobrać z pp_shop_orders:
       - `order_number` (lub `id` jeśli brak), `client_name`/`client_surname`, `date_order`, `total_brutto`
    4. Sformatować email:
       ```
       Job #X (apilo_send_order) — PONAWIANY CO 30 MIN
         Zamówienie: #12345 (ID: 678)
         Klient: Jan Kowalski
         Data zamówienia: 2026-03-19 14:30:00
         Kwota: 199.99 PLN
         Próby: 15
         Błąd: [last_error]
         Ostatnia próba: [updated_at lub scheduled_at]
       ```
    5. Dla jobów permanent failed (nie-order): zachować stary format "trwały błąd"
    6. Temat: dodać numery zamówień jeśli dostępne
    7. Email ma rozróżniać: "PONAWIANY" vs "TRWAŁY BŁĄD" w zależności od typu joba

    **Czyszczenie po sukcesie (w handlerze APILO_SEND_ORDER, po linii ~522-524):**
    8. Po pomyślnym wysłaniu zamówienia (`apilo_order_id` ustawiony):
       - Usunąć/anulować failed/pending joby `apilo_sync_payment` i `apilo_sync_status`
         z payload zawierającym ten sam order_id
       - Użyć: `$mdb->delete('pp_cron_jobs', [...])` lub update status=cancelled
       - To zapobiega zaśmiecaniu kolejki starymi retry jobami

    UWAGA:
    - Nazwy kolumn zamówienia: sprawdź jakie faktycznie są w pp_shop_orders (mogą być polskie)
    - Payload w bazie to JSON string — json_decode($fj['payload'], true)
    - Nie zmieniaj logiki wysyłania zamówień — tylko email i cleanup
  </action>
  <verify>
    1. Przeczytaj zmodyfikowany kod
    2. Zweryfikuj że query do pp_shop_orders używa poprawnych kolumn
    3. Zweryfikuj brak błędów PHP (null handling, json_decode guard)
    4. Uruchom: ./test.ps1
  </verify>
  <done>AC-1, AC-2, AC-5 satisfied: Email czytelny, cleanup po sukcesie</done>
</task>

</tasks>

<boundaries>

## DO NOT CHANGE
- Logikę wysyłania zamówień do Apilo (curl, payload budowanie)
- Logikę exponential backoff dla NIE-order jobów
- Handlery APILO_SYNC_PAYMENT, APILO_SYNC_STATUS, APILO_STATUS_POLL (poza cleanup)
- Odbiorcę emaila i warunki wysyłki (poza rozszerzeniem query)
- Tabelę pp_shop_orders — żadnych nowych kolumn

## SCOPE LIMITS
- Tylko retry logic, email formatting, i cleanup
- Nie dodawać nowych tabel
- Nie zmieniać enqueue() ani fetchNext()

</boundaries>

<verification>
Before declaring plan complete:
- [ ] Order-related Apilo joby nigdy nie dostają status=failed
- [ ] Backoff dla order jobów = stałe 30 min
- [ ] Inne joby zachowują stare zachowanie (exponential, max 10)
- [ ] Email zawiera numer zamówienia gdy dostępny
- [ ] Email rozróżnia "ponawiany" vs "trwały błąd"
- [ ] Po sukcesie wysyłki czyścimy related joby
- [ ] Brak błędów PHP
- [ ] Testy przechodzą (./test.ps1)
- [ ] All acceptance criteria met
</verification>

<success_criteria>
- Zamówienia Apilo są ponawiane w nieskończoność co 30 min
- Email notyfikacji zawiera czytelne dane zamówienia
- Po udanym wysłaniu czyszczone są stare joby
- Zero regresji w testach
</success_criteria>

<output>
After completion, create `.paul/phases/09-apilo-email-fix/09-01-SUMMARY.md`
</output>
