--- phase: 60-order-status-aged-event plan: 01 type: execute wave: 1 depends_on: ["59-01"] files_modified: - src/Modules/Automation/AutomationController.php - src/Modules/Automation/AutomationService.php - src/Modules/Automation/AutomationRepository.php - src/Modules/Cron/CronHandlerFactory.php - src/Modules/Cron/OrderStatusAgedHandler.php - src/Modules/Automation/OrderStatusAgedService.php - database/migrations/20260331_000074_seed_order_status_aged_cron.sql - resources/views/automation/form.php - resources/views/automation/index.php - public/assets/js/modules/automation-form.js autonomous: true --- ## Goal Nowe zdarzenie automatyzacji `order.status_aged` wyzwalane cyklicznie przez cron. Skanuje zamowienia, ktorych status nie zmienil sie od X dni i uruchamia pasujace reguly automatyzacji. ## Purpose Uzytkownik moze np. ustawic regule: "Jesli zamowienie ma status 'wyslane' od 7 dni, zmien na 'zrealizowane'". Przydatne gdy brak trackingu przesylki i nie wiadomo czy dotarla. ## Output - Event `order.status_aged` dostepny w formularzu automatyzacji - Warunek `order_status` (reuse z Phase 59) + nowy warunek `days_in_status` (pole numeryczne) - Cron handler skanujacy `order_status_history` i wyzwalajacy event - Migracja seedujaca cron schedule ## Project Context @.paul/PROJECT.md @.paul/ROADMAP.md @.paul/STATE.md ## Prior Work @.paul/phases/59-order-status-automation-event/59-01-SUMMARY.md — event order.status_changed + warunek order_status ## Source Files @src/Modules/Automation/AutomationController.php @src/Modules/Automation/AutomationService.php @src/Modules/Automation/AutomationRepository.php @src/Modules/Cron/CronHandlerFactory.php @src/Modules/Orders/OrdersRepository.php (recordStatusChange, order_status_history) ## AC-1: Event order.status_aged dostepny w formularzu ```gherkin Given uzytkownik otwiera formularz nowego zadania automatycznego When rozwija dropdown "Zdarzenie" Then widzi opcje "Minelo X dni od zmiany statusu" ``` ## AC-2: Warunek days_in_status z polem numerycznym ```gherkin Given uzytkownik wybral zdarzenie "Minelo X dni od zmiany statusu" When dodaje warunek "Liczba dni w statusie" Then widzi pole numeryczne (min 1) do wpisania ilosci dni ``` ## AC-3: Warunek order_status dziala z tym zdarzeniem ```gherkin Given uzytkownik wybral zdarzenie "Minelo X dni od zmiany statusu" When dodaje warunek "Status zamowienia" i zaznacza "wyslane" And dodaje warunek "Liczba dni w statusie" = 7 Then regula zapisuje sie poprawnie z oboma warunkami ``` ## AC-4: Cron handler skanuje i wyzwala event ```gherkin Given istnieje regula: event=order.status_aged, warunek order_status=wyslane, days_in_status=7, akcja=update_order_status(zrealizowane) And zamowienie #100 ma status "wyslane" od 8 dni (w order_status_history) When cron uruchamia handler order_status_aged Then event order.status_aged jest wyzwolony dla zamowienia #100 And regula jest dopasowana i status zmieniony na "zrealizowane" ``` ## AC-5: Deduplikacja — zamowienie przetworzone raz na cykl ```gherkin Given regula juz wykonala akcje na zamowieniu #100 w poprzednim cyklu crona And zamowienie #100 nadal ma status "wyslane" (bo akcja zmienila na "zrealizowane" ale to inny status) When cron uruchamia handler ponownie Then zamowienie #100 nie jest przetwarzane ponownie (status sie zmienil) ``` Task 1: Migracja cron schedule + serwis skanowania database/migrations/20260331_000074_seed_order_status_aged_cron.sql, src/Modules/Automation/OrderStatusAgedService.php, src/Modules/Cron/OrderStatusAgedHandler.php, src/Modules/Cron/CronHandlerFactory.php **Migracja (seed cron schedule):** - INSERT INTO cron_schedules: job_key=`order_status_aged`, interval_minutes=60 (co godzine), is_active=1 - Idempotentny INSERT (IF NOT EXISTS pattern jak inne migracje) **OrderStatusAgedService.php** (nowa klasa w `App\Modules\Automation`): - Konstruktor: `AutomationRepository $repository`, `AutomationService $automation`, `PDO $db` - Metoda `scan(): void`: 1. Pobierz aktywne reguly z event_type=`order.status_aged` (`$this->repository->findActiveByEvent('order.status_aged')`) 2. Dla kazdej reguly: a. Wyciagnij warunki: `order_status` (kody statusow) i `days_in_status` (int dni) b. Query do DB: zamowienia gdzie `external_status_id` IN (kody) i ostatnia zmiana w `order_status_history` (MAX(changed_at) WHERE to_status_id = external_status_id) jest starsza niz X dni c. Dla kazdego znalezionego zamowienia: `$this->automation->trigger('order.status_aged', $orderId, ['current_status' => ..., 'days_in_status' => ..., 'status_changed_at' => ...])` 3. Deduplikacja naturalna: po wykonaniu akcji (np. zmiana statusu) zamowienie nie spelnia juz warunku w nastepnym cyklu **Query do skanowania (w OrderStatusAgedService):** ```sql SELECT o.id, o.external_status_id, MAX(h.changed_at) AS last_changed FROM orders o INNER JOIN order_status_history h ON h.order_id = o.id AND LOWER(h.to_status_id) = LOWER(o.external_status_id) WHERE LOWER(o.external_status_id) IN (:statuses) GROUP BY o.id, o.external_status_id HAVING MAX(h.changed_at) <= DATE_SUB(NOW(), INTERVAL :days DAY) ``` - Uzyj prepared statements (medoo pattern nie wymagany - raw PDO OK jak w OrdersRepository) - Limit do 100 zamowien na cykl (bezpieczenstwo) **OrderStatusAgedHandler.php** (nowa klasa w `App\Modules\Cron`): - Implementuje `CronHandlerInterface` (sprawdz jaki interfejs uzywaja inne handlery) - Konstruktor: `OrderStatusAgedService $service` - Metoda `handle()`: wywoluje `$this->service->scan()` **CronHandlerFactory.php:** - Dodaj import `use App\Modules\Automation\OrderStatusAgedService;` - Dodaj import `use App\Modules\Cron\OrderStatusAgedHandler;` - W `build()` dodaj handler `'order_status_aged'` do tablicy handlerow: ```php 'order_status_aged' => new OrderStatusAgedHandler( new OrderStatusAgedService($automationRepository, $automationService, $this->db) ), ``` - `$automationRepository` juz jest tworzony w `buildAutomationService()` — wyciagnij go lub utwórz wczesniej Plik migracji istnieje, OrderStatusAgedService.php ma metode scan(), handler zarejestrowany w CronHandlerFactory AC-4, AC-5 satisfied: cron handler skanuje zamowienia i wyzwala event z naturalna deduplikacja Task 2: Backend kontroler + serwis automatyzacji src/Modules/Automation/AutomationController.php, src/Modules/Automation/AutomationService.php **AutomationController.php:** - Dodaj `'order.status_aged'` do `ALLOWED_EVENTS` - Dodaj `'days_in_status'` do `ALLOWED_CONDITION_TYPES` - W `buildRuleFromRequest()` dodaj branch `elseif ($type === 'days_in_status')`: `$value = ['days' => max(1, (int) ($cond['days'] ?? 0))];` - W `parseConditionValue()` dodaj branch `if ($type === 'days_in_status')`: ```php $days = (int) ($condition['days'] ?? 0); return $days >= 1 ? ['days' => $days] : null; ``` **AutomationService.php:** - Dodaj metode `evaluateDaysInStatusCondition(array $value, array $context): bool`: - Porownuje `$value['days']` z `$context['days_in_status']` - Zwraca true jesli `$context['days_in_status'] >= $value['days']` - W `evaluateSingleCondition()` dodaj branch: `if ($type === 'days_in_status') return $this->evaluateDaysInStatusCondition($value, $context);` Grep ALLOWED_EVENTS zawiera order.status_aged; grep evaluateDaysInStatusCondition istnieje; grep days_in_status w parseConditionValue AC-3 satisfied: warunki order_status i days_in_status dzialaja razem Task 3: Frontend — formularz + widok resources/views/automation/form.php, resources/views/automation/index.php, public/assets/js/modules/automation-form.js **form.php:** - Dodaj do `$eventLabels`: `'order.status_aged' => 'Minelo X dni od zmiany statusu'` - W sekcji renderowania warunkow dodaj branch `elseif ($conditionType === 'days_in_status')`: Renderuj `` Wartosc z `$condValue['days'] ?? ''` **index.php:** - Dodaj do `$eventLabels`: `'order.status_aged' => 'Minelo X dni od zmiany statusu'` **automation-form.js:** - Dodaj funkcje `buildDaysInStatusInput(namePrefix)`: Zwraca `` - W `addCondition()` dodaj opcje `` - W `onConditionTypeChange()` dodaj branch: `if (select.value === 'days_in_status') { configDiv.innerHTML = buildDaysInStatusInput(namePrefix); }` Otworz /settings/automation/create, sprawdz ze zdarzenie i warunki sa dostepne w formularzu AC-1, AC-2 satisfied: event w dropdown, warunek z polem numerycznym ## DO NOT CHANGE - src/Modules/Orders/OrdersRepository.php (query skanowania w osobnym serwisie) - Istniejace handlery crona ## SCOPE LIMITS - Tylko event `order.status_aged` i warunek `days_in_status` - Cron co 1 godzine (konfigurowalny z panelu cron schedules) - Limit 100 zamowien na cykl na regule - Brak UI do konfiguracji interwalu crona (juz istnieje w panelu cron) Before declaring plan complete: - [ ] Migracja seeduje cron schedule - [ ] OrderStatusAgedService skanuje zamowienia po order_status_history - [ ] Handler zarejestrowany w CronHandlerFactory - [ ] `order.status_aged` widoczny w dropdown zdarzenia - [ ] Warunek `days_in_status` renderuje pole numeryczne - [ ] Warunek `order_status` dziala z tym zdarzeniem (reuse z Phase 59) - [ ] Zapis i edycja reguly dziala poprawnie - [ ] Naturalna deduplikacja: po zmianie statusu zamowienie nie spelnia warunku - Wszystkie taski wykonane - Wszystkie AC spelnione - Brak bledow PHP/JS - Cron handler bezpieczny (limit, try/catch, non-blocking) After completion, create `.paul/phases/60-order-status-aged-event/60-01-SUMMARY.md`