--- 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 --- ## 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 ## 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) ## 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) ``` Task 1: Infinite retry dla order-related Apilo jobów autoload/Domain/CronJob/CronJobType.php, autoload/Domain/CronJob/CronJobRepository.php **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 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/ AC-3, AC-4 satisfied: Order joby retry w nieskończoność, inne bez zmian Task 2: Lepszy email + ostrzeżenie zamiast trwałego błędu + czyszczenie po sukcesie cron.php **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 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 AC-1, AC-2, AC-5 satisfied: Email czytelny, cleanup po sukcesie ## 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() 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 - 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 After completion, create `.paul/phases/09-apilo-email-fix/09-01-SUMMARY.md`