refactor(automation): rozbij AutomationController na slim + 3 wspolpracownikow
AutomationController.php 677 -> 221 lin. (67% redukcji). Wydzielono: - AutomationRequestParser (422) - stale ALLOWED_*/PAYMENT_*, walidacja, ekstrakcja conditions/actions/ruleData, buildRule. - AutomationFormViewModel (80) - przygotowanie zmiennych template'a automation/form (przejal ReceiptConfigRepository). - AutomationHistoryFilters (58) - filtry historii + aktywna zakladka. Szablon destroy/duplicate/toggleStatus zlozony do runIdAction(). Zero zmian kontraktu HTTP/widokow/DB. Plan: .paul/plans/20260520-1400-refactor-automation-controller/ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,13 +4,18 @@
|
|||||||
**Ostatnia aktualizacja:** 2026-05-20
|
**Ostatnia aktualizacja:** 2026-05-20
|
||||||
|
|
||||||
## Aktywna praca
|
## Aktywna praca
|
||||||
UNIFY zakonczony dla `.paul/plans/20260520-1200-fix-login-page-and-remember-me/`. Petla zamknieta. Strona `/login`: usuniety subtitle, checkbox "Zapamiętaj mnie (30 dni)" w jednej linii (selektor `.form-field.remember-field`), `AuthController::showLogin` woluje `loginFromRememberToken()` -> uzytkownik z waznym cookie (30 dni) wraca automatycznie na `/settings/users` po wygasnieciu sesji. SUMMARY: `.paul/plans/20260520-1200-fix-login-page-and-remember-me/SUMMARY.md`.
|
UNIFY zakonczony dla `.paul/plans/20260520-1400-refactor-automation-controller/`. Petla zamknieta. `AutomationController.php` 677 → 221 lin. (67% redukcji). Wydzielono `AutomationRequestParser` (422), `AutomationFormViewModel` (80), `AutomationHistoryFilters` (58). `AutomationModule` zaktualizowany (3 nowe wpisy DI; `ReceiptConfigRepository` przeniesiony z controllera do view modelu). Powtarzajacy sie szablon `destroy/duplicate/toggleStatus` zlozony do `runIdAction()`. Zero zmian kontraktu HTTP/widokow/DB. `php -l` czysty dla 5/5 plikow. SUMMARY: `.paul/plans/20260520-1400-refactor-automation-controller/SUMMARY.md`.
|
||||||
|
|
||||||
```
|
```
|
||||||
PLAN ──▶ APPLY ──▶ UNIFY
|
PLAN ──▶ APPLY ──▶ UNIFY
|
||||||
✓ ✓ ✓ [Loop complete — gotowe na nastepny PLAN]
|
✓ ✓ ✓ [Loop complete — gotowe na nastepny PLAN]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
⚠ **Do weryfikacji recznej (vendor/phpunit niedostepny w sesji):** smoke-test `/settings/automation` (lista + zakladka history z filtrami), `/settings/automation/create`, `/settings/automation/edit?id=N` (wszystkie typy conditions/actions).
|
||||||
|
|
||||||
|
## Poprzednia praca
|
||||||
|
UNIFY zakonczony dla `.paul/plans/20260520-1200-fix-login-page-and-remember-me/`. Strona `/login`: usuniety subtitle, checkbox "Zapamiętaj mnie (30 dni)" w jednej linii, `AuthController::showLogin` woluje `loginFromRememberToken()` -> auto-login z cookie (30 dni). SUMMARY: `.paul/plans/20260520-1200-fix-login-page-and-remember-me/SUMMARY.md`.
|
||||||
|
|
||||||
Rezultat: `DeliveryStatus.php` 657 -> 170 lin. (fasada zachowujaca pelny kontrakt). Wydzielono `DeliveryStatusProviderMap` (~330), `AllegroDescriptionGuesser` (~150), `DeliveryTrackingUrlBuilder` (~85). Zero zmian w 20 plikach konsumentow; `tests/Unit/DeliveryStatusTest.php` 4/4 bez modyfikacji (primary gate). Pelny suite 3/15 identyczny z baseline (pre-existing). SUMMARY: `.paul/plans/20260519-1730-refactor-delivery-status/SUMMARY.md`.
|
Rezultat: `DeliveryStatus.php` 657 -> 170 lin. (fasada zachowujaca pelny kontrakt). Wydzielono `DeliveryStatusProviderMap` (~330), `AllegroDescriptionGuesser` (~150), `DeliveryTrackingUrlBuilder` (~85). Zero zmian w 20 plikach konsumentow; `tests/Unit/DeliveryStatusTest.php` 4/4 bez modyfikacji (primary gate). Pelny suite 3/15 identyczny z baseline (pre-existing). SUMMARY: `.paul/plans/20260519-1730-refactor-delivery-status/SUMMARY.md`.
|
||||||
|
|
||||||
## Poprzednia praca
|
## Poprzednia praca
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ Korzysci wzgledem poprzedniego monolitycznego `routes/web.php` (859 lin.):
|
|||||||
| Email | `src/Modules/Email/` | SMTP, wysylka maili |
|
| Email | `src/Modules/Email/` | SMTP, wysylka maili |
|
||||||
| Sms | `src/Modules/Sms/` | wysylka SMS, webhook SmsPlanet, konwersacje |
|
| Sms | `src/Modules/Sms/` | wysylka SMS, webhook SmsPlanet, konwersacje |
|
||||||
| Notifications | `src/Modules/Notifications/` | powiadomienia w aplikacji + API |
|
| Notifications | `src/Modules/Notifications/` | powiadomienia w aplikacji + API |
|
||||||
| Automation | `src/Modules/Automation/` | reguly automatyzacji (status-aged, email-once) |
|
| Automation | `src/Modules/Automation/` | reguly automatyzacji (status-aged, email-once); slim `AutomationController` + `AutomationRequestParser` (parsing/walidacja conditions+actions + katalog stalych) + `AutomationFormViewModel` (zmienne template'a) + `AutomationHistoryFilters` (filtry historii + aktywna zakladka) + `AutomationService` (silnik wykonawczy). |
|
||||||
| Settings | `src/Modules/Settings/` | firmy, integracje, mapowania statusow, szablony |
|
| Settings | `src/Modules/Settings/` | firmy, integracje, mapowania statusow, szablony |
|
||||||
| Cron | `src/Modules/Cron/` | handlery + scheduler |
|
| Cron | `src/Modules/Cron/` | handlery + scheduler |
|
||||||
| Info | `src/Modules/Info/` | strona informacyjna |
|
| Info | `src/Modules/Info/` | strona informacyjna |
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Konwencja (`CLAUDE.md`): funkcja/klasa zwykle do 30-50 linii, max 3 poziomy zagn
|
|||||||
| `src/Modules/Automation/AutomationService.php` | 818 | silnik regul — kandydat na pipeline-strategy. |
|
| `src/Modules/Automation/AutomationService.php` | 818 | silnik regul — kandydat na pipeline-strategy. |
|
||||||
| `src/Modules/Shipments/PolkurierShipmentService.php` | 776 | |
|
| `src/Modules/Shipments/PolkurierShipmentService.php` | 776 | |
|
||||||
| `src/Modules/Accounting/InvoiceService.php` | 762 | |
|
| `src/Modules/Accounting/InvoiceService.php` | 762 | |
|
||||||
| `src/Modules/Automation/AutomationController.php` | 677 | |
|
| ~~`src/Modules/Automation/AutomationController.php`~~ | ~~677~~ -> 221 | ✅ Zrefaktorowane 2026-05-20 — wydzielono `AutomationRequestParser` (422), `AutomationFormViewModel` (80), `AutomationHistoryFilters` (58); szablon `destroy/duplicate/toggleStatus` zlozony do `runIdAction()`. Zero zmian kontraktu HTTP/widokow/DB. Patrz `.paul/plans/20260520-1400-refactor-automation-controller/SUMMARY.md`. |
|
||||||
| ~~`src/Modules/Shipments/DeliveryStatus.php`~~ | ~~657~~ -> 170 | ✅ Zrefaktorowane 2026-05-19 — fasada zachowujaca pelny kontrakt + wydzielono `DeliveryStatusProviderMap` (~330), `AllegroDescriptionGuesser` (~150), `DeliveryTrackingUrlBuilder` (~85). Zero zmian w 20 plikach konsumentow. Patrz `.paul/plans/20260519-1730-refactor-delivery-status/SUMMARY.md`. |
|
| ~~`src/Modules/Shipments/DeliveryStatus.php`~~ | ~~657~~ -> 170 | ✅ Zrefaktorowane 2026-05-19 — fasada zachowujaca pelny kontrakt + wydzielono `DeliveryStatusProviderMap` (~330), `AllegroDescriptionGuesser` (~150), `DeliveryTrackingUrlBuilder` (~85). Zero zmian w 20 plikach konsumentow. Patrz `.paul/plans/20260519-1730-refactor-delivery-status/SUMMARY.md`. |
|
||||||
| ~~`src/Modules/Settings/AllegroIntegrationController.php`~~ | ~~653~~ -> 223 | ✅ Zrefaktorowane 2026-05-19 — wydzielono `AllegroIntegrationViewModel` (61), `AllegroOAuthFlowService` (94), `AllegroImportScheduleService` (179), `AllegroImportImageWarningFormatter` (69), `AllegroSaveSettingsValidator` (48). Patrz `.paul/plans/20260519-1600-refactor-allegro-integration-controller/SUMMARY.md`. |
|
| ~~`src/Modules/Settings/AllegroIntegrationController.php`~~ | ~~653~~ -> 223 | ✅ Zrefaktorowane 2026-05-19 — wydzielono `AllegroIntegrationViewModel` (61), `AllegroOAuthFlowService` (94), `AllegroImportScheduleService` (179), `AllegroImportImageWarningFormatter` (69), `AllegroSaveSettingsValidator` (48). Patrz `.paul/plans/20260519-1600-refactor-allegro-integration-controller/SUMMARY.md`. |
|
||||||
| ~~`src/Modules/Statistics/OrdersStatisticsController.php`~~ | ~~640~~ -> 110 | ✅ Zrefaktorowane 2026-05-19 — wydzielono `OrdersStatisticsFilters` (258), `OrdersStatisticsTableBuilder` (101), `OrdersStatisticsSummaryBuilder` (195). Patrz `.paul/plans/20260519-1430-refactor-orders-statistics-controller/SUMMARY.md`. |
|
| ~~`src/Modules/Statistics/OrdersStatisticsController.php`~~ | ~~640~~ -> 110 | ✅ Zrefaktorowane 2026-05-19 — wydzielono `OrdersStatisticsFilters` (258), `OrdersStatisticsTableBuilder` (101), `OrdersStatisticsSummaryBuilder` (195). Patrz `.paul/plans/20260519-1430-refactor-orders-statistics-controller/SUMMARY.md`. |
|
||||||
|
|||||||
@@ -2,6 +2,29 @@
|
|||||||
|
|
||||||
Chronologiczny log zmian technicznych (co i dlaczego). Najnowsze na gorze.
|
Chronologiczny log zmian technicznych (co i dlaczego). Najnowsze na gorze.
|
||||||
|
|
||||||
|
## 2026-05-20 — Dekompozycja AutomationController (slim controller + 3 wspolpracownikow)
|
||||||
|
|
||||||
|
### Co
|
||||||
|
- `src/Modules/Automation/AutomationController.php`: 677 -> 221 lin. (67% redukcji).
|
||||||
|
- Wydzielono trzy klasy w namespace `App\Modules\Automation`:
|
||||||
|
- `AutomationRequestParser` (422 lin.) — wszystkie stale ALLOWED_* + PAYMENT_*, metody `validate`, `extractRuleData`, `extractConditions`, `extractActions`, `buildRule`, `parseConditionValue`, `parseActionConfig` oraz gettery katalogow do widoku.
|
||||||
|
- `AutomationFormViewModel` (80 lin.) — przygotowanie zmiennych template'a `automation/form`: `buildShipmentStatusOptions`, `listActiveReceiptConfigs`, kompozycja wszystkich opcji.
|
||||||
|
- `AutomationHistoryFilters` (58 lin.) — `extract`, `resolveActiveTab`, `hasAny`.
|
||||||
|
- `AutomationModule.php`: zarejestrowane `automation.request_parser`, `automation.form_view_model`, `automation.history_filters`. Konstruktor controllera rozszerzony o 3 zaleznosci, usunieto `ReceiptConfigRepository` (przeniesiony do view modelu).
|
||||||
|
- Powtarzajacy sie szablon `destroy`/`duplicate`/`toggleStatus` zlozony do `runIdAction(Request, callable, ok, err)`.
|
||||||
|
|
||||||
|
### Dlaczego
|
||||||
|
- `AutomationController` byl na liscie kandydatow do dekompozycji w `.paul/codebase/quality_risks.md` (677 lin.).
|
||||||
|
- Mieszal piec odpowiedzialnosci: routing HTTP, parsowanie/walidacja warunkow i akcji, view-model formularza, filtry historii, katalog stalych.
|
||||||
|
- Wzorzec analogiczny do udanych refaktorow Allegro (653 → 223), Statistics (640 → 110), DeliveryStatus (657 → 170).
|
||||||
|
|
||||||
|
### Wplyw
|
||||||
|
- Zero zmian kontraktu HTTP — sciezki `/settings/automation/*` niezmienione.
|
||||||
|
- Zero zmian w widokach `automation/index.php` i `automation/form.php` — klucze template'a 1:1.
|
||||||
|
- Zero zmian schematu DB.
|
||||||
|
- `tests/Unit/AutomationServiceTest.php` nie modyfikowany (nie dotyka controllera).
|
||||||
|
- Plan: `.paul/plans/20260520-1400-refactor-automation-controller/PLAN.md`.
|
||||||
|
|
||||||
## 2026-05-20 — Strona logowania: usuniecie subtitle, inline "Zapamietaj mnie (30 dni)", auto-login z cookie
|
## 2026-05-20 — Strona logowania: usuniecie subtitle, inline "Zapamietaj mnie (30 dni)", auto-login z cookie
|
||||||
|
|
||||||
### Co
|
### Co
|
||||||
|
|||||||
307
.paul/plans/20260520-1400-refactor-automation-controller/PLAN.md
Normal file
307
.paul/plans/20260520-1400-refactor-automation-controller/PLAN.md
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
---
|
||||||
|
plan_id: 20260520-1400-refactor-automation-controller
|
||||||
|
title: Refaktoring AutomationController — dekompozycja na slim controller + 3 wspolpracownikow
|
||||||
|
storage: plan-first
|
||||||
|
legacy_phase: null
|
||||||
|
created: 2026-05-20T14:00:00+02:00
|
||||||
|
status: planned
|
||||||
|
type: execute
|
||||||
|
autonomous: true
|
||||||
|
delegation: auto
|
||||||
|
files_modified:
|
||||||
|
- src/Modules/Automation/AutomationController.php
|
||||||
|
- src/Modules/Automation/AutomationModule.php
|
||||||
|
- src/Modules/Automation/AutomationRequestParser.php
|
||||||
|
- src/Modules/Automation/AutomationFormViewModel.php
|
||||||
|
- src/Modules/Automation/AutomationHistoryFilters.php
|
||||||
|
- .paul/codebase/architecture.md
|
||||||
|
- .paul/codebase/quality_risks.md
|
||||||
|
- .paul/codebase/tech_changelog.md
|
||||||
|
quality_radar: ok
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Cel
|
||||||
|
Zredukowac `src/Modules/Automation/AutomationController.php` z 677 lin. do < 230 lin. przez wydzielenie wspolpracownikow zgodnie z SRP — bez zmiany publicznego kontraktu HTTP, bez zmian w widokach `resources/views/automation/`, bez zmian schematu DB.
|
||||||
|
|
||||||
|
## Po co
|
||||||
|
- `AutomationController` jest na liscie kandydatow do dekompozycji w `.paul/codebase/quality_risks.md` (677 lin., > 500 lin. progu).
|
||||||
|
- Obecnie miesza piec odpowiedzialnosci: routing HTTP, parsowanie/walidacja conditions+actions, view-model formularza, filtry historii, katalog stalych.
|
||||||
|
- Wzorzec dekompozycji udowodniony na `AllegroIntegrationController` (653 -> 223, 66% redukcji) i `OrdersStatisticsController` (640 -> 110, 83%).
|
||||||
|
|
||||||
|
## Co powstanie
|
||||||
|
- `AutomationRequestParser` (~280 lin.) — wszystkie metody odczytu/walidacji wejscia + stale ALLOWED_* katalogu warunkow/akcji.
|
||||||
|
- `AutomationFormViewModel` (~70 lin.) — przygotowanie danych do widoku `automation/form` (opcje statusow, lista aktywnych konfiguracji paragonow).
|
||||||
|
- `AutomationHistoryFilters` (~60 lin.) — odczyt filtrow historii + rozwiazywanie aktywnej zakladki.
|
||||||
|
- Slim `AutomationController` (< 230 lin.) — wylacznie akcje HTTP i delegacja.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## Dokumenty projektu
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
@.paul/codebase/architecture.md
|
||||||
|
@.paul/codebase/impact_map.md
|
||||||
|
@.paul/codebase/quality_risks.md
|
||||||
|
|
||||||
|
## Pliki zrodlowe
|
||||||
|
@src/Modules/Automation/AutomationController.php
|
||||||
|
@src/Modules/Automation/AutomationModule.php
|
||||||
|
@src/Modules/Automation/AutomationRepository.php
|
||||||
|
@src/Modules/Automation/AutomationExecutionLogRepository.php
|
||||||
|
@src/Modules/Settings/ReceiptConfigRepository.php
|
||||||
|
@src/Modules/Shipments/DeliveryStatus.php
|
||||||
|
@resources/views/automation/index.php
|
||||||
|
@resources/views/automation/form.php
|
||||||
|
|
||||||
|
## Referencje (analogiczne refaktory)
|
||||||
|
@.paul/plans/20260519-1600-refactor-allegro-integration-controller/SUMMARY.md
|
||||||
|
@.paul/plans/20260519-1430-refactor-orders-statistics-controller/SUMMARY.md
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<clarifications>
|
||||||
|
- Brak wymaganych. Refactor czysto wewnetrzny — kontrakt HTTP (routes + payload formularzy) niezmieniony.
|
||||||
|
- Stale `ALLOWED_EVENTS`, `ALLOWED_CONDITION_TYPES`, `ALLOWED_ACTION_TYPES`, `ALLOWED_RECIPIENTS`, `ALLOWED_RECEIPT_*`, `PAYMENT_*_OPTIONS` przeniesione do `AutomationRequestParser` jako jedyne miejsce zrodla prawdy. Controller czyta katalogi przez gettery (`parser->allowedEvents()` itp.) tylko dla widoku formularza.
|
||||||
|
</clarifications>
|
||||||
|
|
||||||
|
<impact_scan>
|
||||||
|
## Quality Radar
|
||||||
|
|
||||||
|
**Status:** ok
|
||||||
|
**Tryb:** plan (lekki skan na bazie `.paul/codebase/quality_risks.md` + `impact_map.md` — codebase-memory-mcp dostepny; jscpd/ast-grep wylaczone polityka)
|
||||||
|
|
||||||
|
## Obszary objete zmiana
|
||||||
|
|
||||||
|
- Modul `Automation` — kontroler + provider DI (`AutomationModule`).
|
||||||
|
- Brak wplywu na `AutomationService` (silnik wykonawczy regul) — to osobny refactor zaplanowany w `quality_risks.md` (818 lin.).
|
||||||
|
- Brak zmian w widokach `resources/views/automation/index.php` i `automation/form.php` — kontrakt zmiennych template'a zachowany 1:1.
|
||||||
|
|
||||||
|
## Powiazania zewnetrzne (bez zmian)
|
||||||
|
- `routes/web.php` przez `AutomationModule::routes()` — sygnatura niezmieniona, lazy resolve `automation.controller`.
|
||||||
|
- `AutomationRepository` / `AutomationExecutionLogRepository` / `ReceiptConfigRepository` — wywolania niezmienione.
|
||||||
|
- `DeliveryStatus::getAllStatuses()` / `::getAllOptions()` — uzywane przez nowy parser/viewmodel bez zmiany API.
|
||||||
|
|
||||||
|
## Ryzyka duplikatow / hardkody
|
||||||
|
|
||||||
|
- Stale ALLOWED_* — przed refaktorem zdublowane w kilku miejscach (controller + service?). Zwerifikuje w Task 1 czy `AutomationService` nie powtarza tych list; jezeli tak — zostaje deferral do osobnego planu, controller deleguje do parsera, service nie tykany.
|
||||||
|
|
||||||
|
## Jawne odroczenia
|
||||||
|
|
||||||
|
- Refaktor `AutomationService.php` (818 lin.) — odrebny plan z `quality_risks.md`, poza zakresem.
|
||||||
|
- Wspolny `BaseIntegrationController` / trait — odrebne ryzyko, poza zakresem.
|
||||||
|
</impact_scan>
|
||||||
|
|
||||||
|
<skills>
|
||||||
|
Brak SPECIAL-FLOWS.md — sekcja niewymagana.
|
||||||
|
</skills>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: AutomationController ma < 230 linii i tylko akcje HTTP
|
||||||
|
```gherkin
|
||||||
|
Given plik src/Modules/Automation/AutomationController.php po refaktorze
|
||||||
|
When zliczam linie (`wc -l`)
|
||||||
|
Then wynik jest mniejszy niz 230
|
||||||
|
And klasa zawiera wylacznie publiczne akcje (index, create, edit, store, update, destroy, duplicate, toggleStatus) plus prywatne `renderForm` i `validateCsrf`
|
||||||
|
And metody parseConditionValue / parseActionConfig / extractConditions / extractActions / extractRuleData / buildRuleFromRequest / validateInput / buildShipmentStatusOptions / listActiveReceiptConfigs / extractHistoryFilters / resolveActiveTab / hasHistoryFilters nie istnieja juz w controllerze
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: AutomationRequestParser przejmuje wejscie i katalogi
|
||||||
|
```gherkin
|
||||||
|
Given AutomationController::store, ::update otrzymuja Request
|
||||||
|
When kontroler woluje parser->parse(Request) / parser->validate(Request)
|
||||||
|
Then parser zwraca te same struktury co wczesniejsze extractRuleData/extractConditions/extractActions
|
||||||
|
And stale ALLOWED_EVENTS, ALLOWED_CONDITION_TYPES, ALLOWED_ACTION_TYPES, ALLOWED_RECIPIENTS, ALLOWED_RECEIPT_ISSUE_DATE_MODES, ALLOWED_RECEIPT_DUPLICATE_POLICIES, PAYMENT_STATUS_OPTIONS, PAYMENT_METHOD_OPTIONS sa wylacznie w AutomationRequestParser
|
||||||
|
And controller eksponuje je do widoku poprzez gettery parsera (np. parser->allowedEvents())
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: AutomationFormViewModel przygotowuje dane formularza
|
||||||
|
```gherkin
|
||||||
|
Given AutomationController::renderForm(rule, errorMessage)
|
||||||
|
When viewModel->build(rule, errorMessage) jest wywolany
|
||||||
|
Then zwracana tablica zmiennych dla template'a 'automation/form' ma identyczne klucze i identyczne wartosci jak przed refaktorem (manualne porownanie sygnatur view'a)
|
||||||
|
And buildShipmentStatusOptions oraz listActiveReceiptConfigs zyja wylacznie w viewModelu
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-4: AutomationHistoryFilters obsluguje liste historii
|
||||||
|
```gherkin
|
||||||
|
Given AutomationController::index(Request)
|
||||||
|
When historyFilters->extract(Request), ->resolveActiveTab(Request, filters), ->hasAny(filters) sa wywolane
|
||||||
|
Then zwracane wartosci sa identyczne jak przed refaktorem
|
||||||
|
And extractHistoryFilters / resolveActiveTab / hasHistoryFilters nie istnieja juz w controllerze
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-5: Zero regresji testowej i lintera
|
||||||
|
```gherkin
|
||||||
|
Given pelny suite phpunit `vendor/bin/phpunit`
|
||||||
|
When uruchamiam testy
|
||||||
|
Then liczba przechodzacych testow jest >= baseline (12/15 zielone — 3/15 to pre-existing failures spoza modulu Automation)
|
||||||
|
And `tests/Unit/AutomationServiceTest.php` jest zielony bez modyfikacji
|
||||||
|
And `php -l` na wszystkich zmienionych/utworzonych plikach zwraca No syntax errors
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-6: Module DI dziala bez zmian publicznego kontraktu
|
||||||
|
```gherkin
|
||||||
|
Given AutomationModule::register(ServiceRegistry, Application)
|
||||||
|
When kontener buduje 'automation.controller'
|
||||||
|
Then konstruktor controllera otrzymuje zaleznosci: Template, Translator, AuthService, AutomationRepository, AutomationExecutionLogRepository, ReceiptConfigRepository, AutomationRequestParser, AutomationFormViewModel, AutomationHistoryFilters
|
||||||
|
And klucz 'automation.controller' i routy /settings/automation/* niezmienione
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Wydzielenie AutomationRequestParser</name>
|
||||||
|
<files>
|
||||||
|
src/Modules/Automation/AutomationRequestParser.php (nowy),
|
||||||
|
src/Modules/Automation/AutomationController.php
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
1. Utworz `AutomationRequestParser` (final, namespace `App\Modules\Automation`).
|
||||||
|
2. Przenies do niego stale: ALLOWED_EVENTS, ALLOWED_CONDITION_TYPES, ALLOWED_ACTION_TYPES, ALLOWED_RECIPIENTS, ALLOWED_RECEIPT_ISSUE_DATE_MODES, ALLOWED_RECEIPT_DUPLICATE_POLICIES, PAYMENT_STATUS_OPTIONS, PAYMENT_METHOD_OPTIONS.
|
||||||
|
3. Przenies metody: buildRuleFromRequest, validateInput, extractRuleData, extractConditions, parseConditionValue, extractActions, parseActionConfig.
|
||||||
|
4. Konstruktor parsera: `private readonly AutomationRepository $repository` (potrzebny do listActiveOrderStatuses w parseConditionValue / parseActionConfig).
|
||||||
|
5. Publiczne metody parsera:
|
||||||
|
- `validate(Request): ?string`
|
||||||
|
- `extractRuleData(Request): array`
|
||||||
|
- `extractConditions(Request): array`
|
||||||
|
- `extractActions(Request): array`
|
||||||
|
- `buildRule(Request, ?int $id = null): array`
|
||||||
|
- gettery katalogow: `allowedEvents(): array`, `allowedConditionTypes(): array`, `allowedActionTypes(): array`, `allowedRecipients(): array`, `allowedReceiptIssueDateModes(): array`, `allowedReceiptDuplicatePolicies(): array`, `paymentStatusOptions(): array`, `paymentMethodOptions(): array`.
|
||||||
|
6. W kontrolerze usun przeniesione metody i stale. Akcje store/update/buildRuleFromRequest wywoluja parser.
|
||||||
|
7. NIE modyfikuj na razie `AutomationModule.php` — to zrobimy w Task 4.
|
||||||
|
</action>
|
||||||
|
<verify>`php -l src/Modules/Automation/AutomationRequestParser.php` zwraca "No syntax errors"</verify>
|
||||||
|
<done>AC-2 (czesc parsera) + AC-1 (czesc metod usuniete z controllera)</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Wydzielenie AutomationFormViewModel</name>
|
||||||
|
<files>
|
||||||
|
src/Modules/Automation/AutomationFormViewModel.php (nowy),
|
||||||
|
src/Modules/Automation/AutomationController.php
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
1. Utworz `AutomationFormViewModel` (final, namespace `App\Modules\Automation`).
|
||||||
|
2. Konstruktor: `private readonly AutomationRepository $repository, private readonly ReceiptConfigRepository $receiptConfigs, private readonly AutomationRequestParser $parser`.
|
||||||
|
3. Przenies prywatne `buildShipmentStatusOptions` i `listActiveReceiptConfigs` (z controllera).
|
||||||
|
4. Dodaj publiczne `build(?array $rule, string $errorMessage): array` zwracajace ten sam zestaw kluczy co dotychczasowy `renderForm` (poza `title`, `activeMenu`, `activeSettings`, `user`, `csrfToken` — te zostaja w controllerze, bo wymagaja AuthService/Csrf).
|
||||||
|
5. Konkretnie viewModel zwraca: integrations, emailTemplates, eventTypes, conditionTypes, actionTypes, recipientOptions, receiptConfigs, receiptIssueDateModes, receiptDuplicatePolicies, shipmentStatusOptions, paymentStatusOptions, paymentMethodOptions, orderStatusOptions, rule (przekazane), errorMessage (z fallbackiem na Flash).
|
||||||
|
6. W controller::renderForm: zlozenie `array_merge($viewModel->build($rule, $errorMessage), [HTTP-only keys])` i render template'a.
|
||||||
|
7. Usun z controllera `buildShipmentStatusOptions` i `listActiveReceiptConfigs`.
|
||||||
|
</action>
|
||||||
|
<verify>`php -l src/Modules/Automation/AutomationFormViewModel.php` zwraca "No syntax errors"</verify>
|
||||||
|
<done>AC-3</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Wydzielenie AutomationHistoryFilters</name>
|
||||||
|
<files>
|
||||||
|
src/Modules/Automation/AutomationHistoryFilters.php (nowy),
|
||||||
|
src/Modules/Automation/AutomationController.php
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
1. Utworz `AutomationHistoryFilters` (final, namespace `App\Modules\Automation`).
|
||||||
|
2. Brak zaleznosci — pure logic.
|
||||||
|
3. Publiczne metody:
|
||||||
|
- `extract(Request): array` (zwraca strukture `event_type, execution_status, rule_id, order_id, date_from, date_to`),
|
||||||
|
- `resolveActiveTab(Request, array $filters): string`,
|
||||||
|
- `hasAny(array $filters): bool`.
|
||||||
|
4. W controller::index uzyj nowej klasy zamiast prywatnych metod.
|
||||||
|
5. Usun z controllera `extractHistoryFilters`, `resolveActiveTab`, `hasHistoryFilters`.
|
||||||
|
</action>
|
||||||
|
<verify>`php -l src/Modules/Automation/AutomationHistoryFilters.php` zwraca "No syntax errors"</verify>
|
||||||
|
<done>AC-4</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 4: Aktualizacja AutomationModule + slim AutomationController</name>
|
||||||
|
<files>
|
||||||
|
src/Modules/Automation/AutomationModule.php,
|
||||||
|
src/Modules/Automation/AutomationController.php
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
1. W `AutomationModule::register` zarejestruj:
|
||||||
|
- `automation.request_parser` → `new AutomationRequestParser($s->get('automation.repo'))`
|
||||||
|
- `automation.form_view_model` → `new AutomationFormViewModel($s->get('automation.repo'), $s->get('accounting.receipts.config_repo'), $s->get('automation.request_parser'))`
|
||||||
|
- `automation.history_filters` → `new AutomationHistoryFilters()`
|
||||||
|
2. Wstrzyknij te trzy zaleznosci do `automation.controller` (rozszerz konstruktor controllera o 3 readonly properties).
|
||||||
|
3. ReceiptConfigRepository moze pozostac wstrzykniety do controllera dla kompatybilnosci LUB usuniety jezeli juz nigdzie nie uzywany (sprawdz w controllerze — po Task 2 powinien byc zbedny). Jezeli zbedny: usun parametr konstruktora i wpis w `AutomationModule`.
|
||||||
|
4. Zweryfikuj final szkielet controllera:
|
||||||
|
- public: index, create, edit, store, update, destroy, duplicate, toggleStatus
|
||||||
|
- private: renderForm, validateCsrf
|
||||||
|
- brak innych metod, brak stalych ALLOWED_*/PAYMENT_*
|
||||||
|
5. `wc -l src/Modules/Automation/AutomationController.php` < 230.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
`php -l` na: AutomationController.php, AutomationModule.php, AutomationRequestParser.php, AutomationFormViewModel.php, AutomationHistoryFilters.php — wszystkie "No syntax errors".
|
||||||
|
`wc -l src/Modules/Automation/AutomationController.php` zwraca < 230.
|
||||||
|
</verify>
|
||||||
|
<done>AC-1, AC-6</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 5: Walidacja testami + aktualizacja docs</name>
|
||||||
|
<files>
|
||||||
|
.paul/codebase/architecture.md,
|
||||||
|
.paul/codebase/quality_risks.md,
|
||||||
|
.paul/codebase/tech_changelog.md
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
1. Uruchom `vendor/bin/phpunit` (lub `composer test` jezeli zdefiniowane).
|
||||||
|
2. Potwierdz: `tests/Unit/AutomationServiceTest.php` zielony, nie modyfikowany; baseline 12/15 ogolnie nieuszkodzony (3/15 pre-existing zewnetrzne failures).
|
||||||
|
3. Smoke-test reczny widokow:
|
||||||
|
- `/settings/automation` (lista + zakladka history z filtrami i paginacja),
|
||||||
|
- `/settings/automation/create` i `/settings/automation/edit?id=N` (formularz z conditions + actions wszystkich typow).
|
||||||
|
4. W `.paul/codebase/architecture.md` zaktualizuj wpis modulu Automation:
|
||||||
|
- opisz slim Controller + 3 wspolpracownikow.
|
||||||
|
5. W `.paul/codebase/quality_risks.md` przekresl wiersz AutomationController z notatka o redukcji 677 → < 230, link do SUMMARY.
|
||||||
|
6. Dopisz wpis w `.paul/codebase/tech_changelog.md` (data 2026-05-20).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
Manualny przeglad widokow + `vendor/bin/phpunit --filter Automation` zielony.
|
||||||
|
</verify>
|
||||||
|
<done>AC-5 (zero regresji)</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
## Nie zmieniaj
|
||||||
|
- Publicznego kontraktu HTTP (`routes/web.php` przez `AutomationModule::routes` — sygnatury i sciezki).
|
||||||
|
- Plikow widokow `resources/views/automation/index.php` i `automation/form.php` — kontrakt zmiennych template'a 1:1.
|
||||||
|
- `AutomationService.php` (818 lin.) — refaktor odrebnego serwisu poza zakresem (osobny plan z `quality_risks.md`).
|
||||||
|
- `AutomationRepository` / `AutomationExecutionLogRepository` / `AutomationEmailOnceRepository` — bez zmian sygnatur.
|
||||||
|
- Schematu DB (`automation_rules`, `automation_execution_log`, `automation_email_once_deliveries`).
|
||||||
|
- `tests/Unit/AutomationServiceTest.php` — nie modyfikowac.
|
||||||
|
|
||||||
|
## Ograniczenia zakresu
|
||||||
|
- Brak nowych funkcji UX, brak refaktoru widokow, brak refaktoru `AutomationService`.
|
||||||
|
- Brak ekstrakcji wspolnego `BaseIntegrationController` / trait — to inne ryzyko z `quality_risks.md`.
|
||||||
|
- Brak testow jednostkowych dla nowych klas (kandydat na nastepny plan); refaktor zachowuje istniejace pokrycie poprzez `AutomationServiceTest`.
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- [ ] `php -l src/Modules/Automation/AutomationController.php` → No syntax errors.
|
||||||
|
- [ ] `php -l src/Modules/Automation/AutomationModule.php` → No syntax errors.
|
||||||
|
- [ ] `php -l src/Modules/Automation/AutomationRequestParser.php` → No syntax errors.
|
||||||
|
- [ ] `php -l src/Modules/Automation/AutomationFormViewModel.php` → No syntax errors.
|
||||||
|
- [ ] `php -l src/Modules/Automation/AutomationHistoryFilters.php` → No syntax errors.
|
||||||
|
- [ ] `wc -l src/Modules/Automation/AutomationController.php` < 230.
|
||||||
|
- [ ] `vendor/bin/phpunit` — `tests/Unit/AutomationServiceTest.php` zielony, pelny suite na poziomie baseline (12/15).
|
||||||
|
- [ ] Smoke-test: `/settings/automation`, `/settings/automation/create`, `/settings/automation/edit?id=N` — bez bledow, formularze i listy renderuja sie identycznie.
|
||||||
|
- [ ] Quality Radar: aktualizacja `.paul/codebase/quality_risks.md` (przekreslenie wiersza AutomationController) + `architecture.md` + `tech_changelog.md`.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- [ ] Wszystkie AC-1..AC-6 spelnione.
|
||||||
|
- [ ] Verification zaliczone.
|
||||||
|
- [ ] STATE.md wskazuje na ten plan jako aktywny (po APPLY -> po UNIFY: zamkniety).
|
||||||
|
- [ ] `quality_risks.md` zaktualizowany.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
SUMMARY.md path: `.paul/plans/20260520-1400-refactor-automation-controller/SUMMARY.md`
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
plan_id: 20260520-1400-refactor-automation-controller
|
||||||
|
title: Refaktoring AutomationController — slim controller + 3 wspolpracownikow
|
||||||
|
status: completed
|
||||||
|
completed: 2026-05-20T14:30:00+02:00
|
||||||
|
files_modified:
|
||||||
|
- src/Modules/Automation/AutomationController.php
|
||||||
|
- src/Modules/Automation/AutomationModule.php
|
||||||
|
- src/Modules/Automation/AutomationRequestParser.php (nowy)
|
||||||
|
- src/Modules/Automation/AutomationFormViewModel.php (nowy)
|
||||||
|
- src/Modules/Automation/AutomationHistoryFilters.php (nowy)
|
||||||
|
- .paul/codebase/architecture.md
|
||||||
|
- .paul/codebase/quality_risks.md
|
||||||
|
- .paul/codebase/tech_changelog.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wynik
|
||||||
|
|
||||||
|
`src/Modules/Automation/AutomationController.php`: **677 → 221 lin. (67% redukcji)**. Cel < 230 spelniony.
|
||||||
|
|
||||||
|
### Metryki
|
||||||
|
|
||||||
|
| Plik | Linie | Rola |
|
||||||
|
|---|---|---|
|
||||||
|
| `AutomationController.php` | 221 | Slim controller: 8 akcji HTTP + `runIdAction`/`renderForm`/`validateCsrf` |
|
||||||
|
| `AutomationRequestParser.php` | 422 | Stale ALLOWED_*/PAYMENT_*, walidacja, ekstrakcja conditions/actions/ruleData, `buildRule` |
|
||||||
|
| `AutomationFormViewModel.php` | 80 | Zmienne template'a `automation/form` |
|
||||||
|
| `AutomationHistoryFilters.php` | 58 | `extract` + `resolveActiveTab` + `hasAny` |
|
||||||
|
| `AutomationModule.php` | +14 lin. | Rejestracja 3 nowych serwisow + rozszerzony konstruktor controllera |
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
| AC | Status |
|
||||||
|
|---|---|
|
||||||
|
| AC-1: Controller < 230 lin., tylko HTTP actions + helpery | ✅ 221 lin. |
|
||||||
|
| AC-2: Parser przejmuje wejscie + katalogi stalych | ✅ |
|
||||||
|
| AC-3: ViewModel przygotowuje dane formularza | ✅ klucze template'a 1:1 |
|
||||||
|
| AC-4: HistoryFilters obsluguje liste historii | ✅ |
|
||||||
|
| AC-5: Zero regresji testowej | ⚠ phpunit nieuruchamialny (brak `vendor/`); `php -l` czysty dla 5/5 plikow. Smoke-test pozostaje do reczego potwierdzenia |
|
||||||
|
| AC-6: Module DI dziala bez zmiany kontraktu HTTP | ✅ klucz `automation.controller` i routy niezmienione |
|
||||||
|
|
||||||
|
## Decyzje
|
||||||
|
|
||||||
|
- **`ReceiptConfigRepository` usuniety z konstruktora controllera** — przeniesiony do `AutomationFormViewModel`, ktory jest jedynym konsumentem.
|
||||||
|
- **`runIdAction(Request, callable, ok, err)`** — wprowadzony helper bo destroy/duplicate/toggleStatus mialy identyczny szablon CSRF + guard ID + try/catch + flash + redirect. Skrocenie ~50 lin., poprawa czytelnosci.
|
||||||
|
- **Getter pattern w parserze (`allowedEvents()` itp.)** zamiast publicznych stalych — zachowuje enkapsulacje katalogow w jednej klasie, view model i controller nie znaja konkretnych wartosci.
|
||||||
|
- **`buildRule()` z surowymi (nie zwalidowanymi) condition_value** — zachowane 1:1 z oryginalu, bo widok formularza wymaga raw wartosci do re-rendera po bledzie walidacji (np. zachowanie zaznaczonych checkboxow nawet jezeli ich klucze byly niewlasciwe).
|
||||||
|
|
||||||
|
## Weryfikacja
|
||||||
|
|
||||||
|
- ✅ `php -l` na wszystkich 5 plikach: No syntax errors.
|
||||||
|
- ✅ `wc -l src/Modules/Automation/AutomationController.php` = 221 (< 230).
|
||||||
|
- ⚠ `vendor/bin/phpunit` niedostepny w srodowisku (brak `vendor/`). `tests/Unit/AutomationServiceTest.php` nie byl modyfikowany — nie dotyka controllera, nadal powinien przechodzic po `composer install`.
|
||||||
|
- 🟡 **Do weryfikacji recznie:** `/settings/automation` (lista + zakladka history z filtrami i paginacja), `/settings/automation/create`, `/settings/automation/edit?id=N` (formularz dla wszystkich typow conditions/actions).
|
||||||
|
|
||||||
|
## Deferrals
|
||||||
|
|
||||||
|
- Refaktor `AutomationService.php` (818 lin.) — pozostaje na liscie kandydatow w `quality_risks.md`.
|
||||||
|
- Testy jednostkowe dla nowych klas (`AutomationRequestParser`, `AutomationFormViewModel`, `AutomationHistoryFilters`) — kandydat na nastepny plan.
|
||||||
|
- Wspolny `BaseIntegrationController` / trait — odrebne ryzyko.
|
||||||
@@ -10,52 +10,36 @@ use App\Core\Security\Csrf;
|
|||||||
use App\Core\Support\Flash;
|
use App\Core\Support\Flash;
|
||||||
use App\Core\View\Template;
|
use App\Core\View\Template;
|
||||||
use App\Modules\Auth\AuthService;
|
use App\Modules\Auth\AuthService;
|
||||||
use App\Modules\Settings\ReceiptConfigRepository;
|
|
||||||
use App\Modules\Shipments\DeliveryStatus;
|
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
final class AutomationController
|
final class AutomationController
|
||||||
{
|
{
|
||||||
private const HISTORY_PER_PAGE = 25;
|
private const HISTORY_PER_PAGE = 25;
|
||||||
private const ALLOWED_EVENTS = ['receipt.created', 'shipment.created', 'shipment.status_changed', 'payment.status_changed', 'order.status_changed', 'order.status_aged', 'order.imported'];
|
|
||||||
private const ALLOWED_CONDITION_TYPES = ['integration', 'shipment_status', 'payment_status', 'payment_method', 'order_status', 'days_in_status'];
|
|
||||||
private const PAYMENT_STATUS_OPTIONS = [
|
|
||||||
'0' => 'Nieopłacone',
|
|
||||||
'1' => 'Częściowo opłacone',
|
|
||||||
'2' => 'Opłacone',
|
|
||||||
];
|
|
||||||
private const ALLOWED_ACTION_TYPES = ['send_email', 'issue_receipt', 'update_shipment_status', 'update_order_status'];
|
|
||||||
private const ALLOWED_RECIPIENTS = ['client', 'client_and_company', 'company'];
|
|
||||||
private const ALLOWED_RECEIPT_ISSUE_DATE_MODES = ['today', 'order_date', 'payment_date'];
|
|
||||||
private const ALLOWED_RECEIPT_DUPLICATE_POLICIES = ['skip_if_exists', 'allow_duplicates'];
|
|
||||||
private const PAYMENT_METHOD_OPTIONS = [
|
|
||||||
'cod' => 'Płatność przy odbiorze (COD)',
|
|
||||||
'transfer' => 'Przelew bankowy',
|
|
||||||
'online' => 'Karta / płatność online',
|
|
||||||
'other' => 'Inna',
|
|
||||||
];
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Template $template,
|
private readonly Template $template,
|
||||||
private readonly Translator $translator,
|
private readonly Translator $translator,
|
||||||
private readonly AuthService $auth,
|
private readonly AuthService $auth,
|
||||||
private readonly AutomationRepository $repository,
|
private readonly AutomationRepository $repository,
|
||||||
private readonly AutomationExecutionLogRepository $executionLogs,
|
private readonly AutomationExecutionLogRepository $executionLogs,
|
||||||
private readonly ReceiptConfigRepository $receiptConfigs
|
private readonly AutomationRequestParser $parser,
|
||||||
|
private readonly AutomationFormViewModel $formViewModel,
|
||||||
|
private readonly AutomationHistoryFilters $historyFilters
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index(Request $request): Response
|
public function index(Request $request): Response
|
||||||
{
|
{
|
||||||
$rules = $this->repository->findAll();
|
$rules = $this->repository->findAll();
|
||||||
$historyFilters = $this->extractHistoryFilters($request);
|
$filters = $this->historyFilters->extract($request);
|
||||||
$historyPage = max(1, (int) $request->input('history_page', 1));
|
$historyPage = max(1, (int) $request->input('history_page', 1));
|
||||||
$historyTotal = $this->executionLogs->count($historyFilters);
|
$historyTotal = $this->executionLogs->count($filters);
|
||||||
$historyTotalPages = max(1, (int) ceil($historyTotal / self::HISTORY_PER_PAGE));
|
$historyTotalPages = max(1, (int) ceil($historyTotal / self::HISTORY_PER_PAGE));
|
||||||
if ($historyPage > $historyTotalPages) {
|
if ($historyPage > $historyTotalPages) {
|
||||||
$historyPage = $historyTotalPages;
|
$historyPage = $historyTotalPages;
|
||||||
}
|
}
|
||||||
$historyEntries = $this->executionLogs->paginate($historyFilters, $historyPage, self::HISTORY_PER_PAGE);
|
$historyEntries = $this->executionLogs->paginate($filters, $historyPage, self::HISTORY_PER_PAGE);
|
||||||
$activeTab = $this->resolveActiveTab($request, $historyFilters);
|
$activeTab = $this->historyFilters->resolveActiveTab($request, $filters);
|
||||||
|
|
||||||
$html = $this->template->render('automation/index', [
|
$html = $this->template->render('automation/index', [
|
||||||
'title' => 'Zadania automatyczne',
|
'title' => 'Zadania automatyczne',
|
||||||
@@ -66,8 +50,8 @@ final class AutomationController
|
|||||||
'rules' => $rules,
|
'rules' => $rules,
|
||||||
'activeTab' => $activeTab,
|
'activeTab' => $activeTab,
|
||||||
'historyEntries' => $historyEntries,
|
'historyEntries' => $historyEntries,
|
||||||
'historyFilters' => $historyFilters,
|
'historyFilters' => $filters,
|
||||||
'historyEventTypes' => array_values(array_unique(array_merge(self::ALLOWED_EVENTS, $this->executionLogs->listEventTypes()))),
|
'historyEventTypes' => array_values(array_unique(array_merge($this->parser->allowedEvents(), $this->executionLogs->listEventTypes()))),
|
||||||
'historyRuleOptions' => $this->repository->listRuleOptions(),
|
'historyRuleOptions' => $this->repository->listRuleOptions(),
|
||||||
'historyPagination' => [
|
'historyPagination' => [
|
||||||
'page' => $historyPage,
|
'page' => $historyPage,
|
||||||
@@ -107,20 +91,20 @@ final class AutomationController
|
|||||||
return $error;
|
return $error;
|
||||||
}
|
}
|
||||||
|
|
||||||
$validationError = $this->validateInput($request);
|
$validationError = $this->parser->validate($request);
|
||||||
if ($validationError !== null) {
|
if ($validationError !== null) {
|
||||||
return $this->renderForm($this->buildRuleFromRequest($request), $validationError);
|
return $this->renderForm($this->parser->buildRule($request), $validationError);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->repository->create(
|
$this->repository->create(
|
||||||
$this->extractRuleData($request),
|
$this->parser->extractRuleData($request),
|
||||||
$this->extractConditions($request),
|
$this->parser->extractConditions($request),
|
||||||
$this->extractActions($request)
|
$this->parser->extractActions($request)
|
||||||
);
|
);
|
||||||
Flash::set('settings.automation.success', 'Zadanie automatyczne zostalo utworzone');
|
Flash::set('settings.automation.success', 'Zadanie automatyczne zostalo utworzone');
|
||||||
} catch (Throwable) {
|
} catch (Throwable) {
|
||||||
return $this->renderForm($this->buildRuleFromRequest($request), 'Błąd zapisu zadania automatycznego');
|
return $this->renderForm($this->parser->buildRule($request), 'Błąd zapisu zadania automatycznego');
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response::redirect('/settings/automation');
|
return Response::redirect('/settings/automation');
|
||||||
@@ -139,21 +123,21 @@ final class AutomationController
|
|||||||
return Response::redirect('/settings/automation');
|
return Response::redirect('/settings/automation');
|
||||||
}
|
}
|
||||||
|
|
||||||
$validationError = $this->validateInput($request);
|
$validationError = $this->parser->validate($request);
|
||||||
if ($validationError !== null) {
|
if ($validationError !== null) {
|
||||||
return $this->renderForm($this->buildRuleFromRequest($request, $id), $validationError);
|
return $this->renderForm($this->parser->buildRule($request, $id), $validationError);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->repository->update(
|
$this->repository->update(
|
||||||
$id,
|
$id,
|
||||||
$this->extractRuleData($request),
|
$this->parser->extractRuleData($request),
|
||||||
$this->extractConditions($request),
|
$this->parser->extractConditions($request),
|
||||||
$this->extractActions($request)
|
$this->parser->extractActions($request)
|
||||||
);
|
);
|
||||||
Flash::set('settings.automation.success', 'Zadanie automatyczne zostalo zaktualizowane');
|
Flash::set('settings.automation.success', 'Zadanie automatyczne zostalo zaktualizowane');
|
||||||
} catch (Throwable) {
|
} catch (Throwable) {
|
||||||
return $this->renderForm($this->buildRuleFromRequest($request, $id), 'Błąd aktualizacji zadania automatycznego');
|
return $this->renderForm($this->parser->buildRule($request, $id), 'Błąd aktualizacji zadania automatycznego');
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response::redirect('/settings/automation');
|
return Response::redirect('/settings/automation');
|
||||||
@@ -161,55 +145,39 @@ final class AutomationController
|
|||||||
|
|
||||||
public function destroy(Request $request): Response
|
public function destroy(Request $request): Response
|
||||||
{
|
{
|
||||||
$error = $this->validateCsrf($request);
|
return $this->runIdAction(
|
||||||
if ($error !== null) {
|
$request,
|
||||||
return $error;
|
fn (int $id) => $this->repository->delete($id),
|
||||||
}
|
'Zadanie automatyczne zostalo usuńięte',
|
||||||
|
'Błąd usuwania zadania automatycznego'
|
||||||
$id = (int) $request->input('id', '0');
|
);
|
||||||
if ($id <= 0) {
|
|
||||||
Flash::set('settings.automation.error', 'Nieprawidłowy identyfikator');
|
|
||||||
return Response::redirect('/settings/automation');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$this->repository->delete($id);
|
|
||||||
Flash::set('settings.automation.success', 'Zadanie automatyczne zostalo usuńięte');
|
|
||||||
} catch (Throwable) {
|
|
||||||
Flash::set('settings.automation.error', 'Błąd usuwania zadania automatycznego');
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response::redirect('/settings/automation');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function duplicate(Request $request): Response
|
public function duplicate(Request $request): Response
|
||||||
{
|
{
|
||||||
$error = $this->validateCsrf($request);
|
return $this->runIdAction(
|
||||||
if ($error !== null) {
|
$request,
|
||||||
return $error;
|
fn (int $id) => $this->repository->duplicate($id),
|
||||||
}
|
'Zadanie zostalo zduplikowane',
|
||||||
|
'Błąd duplikowania zadania'
|
||||||
$id = (int) $request->input('id', '0');
|
);
|
||||||
if ($id <= 0) {
|
|
||||||
Flash::set('settings.automation.error', 'Nieprawidłowy identyfikator');
|
|
||||||
return Response::redirect('/settings/automation');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$this->repository->duplicate($id);
|
|
||||||
Flash::set('settings.automation.success', 'Zadanie zostalo zduplikowane');
|
|
||||||
} catch (Throwable) {
|
|
||||||
Flash::set('settings.automation.error', 'Błąd duplikowania zadania');
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response::redirect('/settings/automation');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toggleStatus(Request $request): Response
|
public function toggleStatus(Request $request): Response
|
||||||
{
|
{
|
||||||
$error = $this->validateCsrf($request);
|
return $this->runIdAction(
|
||||||
if ($error !== null) {
|
$request,
|
||||||
return $error;
|
fn (int $id) => $this->repository->toggleActive($id),
|
||||||
|
'Status zadania zostal zmieńiony',
|
||||||
|
'Błąd zmiany statusu'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function runIdAction(Request $request, callable $op, string $okMessage, string $errorMessage): Response
|
||||||
|
{
|
||||||
|
$csrfError = $this->validateCsrf($request);
|
||||||
|
if ($csrfError !== null) {
|
||||||
|
return $csrfError;
|
||||||
}
|
}
|
||||||
|
|
||||||
$id = (int) $request->input('id', '0');
|
$id = (int) $request->input('id', '0');
|
||||||
@@ -219,10 +187,10 @@ final class AutomationController
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->repository->toggleActive($id);
|
$op($id);
|
||||||
Flash::set('settings.automation.success', 'Status zadania zostal zmieńiony');
|
Flash::set('settings.automation.success', $okMessage);
|
||||||
} catch (Throwable) {
|
} catch (Throwable) {
|
||||||
Flash::set('settings.automation.error', 'Błąd zmiany statusu');
|
Flash::set('settings.automation.error', $errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response::redirect('/settings/automation');
|
return Response::redirect('/settings/automation');
|
||||||
@@ -230,87 +198,15 @@ final class AutomationController
|
|||||||
|
|
||||||
private function renderForm(?array $rule, string $errorMessage = ''): Response
|
private function renderForm(?array $rule, string $errorMessage = ''): Response
|
||||||
{
|
{
|
||||||
$html = $this->template->render('automation/form', [
|
$vars = array_merge($this->formViewModel->build($rule, $errorMessage), [
|
||||||
'title' => $rule !== null && isset($rule['id']) ? 'Edytuj zadanie automatyczne' : 'Nowe zadanie automatyczne',
|
'title' => $rule !== null && isset($rule['id']) ? 'Edytuj zadanie automatyczne' : 'Nowe zadanie automatyczne',
|
||||||
'activeMenu' => 'settings',
|
'activeMenu' => 'settings',
|
||||||
'activeSettings' => 'automation',
|
'activeSettings' => 'automation',
|
||||||
'user' => $this->auth->user(),
|
'user' => $this->auth->user(),
|
||||||
'csrfToken' => Csrf::token(),
|
'csrfToken' => Csrf::token(),
|
||||||
'rule' => $rule,
|
]);
|
||||||
'integrations' => $this->repository->listOrderIntegrations(),
|
|
||||||
'emailTemplates' => $this->repository->listEmailTemplates(),
|
|
||||||
'eventTypes' => self::ALLOWED_EVENTS,
|
|
||||||
'conditionTypes' => self::ALLOWED_CONDITION_TYPES,
|
|
||||||
'actionTypes' => self::ALLOWED_ACTION_TYPES,
|
|
||||||
'recipientOptions' => self::ALLOWED_RECIPIENTS,
|
|
||||||
'receiptConfigs' => $this->listActiveReceiptConfigs(),
|
|
||||||
'receiptIssueDateModes' => self::ALLOWED_RECEIPT_ISSUE_DATE_MODES,
|
|
||||||
'receiptDuplicatePolicies' => self::ALLOWED_RECEIPT_DUPLICATE_POLICIES,
|
|
||||||
'shipmentStatusOptions' => $this->buildShipmentStatusOptions(),
|
|
||||||
'paymentStatusOptions' => self::PAYMENT_STATUS_OPTIONS,
|
|
||||||
'paymentMethodOptions' => self::PAYMENT_METHOD_OPTIONS,
|
|
||||||
'orderStatusOptions' => $this->repository->listActiveOrderStatuses(),
|
|
||||||
'errorMessage' => $errorMessage !== '' ? $errorMessage : Flash::get('settings.automation.error', ''),
|
|
||||||
], 'layouts/app');
|
|
||||||
|
|
||||||
return Response::html($html);
|
return Response::html($this->template->render('automation/form', $vars, 'layouts/app'));
|
||||||
}
|
|
||||||
|
|
||||||
private function buildRuleFromRequest(Request $request, ?int $id = null): array
|
|
||||||
{
|
|
||||||
$raw = $request->input('conditions', []);
|
|
||||||
$conditions = [];
|
|
||||||
if (is_array($raw)) {
|
|
||||||
foreach ($raw as $cond) {
|
|
||||||
if (!is_array($cond)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$type = (string) ($cond['type'] ?? '');
|
|
||||||
$value = [];
|
|
||||||
if ($type === 'integration') {
|
|
||||||
$value = ['integration_ids' => is_array($cond['integration_ids'] ?? null) ? $cond['integration_ids'] : []];
|
|
||||||
} elseif ($type === 'shipment_status') {
|
|
||||||
$value = ['status_keys' => is_array($cond['shipment_status_keys'] ?? null) ? $cond['shipment_status_keys'] : []];
|
|
||||||
} elseif ($type === 'payment_status') {
|
|
||||||
$value = ['status_keys' => is_array($cond['payment_status_keys'] ?? null) ? $cond['payment_status_keys'] : []];
|
|
||||||
} elseif ($type === 'payment_method') {
|
|
||||||
$value = ['method_keys' => is_array($cond['payment_method_keys'] ?? null) ? $cond['payment_method_keys'] : []];
|
|
||||||
} elseif ($type === 'order_status') {
|
|
||||||
$value = ['order_status_codes' => is_array($cond['order_status_codes'] ?? null) ? $cond['order_status_codes'] : []];
|
|
||||||
} elseif ($type === 'days_in_status') {
|
|
||||||
$value = ['days' => max(1, (int) ($cond['days'] ?? 0))];
|
|
||||||
}
|
|
||||||
$conditions[] = ['condition_type' => $type, 'condition_value' => $value];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$rawActions = $request->input('actions', []);
|
|
||||||
$actions = [];
|
|
||||||
if (is_array($rawActions)) {
|
|
||||||
foreach ($rawActions as $act) {
|
|
||||||
if (!is_array($act)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$type = (string) ($act['type'] ?? '');
|
|
||||||
$config = $act;
|
|
||||||
unset($config['type']);
|
|
||||||
$actions[] = ['action_type' => $type, 'action_config' => $config];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$rule = [
|
|
||||||
'name' => trim((string) $request->input('name', '')),
|
|
||||||
'event_type' => (string) $request->input('event_type', ''),
|
|
||||||
'is_active' => $request->input('is_active', null) !== null ? 1 : 0,
|
|
||||||
'conditions' => $conditions,
|
|
||||||
'actions' => $actions,
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($id !== null) {
|
|
||||||
$rule['id'] = $id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $rule;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateCsrf(Request $request): ?Response
|
private function validateCsrf(Request $request): ?Response
|
||||||
@@ -322,356 +218,4 @@ final class AutomationController
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateInput(Request $request): ?string
|
|
||||||
{
|
|
||||||
$name = trim((string) $request->input('name', ''));
|
|
||||||
if ($name === '' || mb_strlen($name) > 128) {
|
|
||||||
return 'Nazwa jest wymagana (maks. 128 znaków)';
|
|
||||||
}
|
|
||||||
|
|
||||||
$eventType = (string) $request->input('event_type', '');
|
|
||||||
if (!in_array($eventType, self::ALLOWED_EVENTS, true)) {
|
|
||||||
return 'Nieprawidłowy typ zdarzenia';
|
|
||||||
}
|
|
||||||
|
|
||||||
$conditions = $this->extractConditions($request);
|
|
||||||
if (count($conditions) === 0) {
|
|
||||||
return 'Wymagany jest co najmniej jeden warunek';
|
|
||||||
}
|
|
||||||
|
|
||||||
$actions = $this->extractActions($request);
|
|
||||||
if (count($actions) === 0) {
|
|
||||||
return 'Wymagana jest co najmniej jedna akcja';
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function extractRuleData(Request $request): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'name' => trim((string) $request->input('name', '')),
|
|
||||||
'event_type' => (string) $request->input('event_type', ''),
|
|
||||||
'is_active' => $request->input('is_active', null) !== null ? 1 : 0,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<array{type: string, value: array<string, mixed>}>
|
|
||||||
*/
|
|
||||||
private function extractConditions(Request $request): array
|
|
||||||
{
|
|
||||||
$raw = $request->input('conditions', []);
|
|
||||||
if (!is_array($raw)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = [];
|
|
||||||
foreach ($raw as $condition) {
|
|
||||||
if (!is_array($condition)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$type = (string) ($condition['type'] ?? '');
|
|
||||||
if (!in_array($type, self::ALLOWED_CONDITION_TYPES, true)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$value = $this->parseConditionValue($type, $condition);
|
|
||||||
if ($value === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$result[] = ['type' => $type, 'value' => $value];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $condition
|
|
||||||
* @return array<string, mixed>|null
|
|
||||||
*/
|
|
||||||
private function parseConditionValue(string $type, array $condition): ?array
|
|
||||||
{
|
|
||||||
if ($type === 'integration') {
|
|
||||||
$ids = $condition['integration_ids'] ?? [];
|
|
||||||
if (!is_array($ids)) {
|
|
||||||
$ids = [];
|
|
||||||
}
|
|
||||||
$integrationIds = array_values(array_filter(
|
|
||||||
array_map('intval', $ids),
|
|
||||||
static fn (int $id): bool => $id > 0
|
|
||||||
));
|
|
||||||
|
|
||||||
return count($integrationIds) > 0 ? ['integration_ids' => $integrationIds] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($type === 'shipment_status') {
|
|
||||||
$keys = $condition['shipment_status_keys'] ?? [];
|
|
||||||
if (!is_array($keys)) {
|
|
||||||
$keys = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$allowedKeys = DeliveryStatus::getAllStatuses();
|
|
||||||
$statusKeys = array_values(array_filter(
|
|
||||||
array_map(static fn (mixed $key): string => trim((string) $key), $keys),
|
|
||||||
static fn (string $key): bool => $key !== '' && in_array($key, $allowedKeys, true)
|
|
||||||
));
|
|
||||||
|
|
||||||
return count($statusKeys) > 0 ? ['status_keys' => array_values(array_unique($statusKeys))] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($type === 'payment_status') {
|
|
||||||
$keys = $condition['payment_status_keys'] ?? [];
|
|
||||||
if (!is_array($keys)) {
|
|
||||||
$keys = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$allowedKeys = array_map('strval', array_keys(self::PAYMENT_STATUS_OPTIONS));
|
|
||||||
$statusKeys = array_values(array_filter(
|
|
||||||
array_map(static fn (mixed $key): string => trim((string) $key), $keys),
|
|
||||||
static fn (string $key): bool => $key !== '' && in_array($key, $allowedKeys, true)
|
|
||||||
));
|
|
||||||
|
|
||||||
return count($statusKeys) > 0 ? ['status_keys' => array_values(array_unique($statusKeys))] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($type === 'payment_method') {
|
|
||||||
$keys = $condition['payment_method_keys'] ?? [];
|
|
||||||
if (!is_array($keys)) {
|
|
||||||
$keys = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$allowedKeys = array_keys(self::PAYMENT_METHOD_OPTIONS);
|
|
||||||
$methodKeys = array_values(array_filter(
|
|
||||||
array_map(static fn (mixed $key): string => trim((string) $key), $keys),
|
|
||||||
static fn (string $key): bool => $key !== '' && in_array($key, $allowedKeys, true)
|
|
||||||
));
|
|
||||||
|
|
||||||
return count($methodKeys) > 0 ? ['method_keys' => array_values(array_unique($methodKeys))] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($type === 'order_status') {
|
|
||||||
$codes = $condition['order_status_codes'] ?? [];
|
|
||||||
if (!is_array($codes)) {
|
|
||||||
$codes = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$availableCodes = array_map(
|
|
||||||
static fn (array $row): string => strtolower(trim((string) ($row['code'] ?? ''))),
|
|
||||||
$this->repository->listActiveOrderStatuses()
|
|
||||||
);
|
|
||||||
|
|
||||||
$statusCodes = array_values(array_filter(
|
|
||||||
array_map(static fn (mixed $code): string => strtolower(trim((string) $code)), $codes),
|
|
||||||
static fn (string $code): bool => $code !== '' && in_array($code, $availableCodes, true)
|
|
||||||
));
|
|
||||||
|
|
||||||
return count($statusCodes) > 0 ? ['order_status_codes' => array_values(array_unique($statusCodes))] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($type === 'days_in_status') {
|
|
||||||
$days = (int) ($condition['days'] ?? 0);
|
|
||||||
|
|
||||||
return $days >= 1 ? ['days' => $days] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<array{type: string, config: array<string, mixed>}>
|
|
||||||
*/
|
|
||||||
private function extractActions(Request $request): array
|
|
||||||
{
|
|
||||||
$raw = $request->input('actions', []);
|
|
||||||
if (!is_array($raw)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = [];
|
|
||||||
foreach ($raw as $action) {
|
|
||||||
if (!is_array($action)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$type = (string) ($action['type'] ?? '');
|
|
||||||
if (!in_array($type, self::ALLOWED_ACTION_TYPES, true)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$config = $this->parseActionConfig($type, $action);
|
|
||||||
if ($config === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$result[] = ['type' => $type, 'config' => $config];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $action
|
|
||||||
* @return array<string, mixed>|null
|
|
||||||
*/
|
|
||||||
private function parseActionConfig(string $type, array $action): ?array
|
|
||||||
{
|
|
||||||
if ($type === 'send_email') {
|
|
||||||
$templateId = (int) ($action['template_id'] ?? 0);
|
|
||||||
$recipient = (string) ($action['recipient'] ?? '');
|
|
||||||
$sendOncePerOrder = isset($action['send_once_per_order']) && (int) $action['send_once_per_order'] === 1 ? 1 : 0;
|
|
||||||
|
|
||||||
if ($templateId <= 0 || !in_array($recipient, self::ALLOWED_RECIPIENTS, true)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'template_id' => $templateId,
|
|
||||||
'recipient' => $recipient,
|
|
||||||
'send_once_per_order' => $sendOncePerOrder,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($type === 'issue_receipt') {
|
|
||||||
$configId = (int) ($action['receipt_config_id'] ?? 0);
|
|
||||||
$issueDateMode = (string) ($action['issue_date_mode'] ?? '');
|
|
||||||
$duplicatePolicy = (string) ($action['duplicate_policy'] ?? '');
|
|
||||||
|
|
||||||
if (
|
|
||||||
$configId <= 0
|
|
||||||
|| !in_array($issueDateMode, self::ALLOWED_RECEIPT_ISSUE_DATE_MODES, true)
|
|
||||||
|| !in_array($duplicatePolicy, self::ALLOWED_RECEIPT_DUPLICATE_POLICIES, true)
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'receipt_config_id' => $configId,
|
|
||||||
'issue_date_mode' => $issueDateMode,
|
|
||||||
'duplicate_policy' => $duplicatePolicy,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($type === 'update_shipment_status') {
|
|
||||||
$statusKey = trim((string) ($action['shipment_status_key'] ?? ''));
|
|
||||||
if ($statusKey === '' || !in_array($statusKey, DeliveryStatus::getAllStatuses(), true)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['status_key' => $statusKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($type === 'update_order_status') {
|
|
||||||
$statusCode = trim((string) ($action['order_status_code'] ?? ''));
|
|
||||||
if ($statusCode === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$availableCodes = array_map(
|
|
||||||
static fn (array $row): string => trim((string) ($row['code'] ?? '')),
|
|
||||||
$this->repository->listActiveOrderStatuses()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!in_array($statusCode, $availableCodes, true)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['status_code' => $statusCode];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, array{label:string}>
|
|
||||||
*/
|
|
||||||
private function buildShipmentStatusOptions(): array
|
|
||||||
{
|
|
||||||
$options = [];
|
|
||||||
foreach (DeliveryStatus::getAllOptions() as $key => $label) {
|
|
||||||
$options[(string) $key] = ['label' => (string) $label];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<array{id:int,name:string,number_format:string}>
|
|
||||||
*/
|
|
||||||
private function listActiveReceiptConfigs(): array
|
|
||||||
{
|
|
||||||
$all = $this->receiptConfigs->listAll();
|
|
||||||
$result = [];
|
|
||||||
foreach ($all as $config) {
|
|
||||||
if ((int) ($config['is_active'] ?? 0) !== 1) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$configId = (int) ($config['id'] ?? 0);
|
|
||||||
if ($configId <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$result[] = [
|
|
||||||
'id' => $configId,
|
|
||||||
'name' => (string) ($config['name'] ?? ''),
|
|
||||||
'number_format' => (string) ($config['number_format'] ?? ''),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{event_type:string,execution_status:string,rule_id:int,order_id:int,date_from:string,date_to:string}
|
|
||||||
*/
|
|
||||||
private function extractHistoryFilters(Request $request): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'event_type' => trim((string) $request->input('history_event_type', '')),
|
|
||||||
'execution_status' => trim((string) $request->input('history_status', '')),
|
|
||||||
'rule_id' => max(0, (int) $request->input('history_rule_id', 0)),
|
|
||||||
'order_id' => max(0, (int) $request->input('history_order_id', 0)),
|
|
||||||
'date_from' => trim((string) $request->input('history_date_from', '')),
|
|
||||||
'date_to' => trim((string) $request->input('history_date_to', '')),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array{event_type:string,execution_status:string,rule_id:int,order_id:int,date_from:string,date_to:string} $historyFilters
|
|
||||||
*/
|
|
||||||
private function resolveActiveTab(Request $request, array $historyFilters): string
|
|
||||||
{
|
|
||||||
$activeTab = trim((string) $request->input('tab', 'settings'));
|
|
||||||
if ($activeTab === 'history') {
|
|
||||||
return 'history';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((int) $request->input('history_page', 0) > 1) {
|
|
||||||
return 'history';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->hasHistoryFilters($historyFilters)) {
|
|
||||||
return 'history';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'settings';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array{event_type:string,execution_status:string,rule_id:int,order_id:int,date_from:string,date_to:string} $historyFilters
|
|
||||||
*/
|
|
||||||
private function hasHistoryFilters(array $historyFilters): bool
|
|
||||||
{
|
|
||||||
return $historyFilters['event_type'] !== ''
|
|
||||||
|| $historyFilters['execution_status'] !== ''
|
|
||||||
|| $historyFilters['rule_id'] > 0
|
|
||||||
|| $historyFilters['order_id'] > 0
|
|
||||||
|| $historyFilters['date_from'] !== ''
|
|
||||||
|| $historyFilters['date_to'] !== '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
80
src/Modules/Automation/AutomationFormViewModel.php
Normal file
80
src/Modules/Automation/AutomationFormViewModel.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Automation;
|
||||||
|
|
||||||
|
use App\Core\Support\Flash;
|
||||||
|
use App\Modules\Settings\ReceiptConfigRepository;
|
||||||
|
use App\Modules\Shipments\DeliveryStatus;
|
||||||
|
|
||||||
|
final class AutomationFormViewModel
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly AutomationRepository $repository,
|
||||||
|
private readonly ReceiptConfigRepository $receiptConfigs,
|
||||||
|
private readonly AutomationRequestParser $parser
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function build(?array $rule, string $errorMessage): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'rule' => $rule,
|
||||||
|
'integrations' => $this->repository->listOrderIntegrations(),
|
||||||
|
'emailTemplates' => $this->repository->listEmailTemplates(),
|
||||||
|
'eventTypes' => $this->parser->allowedEvents(),
|
||||||
|
'conditionTypes' => $this->parser->allowedConditionTypes(),
|
||||||
|
'actionTypes' => $this->parser->allowedActionTypes(),
|
||||||
|
'recipientOptions' => $this->parser->allowedRecipients(),
|
||||||
|
'receiptConfigs' => $this->listActiveReceiptConfigs(),
|
||||||
|
'receiptIssueDateModes' => $this->parser->allowedReceiptIssueDateModes(),
|
||||||
|
'receiptDuplicatePolicies' => $this->parser->allowedReceiptDuplicatePolicies(),
|
||||||
|
'shipmentStatusOptions' => $this->buildShipmentStatusOptions(),
|
||||||
|
'paymentStatusOptions' => $this->parser->paymentStatusOptions(),
|
||||||
|
'paymentMethodOptions' => $this->parser->paymentMethodOptions(),
|
||||||
|
'orderStatusOptions' => $this->repository->listActiveOrderStatuses(),
|
||||||
|
'errorMessage' => $errorMessage !== '' ? $errorMessage : Flash::get('settings.automation.error', ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array{label:string}>
|
||||||
|
*/
|
||||||
|
private function buildShipmentStatusOptions(): array
|
||||||
|
{
|
||||||
|
$options = [];
|
||||||
|
foreach (DeliveryStatus::getAllOptions() as $key => $label) {
|
||||||
|
$options[(string) $key] = ['label' => (string) $label];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{id:int,name:string,number_format:string}>
|
||||||
|
*/
|
||||||
|
private function listActiveReceiptConfigs(): array
|
||||||
|
{
|
||||||
|
$all = $this->receiptConfigs->listAll();
|
||||||
|
$result = [];
|
||||||
|
foreach ($all as $config) {
|
||||||
|
if ((int) ($config['is_active'] ?? 0) !== 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$configId = (int) ($config['id'] ?? 0);
|
||||||
|
if ($configId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$result[] = [
|
||||||
|
'id' => $configId,
|
||||||
|
'name' => (string) ($config['name'] ?? ''),
|
||||||
|
'number_format' => (string) ($config['number_format'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/Modules/Automation/AutomationHistoryFilters.php
Normal file
58
src/Modules/Automation/AutomationHistoryFilters.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Automation;
|
||||||
|
|
||||||
|
use App\Core\Http\Request;
|
||||||
|
|
||||||
|
final class AutomationHistoryFilters
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array{event_type:string,execution_status:string,rule_id:int,order_id:int,date_from:string,date_to:string}
|
||||||
|
*/
|
||||||
|
public function extract(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'event_type' => trim((string) $request->input('history_event_type', '')),
|
||||||
|
'execution_status' => trim((string) $request->input('history_status', '')),
|
||||||
|
'rule_id' => max(0, (int) $request->input('history_rule_id', 0)),
|
||||||
|
'order_id' => max(0, (int) $request->input('history_order_id', 0)),
|
||||||
|
'date_from' => trim((string) $request->input('history_date_from', '')),
|
||||||
|
'date_to' => trim((string) $request->input('history_date_to', '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{event_type:string,execution_status:string,rule_id:int,order_id:int,date_from:string,date_to:string} $filters
|
||||||
|
*/
|
||||||
|
public function resolveActiveTab(Request $request, array $filters): string
|
||||||
|
{
|
||||||
|
$activeTab = trim((string) $request->input('tab', 'settings'));
|
||||||
|
if ($activeTab === 'history') {
|
||||||
|
return 'history';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((int) $request->input('history_page', 0) > 1) {
|
||||||
|
return 'history';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->hasAny($filters)) {
|
||||||
|
return 'history';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'settings';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{event_type:string,execution_status:string,rule_id:int,order_id:int,date_from:string,date_to:string} $filters
|
||||||
|
*/
|
||||||
|
public function hasAny(array $filters): bool
|
||||||
|
{
|
||||||
|
return $filters['event_type'] !== ''
|
||||||
|
|| $filters['execution_status'] !== ''
|
||||||
|
|| $filters['rule_id'] > 0
|
||||||
|
|| $filters['order_id'] > 0
|
||||||
|
|| $filters['date_from'] !== ''
|
||||||
|
|| $filters['date_to'] !== '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,13 +17,25 @@ final class AutomationModule implements ModuleProvider
|
|||||||
$services->set('automation.execution_log_repo', static fn () => new AutomationExecutionLogRepository($app->db()));
|
$services->set('automation.execution_log_repo', static fn () => new AutomationExecutionLogRepository($app->db()));
|
||||||
$services->set('automation.email_once_repo', static fn () => new AutomationEmailOnceRepository($app->db()));
|
$services->set('automation.email_once_repo', static fn () => new AutomationEmailOnceRepository($app->db()));
|
||||||
|
|
||||||
|
$services->set('automation.request_parser', static fn (ServiceRegistry $s) => new AutomationRequestParser(
|
||||||
|
$s->get('automation.repo')
|
||||||
|
));
|
||||||
|
$services->set('automation.form_view_model', static fn (ServiceRegistry $s) => new AutomationFormViewModel(
|
||||||
|
$s->get('automation.repo'),
|
||||||
|
$s->get('accounting.receipts.config_repo'),
|
||||||
|
$s->get('automation.request_parser')
|
||||||
|
));
|
||||||
|
$services->set('automation.history_filters', static fn () => new AutomationHistoryFilters());
|
||||||
|
|
||||||
$services->set('automation.controller', static fn (ServiceRegistry $s) => new AutomationController(
|
$services->set('automation.controller', static fn (ServiceRegistry $s) => new AutomationController(
|
||||||
$app->template(),
|
$app->template(),
|
||||||
$app->translator(),
|
$app->translator(),
|
||||||
$app->auth(),
|
$app->auth(),
|
||||||
$s->get('automation.repo'),
|
$s->get('automation.repo'),
|
||||||
$s->get('automation.execution_log_repo'),
|
$s->get('automation.execution_log_repo'),
|
||||||
$s->get('accounting.receipts.config_repo')
|
$s->get('automation.request_parser'),
|
||||||
|
$s->get('automation.form_view_model'),
|
||||||
|
$s->get('automation.history_filters')
|
||||||
));
|
));
|
||||||
|
|
||||||
$services->set('automation.service', static fn (ServiceRegistry $s) => new AutomationService(
|
$services->set('automation.service', static fn (ServiceRegistry $s) => new AutomationService(
|
||||||
|
|||||||
422
src/Modules/Automation/AutomationRequestParser.php
Normal file
422
src/Modules/Automation/AutomationRequestParser.php
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Automation;
|
||||||
|
|
||||||
|
use App\Core\Http\Request;
|
||||||
|
use App\Modules\Shipments\DeliveryStatus;
|
||||||
|
|
||||||
|
final class AutomationRequestParser
|
||||||
|
{
|
||||||
|
private const ALLOWED_EVENTS = ['receipt.created', 'shipment.created', 'shipment.status_changed', 'payment.status_changed', 'order.status_changed', 'order.status_aged', 'order.imported'];
|
||||||
|
private const ALLOWED_CONDITION_TYPES = ['integration', 'shipment_status', 'payment_status', 'payment_method', 'order_status', 'days_in_status'];
|
||||||
|
private const ALLOWED_ACTION_TYPES = ['send_email', 'issue_receipt', 'update_shipment_status', 'update_order_status'];
|
||||||
|
private const ALLOWED_RECIPIENTS = ['client', 'client_and_company', 'company'];
|
||||||
|
private const ALLOWED_RECEIPT_ISSUE_DATE_MODES = ['today', 'order_date', 'payment_date'];
|
||||||
|
private const ALLOWED_RECEIPT_DUPLICATE_POLICIES = ['skip_if_exists', 'allow_duplicates'];
|
||||||
|
private const PAYMENT_STATUS_OPTIONS = [
|
||||||
|
'0' => 'Nieopłacone',
|
||||||
|
'1' => 'Częściowo opłacone',
|
||||||
|
'2' => 'Opłacone',
|
||||||
|
];
|
||||||
|
private const PAYMENT_METHOD_OPTIONS = [
|
||||||
|
'cod' => 'Płatność przy odbiorze (COD)',
|
||||||
|
'transfer' => 'Przelew bankowy',
|
||||||
|
'online' => 'Karta / płatność online',
|
||||||
|
'other' => 'Inna',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly AutomationRepository $repository
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function allowedEvents(): array
|
||||||
|
{
|
||||||
|
return self::ALLOWED_EVENTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function allowedConditionTypes(): array
|
||||||
|
{
|
||||||
|
return self::ALLOWED_CONDITION_TYPES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function allowedActionTypes(): array
|
||||||
|
{
|
||||||
|
return self::ALLOWED_ACTION_TYPES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function allowedRecipients(): array
|
||||||
|
{
|
||||||
|
return self::ALLOWED_RECIPIENTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function allowedReceiptIssueDateModes(): array
|
||||||
|
{
|
||||||
|
return self::ALLOWED_RECEIPT_ISSUE_DATE_MODES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function allowedReceiptDuplicatePolicies(): array
|
||||||
|
{
|
||||||
|
return self::ALLOWED_RECEIPT_DUPLICATE_POLICIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function paymentStatusOptions(): array
|
||||||
|
{
|
||||||
|
return self::PAYMENT_STATUS_OPTIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function paymentMethodOptions(): array
|
||||||
|
{
|
||||||
|
return self::PAYMENT_METHOD_OPTIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validate(Request $request): ?string
|
||||||
|
{
|
||||||
|
$name = trim((string) $request->input('name', ''));
|
||||||
|
if ($name === '' || mb_strlen($name) > 128) {
|
||||||
|
return 'Nazwa jest wymagana (maks. 128 znaków)';
|
||||||
|
}
|
||||||
|
|
||||||
|
$eventType = (string) $request->input('event_type', '');
|
||||||
|
if (!in_array($eventType, self::ALLOWED_EVENTS, true)) {
|
||||||
|
return 'Nieprawidłowy typ zdarzenia';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($this->extractConditions($request)) === 0) {
|
||||||
|
return 'Wymagany jest co najmniej jeden warunek';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($this->extractActions($request)) === 0) {
|
||||||
|
return 'Wymagana jest co najmniej jedna akcja';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function extractRuleData(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => trim((string) $request->input('name', '')),
|
||||||
|
'event_type' => (string) $request->input('event_type', ''),
|
||||||
|
'is_active' => $request->input('is_active', null) !== null ? 1 : 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{type: string, value: array<string, mixed>}>
|
||||||
|
*/
|
||||||
|
public function extractConditions(Request $request): array
|
||||||
|
{
|
||||||
|
$raw = $request->input('conditions', []);
|
||||||
|
if (!is_array($raw)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($raw as $condition) {
|
||||||
|
if (!is_array($condition)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = (string) ($condition['type'] ?? '');
|
||||||
|
if (!in_array($type, self::ALLOWED_CONDITION_TYPES, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $this->parseConditionValue($type, $condition);
|
||||||
|
if ($value === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = ['type' => $type, 'value' => $value];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{type: string, config: array<string, mixed>}>
|
||||||
|
*/
|
||||||
|
public function extractActions(Request $request): array
|
||||||
|
{
|
||||||
|
$raw = $request->input('actions', []);
|
||||||
|
if (!is_array($raw)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($raw as $action) {
|
||||||
|
if (!is_array($action)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = (string) ($action['type'] ?? '');
|
||||||
|
if (!in_array($type, self::ALLOWED_ACTION_TYPES, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = $this->parseActionConfig($type, $action);
|
||||||
|
if ($config === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = ['type' => $type, 'config' => $config];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-buduje rekord regulki z surowego inputu (uzywane do ponownego renderu formularza po bledzie walidacji).
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function buildRule(Request $request, ?int $id = null): array
|
||||||
|
{
|
||||||
|
$raw = $request->input('conditions', []);
|
||||||
|
$conditions = [];
|
||||||
|
if (is_array($raw)) {
|
||||||
|
foreach ($raw as $cond) {
|
||||||
|
if (!is_array($cond)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$type = (string) ($cond['type'] ?? '');
|
||||||
|
$value = [];
|
||||||
|
if ($type === 'integration') {
|
||||||
|
$value = ['integration_ids' => is_array($cond['integration_ids'] ?? null) ? $cond['integration_ids'] : []];
|
||||||
|
} elseif ($type === 'shipment_status') {
|
||||||
|
$value = ['status_keys' => is_array($cond['shipment_status_keys'] ?? null) ? $cond['shipment_status_keys'] : []];
|
||||||
|
} elseif ($type === 'payment_status') {
|
||||||
|
$value = ['status_keys' => is_array($cond['payment_status_keys'] ?? null) ? $cond['payment_status_keys'] : []];
|
||||||
|
} elseif ($type === 'payment_method') {
|
||||||
|
$value = ['method_keys' => is_array($cond['payment_method_keys'] ?? null) ? $cond['payment_method_keys'] : []];
|
||||||
|
} elseif ($type === 'order_status') {
|
||||||
|
$value = ['order_status_codes' => is_array($cond['order_status_codes'] ?? null) ? $cond['order_status_codes'] : []];
|
||||||
|
} elseif ($type === 'days_in_status') {
|
||||||
|
$value = ['days' => max(1, (int) ($cond['days'] ?? 0))];
|
||||||
|
}
|
||||||
|
$conditions[] = ['condition_type' => $type, 'condition_value' => $value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$rawActions = $request->input('actions', []);
|
||||||
|
$actions = [];
|
||||||
|
if (is_array($rawActions)) {
|
||||||
|
foreach ($rawActions as $act) {
|
||||||
|
if (!is_array($act)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$type = (string) ($act['type'] ?? '');
|
||||||
|
$config = $act;
|
||||||
|
unset($config['type']);
|
||||||
|
$actions[] = ['action_type' => $type, 'action_config' => $config];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$rule = [
|
||||||
|
'name' => trim((string) $request->input('name', '')),
|
||||||
|
'event_type' => (string) $request->input('event_type', ''),
|
||||||
|
'is_active' => $request->input('is_active', null) !== null ? 1 : 0,
|
||||||
|
'conditions' => $conditions,
|
||||||
|
'actions' => $actions,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($id !== null) {
|
||||||
|
$rule['id'] = $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $condition
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
private function parseConditionValue(string $type, array $condition): ?array
|
||||||
|
{
|
||||||
|
if ($type === 'integration') {
|
||||||
|
$ids = $condition['integration_ids'] ?? [];
|
||||||
|
if (!is_array($ids)) {
|
||||||
|
$ids = [];
|
||||||
|
}
|
||||||
|
$integrationIds = array_values(array_filter(
|
||||||
|
array_map('intval', $ids),
|
||||||
|
static fn (int $id): bool => $id > 0
|
||||||
|
));
|
||||||
|
|
||||||
|
return count($integrationIds) > 0 ? ['integration_ids' => $integrationIds] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'shipment_status') {
|
||||||
|
$keys = $condition['shipment_status_keys'] ?? [];
|
||||||
|
if (!is_array($keys)) {
|
||||||
|
$keys = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedKeys = DeliveryStatus::getAllStatuses();
|
||||||
|
$statusKeys = array_values(array_filter(
|
||||||
|
array_map(static fn (mixed $key): string => trim((string) $key), $keys),
|
||||||
|
static fn (string $key): bool => $key !== '' && in_array($key, $allowedKeys, true)
|
||||||
|
));
|
||||||
|
|
||||||
|
return count($statusKeys) > 0 ? ['status_keys' => array_values(array_unique($statusKeys))] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'payment_status') {
|
||||||
|
$keys = $condition['payment_status_keys'] ?? [];
|
||||||
|
if (!is_array($keys)) {
|
||||||
|
$keys = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedKeys = array_map('strval', array_keys(self::PAYMENT_STATUS_OPTIONS));
|
||||||
|
$statusKeys = array_values(array_filter(
|
||||||
|
array_map(static fn (mixed $key): string => trim((string) $key), $keys),
|
||||||
|
static fn (string $key): bool => $key !== '' && in_array($key, $allowedKeys, true)
|
||||||
|
));
|
||||||
|
|
||||||
|
return count($statusKeys) > 0 ? ['status_keys' => array_values(array_unique($statusKeys))] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'payment_method') {
|
||||||
|
$keys = $condition['payment_method_keys'] ?? [];
|
||||||
|
if (!is_array($keys)) {
|
||||||
|
$keys = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedKeys = array_keys(self::PAYMENT_METHOD_OPTIONS);
|
||||||
|
$methodKeys = array_values(array_filter(
|
||||||
|
array_map(static fn (mixed $key): string => trim((string) $key), $keys),
|
||||||
|
static fn (string $key): bool => $key !== '' && in_array($key, $allowedKeys, true)
|
||||||
|
));
|
||||||
|
|
||||||
|
return count($methodKeys) > 0 ? ['method_keys' => array_values(array_unique($methodKeys))] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'order_status') {
|
||||||
|
$codes = $condition['order_status_codes'] ?? [];
|
||||||
|
if (!is_array($codes)) {
|
||||||
|
$codes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$availableCodes = array_map(
|
||||||
|
static fn (array $row): string => strtolower(trim((string) ($row['code'] ?? ''))),
|
||||||
|
$this->repository->listActiveOrderStatuses()
|
||||||
|
);
|
||||||
|
|
||||||
|
$statusCodes = array_values(array_filter(
|
||||||
|
array_map(static fn (mixed $code): string => strtolower(trim((string) $code)), $codes),
|
||||||
|
static fn (string $code): bool => $code !== '' && in_array($code, $availableCodes, true)
|
||||||
|
));
|
||||||
|
|
||||||
|
return count($statusCodes) > 0 ? ['order_status_codes' => array_values(array_unique($statusCodes))] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'days_in_status') {
|
||||||
|
$days = (int) ($condition['days'] ?? 0);
|
||||||
|
|
||||||
|
return $days >= 1 ? ['days' => $days] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $action
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
private function parseActionConfig(string $type, array $action): ?array
|
||||||
|
{
|
||||||
|
if ($type === 'send_email') {
|
||||||
|
$templateId = (int) ($action['template_id'] ?? 0);
|
||||||
|
$recipient = (string) ($action['recipient'] ?? '');
|
||||||
|
$sendOncePerOrder = isset($action['send_once_per_order']) && (int) $action['send_once_per_order'] === 1 ? 1 : 0;
|
||||||
|
|
||||||
|
if ($templateId <= 0 || !in_array($recipient, self::ALLOWED_RECIPIENTS, true)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'template_id' => $templateId,
|
||||||
|
'recipient' => $recipient,
|
||||||
|
'send_once_per_order' => $sendOncePerOrder,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'issue_receipt') {
|
||||||
|
$configId = (int) ($action['receipt_config_id'] ?? 0);
|
||||||
|
$issueDateMode = (string) ($action['issue_date_mode'] ?? '');
|
||||||
|
$duplicatePolicy = (string) ($action['duplicate_policy'] ?? '');
|
||||||
|
|
||||||
|
if (
|
||||||
|
$configId <= 0
|
||||||
|
|| !in_array($issueDateMode, self::ALLOWED_RECEIPT_ISSUE_DATE_MODES, true)
|
||||||
|
|| !in_array($duplicatePolicy, self::ALLOWED_RECEIPT_DUPLICATE_POLICIES, true)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'receipt_config_id' => $configId,
|
||||||
|
'issue_date_mode' => $issueDateMode,
|
||||||
|
'duplicate_policy' => $duplicatePolicy,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'update_shipment_status') {
|
||||||
|
$statusKey = trim((string) ($action['shipment_status_key'] ?? ''));
|
||||||
|
if ($statusKey === '' || !in_array($statusKey, DeliveryStatus::getAllStatuses(), true)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['status_key' => $statusKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'update_order_status') {
|
||||||
|
$statusCode = trim((string) ($action['order_status_code'] ?? ''));
|
||||||
|
if ($statusCode === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$availableCodes = array_map(
|
||||||
|
static fn (array $row): string => trim((string) ($row['code'] ?? '')),
|
||||||
|
$this->repository->listActiveOrderStatuses()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!in_array($statusCode, $availableCodes, true)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['status_code' => $statusCode];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user