update
This commit is contained in:
235
.paul/phases/60-order-status-aged-event/60-01-PLAN.md
Normal file
235
.paul/phases/60-order-status-aged-event/60-01-PLAN.md
Normal file
@@ -0,0 +1,235 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
<objective>
|
||||
## 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
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## 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)
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## 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)
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Migracja cron schedule + serwis skanowania</name>
|
||||
<files>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</files>
|
||||
<action>
|
||||
**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
|
||||
</action>
|
||||
<verify>Plik migracji istnieje, OrderStatusAgedService.php ma metode scan(), handler zarejestrowany w CronHandlerFactory</verify>
|
||||
<done>AC-4, AC-5 satisfied: cron handler skanuje zamowienia i wyzwala event z naturalna deduplikacja</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Backend kontroler + serwis automatyzacji</name>
|
||||
<files>src/Modules/Automation/AutomationController.php, src/Modules/Automation/AutomationService.php</files>
|
||||
<action>
|
||||
**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);`
|
||||
</action>
|
||||
<verify>Grep ALLOWED_EVENTS zawiera order.status_aged; grep evaluateDaysInStatusCondition istnieje; grep days_in_status w parseConditionValue</verify>
|
||||
<done>AC-3 satisfied: warunki order_status i days_in_status dzialaja razem</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Frontend — formularz + widok</name>
|
||||
<files>resources/views/automation/form.php, resources/views/automation/index.php, public/assets/js/modules/automation-form.js</files>
|
||||
<action>
|
||||
**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 `<input type="number" min="1" step="1" class="form-control" name="conditions[{idx}][days]" value="{value}" placeholder="Liczba dni">`
|
||||
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 `<input type="number" min="1" step="1" class="form-control" name="{namePrefix}[days]" placeholder="Liczba dni">`
|
||||
- W `addCondition()` dodaj opcje `<option value="days_in_status">Liczba dni w statusie</option>`
|
||||
- W `onConditionTypeChange()` dodaj branch:
|
||||
`if (select.value === 'days_in_status') { configDiv.innerHTML = buildDaysInStatusInput(namePrefix); }`
|
||||
</action>
|
||||
<verify>Otworz /settings/automation/create, sprawdz ze zdarzenie i warunki sa dostepne w formularzu</verify>
|
||||
<done>AC-1, AC-2 satisfied: event w dropdown, warunek z polem numerycznym</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## 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)
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Wszystkie taski wykonane
|
||||
- Wszystkie AC spelnione
|
||||
- Brak bledow PHP/JS
|
||||
- Cron handler bezpieczny (limit, try/catch, non-blocking)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/60-order-status-aged-event/60-01-SUMMARY.md`
|
||||
</output>
|
||||
125
.paul/phases/60-order-status-aged-event/60-01-SUMMARY.md
Normal file
125
.paul/phases/60-order-status-aged-event/60-01-SUMMARY.md
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
phase: 60-order-status-aged-event
|
||||
plan: 01
|
||||
subsystem: automation
|
||||
tags: [automation, cron, order-status, time-based, aged]
|
||||
|
||||
requires:
|
||||
- phase: 59-order-status-automation-event
|
||||
provides: order_status condition type, evaluateOrderStatusCondition
|
||||
provides:
|
||||
- order.status_aged cron-based automation event
|
||||
- days_in_status condition type
|
||||
- OrderStatusAgedService (cron scanner)
|
||||
- OrderStatusAgedHandler (cron handler)
|
||||
affects: [automation workflows, cron infrastructure]
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [cron-based automation event scanning order_status_history]
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- src/Modules/Automation/OrderStatusAgedService.php
|
||||
- src/Modules/Cron/OrderStatusAgedHandler.php
|
||||
- database/migrations/20260331_000074_seed_order_status_aged_cron.sql
|
||||
modified:
|
||||
- src/Modules/Automation/AutomationController.php
|
||||
- src/Modules/Automation/AutomationService.php
|
||||
- src/Modules/Cron/CronHandlerFactory.php
|
||||
- resources/views/automation/form.php
|
||||
- resources/views/automation/index.php
|
||||
- public/assets/js/modules/automation-form.js
|
||||
|
||||
key-decisions:
|
||||
- "Skanowanie przez JOIN order_status_history z HAVING MAX(changed_at) — precyzyjne wykrywanie czasu w statusie"
|
||||
- "Limit 100 zamowien na regule na cykl — bezpieczenstwo przed przeciazeniem"
|
||||
- "Naturalna deduplikacja — po zmianie statusu zamowienie nie spelnia warunku w nastepnym cyklu"
|
||||
- "Cron co 1 godzine (3600s) — kompromis miedzy reaktywnoscia a obciazeniem"
|
||||
|
||||
patterns-established:
|
||||
- "Cron-based automation event: OrderStatusAgedService skanuje DB i emituje event przez AutomationService.trigger()"
|
||||
- "days_in_status condition: pole numeryczne, ewaluacja >= (nie ==)"
|
||||
|
||||
duration: 15min
|
||||
started: 2026-03-31T00:10:00Z
|
||||
completed: 2026-03-31T00:25:00Z
|
||||
---
|
||||
|
||||
# Phase 60 Plan 01: Order Status Aged Event Summary
|
||||
|
||||
**Zdarzenie automatyzacji `order.status_aged` wyzwalane cronem — skanuje zamowienia w danym statusie od X dni i uruchamia reguly**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~15min |
|
||||
| Tasks | 3 completed |
|
||||
| Files created | 3 |
|
||||
| Files modified | 6 |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Event order.status_aged dostepny w formularzu | Pass | Opcja "Minelo X dni od zmiany statusu" w dropdown |
|
||||
| AC-2: Warunek days_in_status z polem numerycznym | Pass | Input number min=1, placeholder "Liczba dni" |
|
||||
| AC-3: Warunek order_status dziala z tym zdarzeniem | Pass | Reuse warunku z Phase 59, oba warunki zapisuja sie poprawnie |
|
||||
| AC-4: Cron handler skanuje i wyzwala event | Pass | OrderStatusAgedService skanuje order_status_history, trigger z kontekstem |
|
||||
| AC-5: Deduplikacja naturalna | Pass | Po zmianie statusu zamowienie nie spelnia warunku w nastepnym cyklu |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Cron-based event `order.status_aged` — pierwsze zdarzenie czasowe w silniku automatyzacji
|
||||
- OrderStatusAgedService skanuje zamowienia po `order_status_history.changed_at` z precyzyjnym query HAVING
|
||||
- Warunek `days_in_status` z polem numerycznym i ewaluacja >= (zamowienie w statusie od 7 dni spelnia warunek "5 dni")
|
||||
- Handler zarejestrowany w CronHandlerFactory, migracja seeduje cron schedule co 1 godzine
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `database/migrations/20260331_000074_seed_order_status_aged_cron.sql` | Created | Seed cron schedule: job_type=order_status_aged, interval=3600s |
|
||||
| `src/Modules/Automation/OrderStatusAgedService.php` | Created | Serwis skanowania: findActiveByEvent, extractStatusCodes/Days, findAgedOrders, trigger |
|
||||
| `src/Modules/Cron/OrderStatusAgedHandler.php` | Created | Cron handler delegujacy do OrderStatusAgedService.scan() |
|
||||
| `src/Modules/Automation/AutomationController.php` | Modified | ALLOWED_EVENTS + order.status_aged, ALLOWED_CONDITION_TYPES + days_in_status, parseConditionValue + days_in_status |
|
||||
| `src/Modules/Automation/AutomationService.php` | Modified | evaluateDaysInStatusCondition() — porownanie context.days_in_status >= value.days |
|
||||
| `src/Modules/Cron/CronHandlerFactory.php` | Modified | Import OrderStatusAgedService, rejestracja handlera order_status_aged |
|
||||
| `resources/views/automation/form.php` | Modified | Etykieta zdarzenia, opcja warunku days_in_status, pole numeryczne |
|
||||
| `resources/views/automation/index.php` | Modified | Etykieta order.status_aged |
|
||||
| `public/assets/js/modules/automation-form.js` | Modified | buildDaysInStatusInput(), opcja w addCondition/onChange |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Query HAVING MAX(changed_at) vs kolumna na orders | Precyzyjne — bazuje na faktycznej dacie zmiany z historii, nie wymaga nowej kolumny | Brak zmian schematu tabeli orders |
|
||||
| Limit 100 zamowien na regule | Bezpieczenstwo — zapobiega przeciazeniu przy wielu zamowieniach w starym statusie | Moze wymagac podwyzszenia przy duzej skali |
|
||||
| Cron co 1 godzine | Kompromis — wystarczajaco czeste dla regul dniowych, nie przeciaza bazy | Konfigurowalny z panelu cron schedules |
|
||||
| Ewaluacja >= (nie ==) | Zamowienie "8 dni w statusie" spelnia regule "7 dni" — intuicyjne | Brak false negatives |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None — plan executed exactly as written.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Pelny zestaw eventow: receipt.created, shipment.created, shipment.status_changed, payment.status_changed, order.status_changed, order.status_aged
|
||||
- Pelny zestaw warunkow: integration, shipment_status, payment_status, order_status, days_in_status
|
||||
- Pierwszy cron-based event — wzorzec do reuzytku dla przyszlych eventow czasowych
|
||||
|
||||
**Concerns:**
|
||||
- None
|
||||
|
||||
**Blockers:**
|
||||
- None
|
||||
|
||||
---
|
||||
*Phase: 60-order-status-aged-event, Plan: 01*
|
||||
*Completed: 2026-03-31*
|
||||
Reference in New Issue
Block a user