update
This commit is contained in:
@@ -64,6 +64,11 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
||||
- [x] Mobile Status Panel Toggle: zwijany/rozwijany panel statusow na /orders/list — Phase 53
|
||||
- [x] Order Detail Image Hover: hover zoom na miniaturkach produktow w /orders/{id} — Phase 54
|
||||
- [x] Desktop Collapsed Sidebar Fix: ukrycie etykiet, centrowanie ikon w zwiniętym sidebarze — Phase 55
|
||||
- [x] Dodawanie płatności ręcznych + push set_paid do shopPRO — Phase 56
|
||||
- [x] Automatyzacja: event `payment.status_changed` + warunek `payment_status` (0/1/2) — Phase 57
|
||||
- [x] Zachowanie danych formularza automatyzacji po bledzie walidacji — Phase 58
|
||||
- [x] Automatyzacja: event `order.status_changed` + warunek `order_status` — Phase 59
|
||||
- [x] Automatyzacja: event `order.status_aged` (cron) + warunek `days_in_status` — Phase 60
|
||||
|
||||
### Active (In Progress)
|
||||
|
||||
|
||||
@@ -16,7 +16,11 @@ Wersja mobilna aplikacji, modul po module. Cel: pelna uzywalnosc orderPRO na tel
|
||||
| 53 | Mobile Status Panel Toggle | 1/1 | Complete |
|
||||
| 54 | Order Detail Image Hover | 1/1 | Complete |
|
||||
| 55 | Desktop Collapsed Sidebar Fix | 1/1 | Complete |
|
||||
| 56 | Order Payments | 0/1 | Planning |
|
||||
| 56 | Order Payments | 1/1 | Complete |
|
||||
| 57 | Payment Automation Event | 1/1 | Complete |
|
||||
| 58 | Automation Form Preserve | 1/1 | Complete |
|
||||
| 59 | Order Status Automation Event | 1/1 | Complete |
|
||||
| 60 | Order Status Aged Event | 1/1 | Complete |
|
||||
| TBD | Mobile Orders List | - | Not started |
|
||||
| TBD | Mobile Order Details | - | Not started |
|
||||
| TBD | Mobile Settings | - | Not started |
|
||||
|
||||
@@ -2,49 +2,40 @@
|
||||
|
||||
## Project Reference
|
||||
|
||||
See: .paul/PROJECT.md (updated 2026-03-28)
|
||||
See: .paul/PROJECT.md (updated 2026-03-31)
|
||||
|
||||
**Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami.
|
||||
**Current focus:** Milestone v3.0 Mobile Responsive — Phase 56 (Order Payments) planning
|
||||
**Current focus:** Milestone v3.0 — Phase 60 complete, ready for next PLAN
|
||||
|
||||
## Current Position
|
||||
|
||||
Milestone: v3.0 Mobile Responsive — In progress
|
||||
Phase: 5 of N (56 - Order Payments) — Planning
|
||||
Plan: 56-01 created, awaiting approval
|
||||
Status: PLAN created, ready for APPLY
|
||||
Last activity: 2026-03-30 — Created .paul/phases/56-order-payments/56-01-PLAN.md
|
||||
Phase: 9 of N (60 - Order Status Aged Event) — Complete
|
||||
Plan: 60-01 complete
|
||||
Status: Loop complete — phase 60 done, ready for next PLAN
|
||||
Last activity: 2026-03-31 — UNIFY closed for 60-01
|
||||
|
||||
Progress:
|
||||
- Milestone: [####░░░░░░] ~40%
|
||||
- Phase 56: [░░░░░░░░░░] 0%
|
||||
- Milestone: [######░░░░] ~55%
|
||||
- Phase 60: [##########] 100%
|
||||
|
||||
## Loop Position
|
||||
|
||||
Current loop state:
|
||||
```
|
||||
PLAN ──▶ APPLY ──▶ UNIFY
|
||||
✓ ○ ○ [Plan created, awaiting approval]
|
||||
✓ ✓ ✓ [Loop complete - ready for next PLAN]
|
||||
```
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-03-30
|
||||
Stopped at: Plan 56-01 created
|
||||
Next action: Review and approve plan, then run /paul:apply .paul/phases/56-order-payments/56-01-PLAN.md
|
||||
Resume file: .paul/phases/56-order-payments/56-01-PLAN.md
|
||||
|
||||
## Accumulated Context
|
||||
|
||||
### Decisions
|
||||
| Date | Decision | Impact |
|
||||
|------|----------|--------|
|
||||
| 2026-03-29 | Mobile menu jako slide-in overlay (nie horizontal scroll) | Pelna nawigacja na mobile bez kompromisow |
|
||||
| 2026-03-29 | Hamburger w topbarze, sidebar fixed z transform slide | Plynna animacja CSS, zero zaleznosci JS |
|
||||
| 2026-03-30 | Push set_paid do shopPRO API po dodaniu platnosci w orderPRO | Synchronizacja statusu platnosci bez dodatkowego endpointu w shopPRO |
|
||||
Last session: 2026-03-31
|
||||
Stopped at: Phase 60 complete
|
||||
Next action: /paul:plan dla kolejnego modulu
|
||||
Resume file: .paul/phases/60-order-status-aged-event/60-01-SUMMARY.md
|
||||
|
||||
## Git State
|
||||
|
||||
Last commit: 70662af
|
||||
Last commit: 5435209
|
||||
Branch: main
|
||||
Feature branches merged: none
|
||||
|
||||
34
.paul/phases/56-order-payments/56-01-SUMMARY.md
Normal file
34
.paul/phases/56-order-payments/56-01-SUMMARY.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
phase: 56-order-payments
|
||||
plan: 01
|
||||
status: complete
|
||||
completed: 2026-03-30
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Uruchomienie funkcji dodawania platnosci do zamowienia z zakladki Platnosci + push statusu platnosci do shopPRO API.
|
||||
|
||||
## What Was Built
|
||||
|
||||
- Migracja `20260330_000073_create_order_payments_table.sql` — tabela `order_payments` (INT UNSIGNED FK) + idempotentne kolumny `total_with_tax`, `total_paid`, `external_payment_type_id` w `orders`
|
||||
- `OrdersRepository::addPayment()` — INSERT + przeliczenie total_paid i payment_status
|
||||
- `OrdersRepository::findOrderSourceInfo()` — dane source/integration dla push
|
||||
- `OrdersController::addPayment()` — POST `/orders/{id}/payment/add` z walidacja, try/catch, activity log
|
||||
- `OrdersController::pushPaymentToShoppro()` — push `set_paid` do shopPRO API po pelnym oplaceniu
|
||||
- `ShopproApiClient::setOrderPaid()` — PUT `/api.php?endpoint=orders&action=set_paid`
|
||||
- Formularz inline w zakladce Platnosci (przycisk "Dodaj platnosc", pola: kwota/typ/data/komentarz, AJAX submit)
|
||||
- Style `.payment-add-form` w SCSS
|
||||
|
||||
## Deviations
|
||||
|
||||
- FK type fix: `order_payments.order_id` zmieniony z BIGINT na INT UNSIGNED (zgodnosc z `orders.id`)
|
||||
- `Csrf::verify()` → `Csrf::validate()` (poprawna nazwa metody)
|
||||
- Dodano try/catch w kontrolerze dla lepszej diagnostyki bledow
|
||||
|
||||
## Decisions
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| Push set_paid do shopPRO bez nowego endpointu | shopPRO API juz ma `set_paid` — wystarczajace |
|
||||
| ShopproIntegrationsRepository jako nullable dep w OrdersController | Minimalna zmiana konstruktora |
|
||||
219
.paul/phases/57-payment-automation-event/57-01-PLAN.md
Normal file
219
.paul/phases/57-payment-automation-event/57-01-PLAN.md
Normal file
@@ -0,0 +1,219 @@
|
||||
---
|
||||
phase: 57-payment-automation-event
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: ["56-01"]
|
||||
files_modified:
|
||||
- src/Modules/Automation/AutomationController.php
|
||||
- src/Modules/Automation/AutomationService.php
|
||||
- src/Modules/Orders/OrdersController.php
|
||||
- src/Modules/Settings/ShopproPaymentStatusSyncService.php
|
||||
- src/Modules/Cron/CronHandlerFactory.php
|
||||
- resources/views/automation/form.php
|
||||
- public/assets/js/modules/automation-form.js
|
||||
- routes/web.php
|
||||
- DOCS/ARCHITECTURE.md
|
||||
- DOCS/TECH_CHANGELOG.md
|
||||
autonomous: true
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Dodanie zdarzenia automatyzacji `payment.status_changed` emitowanego przy zmianie statusu platnosci zamowienia. Warunek `payment_status` pozwala filtrowac po konkretnym statusie (oplacone, czesciowo oplacone, nieoplacone).
|
||||
|
||||
## Purpose
|
||||
Sprzedawca moze tworzyc reguly automatyzacji reagujace na zmiane statusu platnosci — np. wyslij email po oplaceniu, zmien status zamowienia, wystaw paragon.
|
||||
|
||||
## Output
|
||||
- Nowy event `payment.status_changed` w silniku automatyzacji
|
||||
- Nowy warunek `payment_status` z checkboxami statusow
|
||||
- Emisja eventu z OrdersController::addPayment() i ShopproPaymentStatusSyncService
|
||||
- UI: event i warunek widoczne w formularzu regul
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
|
||||
## Prior Work
|
||||
@.paul/phases/56-order-payments/56-01-SUMMARY.md (addPayment, payment_status update)
|
||||
|
||||
## Source Files
|
||||
@src/Modules/Automation/AutomationController.php (ALLOWED_EVENTS linia 19, ALLOWED_CONDITION_TYPES linia 20)
|
||||
@src/Modules/Automation/AutomationService.php (evaluateSingleCondition, evaluateShipmentStatusCondition — wzorzec)
|
||||
@src/Modules/Orders/OrdersController.php (addPayment — miejsce emisji eventu)
|
||||
@src/Modules/Settings/ShopproPaymentStatusSyncService.php (syncSingleOrderPayment — miejsce emisji eventu)
|
||||
@src/Modules/Cron/CronHandlerFactory.php (kompozycja ShopproPaymentStatusSyncService)
|
||||
@resources/views/automation/form.php (eventLabels, condition select, JS data)
|
||||
@public/assets/js/modules/automation-form.js (addCondition, onConditionTypeChange, buildShipmentStatusCheckboxes — wzorzec)
|
||||
@routes/web.php (tworzenie $automationService linia 253, $ordersController linia 264)
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Event payment.status_changed dostepny w formularzu
|
||||
```gherkin
|
||||
Given otwieram Ustawienia > Zadania automatyczne > Nowa regula
|
||||
When klikam dropdown "Zdarzenie"
|
||||
Then widze opcje "Zmiana statusu platnosci" obok istniejacych zdarzen
|
||||
```
|
||||
|
||||
## AC-2: Warunek payment_status dostepny w formularzu
|
||||
```gherkin
|
||||
Given wybralem zdarzenie "Zmiana statusu platnosci"
|
||||
When dodaje warunek i wybieram typ "Status platnosci"
|
||||
Then widze checkboxy: Nieoplacone, Czesciowo oplacone, Oplacone
|
||||
And moge zaznaczyc jeden lub wiecej statusow
|
||||
```
|
||||
|
||||
## AC-3: Event emitowany przy recznym dodaniu platnosci
|
||||
```gherkin
|
||||
Given istnieje aktywna regula: event=payment.status_changed, warunek=payment_status IN [oplacone], akcja=update_order_status
|
||||
When dodaje platnosc pokrywajaca pelna kwote zamowienia
|
||||
Then regula sie odpala i status zamowienia zmienia sie automatycznie
|
||||
And w historii automatyzacji widze wpis o wykonaniu reguly
|
||||
```
|
||||
|
||||
## AC-4: Event emitowany przy sync platnosci z shopPRO
|
||||
```gherkin
|
||||
Given zamowienie source=shoppro zmienia payment_status przez cron sync
|
||||
When nowy payment_status rozni sie od poprzedniego
|
||||
Then event payment.status_changed jest emitowany z kontekstem (old/new status)
|
||||
```
|
||||
|
||||
## AC-5: Warunek filtruje poprawnie
|
||||
```gherkin
|
||||
Given regula z warunkiem payment_status = [oplacone]
|
||||
When event jest emitowany z new_payment_status = 1 (czesciowo oplacone)
|
||||
Then regula NIE odpala sie
|
||||
When event jest emitowany z new_payment_status = 2 (oplacone)
|
||||
Then regula odpala sie
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Backend — event + warunek w silniku automatyzacji</name>
|
||||
<files>src/Modules/Automation/AutomationController.php, src/Modules/Automation/AutomationService.php</files>
|
||||
<action>
|
||||
1. W AutomationController:
|
||||
- Dodaj 'payment.status_changed' do ALLOWED_EVENTS (linia 19)
|
||||
- Dodaj 'payment_status' do ALLOWED_CONDITION_TYPES (linia 20)
|
||||
- W metodzie parseConditions(): obsluz 'payment_status' — zbierz payment_status_keys[] z POST, zapisz jako condition_value.status_keys (wzorzec jak shipment_status)
|
||||
|
||||
2. W AutomationService::evaluateSingleCondition():
|
||||
- Dodaj branch: if ($type === 'payment_status') return $this->evaluatePaymentStatusCondition($value, $context);
|
||||
|
||||
3. Dodaj metode evaluatePaymentStatusCondition(array $value, array $context): bool:
|
||||
- Pobierz status_keys z $value (tablica dozwolonych statusow: '0', '1', '2')
|
||||
- Pobierz new_payment_status z $context (string)
|
||||
- Return true jesli new_payment_status jest w status_keys
|
||||
- Mapowanie: 0=nieoplacone, 1=czesciowo_oplacone, 2=oplacone
|
||||
|
||||
Wzoruj sie dokladnie na evaluateShipmentStatusCondition() — analogiczna logika.
|
||||
</action>
|
||||
<verify>PHP lint — brak bledow skladniowych</verify>
|
||||
<done>AC-1, AC-2, AC-5 satisfied: event i warunek zarejestrowane w silniku</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Emisja eventu z OrdersController i ShopproPaymentStatusSyncService</name>
|
||||
<files>src/Modules/Orders/OrdersController.php, src/Modules/Settings/ShopproPaymentStatusSyncService.php, src/Modules/Cron/CronHandlerFactory.php, routes/web.php</files>
|
||||
<action>
|
||||
1. W OrdersController:
|
||||
- Dodaj ?AutomationService $automation = null do konstruktora (po $shopproIntegrations)
|
||||
- W addPayment(), po zapisie platnosci i activity log, przed pushPaymentToShoppro:
|
||||
try { $this->automation?->trigger('payment.status_changed', $orderId, [
|
||||
'new_payment_status' => (string) $result['payment_status'],
|
||||
'total_paid' => $result['total_paid'],
|
||||
'payment_type_id' => $paymentTypeId,
|
||||
]); } catch (Throwable) {}
|
||||
|
||||
2. W routes/web.php:
|
||||
- Przekaz $automationService do OrdersController (juz istnieje w scope na linii 253)
|
||||
- Dodaj do wywolania new OrdersController(..., $automationService)
|
||||
|
||||
3. W ShopproPaymentStatusSyncService:
|
||||
- Dodaj ?AutomationService $automation = null do konstruktora (po $pdo)
|
||||
- W syncSingleOrderPayment(), po uaktualnieniu payment status (po commit, przed recordActivity):
|
||||
if ($existingPaymentStatus !== $newPaymentStatus) {
|
||||
try { $this->automation?->trigger('payment.status_changed', $orderId, [
|
||||
'new_payment_status' => (string) $newPaymentStatus,
|
||||
'old_payment_status' => $existingPaymentStatus !== null ? (string) $existingPaymentStatus : '',
|
||||
'total_paid' => $newTotalPaid,
|
||||
'payment_method' => $paymentMethod,
|
||||
]); } catch (Throwable) {}
|
||||
}
|
||||
|
||||
4. W CronHandlerFactory:
|
||||
- Przekaz $automationService do ShopproPaymentStatusSyncService
|
||||
- AutomationService jest juz tworzony w CronHandlerFactory (buildAutomationHandler) — uzyj go
|
||||
- UWAGA: ShopproPaymentStatusSyncHandler jest tworzony w innym miejscu niz automation — sprawdz czy $automationService jest dostepny i przekaz go
|
||||
</action>
|
||||
<verify>PHP lint — brak bledow skladniowych. Przejrzyj CronHandlerFactory ze szczegolna uwaga na kolejnosc tworzenia obiektow.</verify>
|
||||
<done>AC-3, AC-4 satisfied: event emitowany z obu zrodel</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: UI — event i warunek w formularzu automatyzacji</name>
|
||||
<files>resources/views/automation/form.php, public/assets/js/modules/automation-form.js, DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md</files>
|
||||
<action>
|
||||
1. W resources/views/automation/form.php:
|
||||
- Dodaj do $eventLabels: 'payment.status_changed' => 'Zmiana statusu platnosci'
|
||||
- W sekcji warunków (condition select): dodaj option value="payment_status">Status platnosci
|
||||
- W sekcji renderowania warunku (PHP): dodaj branch elseif ($conditionType === 'payment_status') z checkboxami:
|
||||
Statusy: ['0' => 'Nieoplacone', '1' => 'Czesciowo oplacone', '2' => 'Oplacone']
|
||||
Input name: conditions[idx][payment_status_keys][]
|
||||
- Dodaj paymentStatusOptions do window.AutomationFormData
|
||||
|
||||
2. W public/assets/js/modules/automation-form.js:
|
||||
- Dodaj function buildPaymentStatusCheckboxes(namePrefix) — analogicznie do buildShipmentStatusCheckboxes ale z paymentStatusOptions
|
||||
- W addCondition(): dodaj option value="payment_status">Status platnosci w select
|
||||
- W onConditionTypeChange(): dodaj branch if (select.value === 'payment_status') { configDiv.innerHTML = buildPaymentStatusCheckboxes(namePrefix); }
|
||||
|
||||
3. Zaktualizuj DOCS/ARCHITECTURE.md i DOCS/TECH_CHANGELOG.md.
|
||||
</action>
|
||||
<verify>Otworz formularz reguly — widoczne nowe zdarzenie i warunek. PHP lint.</verify>
|
||||
<done>AC-1, AC-2 satisfied: UI gotowe</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- database/migrations/* (brak zmian schematu — event_type to VARCHAR, nowy typ nie wymaga migracji)
|
||||
- src/Modules/Automation/AutomationRepository.php (warstwa danych bez zmian)
|
||||
- src/Modules/Automation/AutomationExecutionLogRepository.php
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Tylko event payment.status_changed i warunek payment_status
|
||||
- Nie dodajemy nowych akcji (istniejace: send_email, issue_receipt, update_shipment_status, update_order_status wystarczaja)
|
||||
- Nie modyfikujemy istniejacych eventow ani warunkow
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] PHP lint — brak bledow we wszystkich zmodyfikowanych plikach
|
||||
- [ ] Formularz reguly: widoczne zdarzenie "Zmiana statusu platnosci"
|
||||
- [ ] Formularz reguly: warunek "Status platnosci" z checkboxami
|
||||
- [ ] Dodanie platnosci emituje event
|
||||
- [ ] Sync platnosci z shopPRO emituje event
|
||||
- [ ] DOCS zaktualizowane
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Wszystkie taski ukonczone
|
||||
- Nowy event i warunek widoczne w UI
|
||||
- Event emitowany z obu zrodel (reczne + cron sync)
|
||||
- Warunek filtruje po statusie platnosci
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/57-payment-automation-event/57-01-SUMMARY.md`
|
||||
</output>
|
||||
60
.paul/phases/57-payment-automation-event/57-01-SUMMARY.md
Normal file
60
.paul/phases/57-payment-automation-event/57-01-SUMMARY.md
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
phase: 57-payment-automation-event
|
||||
plan: 01
|
||||
status: complete
|
||||
completed: 2026-03-30
|
||||
---
|
||||
|
||||
# Phase 57 Plan 01: Payment Automation Event Summary
|
||||
|
||||
**Event automatyzacji `payment.status_changed` z warunkiem `payment_status` — emisja z recznego dodania platnosci i cron sync shopPRO**
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Event payment.status_changed dostepny w formularzu | Pass | Opcja "Zmiana statusu platnosci" w dropdown zdarzenia |
|
||||
| AC-2: Warunek payment_status dostepny w formularzu | Pass | Checkboxy: Nieoplacone, Czesciowo oplacone, Oplacone |
|
||||
| AC-3: Event emitowany przy recznym dodaniu platnosci | Pass | trigger() w OrdersController::addPayment() po zapisie |
|
||||
| AC-4: Event emitowany przy sync platnosci z shopPRO | Pass | trigger() w ShopproPaymentStatusSyncService po zmianie statusu |
|
||||
| AC-5: Warunek filtruje poprawnie | Pass | evaluatePaymentStatusCondition porownuje new_payment_status z status_keys |
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| src/Modules/Automation/AutomationController.php | Modified | ALLOWED_EVENTS + payment.status_changed, ALLOWED_CONDITION_TYPES + payment_status, PAYMENT_STATUS_OPTIONS, parseConditions branch, paymentStatusOptions w widoku |
|
||||
| src/Modules/Automation/AutomationService.php | Modified | evaluatePaymentStatusCondition() + branch w evaluateSingleCondition |
|
||||
| src/Modules/Orders/OrdersController.php | Modified | AutomationService dep + trigger event w addPayment() |
|
||||
| src/Modules/Settings/ShopproPaymentStatusSyncService.php | Modified | AutomationService dep + trigger event w syncSingleOrderPayment() |
|
||||
| src/Modules/Cron/CronHandlerFactory.php | Modified | Przeniesienie tworzenia $automationService przed $shopproPaymentSyncService, przekazanie dep |
|
||||
| routes/web.php | Modified | $automationService przekazany do OrdersController |
|
||||
| resources/views/automation/form.php | Modified | Event label, warunek payment_status z checkboxami, paymentStatusOptions w JS data |
|
||||
| public/assets/js/modules/automation-form.js | Modified | buildPaymentStatusCheckboxes(), opcja w addCondition + onConditionTypeChange |
|
||||
| DOCS/ARCHITECTURE.md | Modified | Dokumentacja eventu i warunku |
|
||||
| DOCS/TECH_CHANGELOG.md | Modified | Wpis Phase 57 |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Warunek payment_status uzywa prostego porownania string (nie mapowanie jak shipment_status) | Statusy platnosci to proste wartosci 0/1/2, nie wymagaja mapowania wielopoziomowego | Prostsza implementacja, latwiejsze debugowanie |
|
||||
| $automationService przeniesiony wyzej w CronHandlerFactory | ShopproPaymentSyncService potrzebuje go jako dep — musi byc utworzony wczesniej | Zmiana kolejnosci tworzenia obiektow w factory |
|
||||
| Emisja eventu non-blocking (try/catch) | Blad automatyzacji nie powinien blokowac zapisu platnosci ani sync crona | Odpornosc na bledy regul |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None — plan executed exactly as written.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Event payment.status_changed dziala z obu zrodel (reczne + cron)
|
||||
- Warunek payment_status pozwala filtrowac po statusie
|
||||
- Wszystkie istniejace akcje (send_email, issue_receipt, update_order_status, update_shipment_status) dzialaja z nowym eventem
|
||||
|
||||
**Concerns:**
|
||||
- Brak — standardowy wzorzec automatyzacji
|
||||
|
||||
**Blockers:**
|
||||
- None
|
||||
122
.paul/phases/58-automation-form-preserve/58-01-PLAN.md
Normal file
122
.paul/phases/58-automation-form-preserve/58-01-PLAN.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
phase: 58-automation-form-preserve
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: ["57-01"]
|
||||
files_modified:
|
||||
- src/Modules/Automation/AutomationController.php
|
||||
- DOCS/TECH_CHANGELOG.md
|
||||
autonomous: true
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Zachowanie danych formularza automatyzacji po bledzie walidacji. Zamiast redirect (ktory traci dane) — re-render formularza z wypelnionymi polami i komunikatem bledu.
|
||||
|
||||
## Purpose
|
||||
Uzytkownik nie traci wypelnionych danych (nazwa, zdarzenie, warunki, akcje) po napotkaniu bledu walidacji.
|
||||
|
||||
## Output
|
||||
- Zmiana store() i update() w AutomationController — renderForm() z danymi z request zamiast redirect
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Source Files
|
||||
@src/Modules/Automation/AutomationController.php (store linia 106, update linia 133, renderForm linia 236)
|
||||
@resources/views/automation/form.php (formularz korzysta z $rule do wypelnienia pol)
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Store — dane zachowane po bledzie walidacji
|
||||
```gherkin
|
||||
Given wypelniam formularz nowej reguly automatyzacji
|
||||
When walidacja zwraca blad (np. brak warunkow)
|
||||
Then formularz wyswietla sie ponownie z wypelnionymi danymi
|
||||
And komunikat bledu jest widoczny
|
||||
And nie nastepuje redirect
|
||||
```
|
||||
|
||||
## AC-2: Update — dane zachowane po bledzie walidacji
|
||||
```gherkin
|
||||
Given edytuje istniejaca regule i zmieniam dane
|
||||
When walidacja zwraca blad
|
||||
Then formularz wyswietla sie ponownie z danymi z formularza (nie z DB)
|
||||
And komunikat bledu jest widoczny
|
||||
```
|
||||
|
||||
## AC-3: Zapis sukces — bez zmian w zachowaniu
|
||||
```gherkin
|
||||
Given wypelniam poprawnie formularz
|
||||
When zapisuje
|
||||
Then regula jest zapisana i nastepuje redirect do listy
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Re-render formularza zamiast redirect przy bledzie walidacji</name>
|
||||
<files>src/Modules/Automation/AutomationController.php</files>
|
||||
<action>
|
||||
1. Dodaj prywatna metode buildRuleFromRequest(Request $request, ?int $id = null): array
|
||||
- Zbuduj tablice $rule z POST data w formacie oczekiwanym przez form.php:
|
||||
- 'id' => $id
|
||||
- 'name' => $request->input('name')
|
||||
- 'event_type' => $request->input('event_type')
|
||||
- 'is_active' => $request->input('is_active') !== null ? 1 : 0
|
||||
- 'conditions' => zmapuj z extractConditions — format: [{condition_type, condition_value}]
|
||||
- 'actions' => zmapuj z extractActions — format: [{action_type, action_config}]
|
||||
- Warunki i akcje powinny byc w takim formacie, jaki form.php oczekuje od $rule
|
||||
|
||||
2. Zmien renderForm(?array $rule, string $errorMessage = ''): Response
|
||||
- Dodaj parametr $errorMessage
|
||||
- Uzyj $errorMessage jesli niepusty, inaczej Flash::get
|
||||
|
||||
3. W store():
|
||||
- Zamiast: Flash::set('error') + redirect('/create')
|
||||
- Zrob: return $this->renderForm($this->buildRuleFromRequest($request), $validationError)
|
||||
|
||||
4. W update():
|
||||
- Zamiast: Flash::set('error') + redirect('/edit?id=')
|
||||
- Zrob: return $this->renderForm($this->buildRuleFromRequest($request, $id), $validationError)
|
||||
|
||||
5. Upewnij sie ze try/catch bledy (blad zapisu DB) tez uzywaja renderForm z danymi request — nie redirect.
|
||||
</action>
|
||||
<verify>PHP lint. Sprawdz ze formularz z bledna walidacja renderuje sie z danymi.</verify>
|
||||
<done>AC-1, AC-2, AC-3 satisfied</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- resources/views/automation/form.php (widok juz obsluguje $rule — nie wymaga zmian)
|
||||
- public/assets/js/modules/automation-form.js
|
||||
- src/Modules/Automation/AutomationRepository.php
|
||||
- src/Modules/Automation/AutomationService.php
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Tylko formularz automatyzacji — nie naprawiamy innych formularzy w tej fazie
|
||||
- Nie zmieniamy struktury Flash class
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
- [ ] PHP lint bez bledow
|
||||
- [ ] store() z bledna walidacja — formularz zachowuje dane
|
||||
- [ ] update() z bledna walidacja — formularz zachowuje dane
|
||||
- [ ] store() z poprawnymi danymi — zapis + redirect (bez regresji)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Dane formularza zachowane po bledzie walidacji
|
||||
- Brak regresji w happy path (poprawny zapis)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/58-automation-form-preserve/58-01-SUMMARY.md`
|
||||
</output>
|
||||
21
.paul/phases/58-automation-form-preserve/58-01-SUMMARY.md
Normal file
21
.paul/phases/58-automation-form-preserve/58-01-SUMMARY.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
phase: 58-automation-form-preserve
|
||||
plan: 01
|
||||
status: complete
|
||||
completed: 2026-03-30
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Zachowanie danych formularza automatyzacji po bledzie walidacji — re-render z danymi z request zamiast redirect.
|
||||
|
||||
## What Was Built
|
||||
|
||||
- `AutomationController::buildRuleFromRequest()` — buduje $rule z POST data (conditions, actions, name, event_type, is_active)
|
||||
- `renderForm()` z parametrem $errorMessage — priorytet nad Flash
|
||||
- `store()` i `update()` — re-render zamiast redirect przy bledzie walidacji i DB error
|
||||
- form.php: $isEdit oparty na isset($rule['id']), conditions/actions wyciagane z $rule niezaleznie
|
||||
|
||||
## Deviations
|
||||
|
||||
- Zmiana w form.php (poza boundaries) — konieczna aby $isEdit poprawnie rozroznial nowy formularz od edycji gdy $rule jest ustawione z request data
|
||||
183
.paul/phases/59-order-status-automation-event/59-01-PLAN.md
Normal file
183
.paul/phases/59-order-status-automation-event/59-01-PLAN.md
Normal file
@@ -0,0 +1,183 @@
|
||||
---
|
||||
phase: 59-order-status-automation-event
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/Modules/Automation/AutomationController.php
|
||||
- src/Modules/Automation/AutomationService.php
|
||||
- src/Modules/Orders/OrdersController.php
|
||||
- resources/views/automation/form.php
|
||||
- resources/views/automation/index.php
|
||||
- public/assets/js/modules/automation-form.js
|
||||
autonomous: true
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Dodanie zdarzenia automatyzacji `order.status_changed` emitowanego przy zmianie statusu zamowienia. Warunek `order_status` pozwala filtrowac po konkretnym statusie docelowym.
|
||||
|
||||
## Purpose
|
||||
Uzytkownik moze tworzyc reguly automatyzacji reagujace na zmiane statusu zamowienia — np. wyslanie maila po zmianie na "wyslane" lub wystawienie paragonu po zmianie na "oplacone".
|
||||
|
||||
## Output
|
||||
- Event `order.status_changed` dostepny w formularzu automatyzacji
|
||||
- Warunek `order_status` z checkboxami aktywnych statusow
|
||||
- Emisja eventu z OrdersController (reczna zmiana) i AutomationService (chain z akcji update_order_status)
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
|
||||
## Prior Work
|
||||
@.paul/phases/57-payment-automation-event/57-01-SUMMARY.md — identyczny wzorzec (event + warunek + emisja)
|
||||
|
||||
## Source Files
|
||||
@src/Modules/Automation/AutomationController.php
|
||||
@src/Modules/Automation/AutomationService.php
|
||||
@src/Modules/Orders/OrdersController.php
|
||||
@resources/views/automation/form.php
|
||||
@resources/views/automation/index.php
|
||||
@public/assets/js/modules/automation-form.js
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Event order.status_changed dostepny w formularzu
|
||||
```gherkin
|
||||
Given uzytkownik otwiera formularz nowego zadania automatycznego
|
||||
When rozwija dropdown "Zdarzenie"
|
||||
Then widzi opcje "Zmiana statusu zamowienia"
|
||||
```
|
||||
|
||||
## AC-2: Warunek order_status z checkboxami statusow
|
||||
```gherkin
|
||||
Given uzytkownik wybrarl zdarzenie "Zmiana statusu zamowienia"
|
||||
When dodaje warunek "Status zamowienia"
|
||||
Then widzi checkboxy z aktywnymi statusami zamowien
|
||||
And moze zaznaczyc jeden lub wiecej statusow
|
||||
```
|
||||
|
||||
## AC-3: Emisja eventu przy recznej zmianie statusu
|
||||
```gherkin
|
||||
Given istnieje aktywna regula: event=order.status_changed, warunek=order_status IN [wyslane], akcja=send_email
|
||||
When uzytkownik zmienia status zamowienia na "wyslane" (ze strony szczegolw lub inline z listy)
|
||||
Then event order.status_changed jest emitowany z kontekstem (old_status, new_status)
|
||||
And regula jest dopasowana i akcja wykonana
|
||||
```
|
||||
|
||||
## AC-4: Chain emission z akcji update_order_status
|
||||
```gherkin
|
||||
Given regula A: event=payment.status_changed, akcja=update_order_status(wyslane)
|
||||
And regula B: event=order.status_changed, warunek=order_status IN [wyslane], akcja=send_email
|
||||
When platnosc sie zmienia i regula A zmienia status zamowienia
|
||||
Then regula B jest wyzwalana przez chain event order.status_changed
|
||||
```
|
||||
|
||||
## AC-5: Etykieta w widoku listy regul i historii
|
||||
```gherkin
|
||||
Given istnieja reguly z event_type=order.status_changed
|
||||
When uzytkownik otwiera strone /settings/automation
|
||||
Then w kolumnie "Zdarzenie" widzi "Zmiana statusu zamowienia" (nie surowa nazwe)
|
||||
And w zakladce Historia rowniez widzi czytelna etykiete
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Backend — event + warunek + emisja</name>
|
||||
<files>src/Modules/Automation/AutomationController.php, src/Modules/Automation/AutomationService.php, src/Modules/Orders/OrdersController.php</files>
|
||||
<action>
|
||||
**AutomationController.php:**
|
||||
- Dodaj `'order.status_changed'` do `ALLOWED_EVENTS` (linia 19)
|
||||
- Dodaj `'order_status'` do `ALLOWED_CONDITION_TYPES` (linia 20)
|
||||
- W `buildRuleFromRequest()` dodaj branch `elseif ($type === 'order_status')` analogiczny do `payment_status` — klucz `order_status_codes`, wartosc z `$cond['order_status_codes']`
|
||||
- W `parseConditionValue()` dodaj branch `if ($type === 'order_status')` — walidacja kodow statusow vs `$this->repository->listActiveOrderStatuses()` (analogicznie do payment_status ale kody z DB)
|
||||
|
||||
**AutomationService.php:**
|
||||
- Dodaj metode `evaluateOrderStatusCondition(array $value, array $context): bool` — porownuje `$context['new_status']` z `$value['order_status_codes']`
|
||||
- W `evaluateSingleCondition()` dodaj branch: `if ($type === 'order_status') return $this->evaluateOrderStatusCondition($value, $context);`
|
||||
- W `handleUpdateOrderStatus()` po udanej zmianie (`$updated === true`) dodaj `$this->emitEvent('order.status_changed', ...)` z kontekstem `old_status`, `new_status`, `automation_source`, `automation_rule` — analogicznie do `handleUpdateShipmentStatus()`
|
||||
- Potrzeba pobrac old_status przed zmiana: dodaj query lub uzyj `$this->orders->findDetails()` do pobrania aktualnego statusu PRZED wywolaniem `updateOrderStatus()`
|
||||
|
||||
**OrdersController.php:**
|
||||
- Po udanym `updateOrderStatus()` (linia 280, `$success === true`) dodaj emisje:
|
||||
```php
|
||||
try {
|
||||
$this->automation?->trigger('order.status_changed', $orderId, [
|
||||
'old_status' => $oldStatus,
|
||||
'new_status' => $newStatus,
|
||||
]);
|
||||
} catch (Throwable) {}
|
||||
```
|
||||
- Pobierz `$oldStatus` PRZED wywolaniem `updateOrderStatus()` — dodaj query do pobrania aktualnego `external_status_id`
|
||||
</action>
|
||||
<verify>Grep ALLOWED_EVENTS zawiera order.status_changed; grep evaluateOrderStatusCondition istnieje; grep trigger('order.status_changed' w OrdersController</verify>
|
||||
<done>AC-3, AC-4 satisfied: event emitowany z obu zrodel, warunek ewaluowany poprawnie</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Frontend — formularz + widok listy</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_changed' => 'Zmiana statusu zamowienia'`
|
||||
- W sekcji renderowania warunkow dodaj branch `elseif ($condType === 'order_status')` — renderuj checkboxy z `$orderStatusOptions` (juz przekazywane do widoku przez renderForm), analogicznie do `payment_status` ale z kodem statusu jako value i nazwa jako label
|
||||
|
||||
**index.php:**
|
||||
- Dodaj do `$eventLabels`: `'order.status_changed' => 'Zmiana statusu zamowienia'`
|
||||
|
||||
**automation-form.js:**
|
||||
- W `addConditionRow()` dodaj opcje `<option value="order_status">Status zamowienia</option>` do selecta typu warunku
|
||||
- Dodaj renderowanie checkboxow statusow zamowien gdy type === 'order_status' — uzyj danych z `window.__orderStatusOptions` (lub analogicznego mechanizmu jak payment/shipment status)
|
||||
- W form.php dodaj `<script>window.__orderStatusOptions = <?= json_encode($orderStatusOptions) ?>;</script>` jesli jeszcze nie istnieje (sprawdz czy juz jest dla akcji update_order_status)
|
||||
- W `onConditionTypeChange()` pokaz/ukryj panel checkboxow order_status
|
||||
</action>
|
||||
<verify>Otworz /settings/automation/create, sprawdz ze "Zmiana statusu zamowienia" jest w dropdown zdarzenia, ze warunek "Status zamowienia" renderuje checkboxy, i ze lista regul pokazuje czytelna etykiete</verify>
|
||||
<done>AC-1, AC-2, AC-5 satisfied: event w dropdown, warunek z checkboxami, etykiety czytelne</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- database/migrations/* (brak nowych tabel/kolumn)
|
||||
- src/Modules/Automation/AutomationRepository.php (bez zmian)
|
||||
- src/Modules/Orders/OrdersRepository.php (bez zmian — `updateOrderStatus()` juz istnieje)
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Tylko event `order.status_changed` i warunek `order_status`
|
||||
- Nie dodajemy nowych akcji
|
||||
- Import zamowien z Allegro/shopPRO NIE emituje tego eventu (import ustawia status poczatkowy, nie zmienia istniejacego)
|
||||
- Emisja tylko z recznej zmiany (OrdersController) i chain z akcji automatyzacji (AutomationService)
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] `order.status_changed` widoczny w dropdown zdarzenia w formularzu
|
||||
- [ ] Warunek `order_status` renderuje checkboxy z aktywnymi statusami
|
||||
- [ ] Reczna zmiana statusu zamowienia emituje event
|
||||
- [ ] Akcja `update_order_status` emituje chain event `order.status_changed`
|
||||
- [ ] Etykieta "Zmiana statusu zamowienia" wyswietlana w tabeli regul i historii
|
||||
- [ ] Zapis i edycja reguly z tym zdarzeniem/warunkiem dziala poprawnie
|
||||
- [ ] Brak petli: chain protection (depth + dedup) zapobiega nieskonczonej rekurencji
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Wszystkie taski wykonane
|
||||
- Wszystkie AC spelnione
|
||||
- Brak bledow PHP/JS
|
||||
- Formularz zachowuje dane warunku order_status po bledzie walidacji (wzorzec z Phase 58)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/59-order-status-automation-event/59-01-SUMMARY.md`
|
||||
</output>
|
||||
118
.paul/phases/59-order-status-automation-event/59-01-SUMMARY.md
Normal file
118
.paul/phases/59-order-status-automation-event/59-01-SUMMARY.md
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
phase: 59-order-status-automation-event
|
||||
plan: 01
|
||||
subsystem: automation
|
||||
tags: [automation, events, order-status, conditions]
|
||||
|
||||
requires:
|
||||
- phase: 57-payment-automation-event
|
||||
provides: payment.status_changed event pattern, condition evaluation pattern
|
||||
provides:
|
||||
- order.status_changed automation event
|
||||
- order_status condition type with checkbox UI
|
||||
- chain emission from update_order_status action
|
||||
affects: [future automation events, order workflow]
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [order status condition evaluation analogous to payment_status]
|
||||
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- src/Modules/Automation/AutomationController.php
|
||||
- src/Modules/Automation/AutomationService.php
|
||||
- src/Modules/Orders/OrdersController.php
|
||||
- resources/views/automation/form.php
|
||||
- resources/views/automation/index.php
|
||||
- public/assets/js/modules/automation-form.js
|
||||
|
||||
key-decisions:
|
||||
- "Emisja eventu tylko przy realnej zmianie statusu (old != new)"
|
||||
- "Chain emission z akcji update_order_status przez emitEvent() z depth protection"
|
||||
|
||||
patterns-established:
|
||||
- "order_status condition: order_status_codes array z lowercase kodami statusow"
|
||||
|
||||
duration: 15min
|
||||
started: 2026-03-30T23:45:00Z
|
||||
completed: 2026-03-31T00:00:00Z
|
||||
---
|
||||
|
||||
# Phase 59 Plan 01: Order Status Automation Event Summary
|
||||
|
||||
**Event automatyzacji `order.status_changed` z warunkiem `order_status` — emisja z recznej zmiany statusu i chain z akcji automatyzacji**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~15min |
|
||||
| Tasks | 2 completed |
|
||||
| Files modified | 6 |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Event order.status_changed dostepny w formularzu | Pass | Opcja "Zmiana statusu zamowienia" w dropdown zdarzenia |
|
||||
| AC-2: Warunek order_status z checkboxami statusow | Pass | Checkboxy z aktywnymi statusami zamowien (PHP + JS) |
|
||||
| AC-3: Emisja eventu przy recznej zmianie statusu | Pass | OrdersController emituje po udanym updateOrderStatus() |
|
||||
| AC-4: Chain emission z akcji update_order_status | Pass | AutomationService emituje przez emitEvent() z depth protection |
|
||||
| AC-5: Etykieta w widoku listy regul i historii | Pass | index.php zawiera etykiety payment.status_changed + order.status_changed |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Event `order.status_changed` dostepny w silniku automatyzacji z pelnym UI (dropdown zdarzenia, checkboxy warunku)
|
||||
- Emisja z dwoch zrodel: reczna zmiana statusu (OrdersController) i chain z akcji automatyzacji (AutomationService)
|
||||
- Naprawiono brakujaca etykiete `payment.status_changed` w index.php (bug z Phase 57)
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `src/Modules/Automation/AutomationController.php` | Modified | ALLOWED_EVENTS + order.status_changed, ALLOWED_CONDITION_TYPES + order_status, parseConditionValue + order_status, buildRuleFromRequest + order_status |
|
||||
| `src/Modules/Automation/AutomationService.php` | Modified | evaluateOrderStatusCondition(), chain emit z handleUpdateOrderStatus(), dodano $context param |
|
||||
| `src/Modules/Orders/OrdersController.php` | Modified | Pobranie oldStatus przed zmiana, emisja order.status_changed po sukcesie |
|
||||
| `resources/views/automation/form.php` | Modified | Etykieta zdarzenia, opcja warunku order_status, checkboxy statusow zamowien |
|
||||
| `resources/views/automation/index.php` | Modified | Etykiety payment.status_changed + order.status_changed |
|
||||
| `public/assets/js/modules/automation-form.js` | Modified | buildOrderStatusCheckboxes(), opcja order_status w addCondition, onConditionTypeChange |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Emisja tylko przy old_status != new_status | Brak falszywych triggerow przy ustawieniu tego samego statusu | Spojnosc z wzorcem shipment.status_changed |
|
||||
| handleUpdateOrderStatus otrzymuje $context dla chain | Potrzebny do emitEvent() z depth/dedup protection | Zapobiega petlom automatyzacji |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. Bugfix: brakujaca etykieta payment.status_changed w index.php**
|
||||
- **Found during:** Poczatek sesji (zgloszenie uzytkownika)
|
||||
- **Issue:** Phase 57 nie dodala etykiety do index.php — wyswietlala sie surowa nazwa
|
||||
- **Fix:** Dodano wpis do $eventLabels w index.php
|
||||
- **Files:** resources/views/automation/index.php
|
||||
|
||||
**Total impact:** Jeden bugfix z Phase 57 naprawiony przy okazji
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Pelny zestaw eventow automatyzacji: receipt.created, shipment.created, shipment.status_changed, payment.status_changed, order.status_changed
|
||||
- Wszystkie warianty warunkow: integration, shipment_status, payment_status, order_status
|
||||
|
||||
**Concerns:**
|
||||
- None
|
||||
|
||||
**Blockers:**
|
||||
- None
|
||||
|
||||
---
|
||||
*Phase: 59-order-status-automation-event, Plan: 01*
|
||||
*Completed: 2026-03-31*
|
||||
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