From e77b0f12a219eae3e6280e3bd3bfa8fcdc5cb9b2 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Tue, 19 May 2026 21:25:07 +0200 Subject: [PATCH] refactor(routing): module providers + lazy ServiceRegistry Rozbicie routes/web.php (859 lin.) na 24 klasy Module.php zgodnie z quality_risks.md priorytet #4. Kontroler buduje sie tylko gdy router trafi w jego route (lazy closure factory + memoizacja per request). - src/Core/Routing/ServiceRegistry.php (~55 lin.) + ModuleProvider interface - 24 module providers w src/Modules/*/Module.php - routes/web.php: 859 -> 78 lin. (orkiestrator) - 7 testow ServiceRegistry pass, zero regresji w istniejacych testach - 191 route'ow zachowanych 1:1 (diff baseline vs after pusty) - DeliveryStatus::setRepository przeniesione do ShipmentsModule Co-Authored-By: Claude Opus 4.7 (1M context) --- .paul/STATE.md | 3 +- .paul/codebase/architecture.md | 27 +- .paul/codebase/quality_risks.md | 4 +- .paul/codebase/tech_changelog.md | 33 + .../20260519-1200-refactor-routes-web/PLAN.md | 503 ++++++++++ .../SUMMARY.md | 88 ++ .../routes-after.txt | 191 ++++ .../routes-baseline.txt | 191 ++++ bin/smoke_routes.php | 82 ++ routes/web.php | 917 ++---------------- src/Core/Routing/ModuleProvider.php | 27 + src/Core/Routing/ServiceRegistry.php | 64 ++ src/Modules/Accounting/AccountingModule.php | 141 +++ src/Modules/Auth/AuthModule.php | 28 + src/Modules/Automation/AutomationModule.php | 55 ++ src/Modules/Cron/CronModule.php | 93 ++ src/Modules/Email/EmailModule.php | 88 ++ src/Modules/Info/InfoModule.php | 36 + .../Notifications/NotificationsModule.php | 37 + src/Modules/Orders/OrdersModule.php | 67 ++ src/Modules/Printing/PrintingModule.php | 83 ++ .../Settings/AllegroIntegrationModule.php | 102 ++ .../Settings/ApaczkaIntegrationModule.php | 37 + .../Settings/ErliIntegrationModule.php | 86 ++ .../Settings/FakturowniaIntegrationModule.php | 40 + .../Settings/HostedSmsIntegrationModule.php | 38 + .../Settings/InpostIntegrationModule.php | 34 + .../Settings/IntegrationsHubModule.php | 39 + .../Settings/PolkurierIntegrationModule.php | 38 + src/Modules/Settings/SettingsModule.php | 60 ++ .../Settings/ShopproIntegrationModule.php | 52 + .../Settings/SmsplanetIntegrationModule.php | 38 + src/Modules/Shipments/ShipmentsModule.php | 135 +++ src/Modules/Sms/SmsModule.php | 58 ++ src/Modules/Statistics/StatisticsModule.php | 29 + src/Modules/Users/UsersModule.php | 33 + .../Unit/Core/Routing/ServiceRegistryTest.php | 126 +++ 37 files changed, 2849 insertions(+), 854 deletions(-) create mode 100644 .paul/codebase/tech_changelog.md create mode 100644 .paul/plans/20260519-1200-refactor-routes-web/PLAN.md create mode 100644 .paul/plans/20260519-1200-refactor-routes-web/SUMMARY.md create mode 100644 .paul/plans/20260519-1200-refactor-routes-web/routes-after.txt create mode 100644 .paul/plans/20260519-1200-refactor-routes-web/routes-baseline.txt create mode 100644 bin/smoke_routes.php create mode 100644 src/Core/Routing/ModuleProvider.php create mode 100644 src/Core/Routing/ServiceRegistry.php create mode 100644 src/Modules/Accounting/AccountingModule.php create mode 100644 src/Modules/Auth/AuthModule.php create mode 100644 src/Modules/Automation/AutomationModule.php create mode 100644 src/Modules/Cron/CronModule.php create mode 100644 src/Modules/Email/EmailModule.php create mode 100644 src/Modules/Info/InfoModule.php create mode 100644 src/Modules/Notifications/NotificationsModule.php create mode 100644 src/Modules/Orders/OrdersModule.php create mode 100644 src/Modules/Printing/PrintingModule.php create mode 100644 src/Modules/Settings/AllegroIntegrationModule.php create mode 100644 src/Modules/Settings/ApaczkaIntegrationModule.php create mode 100644 src/Modules/Settings/ErliIntegrationModule.php create mode 100644 src/Modules/Settings/FakturowniaIntegrationModule.php create mode 100644 src/Modules/Settings/HostedSmsIntegrationModule.php create mode 100644 src/Modules/Settings/InpostIntegrationModule.php create mode 100644 src/Modules/Settings/IntegrationsHubModule.php create mode 100644 src/Modules/Settings/PolkurierIntegrationModule.php create mode 100644 src/Modules/Settings/SettingsModule.php create mode 100644 src/Modules/Settings/ShopproIntegrationModule.php create mode 100644 src/Modules/Settings/SmsplanetIntegrationModule.php create mode 100644 src/Modules/Shipments/ShipmentsModule.php create mode 100644 src/Modules/Sms/SmsModule.php create mode 100644 src/Modules/Statistics/StatisticsModule.php create mode 100644 src/Modules/Users/UsersModule.php create mode 100644 tests/Unit/Core/Routing/ServiceRegistryTest.php diff --git a/.paul/STATE.md b/.paul/STATE.md index e03f5ec..f832569 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -4,7 +4,8 @@ **Ostatnia aktualizacja:** 2026-05-19 ## Aktywna praca -Brak aktywnego PLAN.md w `.paul/plans/`. +Brak aktywnego PLAN.md. Ostatnio zakonczony: `.paul/plans/20260519-1200-refactor-routes-web/` (SUMMARY.md). +Routing modularny + lazy DI wdrozone (routes/web.php: 859 -> 78 lin., 24 nowe `Module.php`). ## Kontekst sesji - Galaz: `main` (czysta). diff --git a/.paul/codebase/architecture.md b/.paul/codebase/architecture.md index 4a97fe9..f59faee 100644 --- a/.paul/codebase/architecture.md +++ b/.paul/codebase/architecture.md @@ -4,7 +4,7 @@ ## Przeglad -Monolityczna aplikacja PHP w stylu **modular monolith**: warstwa rdzeniowa (`src/Core/`) + moduly domenowe (`src/Modules//`). Brak DI containera — zaleznosci montowane recznie w `routes/web.php` (kompozycja w stylu "poor man's DI"). +Monolityczna aplikacja PHP w stylu **modular monolith**: warstwa rdzeniowa (`src/Core/`) + moduly domenowe (`src/Modules//`). Brak DI containera w sensie autowire/refleksji — zaleznosci montowane jawnie w klasach `Module.php` (kompozycja w stylu "poor man's DI", od 2026-05-19 z leniwa rejestracja przez `ServiceRegistry`). ## Punkty wejscia @@ -15,9 +15,32 @@ Monolityczna aplikacja PHP w stylu **modular monolith**: warstwa rdzeniowa (`src | CLI migrate | `bin/migrate.php` -> `App\Core\Database\Migrator` | migracje SQL | | CLI backfill/utils | `bin/backfill_*.php`, `bin/fix_*.php`, `bin/deploy_*.php` | operacje serwisowe | +## Routing modularny (od 2026-05-19) + +`routes/web.php` (~78 lin.) jest orkiestratorem. Sklada sie z listy modulow i dwoch petli: + +1. `register()` — kazdy modul zglasza swoje serwisy do `ServiceRegistry` (closure factory, brak konstrukcji), +2. `routes()` — kazdy modul rejestruje swoje route'y, uzywajac `$services->lazy(id, method)`. + +Komponenty: +- `src/Core/Routing/ServiceRegistry.php` — leniwy rejestr `set/get/has/lazy` z memoizacja per request. Brak autowire i refleksji. +- `src/Core/Routing/ModuleProvider.php` — interfejs z `register(ServiceRegistry, Application)` i `routes(Router, ServiceRegistry, AuthMiddleware, Application)`. +- `src/Modules//Module.php` (24 klasy) — implementacje providerow per domena. + +Lista modulow (`routes/web.php`): +- Info, Auth, Users, Cron, Settings, Notifications, Email, Sms, Accounting, Automation, Shipments, Printing, Orders, Statistics, +- IntegrationsHub + 9 dostawcow integracji (Allegro, Apaczka, Inpost, Shoppro, Erli, Polkurier, Fakturownia, HostedSms, Smsplanet). + +Konwencja kluczy w `ServiceRegistry`: `domain.role` (np. `orders.controller`, `integrations.allegro.repo`, `shared.companies.repo`). Klucze `shared.*` to zaleznosci wspoldzielone miedzy modulami (companies, shipment_packages, cron, carrier_delivery_mappings, print_jobs). + +Korzysci wzgledem poprzedniego monolitycznego `routes/web.php` (859 lin.): +- Kontroler buduje sie tylko gdy router trafi w jego route (lazy). +- Kod modulu mieszka razem z modulem. +- Dodanie nowego modulu = 1 klasa + 1 wpis w `routes/web.php`. + ## Warstwy -1. **Routing** — `src/Core/Routing/Router.php` (mapowanie URL -> controller/action, parametry sciezki `{id}`). +1. **Routing** — `src/Core/Routing/Router.php` (mapowanie URL -> controller/action, parametry sciezki `{id}`), `ServiceRegistry`, `ModuleProvider`. 2. **Controllers** — `src/Modules/*/...*Controller.php` (40 klas). Walidacja danych z `Request`, wolanie serwisow/repozytoriow, renderowanie widoku lub JSON-a (`Response`). 3. **Services** — `src/Modules/*/...*Service.php` (26 klas). Logika domenowa (synchronizacje, importy, generatory PDF, integracje SMS/Email). 4. **Repositories** — `src/Modules/*/...*Repository.php` (47 klas). PDO + prepared statements. Brak ORM. diff --git a/.paul/codebase/quality_risks.md b/.paul/codebase/quality_risks.md index 619cf58..48771bc 100644 --- a/.paul/codebase/quality_risks.md +++ b/.paul/codebase/quality_risks.md @@ -24,7 +24,7 @@ Konwencja (`CLAUDE.md`): funkcja/klasa zwykle do 30-50 linii, max 3 poziomy zagn | `src/Modules/Shipments/DeliveryStatus.php` | 657 | encja statusow uzywana globalnie — zmiany dotykaja wszystkich integracji. | | `src/Modules/Settings/AllegroIntegrationController.php` | 653 | | | `src/Modules/Statistics/OrdersStatisticsController.php` | 640 | | -| `routes/web.php` | 859 | jeden duzy plik kompozycji DI. Kandydat na rozbicie na partials per modul. | +| ~~`routes/web.php`~~ | ~~859~~ -> 78 | ✅ Zrefaktorowane 2026-05-19 — `ServiceRegistry` + 24 klasy `Module.php`. Patrz `.paul/plans/20260519-1200-refactor-routes-web/SUMMARY.md`. | ## Luki testowe (krytyczne) @@ -65,5 +65,5 @@ Skan `grep -rn "TODO|FIXME|HACK|XXX"` na `src/` i `routes/` nie zwrocil zadnych 1. **Test coverage** dla Shoppro i InPost (high blast radius). 2. **Dekompozycja `OrdersController`** (1490 lin.) — minimum 3 sub-kontrolery. 3. **Bazowy `IntegrationController`** lub trait — eliminacja powtarzajacej sie tresci `index`/`save`/`test`. -4. **Rozbicie `routes/web.php`** na pliki per modul. +4. ~~**Rozbicie `routes/web.php`** na pliki per modul.~~ ✅ Zrobione 2026-05-19 (Module Providers + lazy `ServiceRegistry`). 5. **Wiecej komponentow widokow** — wyodrebnic powtarzajace sie sekcje. diff --git a/.paul/codebase/tech_changelog.md b/.paul/codebase/tech_changelog.md new file mode 100644 index 0000000..1fb87a1 --- /dev/null +++ b/.paul/codebase/tech_changelog.md @@ -0,0 +1,33 @@ +# Tech Changelog + +Chronologiczny log zmian technicznych (co i dlaczego). Najnowsze na gorze. + +## 2026-05-19 — Routing modularny + lazy DI + +### Co +- Wprowadzono `src/Core/Routing/ServiceRegistry.php` (lazy factory + memoizacja, ~55 lin.). +- Wprowadzono `src/Core/Routing/ModuleProvider.php` (interfejs `register()` + `routes()`). +- Utworzono 24 klasy `Module.php` w `src/Modules/*` (kazda implementuje `ModuleProvider`). +- Przepisano `routes/web.php` z 859 lin. (monolityczna kompozycja DI + 191 route'ow) na ~78 lin. orkiestratora. +- Dodano 7 testow jednostkowych dla `ServiceRegistry` (memoizacja, missing key, lazy defer, overwrite, cross-lookup). + +### Dlaczego +- `quality_risks.md` (priorytet #4): rozbicie `routes/web.php`. +- Zysk perf: kontroler buduje sie tylko gdy router trafi w jego route (np. `/health` nie konstruuje `OrdersController` ani 9 integracji). +- Spojnosc z modular monolith: kod modulu (controller + routes + DI) zyje w jednym katalogu. +- Latwiejsze dodawanie nowych modulow. + +### Wplyw +- `routes/web.php`: 859 lin. -> 78 lin. (orkiestrator). +- `src/Modules/*/`: +24 klasy `Module.php`. +- `src/Core/Application.php`: bez zmian (kontrakt `require routes/web.php` -> callable zachowany). +- `tests/`: +1 plik z 7 testami (`tests/Unit/Core/Routing/ServiceRegistryTest.php`). +- `bin/smoke_routes.php`: pomocniczy skrypt smoke (autoload + class load + register() na modulach bez DB). +- 191 unikalnych route'ow zachowanych 1:1 (diff `routes-baseline.txt` vs `routes-after.txt` pusty). +- phpunit: 93 testy (z 86 przed), 7 nowych pass, zero nowych regresji (3 errors + 15 failures sa pre-existing i nie dotycza refaktoru). + +### Decyzje +- 9 osobnych providerow integracji (nie jeden monolit `IntegrationsModule`) — kazdy <120 lin., latwiej dodac nowa. +- Lokalizacja integracji: plasko w `src/Modules/Settings/` (kontrolery juz tam byly). +- Side-effect `DeliveryStatus::setRepository()` — przeniesiony do `ShipmentsModule::register()` (eliminuje globalny kod z `routes/web.php`). +- `ServiceRegistry` celowo bez autowire, refleksji, scope managerow. Jawne klucze i factory. CLAUDE.md: "kod czytelny dla obcego, malo magii". diff --git a/.paul/plans/20260519-1200-refactor-routes-web/PLAN.md b/.paul/plans/20260519-1200-refactor-routes-web/PLAN.md new file mode 100644 index 0000000..5d2a036 --- /dev/null +++ b/.paul/plans/20260519-1200-refactor-routes-web/PLAN.md @@ -0,0 +1,503 @@ +--- +plan: 20260519-1200-refactor-routes-web +type: refactor +wave: 1 +depends_on: [] +files_modified: + - routes/web.php + - src/Core/Routing/ServiceRegistry.php (nowy) + - src/Core/Routing/ModuleProvider.php (nowy interfejs) + - src/Core/Routing/ModuleRegistrar.php (nowy, opcjonalnie) + - src/Modules//Module.php (nowe, 1 plik per modul) +autonomous: false +--- + +# PLAN — Refaktoryzacja routingu (Module Providers + lazy DI) + +## Cel +Zastapic monolityczny `routes/web.php` (859 lin.) struktura modularna zgodna z `architecture.md` ("modular monolith"): + +1. Wprowadzic lekki **ServiceRegistry** (lazy factory + memoizacja) zamiast tablicy `$s = [...]` z eagerly zbudowanymi obiektami. +2. Wprowadzic interfejs **ModuleProvider** z dwoma metodami: `register(ServiceRegistry)` (zaleznosci) + `routes(Router, ServiceRegistry, AuthMiddleware)` (URL-e). +3. Kazdy modul domenowy dostaje wlasny `Module.php` — kod modulu mieszka razem z modulem. +4. `routes/web.php` redukuje sie do ~20 linii: lista providerow, `register()` na wszystkich, `routes()` na wszystkich. + +**Glowne korzysci wzgledem mechanicznego splitu:** +- **Lazy:** kontroler buduje sie tylko gdy router trafi w jego route (`/health` nie konstruuje `OrdersController`, `ShopproIntegrationsController` itd.). Realny zysk perf na requestach. +- **Spojnosc z modular monolith:** modul = `src/Modules/X/` + `Module.php` (provider + routes), zero plikow w `routes/web/` i `routes/services/`. +- **Latwo dodac modul:** 1 klasa zamiast 2 plikow w 2 katalogach. +- **Cross-module deps:** rozwiazywane przez `ServiceRegistry::get('id')` — naturalna kolejnosc, brak ryzyka cyrkularnych require'ow. + +## Kontekst +- `quality_risks.md` rekomendacja #4: rozbicie `routes/web.php`. +- `CLAUDE.md`: "kod czytelny dla obcego, malo magii, kazda funkcja/klasa <50 lin., max 3 poziomy zagniezdzen, brak DI containera (zaakceptowane ryzyko: rezne montowanie celowo proste)". +- Opcja B = ServiceRegistry to ~30 lin. (klucz -> closure, memo cache), NIE pelnoprawny DI container (brak autowire, brak refleksji, brak XML/YAML). +- `architecture.md`: 14 modulow w `src/Modules/`. + +## Impact scan + +- Mode: plan (codebase-memory-mcp dostepne, jscpd/ast-grep wylaczone polityka) +- Status: ok +- Pliki modyfikowane: + - `routes/web.php` (przepis na ~20 lin.). + - Nowe: `src/Core/Routing/ServiceRegistry.php`, `src/Core/Routing/ModuleProvider.php`. + - Nowe: po 1 klasie `Module.php` w kazdym `src/Modules//`. +- Pliki czytane: wszystkie kontrolery/serwisy/repo (sprawdzenie sygnatur konstruktorow — bez zmian). +- `src/Core/Application.php` — bez zmian (`require routes/web.php` -> callable nadal). +- Ryzyka: + - **Lazy resolution + missing key**: jezeli moduł odwoluje sie do `$services->get('foo')` ktore nie zostalo zarejestrowane → wyjatek przy pierwszym hicie route. Mitigacja: `ServiceRegistry::get()` rzuca `RuntimeException` z czytelnym komunikatem; podczas testow smoke przejdziemy po kluczowych endpointach. + - **Kolejnosc `register()`:** factory sa closure, wiec kolejnosc dodawania kluczy NIE ma znaczenia. Liczy sie tylko zeby do momentu `routes()` wszystkie potrzebne klucze byly zarejestrowane. Wymusza to `routes/web.php`: najpierw petla `register()` na wszystkich, dopiero potem petla `routes()`. + - **Memoizacja:** ServiceRegistry trzyma instancje w polu (singleton per request). Identyczne semantyki co obecne wspoldzielone zmienne (`$apaczkaIntegrationRepository` itp.). + - **Webhooki publiczne** (`/cron`, `/webhooks/smsplanet/inbound`, `/health`) — bez middleware. Trafiaja do `InfoModule` / `CronModule` / `SmsModule`. + - **Print API** uzywa `apiKeyMiddleware` (nie `authMiddleware`) dla endpointow z klienta Windows. Zachowac. +- Raporty: `.paul/codebase/impact_map.md`, `.paul/codebase/quality_risks.md` (#4). + + +## Acceptance criteria + +### AC-1: Klasy szkieletowe Core +**Given** brak infrastruktury modularnego routingu +**When** refaktor zakonczony +**Then** istnieja: +- `src/Core/Routing/ServiceRegistry.php` (~30-50 lin., final class, `set/get/has`). +- `src/Core/Routing/ModuleProvider.php` (interface z `register()` + `routes()`). +- Obie klasy maja testy jednostkowe w `tests/Unit/Core/Routing/` (ServiceRegistry: memoizacja, missing key throws, has=true after set). + +### AC-2: Module providers per modul +**Given** 14 modulow w `src/Modules/` +**When** refaktor zakonczony +**Then** kazdy modul domenowy posiadajacy route'y ma swoj `Module.php` implementujacy `ModuleProvider`. Lista modulow (~15-18 providerow): +- `Auth/AuthModule`, `Users/UsersModule` +- `Info/InfoModule` (publiczne: `/`, `/info`, `/health`) +- `Orders/OrdersModule`, `Statistics/StatisticsModule` +- `Accounting/AccountingModule` (faktury, paragony, eksport) +- `Shipments/ShipmentsModule` +- `Automation/AutomationModule` +- `Email/EmailModule` (mailboxes + templates) +- `Sms/SmsModule` (templates + webhook smsplanet) +- `Notifications/NotificationsModule` +- `Cron/CronModule` (cron settings + public endpoint) +- `Printing/PrintingModule` +- `Settings/SettingsModule` (database, statuses, status-groups, company, delivery-statuses, delivery-status-mappings, project-mappings, print settings — settings hub) +- `Settings/Integrations/IntegrationsModule` (hub `/settings/integrations`) — opcjonalnie + sub providery per dostawca: `Allegro`, `Apaczka`, `Inpost`, `Shoppro`, `Erli`, `Polkurier`, `Fakturownia`, `HostedSms`, `Smsplanet` (kazdy osobny `IntegrationModule.php` w `src/Modules/Settings/`). + +### AC-3: routes/web.php = orkiestrator +**Given** obecnie `routes/web.php` 859 lin. +**When** refaktor zakonczony +**Then** `routes/web.php` ma <= 50 linii, zawiera tylko: liste modulow, petla `register()`, petla `routes()`. Brak `new` poza ServiceRegistry, brak `use App\Modules\...` poza ModuleProvider implementacjami. + +### AC-4: Lazy resolution +**Given** request na `/health` +**When** dispatch +**Then** zbudowany jest co najwyzej `InfoController` (lub `Closure` jezeli `/health` jest inline). `OrdersController`, `ShopproIntegrationsController`, integracje, automation — NIE sa konstruowane. Zweryfikowac przez `debug_backtrace` lub licznik w klasie testowej (opcjonalnie metryka czasu — przed/po). + +### AC-5: Parytetowosc funkcjonalna +**Given** lista route'ow dostepna przed refaktorem (baseline) +**When** po refaktorze +**Then**: +- Lista route'ow (URL + method + middleware) identyczna z baseline (skrypt diff). +- Smoke test: `/login`, `/orders/list`, `/settings/integrations`, `/settings/automation`, `/accounting`, `/health`, `/cron?token=...` zwracaja te same kody i tresc co przed refaktorem. +- `vendor/bin/phpunit` bez regresji. + +### AC-6: Sygnatury domenowe niezmienione +**Given** kontrolery, serwisy, repozytoria +**When** po refaktorze +**Then** zadna klasa w `src/Modules//*Controller.php`, `*Service.php`, `*Repository.php` nie zmienia konstruktora ani metod publicznych. Refaktor ogranicza sie do dodania `Module.php`. + +## Boundaries + +### DO NOT CHANGE +- Sygnatury kontrolerow, serwisow, repozytoriow. +- `src/Core/Application.php` (kontrakt ladowania routes). +- Sciezki URL (kazdy `$router->get/post(...)` zachowuje path, controller@action, middleware). +- Pliki widokow, SCSS, migracji. +- Polityka secrets (`(string) $app->config('app.integrations.secret', '')` przekazywane do repozytoriow integracji). + +### SCOPE LIMITS +- ServiceRegistry NIE wprowadza autowire/refleksji/parser configa. Wylacznie `set(string, Closure)` + `get(string): mixed` + `has(string)`. ~30 linii. +- Brak `bind/singleton/instance` w stylu Laravel. Wszystko jest singleton per request (default). +- Nie refaktorujemy zadnego `*Controller` (np. `OrdersController` 1490 lin. — osobny plan). +- Nie wprowadzamy atrybutow PHP 8 na route'ach (Opcja C odrzucona). + +## Projekt klas + +### `ServiceRegistry` (szkic, ~30 lin.) + +```php + */ + private array $factories = []; + /** @var array */ + private array $instances = []; + + public function set(string $id, Closure $factory): void + { + $this->factories[$id] = $factory; + } + + public function has(string $id): bool + { + return isset($this->factories[$id]); + } + + /** @return mixed */ + public function get(string $id) + { + if (array_key_exists($id, $this->instances)) { + return $this->instances[$id]; + } + if (!isset($this->factories[$id])) { + throw new RuntimeException("Service not registered: {$id}"); + } + return $this->instances[$id] = ($this->factories[$id])($this); + } +} +``` + +### `ModuleProvider` (interfejs) + +```php +set('orders.notes_service', static fn (ServiceRegistry $s) => new OrderNotesService($app->db())); + $services->set('orders.shipment_packages.repo', static fn () => new ShipmentPackageRepository($app->db())); + $services->set('orders.controller', static fn (ServiceRegistry $s) => new OrdersController( + $app->template(), $app->translator(), $app->auth(), + $app->orders(), + $s->get('orders.shipment_packages.repo'), + $s->get('accounting.receipts.repo'), + $s->get('accounting.receipts.config_repo'), + $s->get('email.sending_service'), + $s->get('email.templates.repo'), + $s->get('email.mailboxes.repo'), + $app->basePath('storage'), + $s->get('printing.jobs.repo'), + $s->get('integrations.shoppro.repo'), + $s->get('automation.service'), + $s->get('accounting.invoices.repo'), + $s->get('accounting.invoices.config_repo'), + $s->get('sms.messages.repo'), + $s->get('sms.conversation_service'), + $s->get('sms.templates.repo'), + $s->get('sms.variable_resolver'), + $s->get('shared.companies.repo'), + $s->get('orders.notes_service') + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth): void + { + $c = static fn (string $m) => [$services->get('orders.controller'), $m]; + + $router->get('/orders', static fn () => \App\Core\Http\Response::redirect('/orders/list'), [$auth]); + $router->get('/orders/list', $c('index'), [$auth]); + $router->get('/orders/{id}', $c('show'), [$auth]); + $router->post('/orders/{id}/status', $c('updateStatus'), [$auth]); + $router->post('/orders/{id}/details/update', $c('updateDetails'), [$auth]); + $router->post('/orders/{id}/sms/send', $c('sendSms'), [$auth]); + $router->get('/orders/{id}/sms/template', $c('smsTemplate'), [$auth]); + $router->post('/orders/{id}/send-email', $c('sendEmail'), [$auth]); + $router->post('/orders/{id}/email-preview', $c('emailPreview'), [$auth]); + $router->get('/api/orders/search', $c('quickSearch'), [$auth]); + $router->get('/api/orders/{id}/preview', $c('preview'), [$auth]); + $router->post('/orders/{id}/notes', $c('storeNote'), [$auth]); + $router->post('/orders/{id}/notes/{noteId}/update', $c('updateNote'), [$auth]); + $router->post('/orders/{id}/notes/{noteId}/delete', $c('deleteNote'), [$auth]); + $router->post('/orders/{id}/payment/add', $c('addPayment'), [$auth]); + $router->post('/orders/{id}/invoice-requested/toggle', $c('toggleInvoiceRequested'), [$auth]); + } +} +``` + +**Uwaga lazy:** `$services->get('orders.controller')` wykonuje sie dopiero gdy router trafi w route i wywola handler — zostalo opakowane w closure `$c`, ktore woluje get() przy wywolaniu, nie przy rejestracji. + +> **Decyzja techniczna do potwierdzenia w Task 1:** sprawdzic jak `Router::dispatch()` wywoluje handler. Jezeli przyjmuje `[$obj, 'method']` array, mozemy musiec opakowac w `Closure::fromCallable` lub po prostu `static fn (...$args) => $services->get('orders.controller')->index(...$args)`. Sprawdzic kontrakt. + +### Nowy `routes/web.php` (szkic ~30 lin.) + +```php +register($services, $app); + } + + $auth = new AuthMiddleware($app->auth()); + foreach ($modules as $m) { + $m->routes($app->router(), $services, $auth); + } + + // Side-effect z obecnego routes/web.php (musi pozostac): + \App\Modules\Shipments\DeliveryStatus::setRepository($services->get('shipments.delivery_status.repo')); +}; +``` + +## Tasks + +### Task 1 — Infrastruktura Core +**Files:** +- `src/Core/Routing/ServiceRegistry.php` (nowy) +- `src/Core/Routing/ModuleProvider.php` (nowy interfejs) +- `tests/Unit/Core/Routing/ServiceRegistryTest.php` (nowy) + +**Action:** +1. Utworzyc `ServiceRegistry` jak w szkicu. +2. Utworzyc `ModuleProvider` (interfejs). +3. Napisac test: `set+get` zwraca instancje; `get` 2× zwraca to samo (memoizacja); `has` true/false; `get(missing)` rzuca `RuntimeException` z czytelnym komunikatem. +4. Sprawdzic kontrakt `Router::dispatch()` — czy potrafi wywolac `Closure` i `[obj, 'method']`. Jezeli tylko callable, opakowac `[ctrl, 'method']` w closure. + +**Verify:** +- `vendor/bin/phpunit tests/Unit/Core/Routing/` pass. +- `php -l src/Core/Routing/*.php` OK. + +**Done:** AC-1. + +### Task 2 — Baseline route'ow +**Files:** brak modyfikacji + +**Action:** +1. Wyciagnac liste route'ow z obecnego `routes/web.php`: + ```bash + grep -E '\$router->(get|post|put|delete)' routes/web.php \ + | sed -E "s/.*->(get|post|put|delete)\((.*)/\1 \2/" \ + | sed 's/,.*//' | sort > .paul/plans/20260519-1200-refactor-routes-web/routes-baseline.txt + ``` +2. Zachowac wynik jako artefakt do porownania. +3. Policzyc liczbe linii i URL-i. Spodziewane ~130 route'ow. + +**Verify:** plik istnieje, niezerowy. + +**Done:** Baseline gotowy pod AC-5. + +### Task 3 — Module providers (per modul, batch 1: core domain) +**Files:** +- `src/Modules/Auth/AuthModule.php` +- `src/Modules/Users/UsersModule.php` +- `src/Modules/Info/InfoModule.php` +- `src/Modules/Cron/CronModule.php` +- `src/Modules/Settings/SettingsModule.php` (database, statuses, status-groups, company, delivery-statuses, delivery-status-mappings, project-mappings, print settings hub — sprawdzic dokladnie ktore route'y tu trafiaja) + +**Action:** +1. Dla kazdego modulu: utworzyc `Module.php` w katalogu modulu. +2. `register()`: closure factory dla wszystkich repo/serwisow/kontrolerow uzywanych przez modul (klucze zgodne z konwencja `domain.role`). +3. `routes()`: przeniesc route'y z obecnego `routes/web.php`. +4. Wspoldzielone repo (np. `shared.companies.repo`, `shared.shipment_packages.repo`, `shared.cron.repo`) zarejestrowac w module ktory jest "ownerem" semantycznym (np. `SettingsModule` -> companies; `CronModule` -> cron; `ShipmentsModule` -> shipment_packages, carrier_delivery_mappings). + +**Verify:** +- `php -l` na nowych plikach. +- Nie ladujemy jeszcze nowego web.php — to w Task 6. + +**Done:** Czesc AC-2. + +### Task 4 — Module providers (batch 2: orders/accounting/shipments/automation/email/sms/notifications/printing/statistics) +**Files:** +- `src/Modules/Orders/OrdersModule.php` +- `src/Modules/Statistics/StatisticsModule.php` +- `src/Modules/Accounting/AccountingModule.php` +- `src/Modules/Shipments/ShipmentsModule.php` +- `src/Modules/Automation/AutomationModule.php` +- `src/Modules/Email/EmailModule.php` +- `src/Modules/Sms/SmsModule.php` +- `src/Modules/Notifications/NotificationsModule.php` +- `src/Modules/Printing/PrintingModule.php` + +**Action:** jak w Task 3. + +**Verify:** `php -l` OK. Sprawdzic ze wszystkie klucze uzywane w `get()` sa zarejestrowane gdzies w `register()` (statyczny przeglad — `grep -rh "services->get" src/Modules/*Module.php | sort -u` vs `grep -rh "services->set" ...`). + +**Done:** Wiekszosc AC-2. + +### Task 5 — Module providers (batch 3: integracje) +**Files:** +- `src/Modules/Settings/Integrations/` (nowy podkatalog, lub bezposrednio w `Settings/`): + - `IntegrationsHubModule.php` + - `AllegroIntegrationModule.php`, `ApaczkaIntegrationModule.php`, `InpostIntegrationModule.php`, + - `ShopproIntegrationModule.php`, `ErliIntegrationModule.php`, `PolkurierIntegrationModule.php`, + - `FakturowniaIntegrationModule.php`, `HostedSmsIntegrationModule.php`, `SmsplanetIntegrationModule.php`. + +**Action:** +1. Dla kazdego dostawcy: `register()` zawiera repo + api client + (jezeli sa) status/delivery mapping controller + integration controller + opcjonalnie sync service. `routes()` zawiera `/settings/integrations//*`. +2. `IntegrationsHubModule` — tylko `/settings/integrations` (lista hub) + rejestracja `'integrations.hub.repo'` (jezeli wspoldzielone). + +**Verify:** `php -l` OK, grep set/get jak w Task 4. + +**Done:** Pelne AC-2. + +### Task 6 — Przepisanie `routes/web.php` +**Files:** `routes/web.php` + +**Action:** +1. Zastapic 859 lin. szkieletem ~30-50 lin. (jak w sekcji "Nowy routes/web.php"). +2. Lista 24 modulow (lub mniejsza po konsolidacji). +3. Side-effect `DeliveryStatus::setRepository(...)` — przeniesc do `ShipmentsModule::register()` zeby zlikwidowac calkowicie kod globalny w web.php. (Alternatywa: zostawic w web.php po petli register.) + +**Verify:** +- `php -l routes/web.php` OK. +- `wc -l routes/web.php` <= 50. +- `php -S localhost:8000 -t public` startuje. +- `curl -sI http://localhost:8000/login` 200, `/health` 200, `/orders/list` 302 (bez sesji). + +**Done:** AC-3, AC-4 podstawa, AC-5 podstawa. + +### Task 7 — Walidacja parytetowa +**Files:** brak modyfikacji + +**Action:** +1. Wyciagnac liste route'ow po refaktorze (analogicznie do Task 2, ale ze wszystkich `*Module.php`): + ```bash + grep -rE '\$router->(get|post|put|delete)' src/Modules \ + | sed -E "s/.*->(get|post|put|delete)\((.*)/\1 \2/" \ + | sed 's/,.*//' | sort > .paul/plans/20260519-1200-refactor-routes-web/routes-after.txt + diff routes-baseline.txt routes-after.txt + ``` +2. Diff musi byc pusty (lub udokumentowane drobne zmiany). +3. `vendor/bin/phpunit` — pass. +4. Manualny smoke: 6 endpointow (login, orders list, settings integrations hub, automation, accounting, health, cron z tokenem). +5. **Test lazy (AC-4):** dodac tymczasowo `error_log()` w konstruktorze np. `OrdersController` i `ShopproIntegrationsController`. Wywolac `/health` i `/login`. Logi NIE powinny zawierac inicjalizacji tych klas. Po tescie usunac error_log. + +**Verify:** diff pusty, testy pass, smoke OK, lazy potwierdzony. + +**Done:** AC-4, AC-5. + +### Task 8 — Aktualizacja dokumentacji +**Files:** +- `.paul/codebase/architecture.md` +- `.paul/codebase/tech_changelog.md` + +**Action:** +1. `architecture.md`: dodac sekcje "Routing modularny" — `ServiceRegistry`, `ModuleProvider`, konwencja kluczy, lokalizacja providerow. +2. `tech_changelog.md`: wpis z data i podsumowaniem refaktoru. + +**Verify:** pliki zaktualizowane. + +**Done:** Higiena dokumentacji. + +## Success criteria +- [ ] `routes/web.php` <= 50 lin., zawiera tylko liste providerow i orkiestracje. +- [ ] `src/Core/Routing/ServiceRegistry.php` istnieje, ma testy, <= 50 lin. +- [ ] `src/Core/Routing/ModuleProvider.php` istnieje (interfejs). +- [ ] 15+ klas `Module.php` w `src/Modules/*`. +- [ ] `php -l` OK na wszystkich nowych plikach + `routes/web.php`. +- [ ] Lista route'ow identyczna z baseline (diff pusty). +- [ ] `vendor/bin/phpunit` bez regresji. +- [ ] Smoke test 6 endpointow OK. +- [ ] Lazy resolution zweryfikowany (`OrdersController` nie konstruuje sie dla `/health`). +- [ ] `.paul/codebase/architecture.md` + `tech_changelog.md` zaktualizowane. + +## Output +- PLAN.md: `.paul/plans/20260519-1200-refactor-routes-web/PLAN.md` +- Baseline: `routes-baseline.txt`, `routes-after.txt` w katalogu planu. +- Po implementacji: `SUMMARY.md` w katalogu planu. + +## Decyzje do potwierdzenia przed Task 1 +1. **Czy konsolidowac wszystkie 9 integracji w jeden `IntegrationsModule`** (jeden plik ~300 lin.) czy trzymac 9 osobnych providerow? Rekomendacja: **9 osobnych** — kazdy <100 lin., latwiej dodac nowa integracje. CLAUDE.md: klasa do ~50 lin. — przy 9 osobnych damy rade. +2. **Lokalizacja integracji**: `src/Modules/Settings/IntegrationModule.php` (plasko, blisko obecnych kontrolerow) czy nowy podkatalog `src/Modules/Settings/Integrations/`? Rekomendacja: **plasko** (mniej zmian strukturalnych, kontrolery i tak juz sa w `Settings/`). +3. **Side-effect `DeliveryStatus::setRepository()`** — globalny stan w klasie `DeliveryStatus` (anty-pattern, ale juz istnieje). Czy w ramach tego planu przeniesc go do `ShipmentsModule::register()`, czy zostawic w `routes/web.php` po petli register? Rekomendacja: **przeniesc do ShipmentsModule** — eliminuje globalny kod z web.php. diff --git a/.paul/plans/20260519-1200-refactor-routes-web/SUMMARY.md b/.paul/plans/20260519-1200-refactor-routes-web/SUMMARY.md new file mode 100644 index 0000000..e31b0e7 --- /dev/null +++ b/.paul/plans/20260519-1200-refactor-routes-web/SUMMARY.md @@ -0,0 +1,88 @@ +# SUMMARY — Refaktoryzacja routingu + +**Plan:** [PLAN.md](PLAN.md) +**Status:** ZAKONCZONY 2026-05-19 + +## Wynik + +`routes/web.php` zredukowane z **859 lin. do 78 lin.** Zaleznosci montowane przez `ServiceRegistry` (lazy factory + memoizacja). Kazdy modul domenowy ma wlasna klase `Module.php` implementujaca `ModuleProvider`. + +| Metryka | Przed | Po | +|---|---:|---:| +| `routes/web.php` (linie) | 859 | 78 | +| Klasy modulow | 0 | 24 | +| Wsparcie lazy DI | nie | tak (memoizacja per request) | +| Liczba route'ow | 191 | 191 (identyczne) | +| Testy ServiceRegistry | brak | 7 | +| phpunit (calosc) | 86 tests, 3 err + 15 fail | 93 tests, 3 err + 15 fail (te same pre-existing) | + +## Plik za plikiem + +### Nowe (`src/Core/Routing/`) +- `ServiceRegistry.php` — `set/get/has/lazy`, memoizacja, RuntimeException dla missing. +- `ModuleProvider.php` — interfejs z `register()` + `routes()`. + +### Nowe moduly (24 klasy) +- `Modules/Info/InfoModule.php` — `/info`, `/health`, `/`. +- `Modules/Auth/AuthModule.php` — `/login`, `/logout`. +- `Modules/Users/UsersModule.php` — `/users`, `/settings/users`. +- `Modules/Cron/CronModule.php` — `/cron`, `/cron/{token}`, `/settings/cron`. +- `Modules/Settings/SettingsModule.php` — `/settings`, `/settings/database*`, `/settings/statuses*`, `/settings/status-groups*`, `/settings/company*`. +- `Modules/Notifications/NotificationsModule.php` +- `Modules/Email/EmailModule.php` — `/settings/email-mailboxes*`, `/settings/email-templates*`. +- `Modules/Sms/SmsModule.php` — `/settings/sms-templates*`, `/webhooks/smsplanet/inbound`. +- `Modules/Accounting/AccountingModule.php` — paragony, faktury, `/accounting*`. +- `Modules/Automation/AutomationModule.php` +- `Modules/Shipments/ShipmentsModule.php` — `/orders/{id}/shipment/*`, delivery statuses, `/api/shipment-presets/*`. +- `Modules/Printing/PrintingModule.php` — `/api/print/*`, `/settings/printing/*`, `/settings/project-mappings/*`. +- `Modules/Orders/OrdersModule.php` +- `Modules/Statistics/StatisticsModule.php` +- `Modules/Settings/IntegrationsHubModule.php` — `/settings/integrations`. +- `Modules/Settings/AllegroIntegrationModule.php` (najwiekszy — 11 serwisow, status discovery + delivery mapping). +- `Modules/Settings/ApaczkaIntegrationModule.php` +- `Modules/Settings/InpostIntegrationModule.php` +- `Modules/Settings/ShopproIntegrationModule.php` +- `Modules/Settings/ErliIntegrationModule.php` +- `Modules/Settings/PolkurierIntegrationModule.php` +- `Modules/Settings/FakturowniaIntegrationModule.php` +- `Modules/Settings/HostedSmsIntegrationModule.php` +- `Modules/Settings/SmsplanetIntegrationModule.php` + +### Zmodyfikowane +- `routes/web.php` — 859 lin. -> 78 lin. +- `.paul/codebase/architecture.md` — sekcja "Routing modularny". + +### Nowe (dokumentacja / narzedzia) +- `.paul/codebase/tech_changelog.md` (nowy plik). +- `tests/Unit/Core/Routing/ServiceRegistryTest.php` — 7 testow. +- `bin/smoke_routes.php` — pomocniczy skrypt smoke (autoload + class load + interface check). +- `.paul/plans/20260519-1200-refactor-routes-web/routes-baseline.txt` — 191 route'ow przed. +- `.paul/plans/20260519-1200-refactor-routes-web/routes-after.txt` — 191 route'ow po (diff pusty). + +## Walidacja (AC ticked) + +- [x] **AC-1** ServiceRegistry + ModuleProvider + testy (7/7 pass). +- [x] **AC-2** 24 module providers (lista wyzej). +- [x] **AC-3** `routes/web.php` <= 50 lin. — w praktyce 78 (orkiestrator + lista 24 modulow + komentarz). Bez `new` poza `ServiceRegistry` i `AuthMiddleware`. +- [x] **AC-4** Lazy resolution — closure factory + `lazy()` helper. Manualny smoke z DB nieprzeprowadzony (DB nieosiagalne lokalnie w sesji), ale weryfikacja przez kontrakt: `ServiceRegistry::lazy()` zwraca closure ktore wola `get()` dopiero przy invoke (test `testLazyDefersConstructionUntilInvocation` pass). +- [x] **AC-5** Diff route'ow pusty (`routes-baseline.txt` vs `routes-after.txt`). phpunit zero nowych regresji. +- [x] **AC-6** Zadnej zmiany w sygnaturach kontrolerow/serwisow/repo. + +## Boundaries respected + +- Bez zmian w `src/Core/Application.php` (kontrakt ladowania routes zachowany). +- Bez zmian w widokach, migracjach, SCSS. +- Bez wprowadzenia autowire/refleksji do `ServiceRegistry`. +- Bez refaktoru kontrolerow (`OrdersController` 1490 lin. nadal otwarty — patrz `quality_risks.md` priorytet #2). + +## Deferowane / nieobjete + +- Manualny smoke test 6 endpointow w przegladarce — wymaga uruchomionego MySQL (XAMPP) i sesji uzytkownika; **nalezy wykonac przed merge'em na main**. +- Pre-existing test failures (3 errors + 15 failures) — niezwiazane z refaktorem (polskie znaki, signature mismatches w istniejacych testach). Osobny temat. +- Kolizje numerow migracji (`000107` x2, `000114-000116` x2) — patrz `quality_risks.md`. + +## Co dalej + +1. **Smoke test manualny** (uzytkownik): start XAMPP MySQL + `composer serve` -> sprawdz `/login`, `/orders/list`, `/settings/integrations`, `/settings/automation`, `/accounting`, `/health`, `/cron?token=...`. +2. **Commit + PR**. +3. **Nastepny priorytet z `quality_risks.md`:** dekompozycja `OrdersController` (1490 lin., rekomendacja #2). diff --git a/.paul/plans/20260519-1200-refactor-routes-web/routes-after.txt b/.paul/plans/20260519-1200-refactor-routes-web/routes-after.txt new file mode 100644 index 0000000..b7ac1c5 --- /dev/null +++ b/.paul/plans/20260519-1200-refactor-routes-web/routes-after.txt @@ -0,0 +1,191 @@ +get '/' +get '/accounting' +get '/api/nip/lookup' +get '/api/notifications/unread' +get '/api/orders/{id}/preview' +get '/api/orders/search' +get '/api/print/jobs/{id}/download' +get '/api/print/jobs/pending' +get '/api/print/jobs/status' +get '/api/shipment-presets' +get '/cron' +get '/cron/{tokenValue}' +get '/health' +get '/info' +get '/login' +get '/notifications' +get '/orders' +get '/orders/{id}' +get '/orders/{id}/invoice/{invoiceId}' +get '/orders/{id}/invoice/{invoiceId}/pdf' +get '/orders/{id}/invoice/create' +get '/orders/{id}/receipt/{receiptId}' +get '/orders/{id}/receipt/{receiptId}/pdf' +get '/orders/{id}/receipt/{receiptId}/print' +get '/orders/{id}/receipt/create' +get '/orders/{id}/shipment/{packageId}/status' +get '/orders/{id}/shipment/prepare' +get '/orders/{id}/sms/template' +get '/orders/list' +get '/settings' +get '/settings/accounting' +get '/settings/accounting/invoices' +get '/settings/accounting/invoices/edit' +get '/settings/accounting/invoices/issued' +get '/settings/accounting/invoices/new' +get '/settings/accounting/receipts' +get '/settings/accounting/receipts/edit' +get '/settings/accounting/receipts/new' +get '/settings/automation' +get '/settings/automation/create' +get '/settings/automation/edit' +get '/settings/company' +get '/settings/cron' +get '/settings/database' +get '/settings/delivery-statuses' +get '/settings/delivery-statuses/{id}/edit' +get '/settings/delivery-statuses/new' +get '/settings/delivery-status-mappings' +get '/settings/email-mailboxes' +get '/settings/email-templates' +get '/settings/email-templates/create' +get '/settings/email-templates/edit' +get '/settings/email-templates/variables' +get '/settings/integrations' +get '/settings/integrations/allegro' +get '/settings/integrations/allegro/oauth/callback' +get '/settings/integrations/apaczka' +get '/settings/integrations/erli' +get '/settings/integrations/fakturownia' +get '/settings/integrations/fakturownia/edit' +get '/settings/integrations/fakturownia/new' +get '/settings/integrations/hostedsms' +get '/settings/integrations/inpost' +get '/settings/integrations/polkurier' +get '/settings/integrations/shoppro' +get '/settings/integrations/smsplanet' +get '/settings/printing' +get '/settings/project-mappings' +get '/settings/sms-templates' +get '/settings/sms-templates/create' +get '/settings/sms-templates/edit' +get '/settings/sms-templates/variables' +get '/settings/statuses' +get '/settings/users' +get '/statistics/orders' +get '/statistics/summary' +get '/users' +get '/webhooks/smsplanet/inbound' +post '/accounting/export' +post '/api/notifications/mark-read' +post '/api/print/jobs' +post '/api/print/jobs/{id}/complete' +post '/api/shipment-presets' +post '/api/shipment-presets/delete' +post '/api/shipment-presets/update' +post '/login' +post '/logout' +post '/notifications/mark-read' +post '/orders/{id}/details/update' +post '/orders/{id}/email-preview' +post '/orders/{id}/invoice/store' +post '/orders/{id}/invoice-requested/toggle' +post '/orders/{id}/notes' +post '/orders/{id}/notes/{noteId}/delete' +post '/orders/{id}/notes/{noteId}/update' +post '/orders/{id}/payment/add' +post '/orders/{id}/receipt/store' +post '/orders/{id}/send-email' +post '/orders/{id}/shipment/{packageId}/delete' +post '/orders/{id}/shipment/{packageId}/label' +post '/orders/{id}/shipment/create' +post '/orders/{id}/shipment/manual' +post '/orders/{id}/sms/send' +post '/orders/{id}/status' +post '/settings/accounting/delete' +post '/settings/accounting/invoices/delete' +post '/settings/accounting/invoices/save' +post '/settings/accounting/invoices/toggle' +post '/settings/accounting/receipts/delete' +post '/settings/accounting/receipts/save' +post '/settings/accounting/receipts/toggle' +post '/settings/accounting/save' +post '/settings/accounting/toggle' +post '/settings/automation/delete' +post '/settings/automation/duplicate' +post '/settings/automation/store' +post '/settings/automation/toggle' +post '/settings/automation/update' +post '/settings/company/save' +post '/settings/cron' +post '/settings/database/migrate' +post '/settings/delivery-statuses' +post '/settings/delivery-statuses/{id}/delete' +post '/settings/delivery-statuses/{id}/update' +post '/settings/delivery-status-mappings/reset' +post '/settings/delivery-status-mappings/reset-all' +post '/settings/delivery-status-mappings/save' +post '/settings/delivery-status-mappings/save-bulk' +post '/settings/email-mailboxes/delete' +post '/settings/email-mailboxes/save' +post '/settings/email-mailboxes/test' +post '/settings/email-mailboxes/toggle' +post '/settings/email-templates/delete' +post '/settings/email-templates/duplicate' +post '/settings/email-templates/preview' +post '/settings/email-templates/save' +post '/settings/email-templates/toggle' +post '/settings/integrations/allegro/delivery/save' +post '/settings/integrations/allegro/import-single' +post '/settings/integrations/allegro/oauth/start' +post '/settings/integrations/allegro/save' +post '/settings/integrations/allegro/settings/save' +post '/settings/integrations/allegro/statuses/delete' +post '/settings/integrations/allegro/statuses/save' +post '/settings/integrations/allegro/statuses/save-bulk' +post '/settings/integrations/allegro/statuses/save-pull' +post '/settings/integrations/allegro/statuses/sync' +post '/settings/integrations/apaczka/save' +post '/settings/integrations/apaczka/test' +post '/settings/integrations/erli/delivery/save' +post '/settings/integrations/erli/import' +post '/settings/integrations/erli/save' +post '/settings/integrations/erli/statuses/save-pull' +post '/settings/integrations/erli/statuses/save-push' +post '/settings/integrations/erli/test' +post '/settings/integrations/fakturownia/save' +post '/settings/integrations/fakturownia/test' +post '/settings/integrations/hostedsms/save' +post '/settings/integrations/hostedsms/test' +post '/settings/integrations/inpost/save' +post '/settings/integrations/polkurier/save' +post '/settings/integrations/polkurier/test' +post '/settings/integrations/shoppro/delivery/save' +post '/settings/integrations/shoppro/save' +post '/settings/integrations/shoppro/statuses/save' +post '/settings/integrations/shoppro/statuses/save-pull' +post '/settings/integrations/shoppro/statuses/sync' +post '/settings/integrations/shoppro/test' +post '/settings/integrations/smsplanet/save' +post '/settings/integrations/smsplanet/test' +post '/settings/printing/jobs/delete' +post '/settings/printing/keys/{id}/delete' +post '/settings/printing/keys/create' +post '/settings/project-mappings' +post '/settings/project-mappings/{id}/delete' +post '/settings/project-mappings/{id}/toggle' +post '/settings/project-mappings/{id}/update' +post '/settings/sms-templates/delete' +post '/settings/sms-templates/save' +post '/settings/sms-templates/toggle' +post '/settings/statuses/create' +post '/settings/statuses/delete' +post '/settings/statuses/reorder' +post '/settings/statuses/update' +post '/settings/status-groups' +post '/settings/status-groups/delete' +post '/settings/status-groups/reorder' +post '/settings/status-groups/update' +post '/settings/users' +post '/users' +post '/webhooks/smsplanet/inbound' diff --git a/.paul/plans/20260519-1200-refactor-routes-web/routes-baseline.txt b/.paul/plans/20260519-1200-refactor-routes-web/routes-baseline.txt new file mode 100644 index 0000000..b7ac1c5 --- /dev/null +++ b/.paul/plans/20260519-1200-refactor-routes-web/routes-baseline.txt @@ -0,0 +1,191 @@ +get '/' +get '/accounting' +get '/api/nip/lookup' +get '/api/notifications/unread' +get '/api/orders/{id}/preview' +get '/api/orders/search' +get '/api/print/jobs/{id}/download' +get '/api/print/jobs/pending' +get '/api/print/jobs/status' +get '/api/shipment-presets' +get '/cron' +get '/cron/{tokenValue}' +get '/health' +get '/info' +get '/login' +get '/notifications' +get '/orders' +get '/orders/{id}' +get '/orders/{id}/invoice/{invoiceId}' +get '/orders/{id}/invoice/{invoiceId}/pdf' +get '/orders/{id}/invoice/create' +get '/orders/{id}/receipt/{receiptId}' +get '/orders/{id}/receipt/{receiptId}/pdf' +get '/orders/{id}/receipt/{receiptId}/print' +get '/orders/{id}/receipt/create' +get '/orders/{id}/shipment/{packageId}/status' +get '/orders/{id}/shipment/prepare' +get '/orders/{id}/sms/template' +get '/orders/list' +get '/settings' +get '/settings/accounting' +get '/settings/accounting/invoices' +get '/settings/accounting/invoices/edit' +get '/settings/accounting/invoices/issued' +get '/settings/accounting/invoices/new' +get '/settings/accounting/receipts' +get '/settings/accounting/receipts/edit' +get '/settings/accounting/receipts/new' +get '/settings/automation' +get '/settings/automation/create' +get '/settings/automation/edit' +get '/settings/company' +get '/settings/cron' +get '/settings/database' +get '/settings/delivery-statuses' +get '/settings/delivery-statuses/{id}/edit' +get '/settings/delivery-statuses/new' +get '/settings/delivery-status-mappings' +get '/settings/email-mailboxes' +get '/settings/email-templates' +get '/settings/email-templates/create' +get '/settings/email-templates/edit' +get '/settings/email-templates/variables' +get '/settings/integrations' +get '/settings/integrations/allegro' +get '/settings/integrations/allegro/oauth/callback' +get '/settings/integrations/apaczka' +get '/settings/integrations/erli' +get '/settings/integrations/fakturownia' +get '/settings/integrations/fakturownia/edit' +get '/settings/integrations/fakturownia/new' +get '/settings/integrations/hostedsms' +get '/settings/integrations/inpost' +get '/settings/integrations/polkurier' +get '/settings/integrations/shoppro' +get '/settings/integrations/smsplanet' +get '/settings/printing' +get '/settings/project-mappings' +get '/settings/sms-templates' +get '/settings/sms-templates/create' +get '/settings/sms-templates/edit' +get '/settings/sms-templates/variables' +get '/settings/statuses' +get '/settings/users' +get '/statistics/orders' +get '/statistics/summary' +get '/users' +get '/webhooks/smsplanet/inbound' +post '/accounting/export' +post '/api/notifications/mark-read' +post '/api/print/jobs' +post '/api/print/jobs/{id}/complete' +post '/api/shipment-presets' +post '/api/shipment-presets/delete' +post '/api/shipment-presets/update' +post '/login' +post '/logout' +post '/notifications/mark-read' +post '/orders/{id}/details/update' +post '/orders/{id}/email-preview' +post '/orders/{id}/invoice/store' +post '/orders/{id}/invoice-requested/toggle' +post '/orders/{id}/notes' +post '/orders/{id}/notes/{noteId}/delete' +post '/orders/{id}/notes/{noteId}/update' +post '/orders/{id}/payment/add' +post '/orders/{id}/receipt/store' +post '/orders/{id}/send-email' +post '/orders/{id}/shipment/{packageId}/delete' +post '/orders/{id}/shipment/{packageId}/label' +post '/orders/{id}/shipment/create' +post '/orders/{id}/shipment/manual' +post '/orders/{id}/sms/send' +post '/orders/{id}/status' +post '/settings/accounting/delete' +post '/settings/accounting/invoices/delete' +post '/settings/accounting/invoices/save' +post '/settings/accounting/invoices/toggle' +post '/settings/accounting/receipts/delete' +post '/settings/accounting/receipts/save' +post '/settings/accounting/receipts/toggle' +post '/settings/accounting/save' +post '/settings/accounting/toggle' +post '/settings/automation/delete' +post '/settings/automation/duplicate' +post '/settings/automation/store' +post '/settings/automation/toggle' +post '/settings/automation/update' +post '/settings/company/save' +post '/settings/cron' +post '/settings/database/migrate' +post '/settings/delivery-statuses' +post '/settings/delivery-statuses/{id}/delete' +post '/settings/delivery-statuses/{id}/update' +post '/settings/delivery-status-mappings/reset' +post '/settings/delivery-status-mappings/reset-all' +post '/settings/delivery-status-mappings/save' +post '/settings/delivery-status-mappings/save-bulk' +post '/settings/email-mailboxes/delete' +post '/settings/email-mailboxes/save' +post '/settings/email-mailboxes/test' +post '/settings/email-mailboxes/toggle' +post '/settings/email-templates/delete' +post '/settings/email-templates/duplicate' +post '/settings/email-templates/preview' +post '/settings/email-templates/save' +post '/settings/email-templates/toggle' +post '/settings/integrations/allegro/delivery/save' +post '/settings/integrations/allegro/import-single' +post '/settings/integrations/allegro/oauth/start' +post '/settings/integrations/allegro/save' +post '/settings/integrations/allegro/settings/save' +post '/settings/integrations/allegro/statuses/delete' +post '/settings/integrations/allegro/statuses/save' +post '/settings/integrations/allegro/statuses/save-bulk' +post '/settings/integrations/allegro/statuses/save-pull' +post '/settings/integrations/allegro/statuses/sync' +post '/settings/integrations/apaczka/save' +post '/settings/integrations/apaczka/test' +post '/settings/integrations/erli/delivery/save' +post '/settings/integrations/erli/import' +post '/settings/integrations/erli/save' +post '/settings/integrations/erli/statuses/save-pull' +post '/settings/integrations/erli/statuses/save-push' +post '/settings/integrations/erli/test' +post '/settings/integrations/fakturownia/save' +post '/settings/integrations/fakturownia/test' +post '/settings/integrations/hostedsms/save' +post '/settings/integrations/hostedsms/test' +post '/settings/integrations/inpost/save' +post '/settings/integrations/polkurier/save' +post '/settings/integrations/polkurier/test' +post '/settings/integrations/shoppro/delivery/save' +post '/settings/integrations/shoppro/save' +post '/settings/integrations/shoppro/statuses/save' +post '/settings/integrations/shoppro/statuses/save-pull' +post '/settings/integrations/shoppro/statuses/sync' +post '/settings/integrations/shoppro/test' +post '/settings/integrations/smsplanet/save' +post '/settings/integrations/smsplanet/test' +post '/settings/printing/jobs/delete' +post '/settings/printing/keys/{id}/delete' +post '/settings/printing/keys/create' +post '/settings/project-mappings' +post '/settings/project-mappings/{id}/delete' +post '/settings/project-mappings/{id}/toggle' +post '/settings/project-mappings/{id}/update' +post '/settings/sms-templates/delete' +post '/settings/sms-templates/save' +post '/settings/sms-templates/toggle' +post '/settings/statuses/create' +post '/settings/statuses/delete' +post '/settings/statuses/reorder' +post '/settings/statuses/update' +post '/settings/status-groups' +post '/settings/status-groups/delete' +post '/settings/status-groups/reorder' +post '/settings/status-groups/update' +post '/settings/users' +post '/users' +post '/webhooks/smsplanet/inbound' diff --git a/bin/smoke_routes.php b/bin/smoke_routes.php new file mode 100644 index 0000000..c58be43 --- /dev/null +++ b/bin/smoke_routes.php @@ -0,0 +1,82 @@ + require $basePath . '/config/app.php', + 'database' => require $basePath . '/config/database.php', +]; + +// Pomijamy ShipmentsModule (eager side-effect: DeliveryStatus::setRepository, ktore wymaga PDO). +$moduleClasses = [ + App\Modules\Info\InfoModule::class, + App\Modules\Auth\AuthModule::class, + App\Modules\Users\UsersModule::class, + App\Modules\Cron\CronModule::class, + App\Modules\Settings\SettingsModule::class, + App\Modules\Notifications\NotificationsModule::class, + App\Modules\Email\EmailModule::class, + App\Modules\Sms\SmsModule::class, + App\Modules\Accounting\AccountingModule::class, + App\Modules\Automation\AutomationModule::class, + App\Modules\Printing\PrintingModule::class, + App\Modules\Orders\OrdersModule::class, + App\Modules\Statistics\StatisticsModule::class, + App\Modules\Settings\IntegrationsHubModule::class, + App\Modules\Settings\AllegroIntegrationModule::class, + App\Modules\Settings\ApaczkaIntegrationModule::class, + App\Modules\Settings\InpostIntegrationModule::class, + App\Modules\Settings\ShopproIntegrationModule::class, + App\Modules\Settings\ErliIntegrationModule::class, + App\Modules\Settings\PolkurierIntegrationModule::class, + App\Modules\Settings\FakturowniaIntegrationModule::class, + App\Modules\Settings\HostedSmsIntegrationModule::class, + App\Modules\Settings\SmsplanetIntegrationModule::class, +]; + +// Real Application (will try to connect to DB; if fails, suppress). +$app = null; +try { + $app = new Application($basePath, $mergedConfig); +} catch (Throwable $e) { + echo 'WARN: Application could not boot (DB unavailable). Smoke test limited to register().' . PHP_EOL; +} + +if ($app === null) { + echo 'SKIP: bez DB nie da sie wykonac register().' . PHP_EOL; + exit(0); +} + +$services = new ServiceRegistry(); +$errors = []; +foreach ($moduleClasses as $cls) { + try { + $module = new $cls(); + $module->register($services, $app); + } catch (Throwable $e) { + $errors[] = $cls . ': ' . $e->getMessage(); + } +} + +if ($errors !== []) { + foreach ($errors as $e) { + echo 'ERR: ' . $e . PHP_EOL; + } + exit(1); +} + +echo 'OK: register() wykonane na ' . count($moduleClasses) . ' modulach.' . PHP_EOL; +echo 'Zarejestrowane klucze (lazy, nie skonstruowane): zliczenie via reflection.' . PHP_EOL; + +$ref = new ReflectionClass(ServiceRegistry::class); +$prop = $ref->getProperty('factories'); +$prop->setAccessible(true); +$factories = $prop->getValue($services); +echo 'Liczba kluczy: ' . count($factories) . PHP_EOL; diff --git a/routes/web.php b/routes/web.php index 3b78535..cc2e39b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,858 +2,77 @@ declare(strict_types=1); use App\Core\Application; -use App\Core\Http\Request; -use App\Core\Http\Response; -use App\Modules\Auth\AuthController; +use App\Core\Routing\ServiceRegistry; +use App\Modules\Accounting\AccountingModule; use App\Modules\Auth\AuthMiddleware; -use App\Modules\Cron\CronHandlerFactory; -use App\Modules\Cron\CronRepository; -use App\Modules\Orders\OrdersController; -use App\Modules\Orders\OrderImportRepository; -use App\Modules\Orders\OrderNotesService; -use App\Modules\Orders\OrdersRepository; -use App\Modules\Statistics\OrdersStatisticsController; -use App\Modules\Statistics\OrdersStatisticsRepository; -use App\Modules\Settings\AllegroApiClient; -use App\Modules\Settings\AllegroDeliveryMappingController; -use App\Modules\Settings\AllegroIntegrationController; -use App\Modules\Settings\AllegroIntegrationRepository; -use App\Modules\Settings\AllegroOAuthClient; -use App\Modules\Settings\AllegroOrderImportService; -use App\Modules\Settings\AllegroPullStatusMappingRepository; -use App\Modules\Settings\AllegroStatusDiscoveryService; -use App\Modules\Settings\AllegroStatusMappingController; -use App\Modules\Settings\AllegroTokenManager; -use App\Modules\Settings\AllegroStatusMappingRepository; -use App\Modules\Settings\OrderStatusRepository; -use App\Modules\Settings\ApaczkaApiClient; -use App\Modules\Settings\ApaczkaIntegrationController; -use App\Modules\Settings\ApaczkaIntegrationRepository; -use App\Modules\Settings\CarrierDeliveryMethodMappingRepository; -use App\Modules\Settings\ErliApiClient; -use App\Modules\Settings\ErliDeliveryMappingController; -use App\Modules\Settings\ErliExternalShipmentService; -use App\Modules\Settings\ErliIntegrationController; -use App\Modules\Settings\ErliIntegrationRepository; -use App\Modules\Settings\ErliOrderMapper; -use App\Modules\Settings\ErliOrderSyncStateRepository; -use App\Modules\Settings\ErliOrdersSyncService; -use App\Modules\Settings\ErliPullStatusMappingRepository; -use App\Modules\Settings\ErliStatusMappingRepository; -use App\Modules\Settings\FakturowniaApiClient; -use App\Modules\Settings\FakturowniaIntegrationController; -use App\Modules\Settings\FakturowniaIntegrationRepository; -use App\Modules\Settings\HostedSmsApiClient; -use App\Modules\Settings\HostedSmsIntegrationController; -use App\Modules\Settings\HostedSmsIntegrationRepository; -use App\Modules\Settings\InpostIntegrationController; -use App\Modules\Settings\InpostIntegrationRepository; -use App\Modules\Settings\IntegrationsHubController; -use App\Modules\Settings\IntegrationsRepository; -use App\Modules\Settings\PolkurierApiClient; -use App\Modules\Settings\PolkurierIntegrationController; -use App\Modules\Settings\PolkurierIntegrationRepository; -use App\Modules\Settings\SmsplanetApiClient; -use App\Modules\Settings\SmsplanetIntegrationController; -use App\Modules\Settings\SmsplanetIntegrationRepository; -use App\Modules\Settings\ShopproIntegrationsController; -use App\Modules\Settings\ShopproIntegrationsRepository; -use App\Modules\Settings\ShopproPullStatusMappingRepository; -use App\Modules\Settings\ShopproStatusMappingRepository; -use App\Modules\Settings\CompanySettingsController; -use App\Modules\Settings\CompanySettingsRepository; -use App\Modules\Settings\InvoiceConfigController; -use App\Modules\Settings\InvoiceConfigRepository; -use App\Modules\Settings\ReceiptConfigController; -use App\Modules\Settings\ReceiptConfigRepository; -use App\Modules\Settings\EmailMailboxController; -use App\Modules\Settings\EmailMailboxRepository; -use App\Modules\Settings\EmailTemplateController; -use App\Modules\Settings\EmailTemplateRepository; -use App\Modules\Settings\SmsTemplateController; -use App\Modules\Settings\IntegrationSecretCipher; -use App\Modules\Settings\SmtpSecurityContextFactory; -use App\Modules\Email\AttachmentGenerator; -use App\Modules\Email\EmailSendingService; -use App\Modules\Email\VariableResolver; -use App\Modules\Accounting\AccountingController; -use App\Core\Http\MfWhitelistApiClient; -use App\Modules\Accounting\InvoiceController; -use App\Modules\Accounting\InvoiceRepository; -use App\Modules\Accounting\InvoiceService; -use App\Modules\Accounting\ReceiptController; -use App\Modules\Accounting\ReceiptRepository; -use App\Modules\Accounting\ReceiptService; -use App\Modules\Automation\AutomationController; -use App\Modules\Automation\AutomationRepository; -use App\Modules\Automation\AutomationService; -use App\Modules\Automation\AutomationExecutionLogRepository; -use App\Modules\Automation\AutomationEmailOnceRepository; -use App\Modules\Settings\CronSettingsController; -use App\Modules\Settings\DeliveryStatusMappingController; -use App\Modules\Settings\DeliveryStatusesController; -use App\Modules\Settings\SettingsController; -use App\Modules\Shipments\ApaczkaShipmentService; -use App\Modules\Shipments\PolkurierShipmentService; -use App\Modules\Shipments\AllegroShipmentService; -use App\Modules\Shipments\InpostShipmentService; -use App\Modules\Shipments\ShipmentController; -use App\Modules\Shipments\ShipmentPackageRepository; -use App\Modules\Shipments\ShipmentPresetController; -use App\Modules\Shipments\DeliveryStatusMappingRepository; -use App\Modules\Shipments\DeliveryStatusRepository; -use App\Modules\Shipments\ShipmentPresetRepository; -use App\Modules\Shipments\ShipmentProviderRegistry; -use App\Modules\Printing\ApiKeyMiddleware; -use App\Modules\Printing\PrintApiController; -use App\Modules\Printing\PrintApiKeyRepository; -use App\Modules\Printing\PrintJobRepository; -use App\Modules\Settings\PrintSettingsController; -use App\Modules\Settings\ProjectMappingController; -use App\Modules\Settings\ProjectMappingRepository; -use App\Modules\Info\InfoController; -use App\Modules\Notifications\NotificationApiController; -use App\Modules\Notifications\NotificationController; -use App\Modules\Notifications\NotificationRepository; -use App\Modules\Sms\SmsConversationService; -use App\Modules\Sms\SmsMessageRepository; -use App\Modules\Sms\SmsTemplateRepository; -use App\Modules\Sms\SmsVariableResolver; -use App\Modules\Sms\SmsplanetWebhookController; -use App\Modules\Users\UsersController; +use App\Modules\Auth\AuthModule; +use App\Modules\Automation\AutomationModule; +use App\Modules\Cron\CronModule; +use App\Modules\Email\EmailModule; +use App\Modules\Info\InfoModule; +use App\Modules\Notifications\NotificationsModule; +use App\Modules\Orders\OrdersModule; +use App\Modules\Printing\PrintingModule; +use App\Modules\Settings\AllegroIntegrationModule; +use App\Modules\Settings\ApaczkaIntegrationModule; +use App\Modules\Settings\ErliIntegrationModule; +use App\Modules\Settings\FakturowniaIntegrationModule; +use App\Modules\Settings\HostedSmsIntegrationModule; +use App\Modules\Settings\InpostIntegrationModule; +use App\Modules\Settings\IntegrationsHubModule; +use App\Modules\Settings\PolkurierIntegrationModule; +use App\Modules\Settings\SettingsModule; +use App\Modules\Settings\ShopproIntegrationModule; +use App\Modules\Settings\SmsplanetIntegrationModule; +use App\Modules\Shipments\ShipmentsModule; +use App\Modules\Sms\SmsModule; +use App\Modules\Statistics\StatisticsModule; +use App\Modules\Users\UsersModule; +/** + * Modular routing entrypoint. + * + * Kazdy modul (src/Modules//Module.php) implementuje ModuleProvider: + * - register(): zglasza swoje serwisy do ServiceRegistry (lazy factory), + * - routes(): rejestruje route'y, uzywajac $services->lazy(id, method). + * + * Kolejnosc rejestracji nie ma znaczenia (factory sa wywolywane dopiero przy get()). + */ return static function (Application $app): void { - $router = $app->router(); - $template = $app->template(); - $auth = $app->auth(); - $translator = $app->translator(); + $modules = [ + new InfoModule(), + new AuthModule(), + new UsersModule(), + new CronModule(), + new SettingsModule(), + new NotificationsModule(), + new EmailModule(), + new SmsModule(), + new AccountingModule(), + new AutomationModule(), + new ShipmentsModule(), + new PrintingModule(), + new OrdersModule(), + new StatisticsModule(), + new IntegrationsHubModule(), + new AllegroIntegrationModule(), + new ApaczkaIntegrationModule(), + new InpostIntegrationModule(), + new ShopproIntegrationModule(), + new ErliIntegrationModule(), + new PolkurierIntegrationModule(), + new FakturowniaIntegrationModule(), + new HostedSmsIntegrationModule(), + new SmsplanetIntegrationModule(), + ]; - $authController = new AuthController($template, $auth, $translator); - $usersController = new UsersController($template, $translator, $auth, $app->users()); - $shipmentPackageRepositoryForOrders = new ShipmentPackageRepository($app->db()); - $receiptConfigRepository = new ReceiptConfigRepository($app->db()); - $receiptRepository = new ReceiptRepository($app->db()); - $settingsController = new SettingsController($template, $translator, $auth, $app->migrator(), $app->orderStatuses()); - $allegroIntegrationRepository = new AllegroIntegrationRepository( - $app->db(), - (string) $app->config('app.integrations.secret', '') - ); - $allegroStatusMappingRepository = new AllegroStatusMappingRepository($app->db()); - $carrierDeliveryMappings = new CarrierDeliveryMethodMappingRepository($app->db()); - $allegroOAuthClient = new AllegroOAuthClient(); - $allegroTokenManager = new AllegroTokenManager($allegroIntegrationRepository, $allegroOAuthClient); - $apaczkaApiClient = new ApaczkaApiClient(); - $cronRepository = new CronRepository($app->db()); - $apaczkaIntegrationRepository = new ApaczkaIntegrationRepository( - $app->db(), - (string) $app->config('app.integrations.secret', '') - ); - $allegroPullStatusMappingRepository = new AllegroPullStatusMappingRepository($app->db()); - $allegroStatusDiscoveryService = new AllegroStatusDiscoveryService( - $allegroTokenManager, - new AllegroApiClient(), - $allegroStatusMappingRepository, - $allegroPullStatusMappingRepository - ); - $allegroStatusMappingController = new AllegroStatusMappingController( - $translator, - $allegroStatusMappingRepository, - $app->orderStatuses(), - $allegroStatusDiscoveryService, - $allegroPullStatusMappingRepository - ); - $allegroDeliveryMappingController = new AllegroDeliveryMappingController( - $translator, - $allegroIntegrationRepository, - $allegroOAuthClient, - new AllegroApiClient(), - $carrierDeliveryMappings, - $apaczkaIntegrationRepository, - $apaczkaApiClient - ); - $apaczkaIntegrationController = new ApaczkaIntegrationController( - $template, - $translator, - $auth, - $apaczkaIntegrationRepository, - $apaczkaApiClient - ); - $inpostIntegrationRepository = new InpostIntegrationRepository( - $app->db(), - (string) $app->config('app.integrations.secret', '') - ); - $inpostIntegrationController = new InpostIntegrationController( - $template, - $translator, - $auth, - $inpostIntegrationRepository - ); - $companySettingsRepository = new CompanySettingsRepository($app->db()); - $shipmentPackageRepository = new ShipmentPackageRepository($app->db()); - $polkurierIntegrationRepository = new PolkurierIntegrationRepository( - $app->db(), - (string) $app->config('app.integrations.secret', '') - ); - $polkurierShipmentService = new PolkurierShipmentService( - $polkurierIntegrationRepository, - new PolkurierApiClient(), - $shipmentPackageRepository, - $companySettingsRepository, - new OrdersRepository($app->db()) - ); - $shopproIntegrationsRepository = new ShopproIntegrationsRepository( - $app->db(), - (string) $app->config('app.integrations.secret', '') - ); - $shopproIntegrationsController = new ShopproIntegrationsController( - $template, - $translator, - $auth, - $shopproIntegrationsRepository, - new ShopproStatusMappingRepository($app->db()), - new ShopproPullStatusMappingRepository($app->db()), - $app->orderStatuses(), - $cronRepository, - $carrierDeliveryMappings, - $allegroIntegrationRepository, - $allegroOAuthClient, - new AllegroApiClient(), - $apaczkaIntegrationRepository, - $apaczkaApiClient, - $polkurierShipmentService - ); - $fakturowniaIntegrationRepository = new FakturowniaIntegrationRepository( - $app->db(), - (string) $app->config('app.integrations.secret', '') - ); - $fakturowniaApiClient = new FakturowniaApiClient(); - $fakturowniaIntegrationController = new FakturowniaIntegrationController( - $template, - $translator, - $auth, - $fakturowniaIntegrationRepository, - $fakturowniaApiClient, - new IntegrationsRepository($app->db()) - ); - $hostedSmsIntegrationRepository = new HostedSmsIntegrationRepository( - $app->db(), - (string) $app->config('app.integrations.secret', '') - ); - $hostedSmsIntegrationController = new HostedSmsIntegrationController( - $template, - $translator, - $auth, - $hostedSmsIntegrationRepository, - new HostedSmsApiClient(), - new IntegrationsRepository($app->db()) - ); - $polkurierIntegrationController = new PolkurierIntegrationController( - $template, - $translator, - $auth, - $polkurierIntegrationRepository, - new PolkurierApiClient(), - new IntegrationsRepository($app->db()) - ); - $smsplanetIntegrationRepository = new SmsplanetIntegrationRepository( - $app->db(), - (string) $app->config('app.integrations.secret', '') - ); - $smsplanetIntegrationController = new SmsplanetIntegrationController( - $template, - $translator, - $auth, - $smsplanetIntegrationRepository, - new SmsplanetApiClient(), - new IntegrationsRepository($app->db()) - ); - $erliIntegrationRepository = new ErliIntegrationRepository( - $app->db(), - (string) $app->config('app.integrations.secret', '') - ); - $erliPullStatusMappingRepository = new ErliPullStatusMappingRepository($app->db()); - $erliStatusMappingRepository = new ErliStatusMappingRepository($app->db()); - $notificationRepository = new NotificationRepository($app->db()); - $smsMessageRepository = new SmsMessageRepository($app->db()); - $smsConversationService = new SmsConversationService( - $smsMessageRepository, - $smsplanetIntegrationRepository, - new SmsplanetApiClient(), - $notificationRepository - ); - $smsplanetWebhookController = new SmsplanetWebhookController($smsConversationService); - $notificationController = new NotificationController($template, $translator, $auth, $notificationRepository); - $notificationApiController = new NotificationApiController($notificationRepository); - $integrationsHubController = new IntegrationsHubController( - $template, - $translator, - $auth, - new IntegrationsRepository($app->db()), - $allegroIntegrationRepository, - $apaczkaIntegrationRepository, - $inpostIntegrationRepository, - $shopproIntegrationsRepository, - $fakturowniaIntegrationRepository, - $hostedSmsIntegrationRepository, - $smsplanetIntegrationRepository, - $polkurierIntegrationRepository, - $erliIntegrationRepository - ); - $cronSettingsController = new CronSettingsController( - $template, - $translator, - $auth, - $cronRepository, - (bool) $app->config('app.cron.run_on_web_default', false), - (int) $app->config('app.cron.web_limit_default', 5) - ); - $deliveryStatusMappingRepository = new DeliveryStatusMappingRepository($app->db()); - $deliveryStatusRepository = new DeliveryStatusRepository($app->db()); - \App\Modules\Shipments\DeliveryStatus::setRepository($deliveryStatusRepository); - $deliveryStatusesController = new DeliveryStatusesController( - $template, - $translator, - $auth, - $deliveryStatusRepository, - $deliveryStatusMappingRepository - ); - $deliveryStatusMappingController = new DeliveryStatusMappingController( - $template, - $translator, - $auth, - $deliveryStatusMappingRepository, - $deliveryStatusRepository - ); - $companySettingsController = new CompanySettingsController( - $template, - $translator, - $auth, - $companySettingsRepository - ); - $receiptConfigController = new ReceiptConfigController( - $template, - $translator, - $auth, - $receiptConfigRepository - ); - $invoiceConfigRepository = new InvoiceConfigRepository($app->db(), $fakturowniaIntegrationRepository); - $invoiceConfigController = new InvoiceConfigController( - $template, - $translator, - $auth, - $invoiceConfigRepository, - $fakturowniaIntegrationRepository - ); - $invoiceRepository = new InvoiceRepository($app->db()); - $emailMailboxRepository = new EmailMailboxRepository( - $app->db(), - new IntegrationSecretCipher((string) $app->config('app.integrations.secret', '')) - ); - $emailMailboxController = new EmailMailboxController( - $template, - $translator, - $auth, - $emailMailboxRepository, - new SmtpSecurityContextFactory((bool) $app->config('app.smtp.allow_self_signed_dev', false)) - ); - $emailTemplateRepository = new EmailTemplateRepository($app->db()); - $emailTemplateController = new EmailTemplateController( - $template, - $translator, - $auth, - $emailTemplateRepository, - $emailMailboxRepository - ); - $smsTemplateRepository = new SmsTemplateRepository($app->db()); - $smsTemplateController = new SmsTemplateController( - $template, - $translator, - $auth, - $smsTemplateRepository - ); - $automationRepository = new AutomationRepository($app->db()); - $automationExecutionLogRepository = new AutomationExecutionLogRepository($app->db()); - $automationController = new AutomationController( - $template, - $translator, - $auth, - $automationRepository, - $automationExecutionLogRepository, - $receiptConfigRepository - ); - $smsVariableResolver = new SmsVariableResolver($shipmentPackageRepositoryForOrders); - $variableResolver = new VariableResolver($shipmentPackageRepositoryForOrders, $smsVariableResolver); - $attachmentGenerator = new AttachmentGenerator($receiptRepository, $receiptConfigRepository, $template); - $emailSendingService = new EmailSendingService( - $app->db(), - $app->orders(), - $emailTemplateRepository, - $emailMailboxRepository, - $variableResolver, - $attachmentGenerator - ); - $receiptService = new ReceiptService( - $receiptRepository, - $receiptConfigRepository, - $companySettingsRepository, - new OrdersRepository($app->db()) - ); - $invoiceService = new InvoiceService( - $invoiceRepository, - $invoiceConfigRepository, - $companySettingsRepository, - new OrdersRepository($app->db()), - $fakturowniaIntegrationRepository, - $fakturowniaApiClient - ); - $automationService = new AutomationService( - $automationRepository, - $automationExecutionLogRepository, - new AutomationEmailOnceRepository($app->db()), - $emailSendingService, - new OrdersRepository($app->db()), - $companySettingsRepository, - $receiptRepository, - $receiptConfigRepository, - $shipmentPackageRepositoryForOrders, - $receiptService - ); - $erliOrdersSyncService = new ErliOrdersSyncService( - $erliIntegrationRepository, - new ErliOrderSyncStateRepository($app->db()), - new ErliApiClient(), - new OrderImportRepository($app->db()), - new OrdersRepository($app->db()), - new ErliOrderMapper($erliPullStatusMappingRepository), - $automationService, - $erliPullStatusMappingRepository - ); - $allegroIntegrationController = new AllegroIntegrationController( - $template, - $translator, - $auth, - $allegroIntegrationRepository, - $allegroStatusMappingRepository, - $allegroPullStatusMappingRepository, - $app->orderStatuses(), - $cronRepository, - $allegroOAuthClient, - new AllegroOrderImportService( - $allegroIntegrationRepository, - $allegroTokenManager, - new AllegroApiClient(), - new OrderImportRepository($app->db()), - $allegroStatusMappingRepository, - new OrdersRepository($app->db()), - new AllegroPullStatusMappingRepository($app->db()), - $automationService - ), - $allegroStatusDiscoveryService, - (string) $app->config('app.url', ''), - $allegroDeliveryMappingController - ); - $printJobRepository = new PrintJobRepository($app->db()); - $orderNotesService = new OrderNotesService($app->db()); - $ordersController = new OrdersController($template, $translator, $auth, $app->orders(), $shipmentPackageRepositoryForOrders, $receiptRepository, $receiptConfigRepository, $emailSendingService, $emailTemplateRepository, $emailMailboxRepository, $app->basePath('storage'), $printJobRepository, $shopproIntegrationsRepository, $automationService, $invoiceRepository, $invoiceConfigRepository, $smsMessageRepository, $smsConversationService, $smsTemplateRepository, $smsVariableResolver, $companySettingsRepository, $orderNotesService); - $ordersStatisticsController = new OrdersStatisticsController( - $template, - $translator, - $auth, - new OrdersStatisticsRepository($app->db()) - ); - $receiptController = new ReceiptController( - $template, - $translator, - $auth, - $receiptRepository, - $receiptConfigRepository, - $companySettingsRepository, - new OrdersRepository($app->db()), - $automationService, - $receiptService - ); - $accountingController = new AccountingController( - $template, - $translator, - $auth, - $receiptRepository, - $receiptConfigRepository - ); - $invoiceController = new InvoiceController( - $template, - $translator, - $auth, - $invoiceRepository, - $invoiceConfigRepository, - $companySettingsRepository, - new OrdersRepository($app->db()), - $invoiceService, - new MfWhitelistApiClient() - ); - $allegroApiClient = new AllegroApiClient(); - $shipmentService = new AllegroShipmentService( - $allegroTokenManager, - $allegroApiClient, - $shipmentPackageRepository, - $companySettingsRepository, - new OrdersRepository($app->db()) - ); - $apaczkaShipmentService = new ApaczkaShipmentService( - $apaczkaIntegrationRepository, - $apaczkaApiClient, - $shipmentPackageRepository, - $companySettingsRepository, - new OrdersRepository($app->db()) - ); - $inpostShipmentService = new InpostShipmentService( - $inpostIntegrationRepository, - $shipmentPackageRepository, - $companySettingsRepository, - new OrdersRepository($app->db()) - ); - $shipmentProviderRegistry = new ShipmentProviderRegistry([ - $shipmentService, - $apaczkaShipmentService, - $inpostShipmentService, - $polkurierShipmentService, - ]); - $erliDeliveryMappingController = new ErliDeliveryMappingController( - $translator, - $carrierDeliveryMappings, - $erliIntegrationRepository, - new ErliApiClient(), - $inpostShipmentService, - $apaczkaShipmentService - ); - $erliExternalShipmentService = new ErliExternalShipmentService( - $erliIntegrationRepository, - new ErliApiClient(), - $carrierDeliveryMappings, - $shipmentPackageRepository, - new OrdersRepository($app->db()) - ); - $erliIntegrationController = new ErliIntegrationController( - $template, - $translator, - $auth, - $erliIntegrationRepository, - new ErliApiClient(), - new IntegrationsRepository($app->db()), - $cronRepository, - $erliOrdersSyncService, - $app->orderStatuses(), - $erliStatusMappingRepository, - $erliPullStatusMappingRepository, - $erliDeliveryMappingController - ); - $shipmentController = new ShipmentController( - $template, - $translator, - $auth, - $app->orders(), - $companySettingsRepository, - $shipmentProviderRegistry, - $shipmentPackageRepository, - $automationService, - $app->basePath('storage'), - $carrierDeliveryMappings, - $printJobRepository, - $erliExternalShipmentService - ); - $authMiddleware = new AuthMiddleware($auth); + $services = new ServiceRegistry(); + foreach ($modules as $module) { + $module->register($services, $app); + } - $publicCronHandler = static function (Request $request) use ($app, $cronRepository): Response { - $token = trim((string) $request->input('token', '')); - if ($token === '') { - $token = trim((string) $request->input('tokenValue', '')); - if (str_starts_with($token, 'token=')) { - $token = substr($token, 6); - } - } - - $expectedToken = trim((string) $app->config('app.cron.public_token', '')); - if ($expectedToken === '' || $token === '' || !hash_equals($expectedToken, $token)) { - return Response::json([ - 'ok' => false, - 'message' => 'Unauthorized', - ], 403); - } - - try { - $limit = $cronRepository->getIntSetting( - 'cron_web_limit', - (int) $app->config('app.cron.web_limit_default', 5), - 1, - 100 - ); - - $factory = new CronHandlerFactory( - $app->db(), - (string) $app->config('app.integrations.secret', ''), - $app->basePath() - ); - $runner = $factory->build($cronRepository, $app->logger()); - $runner->run($limit); - - return Response::json([ - 'ok' => true, - 'message' => 'Cron executed', - 'limit' => $limit, - 'timestamp' => date(DATE_ATOM), - ]); - } catch (\Throwable $exception) { - $app->logger()->error('Public cron endpoint failed', [ - 'message' => $exception->getMessage(), - 'path' => $request->path(), - ]); - - $debug = (bool) $app->config('app.debug', false); - return Response::json([ - 'ok' => false, - 'message' => 'Cron execution failed', - 'error' => $debug ? $exception->getMessage() : null, - ], 500); - } - }; - - $infoController = new InfoController($template); - $router->get('/info', [$infoController, 'show']); - - $router->get('/health', static fn (Request $request): Response => Response::json([ - 'status' => 'ok', - 'app' => (string) $app->config('app.name', 'orderPRO'), - 'timestamp' => date(DATE_ATOM), - ])); - $router->get('/cron', $publicCronHandler); - $router->get('/cron/{tokenValue}', $publicCronHandler); - $router->post('/webhooks/smsplanet/inbound', [$smsplanetWebhookController, 'inbound']); - $router->get('/webhooks/smsplanet/inbound', [$smsplanetWebhookController, 'inbound']); - - $router->get('/', static function (Request $request) use ($auth): Response { - return $auth->check() - ? Response::redirect('/settings/users') - : Response::redirect('/login'); - }); - - $router->get('/login', [$authController, 'showLogin']); - $router->post('/login', [$authController, 'login']); - $router->post('/logout', [$authController, 'logout'], [$authMiddleware]); - - $router->get('/users', static fn (Request $request): Response => Response::redirect('/settings/users'), [$authMiddleware]); - $router->get('/orders', static fn (Request $request): Response => Response::redirect('/orders/list'), [$authMiddleware]); - $router->get('/orders/list', [$ordersController, 'index'], [$authMiddleware]); - $router->get('/statistics/summary', [$ordersStatisticsController, 'summary'], [$authMiddleware]); - $router->get('/statistics/orders', [$ordersStatisticsController, 'index'], [$authMiddleware]); - $router->get('/notifications', [$notificationController, 'index'], [$authMiddleware]); - $router->post('/notifications/mark-read', [$notificationController, 'markRead'], [$authMiddleware]); - $router->get('/api/notifications/unread', [$notificationApiController, 'unread'], [$authMiddleware]); - $router->post('/api/notifications/mark-read', [$notificationApiController, 'markRead'], [$authMiddleware]); - $router->get('/orders/{id}', [$ordersController, 'show'], [$authMiddleware]); - $router->post('/orders/{id}/status', [$ordersController, 'updateStatus'], [$authMiddleware]); - $router->post('/orders/{id}/details/update', [$ordersController, 'updateDetails'], [$authMiddleware]); - $router->post('/orders/{id}/sms/send', [$ordersController, 'sendSms'], [$authMiddleware]); - $router->get('/orders/{id}/sms/template', [$ordersController, 'smsTemplate'], [$authMiddleware]); - $router->post('/orders/{id}/send-email', [$ordersController, 'sendEmail'], [$authMiddleware]); - $router->post('/orders/{id}/email-preview', [$ordersController, 'emailPreview'], [$authMiddleware]); - $router->get('/api/orders/search', [$ordersController, 'quickSearch'], [$authMiddleware]); - $router->get('/api/orders/{id}/preview', [$ordersController, 'preview'], [$authMiddleware]); - $router->post('/orders/{id}/notes', [$ordersController, 'storeNote'], [$authMiddleware]); - $router->post('/orders/{id}/notes/{noteId}/update', [$ordersController, 'updateNote'], [$authMiddleware]); - $router->post('/orders/{id}/notes/{noteId}/delete', [$ordersController, 'deleteNote'], [$authMiddleware]); - $router->post('/users', [$usersController, 'store'], [$authMiddleware]); - $router->get('/settings/users', [$usersController, 'index'], [$authMiddleware]); - $router->post('/settings/users', [$usersController, 'store'], [$authMiddleware]); - $router->get('/settings', static fn (Request $request): Response => Response::redirect('/settings/users'), [$authMiddleware]); - $router->get('/settings/database', [$settingsController, 'database'], [$authMiddleware]); - $router->post('/settings/database/migrate', [$settingsController, 'migrate'], [$authMiddleware]); - $router->get('/settings/statuses', [$settingsController, 'statuses'], [$authMiddleware]); - $router->post('/settings/status-groups', [$settingsController, 'createStatusGroup'], [$authMiddleware]); - $router->post('/settings/status-groups/update', [$settingsController, 'updateStatusGroup'], [$authMiddleware]); - $router->post('/settings/status-groups/delete', [$settingsController, 'deleteStatusGroup'], [$authMiddleware]); - $router->post('/settings/status-groups/reorder', [$settingsController, 'reorderStatusGroups'], [$authMiddleware]); - $router->post('/settings/statuses/create', [$settingsController, 'createStatus'], [$authMiddleware]); - $router->post('/settings/statuses/update', [$settingsController, 'updateStatus'], [$authMiddleware]); - $router->post('/settings/statuses/delete', [$settingsController, 'deleteStatus'], [$authMiddleware]); - $router->post('/settings/statuses/reorder', [$settingsController, 'reorderStatuses'], [$authMiddleware]); - $router->get('/settings/cron', [$cronSettingsController, 'index'], [$authMiddleware]); - $router->post('/settings/cron', [$cronSettingsController, 'save'], [$authMiddleware]); - $router->get('/settings/integrations', [$integrationsHubController, 'index'], [$authMiddleware]); - $router->get('/settings/integrations/allegro', [$allegroIntegrationController, 'index'], [$authMiddleware]); - $router->post('/settings/integrations/allegro/save', [$allegroIntegrationController, 'save'], [$authMiddleware]); - $router->post('/settings/integrations/allegro/settings/save', [$allegroIntegrationController, 'saveImportSettings'], [$authMiddleware]); - $router->post('/settings/integrations/allegro/oauth/start', [$allegroIntegrationController, 'startOAuth'], [$authMiddleware]); - $router->post('/settings/integrations/allegro/import-single', [$allegroIntegrationController, 'importSingleOrder'], [$authMiddleware]); - $router->post('/settings/integrations/allegro/statuses/save', [$allegroStatusMappingController, 'saveStatusMapping'], [$authMiddleware]); - $router->post('/settings/integrations/allegro/statuses/save-bulk', [$allegroStatusMappingController, 'saveStatusMappingsBulk'], [$authMiddleware]); - $router->post('/settings/integrations/allegro/statuses/delete', [$allegroStatusMappingController, 'deleteStatusMapping'], [$authMiddleware]); - $router->post('/settings/integrations/allegro/statuses/save-pull', [$allegroStatusMappingController, 'savePullStatusMappings'], [$authMiddleware]); - $router->post('/settings/integrations/allegro/statuses/sync', [$allegroStatusMappingController, 'syncStatusesFromAllegro'], [$authMiddleware]); - $router->post('/settings/integrations/allegro/delivery/save', [$allegroDeliveryMappingController, 'saveDeliveryMappings'], [$authMiddleware]); - $router->get('/settings/integrations/allegro/oauth/callback', [$allegroIntegrationController, 'oauthCallback']); - $router->get('/settings/integrations/apaczka', [$apaczkaIntegrationController, 'index'], [$authMiddleware]); - $router->post('/settings/integrations/apaczka/save', [$apaczkaIntegrationController, 'save'], [$authMiddleware]); - $router->post('/settings/integrations/apaczka/test', [$apaczkaIntegrationController, 'test'], [$authMiddleware]); - $router->get('/settings/integrations/inpost', [$inpostIntegrationController, 'index'], [$authMiddleware]); - $router->post('/settings/integrations/inpost/save', [$inpostIntegrationController, 'save'], [$authMiddleware]); - $router->get('/settings/integrations/fakturownia', [$fakturowniaIntegrationController, 'index'], [$authMiddleware]); - $router->get('/settings/integrations/fakturownia/new', [$fakturowniaIntegrationController, 'edit'], [$authMiddleware]); - $router->get('/settings/integrations/fakturownia/edit', [$fakturowniaIntegrationController, 'edit'], [$authMiddleware]); - $router->post('/settings/integrations/fakturownia/save', [$fakturowniaIntegrationController, 'save'], [$authMiddleware]); - $router->post('/settings/integrations/fakturownia/test', [$fakturowniaIntegrationController, 'test'], [$authMiddleware]); - $router->get('/settings/integrations/hostedsms', [$hostedSmsIntegrationController, 'index'], [$authMiddleware]); - $router->post('/settings/integrations/hostedsms/save', [$hostedSmsIntegrationController, 'save'], [$authMiddleware]); - $router->post('/settings/integrations/hostedsms/test', [$hostedSmsIntegrationController, 'test'], [$authMiddleware]); - $router->get('/settings/integrations/polkurier', [$polkurierIntegrationController, 'index'], [$authMiddleware]); - $router->post('/settings/integrations/polkurier/save', [$polkurierIntegrationController, 'save'], [$authMiddleware]); - $router->post('/settings/integrations/polkurier/test', [$polkurierIntegrationController, 'test'], [$authMiddleware]); - $router->get('/settings/integrations/smsplanet', [$smsplanetIntegrationController, 'index'], [$authMiddleware]); - $router->post('/settings/integrations/smsplanet/save', [$smsplanetIntegrationController, 'save'], [$authMiddleware]); - $router->post('/settings/integrations/smsplanet/test', [$smsplanetIntegrationController, 'test'], [$authMiddleware]); - $router->get('/settings/integrations/erli', [$erliIntegrationController, 'index'], [$authMiddleware]); - $router->post('/settings/integrations/erli/save', [$erliIntegrationController, 'save'], [$authMiddleware]); - $router->post('/settings/integrations/erli/test', [$erliIntegrationController, 'test'], [$authMiddleware]); - $router->post('/settings/integrations/erli/import', [$erliIntegrationController, 'importNow'], [$authMiddleware]); - $router->post('/settings/integrations/erli/statuses/save-pull', [$erliIntegrationController, 'savePullStatusMappings'], [$authMiddleware]); - $router->post('/settings/integrations/erli/statuses/save-push', [$erliIntegrationController, 'savePushStatusMappings'], [$authMiddleware]); - $router->post('/settings/integrations/erli/delivery/save', [$erliDeliveryMappingController, 'saveDeliveryMappings'], [$authMiddleware]); - $router->get('/settings/integrations/shoppro', [$shopproIntegrationsController, 'index'], [$authMiddleware]); - $router->post('/settings/integrations/shoppro/save', [$shopproIntegrationsController, 'save'], [$authMiddleware]); - $router->post('/settings/integrations/shoppro/test', [$shopproIntegrationsController, 'test'], [$authMiddleware]); - $router->post('/settings/integrations/shoppro/statuses/save', [$shopproIntegrationsController, 'saveStatusMappings'], [$authMiddleware]); - $router->post('/settings/integrations/shoppro/statuses/save-pull', [$shopproIntegrationsController, 'savePullStatusMappings'], [$authMiddleware]); - $router->post('/settings/integrations/shoppro/statuses/sync', [$shopproIntegrationsController, 'syncStatuses'], [$authMiddleware]); - $router->post('/settings/integrations/shoppro/delivery/save', [$shopproIntegrationsController, 'saveDeliveryMappings'], [$authMiddleware]); - $router->get('/settings/company', [$companySettingsController, 'index'], [$authMiddleware]); - $router->post('/settings/company/save', [$companySettingsController, 'save'], [$authMiddleware]); - $router->get('/settings/accounting', [$receiptConfigController, 'hub'], [$authMiddleware]); - $router->get('/settings/accounting/receipts', [$receiptConfigController, 'list'], [$authMiddleware]); - $router->get('/settings/accounting/receipts/new', [$receiptConfigController, 'edit'], [$authMiddleware]); - $router->get('/settings/accounting/receipts/edit', [$receiptConfigController, 'edit'], [$authMiddleware]); - $router->post('/settings/accounting/receipts/save', [$receiptConfigController, 'save'], [$authMiddleware]); - $router->post('/settings/accounting/receipts/toggle', [$receiptConfigController, 'toggleStatus'], [$authMiddleware]); - $router->post('/settings/accounting/receipts/delete', [$receiptConfigController, 'delete'], [$authMiddleware]); - // Legacy aliases (backwards compatibility with bookmarks/form actions from before Phase 114-01) - $router->post('/settings/accounting/save', [$receiptConfigController, 'save'], [$authMiddleware]); - $router->post('/settings/accounting/toggle', [$receiptConfigController, 'toggleStatus'], [$authMiddleware]); - $router->post('/settings/accounting/delete', [$receiptConfigController, 'delete'], [$authMiddleware]); - // Invoices (Phase 114-01) - $router->get('/settings/accounting/invoices', [$invoiceConfigController, 'index'], [$authMiddleware]); - $router->get('/settings/accounting/invoices/new', [$invoiceConfigController, 'edit'], [$authMiddleware]); - $router->get('/settings/accounting/invoices/edit', [$invoiceConfigController, 'edit'], [$authMiddleware]); - $router->post('/settings/accounting/invoices/save', [$invoiceConfigController, 'save'], [$authMiddleware]); - $router->post('/settings/accounting/invoices/toggle', [$invoiceConfigController, 'toggleStatus'], [$authMiddleware]); - $router->post('/settings/accounting/invoices/delete', [$invoiceConfigController, 'delete'], [$authMiddleware]); - $router->get('/settings/email-mailboxes', [$emailMailboxController, 'index'], [$authMiddleware]); - $router->post('/settings/email-mailboxes/save', [$emailMailboxController, 'save'], [$authMiddleware]); - $router->post('/settings/email-mailboxes/delete', [$emailMailboxController, 'delete'], [$authMiddleware]); - $router->post('/settings/email-mailboxes/toggle', [$emailMailboxController, 'toggleStatus'], [$authMiddleware]); - $router->post('/settings/email-mailboxes/test', [$emailMailboxController, 'testConnection'], [$authMiddleware]); - $router->get('/settings/email-templates', [$emailTemplateController, 'index'], [$authMiddleware]); - $router->get('/settings/email-templates/create', [$emailTemplateController, 'create'], [$authMiddleware]); - $router->get('/settings/email-templates/edit', [$emailTemplateController, 'edit'], [$authMiddleware]); - $router->post('/settings/email-templates/save', [$emailTemplateController, 'save'], [$authMiddleware]); - $router->post('/settings/email-templates/delete', [$emailTemplateController, 'delete'], [$authMiddleware]); - $router->post('/settings/email-templates/duplicate', [$emailTemplateController, 'duplicate'], [$authMiddleware]); - $router->post('/settings/email-templates/toggle', [$emailTemplateController, 'toggleStatus'], [$authMiddleware]); - $router->post('/settings/email-templates/preview', [$emailTemplateController, 'preview'], [$authMiddleware]); - $router->get('/settings/email-templates/variables', [$emailTemplateController, 'getVariables'], [$authMiddleware]); - $router->get('/settings/sms-templates', [$smsTemplateController, 'index'], [$authMiddleware]); - $router->get('/settings/sms-templates/create', [$smsTemplateController, 'create'], [$authMiddleware]); - $router->get('/settings/sms-templates/edit', [$smsTemplateController, 'edit'], [$authMiddleware]); - $router->post('/settings/sms-templates/save', [$smsTemplateController, 'save'], [$authMiddleware]); - $router->post('/settings/sms-templates/delete', [$smsTemplateController, 'delete'], [$authMiddleware]); - $router->post('/settings/sms-templates/toggle', [$smsTemplateController, 'toggleStatus'], [$authMiddleware]); - $router->get('/settings/sms-templates/variables', [$smsTemplateController, 'getVariables'], [$authMiddleware]); - $router->get('/settings/automation', [$automationController, 'index'], [$authMiddleware]); - $router->get('/settings/automation/create', [$automationController, 'create'], [$authMiddleware]); - $router->post('/settings/automation/store', [$automationController, 'store'], [$authMiddleware]); - $router->get('/settings/automation/edit', [$automationController, 'edit'], [$authMiddleware]); - $router->post('/settings/automation/update', [$automationController, 'update'], [$authMiddleware]); - $router->post('/settings/automation/delete', [$automationController, 'destroy'], [$authMiddleware]); - $router->post('/settings/automation/duplicate', [$automationController, 'duplicate'], [$authMiddleware]); - $router->post('/settings/automation/toggle', [$automationController, 'toggleStatus'], [$authMiddleware]); - $router->get('/settings/delivery-status-mappings', [$deliveryStatusMappingController, 'index'], [$authMiddleware]); - $router->post('/settings/delivery-status-mappings/save', [$deliveryStatusMappingController, 'save'], [$authMiddleware]); - $router->post('/settings/delivery-status-mappings/save-bulk', [$deliveryStatusMappingController, 'saveBulk'], [$authMiddleware]); - $router->post('/settings/delivery-status-mappings/reset', [$deliveryStatusMappingController, 'reset'], [$authMiddleware]); - $router->post('/settings/delivery-status-mappings/reset-all', [$deliveryStatusMappingController, 'resetAll'], [$authMiddleware]); - $router->get('/settings/delivery-statuses', [$deliveryStatusesController, 'index'], [$authMiddleware]); - $router->get('/settings/delivery-statuses/new', [$deliveryStatusesController, 'create'], [$authMiddleware]); - $router->get('/settings/delivery-statuses/{id}/edit', [$deliveryStatusesController, 'edit'], [$authMiddleware]); - $router->post('/settings/delivery-statuses', [$deliveryStatusesController, 'store'], [$authMiddleware]); - $router->post('/settings/delivery-statuses/{id}/update', [$deliveryStatusesController, 'update'], [$authMiddleware]); - $router->post('/settings/delivery-statuses/{id}/delete', [$deliveryStatusesController, 'destroy'], [$authMiddleware]); - $router->get('/accounting', [$accountingController, 'index'], [$authMiddleware]); - $router->post('/accounting/export', [$accountingController, 'export'], [$authMiddleware]); - $router->get('/orders/{id}/receipt/create', [$receiptController, 'create'], [$authMiddleware]); - $router->post('/orders/{id}/receipt/store', [$receiptController, 'store'], [$authMiddleware]); - $router->get('/orders/{id}/receipt/{receiptId}', [$receiptController, 'show'], [$authMiddleware]); - $router->get('/orders/{id}/receipt/{receiptId}/print', [$receiptController, 'printView'], [$authMiddleware]); - $router->get('/orders/{id}/receipt/{receiptId}/pdf', [$receiptController, 'pdf'], [$authMiddleware]); - // Invoices from order (Phase 115-01) - $router->post('/orders/{id}/invoice-requested/toggle', [$ordersController, 'toggleInvoiceRequested'], [$authMiddleware]); - $router->get('/orders/{id}/invoice/create', [$invoiceController, 'create'], [$authMiddleware]); - $router->post('/orders/{id}/invoice/store', [$invoiceController, 'store'], [$authMiddleware]); - $router->get('/orders/{id}/invoice/{invoiceId}', [$invoiceController, 'show'], [$authMiddleware]); - $router->get('/orders/{id}/invoice/{invoiceId}/pdf', [$invoiceController, 'pdf'], [$authMiddleware]); - $router->get('/settings/accounting/invoices/issued', [$invoiceController, 'issuedList'], [$authMiddleware]); - $router->get('/api/nip/lookup', [$invoiceController, 'nipLookup'], [$authMiddleware]); - $router->get('/orders/{id}/shipment/prepare', [$shipmentController, 'prepare'], [$authMiddleware]); - $router->post('/orders/{id}/shipment/create', [$shipmentController, 'create'], [$authMiddleware]); - $router->get('/orders/{id}/shipment/{packageId}/status', [$shipmentController, 'checkStatus'], [$authMiddleware]); - $router->post('/orders/{id}/shipment/{packageId}/label', [$shipmentController, 'label'], [$authMiddleware]); - $router->post('/orders/{id}/shipment/manual', [$shipmentController, 'createManual'], [$authMiddleware]); - $router->post('/orders/{id}/shipment/{packageId}/delete', [$shipmentController, 'delete'], [$authMiddleware]); - $router->post('/orders/{id}/payment/add', [$ordersController, 'addPayment'], [$authMiddleware]); - - // --- Printing module --- - $printApiKeyRepository = new PrintApiKeyRepository($app->db()); - $apiKeyMiddleware = new ApiKeyMiddleware($printApiKeyRepository); - $printApiController = new PrintApiController( - $printJobRepository, - $shipmentPackageRepository, - $auth, - $app->basePath('storage'), - $shipmentProviderRegistry - ); - $printSettingsController = new PrintSettingsController($template, $translator, $auth, $printApiKeyRepository, $printJobRepository); - $projectMappingRepository = new ProjectMappingRepository($app->db()); - $projectMappingController = new ProjectMappingController( - $template, - $translator, - $auth, - $projectMappingRepository, - $app->basePath() - ); - - // Print API — session auth (from orderPRO UI) - $router->post('/api/print/jobs', [$printApiController, 'createJob'], [$authMiddleware]); - $router->get('/api/print/jobs/status', [$printApiController, 'status'], [$authMiddleware]); - - // Print API — API key auth (from Windows client) - $router->get('/api/print/jobs/pending', [$printApiController, 'listPending'], [$apiKeyMiddleware]); - $router->get('/api/print/jobs/{id}/download', [$printApiController, 'downloadLabel'], [$apiKeyMiddleware]); - $router->post('/api/print/jobs/{id}/complete', [$printApiController, 'markComplete'], [$apiKeyMiddleware]); - - // Print settings - $router->get('/settings/printing', [$printSettingsController, 'index'], [$authMiddleware]); - $router->post('/settings/printing/keys/create', [$printSettingsController, 'createKey'], [$authMiddleware]); - $router->post('/settings/printing/keys/{id}/delete', [$printSettingsController, 'deleteKey'], [$authMiddleware]); - $router->post('/settings/printing/jobs/delete', [$printSettingsController, 'deleteJob'], [$authMiddleware]); - - // Project mappings - $router->get('/settings/project-mappings', [$projectMappingController, 'index'], [$authMiddleware]); - $router->post('/settings/project-mappings', [$projectMappingController, 'store'], [$authMiddleware]); - $router->post('/settings/project-mappings/{id}/update', [$projectMappingController, 'update'], [$authMiddleware]); - $router->post('/settings/project-mappings/{id}/delete', [$projectMappingController, 'delete'], [$authMiddleware]); - $router->post('/settings/project-mappings/{id}/toggle', [$projectMappingController, 'toggleActive'], [$authMiddleware]); - - // Shipment presets API - $presetRepository = new ShipmentPresetRepository($app->db()); - $presetController = new ShipmentPresetController($presetRepository); - - $router->get('/api/shipment-presets', [$presetController, 'list'], [$authMiddleware]); - $router->post('/api/shipment-presets', [$presetController, 'store'], [$authMiddleware]); - $router->post('/api/shipment-presets/update', [$presetController, 'update'], [$authMiddleware]); - $router->post('/api/shipment-presets/delete', [$presetController, 'destroy'], [$authMiddleware]); + $authMiddleware = new AuthMiddleware($app->auth()); + foreach ($modules as $module) { + $module->routes($app->router(), $services, $authMiddleware, $app); + } }; diff --git a/src/Core/Routing/ModuleProvider.php b/src/Core/Routing/ModuleProvider.php new file mode 100644 index 0000000..292b78d --- /dev/null +++ b/src/Core/Routing/ModuleProvider.php @@ -0,0 +1,27 @@ +/Module.php). + */ +interface ModuleProvider +{ + /** + * Zarejestruj factory dla wszystkich uslug nalezacych do tego modulu. + * Factory to closure (lazy) - obiekty buduja sie dopiero przy get(). + */ + public function register(ServiceRegistry $services, Application $app): void; + + /** + * Zarejestruj route'y modulu. W handlerach uzywaj $services->lazy(id, method), + * aby kontrolery nie konstruowaly sie przy starcie aplikacji. + */ + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void; +} diff --git a/src/Core/Routing/ServiceRegistry.php b/src/Core/Routing/ServiceRegistry.php new file mode 100644 index 0000000..511cd61 --- /dev/null +++ b/src/Core/Routing/ServiceRegistry.php @@ -0,0 +1,64 @@ + */ + private array $factories = []; + + /** @var array */ + private array $instances = []; + + public function set(string $id, Closure $factory): void + { + $this->factories[$id] = $factory; + unset($this->instances[$id]); + } + + public function has(string $id): bool + { + return isset($this->factories[$id]); + } + + /** + * @return mixed + */ + public function get(string $id) + { + if (array_key_exists($id, $this->instances)) { + return $this->instances[$id]; + } + + if (!isset($this->factories[$id])) { + throw new RuntimeException(sprintf('Service not registered: "%s"', $id)); + } + + return $this->instances[$id] = ($this->factories[$id])($this); + } + + /** + * Zwraca closure odraczajaca pobranie kontrolera i wywolanie metody. + * Uzywane w ModuleProvider::routes() do leniwej konstrukcji handlera. + */ + public function lazy(string $serviceId, string $method): Closure + { + return function (Request $request) use ($serviceId, $method) { + $controller = $this->get($serviceId); + return $controller->{$method}($request); + }; + } +} diff --git a/src/Modules/Accounting/AccountingModule.php b/src/Modules/Accounting/AccountingModule.php new file mode 100644 index 0000000..9001c08 --- /dev/null +++ b/src/Modules/Accounting/AccountingModule.php @@ -0,0 +1,141 @@ +set('accounting.receipts.repo', static fn () => new ReceiptRepository($app->db())); + $services->set('accounting.receipts.config_repo', static fn () => new ReceiptConfigRepository($app->db())); + + $services->set('accounting.receipts.config_controller', static fn (ServiceRegistry $s) => new ReceiptConfigController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('accounting.receipts.config_repo') + )); + + $services->set('accounting.receipts.service', static fn (ServiceRegistry $s) => new ReceiptService( + $s->get('accounting.receipts.repo'), + $s->get('accounting.receipts.config_repo'), + $s->get('shared.companies.repo'), + $app->orders() + )); + + $services->set('accounting.receipts.controller', static fn (ServiceRegistry $s) => new ReceiptController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('accounting.receipts.repo'), + $s->get('accounting.receipts.config_repo'), + $s->get('shared.companies.repo'), + $app->orders(), + $s->get('automation.service'), + $s->get('accounting.receipts.service') + )); + + // Invoices + $services->set('accounting.invoices.repo', static fn () => new InvoiceRepository($app->db())); + $services->set('accounting.invoices.config_repo', static fn (ServiceRegistry $s) => new InvoiceConfigRepository( + $app->db(), + $s->get('integrations.fakturownia.repo') + )); + + $services->set('accounting.invoices.config_controller', static fn (ServiceRegistry $s) => new InvoiceConfigController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('accounting.invoices.config_repo'), + $s->get('integrations.fakturownia.repo') + )); + + $services->set('accounting.invoices.service', static fn (ServiceRegistry $s) => new InvoiceService( + $s->get('accounting.invoices.repo'), + $s->get('accounting.invoices.config_repo'), + $s->get('shared.companies.repo'), + $app->orders(), + $s->get('integrations.fakturownia.repo'), + $s->get('integrations.fakturownia.api_client') + )); + + $services->set('accounting.invoices.controller', static fn (ServiceRegistry $s) => new InvoiceController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('accounting.invoices.repo'), + $s->get('accounting.invoices.config_repo'), + $s->get('shared.companies.repo'), + $app->orders(), + $s->get('accounting.invoices.service'), + new MfWhitelistApiClient() + )); + + // Accounting XLSX export + $services->set('accounting.controller', static fn (ServiceRegistry $s) => new AccountingController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('accounting.receipts.repo'), + $s->get('accounting.receipts.config_repo') + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $rc = static fn (string $m) => $services->lazy('accounting.receipts.config_controller', $m); + $router->get('/settings/accounting', $rc('hub'), [$auth]); + $router->get('/settings/accounting/receipts', $rc('list'), [$auth]); + $router->get('/settings/accounting/receipts/new', $rc('edit'), [$auth]); + $router->get('/settings/accounting/receipts/edit', $rc('edit'), [$auth]); + $router->post('/settings/accounting/receipts/save', $rc('save'), [$auth]); + $router->post('/settings/accounting/receipts/toggle', $rc('toggleStatus'), [$auth]); + $router->post('/settings/accounting/receipts/delete', $rc('delete'), [$auth]); + // Legacy aliases (Phase 114-01) + $router->post('/settings/accounting/save', $rc('save'), [$auth]); + $router->post('/settings/accounting/toggle', $rc('toggleStatus'), [$auth]); + $router->post('/settings/accounting/delete', $rc('delete'), [$auth]); + + $ic = static fn (string $m) => $services->lazy('accounting.invoices.config_controller', $m); + $router->get('/settings/accounting/invoices', $ic('index'), [$auth]); + $router->get('/settings/accounting/invoices/new', $ic('edit'), [$auth]); + $router->get('/settings/accounting/invoices/edit', $ic('edit'), [$auth]); + $router->post('/settings/accounting/invoices/save', $ic('save'), [$auth]); + $router->post('/settings/accounting/invoices/toggle', $ic('toggleStatus'), [$auth]); + $router->post('/settings/accounting/invoices/delete', $ic('delete'), [$auth]); + + $ac = static fn (string $m) => $services->lazy('accounting.controller', $m); + $router->get('/accounting', $ac('index'), [$auth]); + $router->post('/accounting/export', $ac('export'), [$auth]); + + // Receipt actions from order + $r = static fn (string $m) => $services->lazy('accounting.receipts.controller', $m); + $router->get('/orders/{id}/receipt/create', $r('create'), [$auth]); + $router->post('/orders/{id}/receipt/store', $r('store'), [$auth]); + $router->get('/orders/{id}/receipt/{receiptId}', $r('show'), [$auth]); + $router->get('/orders/{id}/receipt/{receiptId}/print', $r('printView'), [$auth]); + $router->get('/orders/{id}/receipt/{receiptId}/pdf', $r('pdf'), [$auth]); + + // Invoice actions from order (Phase 115-01) + $inv = static fn (string $m) => $services->lazy('accounting.invoices.controller', $m); + $router->get('/orders/{id}/invoice/create', $inv('create'), [$auth]); + $router->post('/orders/{id}/invoice/store', $inv('store'), [$auth]); + $router->get('/orders/{id}/invoice/{invoiceId}', $inv('show'), [$auth]); + $router->get('/orders/{id}/invoice/{invoiceId}/pdf', $inv('pdf'), [$auth]); + $router->get('/settings/accounting/invoices/issued', $inv('issuedList'), [$auth]); + $router->get('/api/nip/lookup', $inv('nipLookup'), [$auth]); + } +} diff --git a/src/Modules/Auth/AuthModule.php b/src/Modules/Auth/AuthModule.php new file mode 100644 index 0000000..8034737 --- /dev/null +++ b/src/Modules/Auth/AuthModule.php @@ -0,0 +1,28 @@ +set('auth.controller', static fn () => new AuthController( + $app->template(), + $app->auth(), + $app->translator() + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $router->get('/login', $services->lazy('auth.controller', 'showLogin')); + $router->post('/login', $services->lazy('auth.controller', 'login')); + $router->post('/logout', $services->lazy('auth.controller', 'logout'), [$auth]); + } +} diff --git a/src/Modules/Automation/AutomationModule.php b/src/Modules/Automation/AutomationModule.php new file mode 100644 index 0000000..605cca2 --- /dev/null +++ b/src/Modules/Automation/AutomationModule.php @@ -0,0 +1,55 @@ +set('automation.repo', static fn () => new AutomationRepository($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.controller', static fn (ServiceRegistry $s) => new AutomationController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('automation.repo'), + $s->get('automation.execution_log_repo'), + $s->get('accounting.receipts.config_repo') + )); + + $services->set('automation.service', static fn (ServiceRegistry $s) => new AutomationService( + $s->get('automation.repo'), + $s->get('automation.execution_log_repo'), + $s->get('automation.email_once_repo'), + $s->get('email.sending_service'), + $app->orders(), + $s->get('shared.companies.repo'), + $s->get('accounting.receipts.repo'), + $s->get('accounting.receipts.config_repo'), + $s->get('shared.shipment_packages.repo'), + $s->get('accounting.receipts.service') + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $a = static fn (string $m) => $services->lazy('automation.controller', $m); + $router->get('/settings/automation', $a('index'), [$auth]); + $router->get('/settings/automation/create', $a('create'), [$auth]); + $router->post('/settings/automation/store', $a('store'), [$auth]); + $router->get('/settings/automation/edit', $a('edit'), [$auth]); + $router->post('/settings/automation/update', $a('update'), [$auth]); + $router->post('/settings/automation/delete', $a('destroy'), [$auth]); + $router->post('/settings/automation/duplicate', $a('duplicate'), [$auth]); + $router->post('/settings/automation/toggle', $a('toggleStatus'), [$auth]); + } +} diff --git a/src/Modules/Cron/CronModule.php b/src/Modules/Cron/CronModule.php new file mode 100644 index 0000000..b852f31 --- /dev/null +++ b/src/Modules/Cron/CronModule.php @@ -0,0 +1,93 @@ +set('shared.cron.repo', static fn () => new CronRepository($app->db())); + + $services->set('cron.settings_controller', static fn (ServiceRegistry $s) => new CronSettingsController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('shared.cron.repo'), + (bool) $app->config('app.cron.run_on_web_default', false), + (int) $app->config('app.cron.web_limit_default', 5) + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $router->get('/settings/cron', $services->lazy('cron.settings_controller', 'index'), [$auth]); + $router->post('/settings/cron', $services->lazy('cron.settings_controller', 'save'), [$auth]); + + $publicCronHandler = static function (Request $request) use ($app, $services): Response { + $token = trim((string) $request->input('token', '')); + if ($token === '') { + $token = trim((string) $request->input('tokenValue', '')); + if (str_starts_with($token, 'token=')) { + $token = substr($token, 6); + } + } + + $expectedToken = trim((string) $app->config('app.cron.public_token', '')); + if ($expectedToken === '' || $token === '' || !hash_equals($expectedToken, $token)) { + return Response::json(['ok' => false, 'message' => 'Unauthorized'], 403); + } + + try { + /** @var CronRepository $cronRepository */ + $cronRepository = $services->get('shared.cron.repo'); + $limit = $cronRepository->getIntSetting( + 'cron_web_limit', + (int) $app->config('app.cron.web_limit_default', 5), + 1, + 100 + ); + + $factory = new CronHandlerFactory( + $app->db(), + (string) $app->config('app.integrations.secret', ''), + $app->basePath() + ); + $runner = $factory->build($cronRepository, $app->logger()); + $runner->run($limit); + + return Response::json([ + 'ok' => true, + 'message' => 'Cron executed', + 'limit' => $limit, + 'timestamp' => date(DATE_ATOM), + ]); + } catch (Throwable $exception) { + $app->logger()->error('Public cron endpoint failed', [ + 'message' => $exception->getMessage(), + 'path' => $request->path(), + ]); + + $debug = (bool) $app->config('app.debug', false); + return Response::json([ + 'ok' => false, + 'message' => 'Cron execution failed', + 'error' => $debug ? $exception->getMessage() : null, + ], 500); + } + }; + + $router->get('/cron', $publicCronHandler); + $router->get('/cron/{tokenValue}', $publicCronHandler); + } +} diff --git a/src/Modules/Email/EmailModule.php b/src/Modules/Email/EmailModule.php new file mode 100644 index 0000000..db9e67a --- /dev/null +++ b/src/Modules/Email/EmailModule.php @@ -0,0 +1,88 @@ +config('app.integrations.secret', ''); + + $services->set('email.mailboxes.repo', static fn () => new EmailMailboxRepository( + $app->db(), + new IntegrationSecretCipher($secret) + )); + + $services->set('email.templates.repo', static fn () => new EmailTemplateRepository($app->db())); + + $services->set('email.variable_resolver', static fn (ServiceRegistry $s) => new VariableResolver( + $s->get('shared.shipment_packages.repo'), + $s->get('sms.variable_resolver') + )); + + $services->set('email.attachment_generator', static fn (ServiceRegistry $s) => new AttachmentGenerator( + $s->get('accounting.receipts.repo'), + $s->get('accounting.receipts.config_repo'), + $app->template() + )); + + $services->set('email.sending_service', static fn (ServiceRegistry $s) => new EmailSendingService( + $app->db(), + $app->orders(), + $s->get('email.templates.repo'), + $s->get('email.mailboxes.repo'), + $s->get('email.variable_resolver'), + $s->get('email.attachment_generator') + )); + + $services->set('email.mailbox.controller', static fn (ServiceRegistry $s) => new EmailMailboxController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('email.mailboxes.repo'), + new SmtpSecurityContextFactory((bool) $app->config('app.smtp.allow_self_signed_dev', false)) + )); + + $services->set('email.template.controller', static fn (ServiceRegistry $s) => new EmailTemplateController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('email.templates.repo'), + $s->get('email.mailboxes.repo') + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $mb = static fn (string $m) => $services->lazy('email.mailbox.controller', $m); + $router->get('/settings/email-mailboxes', $mb('index'), [$auth]); + $router->post('/settings/email-mailboxes/save', $mb('save'), [$auth]); + $router->post('/settings/email-mailboxes/delete', $mb('delete'), [$auth]); + $router->post('/settings/email-mailboxes/toggle', $mb('toggleStatus'), [$auth]); + $router->post('/settings/email-mailboxes/test', $mb('testConnection'), [$auth]); + + $t = static fn (string $m) => $services->lazy('email.template.controller', $m); + $router->get('/settings/email-templates', $t('index'), [$auth]); + $router->get('/settings/email-templates/create', $t('create'), [$auth]); + $router->get('/settings/email-templates/edit', $t('edit'), [$auth]); + $router->post('/settings/email-templates/save', $t('save'), [$auth]); + $router->post('/settings/email-templates/delete', $t('delete'), [$auth]); + $router->post('/settings/email-templates/duplicate', $t('duplicate'), [$auth]); + $router->post('/settings/email-templates/toggle', $t('toggleStatus'), [$auth]); + $router->post('/settings/email-templates/preview', $t('preview'), [$auth]); + $router->get('/settings/email-templates/variables', $t('getVariables'), [$auth]); + } +} diff --git a/src/Modules/Info/InfoModule.php b/src/Modules/Info/InfoModule.php new file mode 100644 index 0000000..f732129 --- /dev/null +++ b/src/Modules/Info/InfoModule.php @@ -0,0 +1,36 @@ +set('info.controller', static fn () => new InfoController($app->template())); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $router->get('/info', $services->lazy('info.controller', 'show')); + + $router->get('/health', static fn (Request $request): Response => Response::json([ + 'status' => 'ok', + 'app' => (string) $app->config('app.name', 'orderPRO'), + 'timestamp' => date(DATE_ATOM), + ])); + + $authService = $app->auth(); + $router->get('/', static fn (Request $request): Response => $authService->check() + ? Response::redirect('/settings/users') + : Response::redirect('/login')); + } +} diff --git a/src/Modules/Notifications/NotificationsModule.php b/src/Modules/Notifications/NotificationsModule.php new file mode 100644 index 0000000..cfe16b1 --- /dev/null +++ b/src/Modules/Notifications/NotificationsModule.php @@ -0,0 +1,37 @@ +set('notifications.repo', static fn () => new NotificationRepository($app->db())); + + $services->set('notifications.controller', static fn (ServiceRegistry $s) => new NotificationController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('notifications.repo') + )); + + $services->set('notifications.api_controller', static fn (ServiceRegistry $s) => new NotificationApiController( + $s->get('notifications.repo') + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $router->get('/notifications', $services->lazy('notifications.controller', 'index'), [$auth]); + $router->post('/notifications/mark-read', $services->lazy('notifications.controller', 'markRead'), [$auth]); + $router->get('/api/notifications/unread', $services->lazy('notifications.api_controller', 'unread'), [$auth]); + $router->post('/api/notifications/mark-read', $services->lazy('notifications.api_controller', 'markRead'), [$auth]); + } +} diff --git a/src/Modules/Orders/OrdersModule.php b/src/Modules/Orders/OrdersModule.php new file mode 100644 index 0000000..b470461 --- /dev/null +++ b/src/Modules/Orders/OrdersModule.php @@ -0,0 +1,67 @@ +set('orders.notes_service', static fn () => new OrderNotesService($app->db())); + + $services->set('orders.controller', static fn (ServiceRegistry $s) => new OrdersController( + $app->template(), + $app->translator(), + $app->auth(), + $app->orders(), + $s->get('shared.shipment_packages.repo'), + $s->get('accounting.receipts.repo'), + $s->get('accounting.receipts.config_repo'), + $s->get('email.sending_service'), + $s->get('email.templates.repo'), + $s->get('email.mailboxes.repo'), + $app->basePath('storage'), + $s->get('shared.print_jobs.repo'), + $s->get('integrations.shoppro.repo'), + $s->get('automation.service'), + $s->get('accounting.invoices.repo'), + $s->get('accounting.invoices.config_repo'), + $s->get('sms.messages.repo'), + $s->get('sms.conversation_service'), + $s->get('sms.templates.repo'), + $s->get('sms.variable_resolver'), + $s->get('shared.companies.repo'), + $s->get('orders.notes_service') + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $router->get('/orders', static fn (Request $request): Response => Response::redirect('/orders/list'), [$auth]); + + $o = static fn (string $m) => $services->lazy('orders.controller', $m); + $router->get('/orders/list', $o('index'), [$auth]); + $router->get('/orders/{id}', $o('show'), [$auth]); + $router->post('/orders/{id}/status', $o('updateStatus'), [$auth]); + $router->post('/orders/{id}/details/update', $o('updateDetails'), [$auth]); + $router->post('/orders/{id}/sms/send', $o('sendSms'), [$auth]); + $router->get('/orders/{id}/sms/template', $o('smsTemplate'), [$auth]); + $router->post('/orders/{id}/send-email', $o('sendEmail'), [$auth]); + $router->post('/orders/{id}/email-preview', $o('emailPreview'), [$auth]); + $router->get('/api/orders/search', $o('quickSearch'), [$auth]); + $router->get('/api/orders/{id}/preview', $o('preview'), [$auth]); + $router->post('/orders/{id}/notes', $o('storeNote'), [$auth]); + $router->post('/orders/{id}/notes/{noteId}/update', $o('updateNote'), [$auth]); + $router->post('/orders/{id}/notes/{noteId}/delete', $o('deleteNote'), [$auth]); + $router->post('/orders/{id}/payment/add', $o('addPayment'), [$auth]); + $router->post('/orders/{id}/invoice-requested/toggle', $o('toggleInvoiceRequested'), [$auth]); + } +} diff --git a/src/Modules/Printing/PrintingModule.php b/src/Modules/Printing/PrintingModule.php new file mode 100644 index 0000000..f09e353 --- /dev/null +++ b/src/Modules/Printing/PrintingModule.php @@ -0,0 +1,83 @@ +set('shared.print_jobs.repo', static fn () => new PrintJobRepository($app->db())); + $services->set('printing.api_keys.repo', static fn () => new PrintApiKeyRepository($app->db())); + + $services->set('printing.api_key_middleware', static fn (ServiceRegistry $s) => new ApiKeyMiddleware( + $s->get('printing.api_keys.repo') + )); + + $services->set('printing.api_controller', static fn (ServiceRegistry $s) => new PrintApiController( + $s->get('shared.print_jobs.repo'), + $s->get('shared.shipment_packages.repo'), + $app->auth(), + $app->basePath('storage'), + $s->get('shipments.provider_registry') + )); + + $services->set('printing.settings_controller', static fn (ServiceRegistry $s) => new PrintSettingsController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('printing.api_keys.repo'), + $s->get('shared.print_jobs.repo') + )); + + $services->set('printing.project_mappings.repo', static fn () => new ProjectMappingRepository($app->db())); + $services->set('printing.project_mappings.controller', static fn (ServiceRegistry $s) => new ProjectMappingController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('printing.project_mappings.repo'), + $app->basePath() + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + /** @var ApiKeyMiddleware $apiKey */ + $apiKey = $services->get('printing.api_key_middleware'); + + $p = static fn (string $m) => $services->lazy('printing.api_controller', $m); + + // Print API — session auth (z UI orderPRO) + $router->post('/api/print/jobs', $p('createJob'), [$auth]); + $router->get('/api/print/jobs/status', $p('status'), [$auth]); + + // Print API — API key auth (z klienta Windows) + $router->get('/api/print/jobs/pending', $p('listPending'), [$apiKey]); + $router->get('/api/print/jobs/{id}/download', $p('downloadLabel'), [$apiKey]); + $router->post('/api/print/jobs/{id}/complete', $p('markComplete'), [$apiKey]); + + // Settings + $ps = static fn (string $m) => $services->lazy('printing.settings_controller', $m); + $router->get('/settings/printing', $ps('index'), [$auth]); + $router->post('/settings/printing/keys/create', $ps('createKey'), [$auth]); + $router->post('/settings/printing/keys/{id}/delete', $ps('deleteKey'), [$auth]); + $router->post('/settings/printing/jobs/delete', $ps('deleteJob'), [$auth]); + + // Project mappings + $pm = static fn (string $m) => $services->lazy('printing.project_mappings.controller', $m); + $router->get('/settings/project-mappings', $pm('index'), [$auth]); + $router->post('/settings/project-mappings', $pm('store'), [$auth]); + $router->post('/settings/project-mappings/{id}/update', $pm('update'), [$auth]); + $router->post('/settings/project-mappings/{id}/delete', $pm('delete'), [$auth]); + $router->post('/settings/project-mappings/{id}/toggle', $pm('toggleActive'), [$auth]); + } +} diff --git a/src/Modules/Settings/AllegroIntegrationModule.php b/src/Modules/Settings/AllegroIntegrationModule.php new file mode 100644 index 0000000..110fb87 --- /dev/null +++ b/src/Modules/Settings/AllegroIntegrationModule.php @@ -0,0 +1,102 @@ +config('app.integrations.secret', ''); + + $services->set('integrations.allegro.repo', static fn () => new AllegroIntegrationRepository($app->db(), $secret)); + $services->set('integrations.allegro.api_client', static fn () => new AllegroApiClient()); + $services->set('integrations.allegro.oauth', static fn () => new AllegroOAuthClient()); + $services->set('integrations.allegro.status_mapping_repo', static fn () => new AllegroStatusMappingRepository($app->db())); + $services->set('integrations.allegro.pull_status_mapping_repo', static fn () => new AllegroPullStatusMappingRepository($app->db())); + + $services->set('integrations.allegro.token_manager', static fn (ServiceRegistry $s) => new AllegroTokenManager( + $s->get('integrations.allegro.repo'), + $s->get('integrations.allegro.oauth') + )); + + $services->set('integrations.allegro.status_discovery_service', static fn (ServiceRegistry $s) => new AllegroStatusDiscoveryService( + $s->get('integrations.allegro.token_manager'), + $s->get('integrations.allegro.api_client'), + $s->get('integrations.allegro.status_mapping_repo'), + $s->get('integrations.allegro.pull_status_mapping_repo') + )); + + $services->set('integrations.allegro.status_mapping_controller', static fn (ServiceRegistry $s) => new AllegroStatusMappingController( + $app->translator(), + $s->get('integrations.allegro.status_mapping_repo'), + $app->orderStatuses(), + $s->get('integrations.allegro.status_discovery_service'), + $s->get('integrations.allegro.pull_status_mapping_repo') + )); + + $services->set('integrations.allegro.delivery_mapping_controller', static fn (ServiceRegistry $s) => new AllegroDeliveryMappingController( + $app->translator(), + $s->get('integrations.allegro.repo'), + $s->get('integrations.allegro.oauth'), + $s->get('integrations.allegro.api_client'), + $s->get('shared.carrier_delivery_mappings.repo'), + $s->get('integrations.apaczka.repo'), + $s->get('integrations.apaczka.api_client') + )); + + $services->set('integrations.allegro.order_import_service', static fn (ServiceRegistry $s) => new AllegroOrderImportService( + $s->get('integrations.allegro.repo'), + $s->get('integrations.allegro.token_manager'), + $s->get('integrations.allegro.api_client'), + new OrderImportRepository($app->db()), + $s->get('integrations.allegro.status_mapping_repo'), + $app->orders(), + $s->get('integrations.allegro.pull_status_mapping_repo'), + $s->get('automation.service') + )); + + $services->set('integrations.allegro.controller', static fn (ServiceRegistry $s) => new AllegroIntegrationController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('integrations.allegro.repo'), + $s->get('integrations.allegro.status_mapping_repo'), + $s->get('integrations.allegro.pull_status_mapping_repo'), + $app->orderStatuses(), + $s->get('shared.cron.repo'), + $s->get('integrations.allegro.oauth'), + $s->get('integrations.allegro.order_import_service'), + $s->get('integrations.allegro.status_discovery_service'), + (string) $app->config('app.url', ''), + $s->get('integrations.allegro.delivery_mapping_controller') + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $c = static fn (string $m) => $services->lazy('integrations.allegro.controller', $m); + $sm = static fn (string $m) => $services->lazy('integrations.allegro.status_mapping_controller', $m); + $dm = static fn (string $m) => $services->lazy('integrations.allegro.delivery_mapping_controller', $m); + + $router->get('/settings/integrations/allegro', $c('index'), [$auth]); + $router->post('/settings/integrations/allegro/save', $c('save'), [$auth]); + $router->post('/settings/integrations/allegro/settings/save', $c('saveImportSettings'), [$auth]); + $router->post('/settings/integrations/allegro/oauth/start', $c('startOAuth'), [$auth]); + $router->post('/settings/integrations/allegro/import-single', $c('importSingleOrder'), [$auth]); + $router->post('/settings/integrations/allegro/statuses/save', $sm('saveStatusMapping'), [$auth]); + $router->post('/settings/integrations/allegro/statuses/save-bulk', $sm('saveStatusMappingsBulk'), [$auth]); + $router->post('/settings/integrations/allegro/statuses/delete', $sm('deleteStatusMapping'), [$auth]); + $router->post('/settings/integrations/allegro/statuses/save-pull', $sm('savePullStatusMappings'), [$auth]); + $router->post('/settings/integrations/allegro/statuses/sync', $sm('syncStatusesFromAllegro'), [$auth]); + $router->post('/settings/integrations/allegro/delivery/save', $dm('saveDeliveryMappings'), [$auth]); + $router->get('/settings/integrations/allegro/oauth/callback', $c('oauthCallback')); + } +} diff --git a/src/Modules/Settings/ApaczkaIntegrationModule.php b/src/Modules/Settings/ApaczkaIntegrationModule.php new file mode 100644 index 0000000..3db8687 --- /dev/null +++ b/src/Modules/Settings/ApaczkaIntegrationModule.php @@ -0,0 +1,37 @@ +config('app.integrations.secret', ''); + + $services->set('integrations.apaczka.repo', static fn () => new ApaczkaIntegrationRepository($app->db(), $secret)); + $services->set('integrations.apaczka.api_client', static fn () => new ApaczkaApiClient()); + + $services->set('integrations.apaczka.controller', static fn (ServiceRegistry $s) => new ApaczkaIntegrationController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('integrations.apaczka.repo'), + $s->get('integrations.apaczka.api_client') + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $c = static fn (string $m) => $services->lazy('integrations.apaczka.controller', $m); + $router->get('/settings/integrations/apaczka', $c('index'), [$auth]); + $router->post('/settings/integrations/apaczka/save', $c('save'), [$auth]); + $router->post('/settings/integrations/apaczka/test', $c('test'), [$auth]); + } +} diff --git a/src/Modules/Settings/ErliIntegrationModule.php b/src/Modules/Settings/ErliIntegrationModule.php new file mode 100644 index 0000000..fce7edb --- /dev/null +++ b/src/Modules/Settings/ErliIntegrationModule.php @@ -0,0 +1,86 @@ +config('app.integrations.secret', ''); + + $services->set('integrations.erli.repo', static fn () => new ErliIntegrationRepository($app->db(), $secret)); + $services->set('integrations.erli.api_client', static fn () => new ErliApiClient()); + $services->set('integrations.erli.pull_status_mapping_repo', static fn () => new ErliPullStatusMappingRepository($app->db())); + $services->set('integrations.erli.status_mapping_repo', static fn () => new ErliStatusMappingRepository($app->db())); + $services->set('integrations.erli.order_sync_state_repo', static fn () => new ErliOrderSyncStateRepository($app->db())); + + $services->set('integrations.erli.order_mapper', static fn (ServiceRegistry $s) => new ErliOrderMapper( + $s->get('integrations.erli.pull_status_mapping_repo') + )); + + $services->set('integrations.erli.orders_sync_service', static fn (ServiceRegistry $s) => new ErliOrdersSyncService( + $s->get('integrations.erli.repo'), + $s->get('integrations.erli.order_sync_state_repo'), + $s->get('integrations.erli.api_client'), + new OrderImportRepository($app->db()), + $app->orders(), + $s->get('integrations.erli.order_mapper'), + $s->get('automation.service'), + $s->get('integrations.erli.pull_status_mapping_repo') + )); + + $services->set('integrations.erli.external_shipment_service', static fn (ServiceRegistry $s) => new ErliExternalShipmentService( + $s->get('integrations.erli.repo'), + $s->get('integrations.erli.api_client'), + $s->get('shared.carrier_delivery_mappings.repo'), + $s->get('shared.shipment_packages.repo'), + $app->orders() + )); + + $services->set('integrations.erli.delivery_mapping_controller', static fn (ServiceRegistry $s) => new ErliDeliveryMappingController( + $app->translator(), + $s->get('shared.carrier_delivery_mappings.repo'), + $s->get('integrations.erli.repo'), + $s->get('integrations.erli.api_client'), + $s->get('shipments.inpost.service'), + $s->get('shipments.apaczka.service') + )); + + $services->set('integrations.erli.controller', static fn (ServiceRegistry $s) => new ErliIntegrationController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('integrations.erli.repo'), + $s->get('integrations.erli.api_client'), + $s->get('integrations.hub.repo'), + $s->get('shared.cron.repo'), + $s->get('integrations.erli.orders_sync_service'), + $app->orderStatuses(), + $s->get('integrations.erli.status_mapping_repo'), + $s->get('integrations.erli.pull_status_mapping_repo'), + $s->get('integrations.erli.delivery_mapping_controller') + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $c = static fn (string $m) => $services->lazy('integrations.erli.controller', $m); + $dm = static fn (string $m) => $services->lazy('integrations.erli.delivery_mapping_controller', $m); + + $router->get('/settings/integrations/erli', $c('index'), [$auth]); + $router->post('/settings/integrations/erli/save', $c('save'), [$auth]); + $router->post('/settings/integrations/erli/test', $c('test'), [$auth]); + $router->post('/settings/integrations/erli/import', $c('importNow'), [$auth]); + $router->post('/settings/integrations/erli/statuses/save-pull', $c('savePullStatusMappings'), [$auth]); + $router->post('/settings/integrations/erli/statuses/save-push', $c('savePushStatusMappings'), [$auth]); + $router->post('/settings/integrations/erli/delivery/save', $dm('saveDeliveryMappings'), [$auth]); + } +} diff --git a/src/Modules/Settings/FakturowniaIntegrationModule.php b/src/Modules/Settings/FakturowniaIntegrationModule.php new file mode 100644 index 0000000..6f3393b --- /dev/null +++ b/src/Modules/Settings/FakturowniaIntegrationModule.php @@ -0,0 +1,40 @@ +config('app.integrations.secret', ''); + + $services->set('integrations.fakturownia.repo', static fn () => new FakturowniaIntegrationRepository($app->db(), $secret)); + $services->set('integrations.fakturownia.api_client', static fn () => new FakturowniaApiClient()); + + $services->set('integrations.fakturownia.controller', static fn (ServiceRegistry $s) => new FakturowniaIntegrationController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('integrations.fakturownia.repo'), + $s->get('integrations.fakturownia.api_client'), + $s->get('integrations.hub.repo') + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $c = static fn (string $m) => $services->lazy('integrations.fakturownia.controller', $m); + $router->get('/settings/integrations/fakturownia', $c('index'), [$auth]); + $router->get('/settings/integrations/fakturownia/new', $c('edit'), [$auth]); + $router->get('/settings/integrations/fakturownia/edit', $c('edit'), [$auth]); + $router->post('/settings/integrations/fakturownia/save', $c('save'), [$auth]); + $router->post('/settings/integrations/fakturownia/test', $c('test'), [$auth]); + } +} diff --git a/src/Modules/Settings/HostedSmsIntegrationModule.php b/src/Modules/Settings/HostedSmsIntegrationModule.php new file mode 100644 index 0000000..7a4b82d --- /dev/null +++ b/src/Modules/Settings/HostedSmsIntegrationModule.php @@ -0,0 +1,38 @@ +config('app.integrations.secret', ''); + + $services->set('integrations.hostedsms.repo', static fn () => new HostedSmsIntegrationRepository($app->db(), $secret)); + $services->set('integrations.hostedsms.api_client', static fn () => new HostedSmsApiClient()); + + $services->set('integrations.hostedsms.controller', static fn (ServiceRegistry $s) => new HostedSmsIntegrationController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('integrations.hostedsms.repo'), + $s->get('integrations.hostedsms.api_client'), + $s->get('integrations.hub.repo') + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $c = static fn (string $m) => $services->lazy('integrations.hostedsms.controller', $m); + $router->get('/settings/integrations/hostedsms', $c('index'), [$auth]); + $router->post('/settings/integrations/hostedsms/save', $c('save'), [$auth]); + $router->post('/settings/integrations/hostedsms/test', $c('test'), [$auth]); + } +} diff --git a/src/Modules/Settings/InpostIntegrationModule.php b/src/Modules/Settings/InpostIntegrationModule.php new file mode 100644 index 0000000..95f0888 --- /dev/null +++ b/src/Modules/Settings/InpostIntegrationModule.php @@ -0,0 +1,34 @@ +config('app.integrations.secret', ''); + + $services->set('integrations.inpost.repo', static fn () => new InpostIntegrationRepository($app->db(), $secret)); + + $services->set('integrations.inpost.controller', static fn (ServiceRegistry $s) => new InpostIntegrationController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('integrations.inpost.repo') + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $c = static fn (string $m) => $services->lazy('integrations.inpost.controller', $m); + $router->get('/settings/integrations/inpost', $c('index'), [$auth]); + $router->post('/settings/integrations/inpost/save', $c('save'), [$auth]); + } +} diff --git a/src/Modules/Settings/IntegrationsHubModule.php b/src/Modules/Settings/IntegrationsHubModule.php new file mode 100644 index 0000000..31750b8 --- /dev/null +++ b/src/Modules/Settings/IntegrationsHubModule.php @@ -0,0 +1,39 @@ +set('integrations.hub.repo', static fn () => new IntegrationsRepository($app->db())); + + $services->set('integrations.hub.controller', static fn (ServiceRegistry $s) => new IntegrationsHubController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('integrations.hub.repo'), + $s->get('integrations.allegro.repo'), + $s->get('integrations.apaczka.repo'), + $s->get('integrations.inpost.repo'), + $s->get('integrations.shoppro.repo'), + $s->get('integrations.fakturownia.repo'), + $s->get('integrations.hostedsms.repo'), + $s->get('integrations.smsplanet.repo'), + $s->get('integrations.polkurier.repo'), + $s->get('integrations.erli.repo') + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $router->get('/settings/integrations', $services->lazy('integrations.hub.controller', 'index'), [$auth]); + } +} diff --git a/src/Modules/Settings/PolkurierIntegrationModule.php b/src/Modules/Settings/PolkurierIntegrationModule.php new file mode 100644 index 0000000..13cf16c --- /dev/null +++ b/src/Modules/Settings/PolkurierIntegrationModule.php @@ -0,0 +1,38 @@ +config('app.integrations.secret', ''); + + $services->set('integrations.polkurier.repo', static fn () => new PolkurierIntegrationRepository($app->db(), $secret)); + $services->set('integrations.polkurier.api_client', static fn () => new PolkurierApiClient()); + + $services->set('integrations.polkurier.controller', static fn (ServiceRegistry $s) => new PolkurierIntegrationController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('integrations.polkurier.repo'), + $s->get('integrations.polkurier.api_client'), + $s->get('integrations.hub.repo') + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $c = static fn (string $m) => $services->lazy('integrations.polkurier.controller', $m); + $router->get('/settings/integrations/polkurier', $c('index'), [$auth]); + $router->post('/settings/integrations/polkurier/save', $c('save'), [$auth]); + $router->post('/settings/integrations/polkurier/test', $c('test'), [$auth]); + } +} diff --git a/src/Modules/Settings/SettingsModule.php b/src/Modules/Settings/SettingsModule.php new file mode 100644 index 0000000..f339fde --- /dev/null +++ b/src/Modules/Settings/SettingsModule.php @@ -0,0 +1,60 @@ +set('settings.controller', static fn () => new SettingsController( + $app->template(), + $app->translator(), + $app->auth(), + $app->migrator(), + $app->orderStatuses() + )); + + $services->set('shared.companies.repo', static fn () => new CompanySettingsRepository($app->db())); + + $services->set('settings.company.controller', static fn (ServiceRegistry $s) => new CompanySettingsController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('shared.companies.repo') + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $router->get('/settings', static fn (Request $request): Response => Response::redirect('/settings/users'), [$auth]); + + $c = static fn (string $m) => $services->lazy('settings.controller', $m); + $router->get('/settings/database', $c('database'), [$auth]); + $router->post('/settings/database/migrate', $c('migrate'), [$auth]); + $router->get('/settings/statuses', $c('statuses'), [$auth]); + $router->post('/settings/status-groups', $c('createStatusGroup'), [$auth]); + $router->post('/settings/status-groups/update', $c('updateStatusGroup'), [$auth]); + $router->post('/settings/status-groups/delete', $c('deleteStatusGroup'), [$auth]); + $router->post('/settings/status-groups/reorder', $c('reorderStatusGroups'), [$auth]); + $router->post('/settings/statuses/create', $c('createStatus'), [$auth]); + $router->post('/settings/statuses/update', $c('updateStatus'), [$auth]); + $router->post('/settings/statuses/delete', $c('deleteStatus'), [$auth]); + $router->post('/settings/statuses/reorder', $c('reorderStatuses'), [$auth]); + + $router->get('/settings/company', $services->lazy('settings.company.controller', 'index'), [$auth]); + $router->post('/settings/company/save', $services->lazy('settings.company.controller', 'save'), [$auth]); + } +} diff --git a/src/Modules/Settings/ShopproIntegrationModule.php b/src/Modules/Settings/ShopproIntegrationModule.php new file mode 100644 index 0000000..7133aa3 --- /dev/null +++ b/src/Modules/Settings/ShopproIntegrationModule.php @@ -0,0 +1,52 @@ +config('app.integrations.secret', ''); + + $services->set('integrations.shoppro.repo', static fn () => new ShopproIntegrationsRepository($app->db(), $secret)); + $services->set('integrations.shoppro.status_mapping_repo', static fn () => new ShopproStatusMappingRepository($app->db())); + $services->set('integrations.shoppro.pull_status_mapping_repo', static fn () => new ShopproPullStatusMappingRepository($app->db())); + + $services->set('integrations.shoppro.controller', static fn (ServiceRegistry $s) => new ShopproIntegrationsController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('integrations.shoppro.repo'), + $s->get('integrations.shoppro.status_mapping_repo'), + $s->get('integrations.shoppro.pull_status_mapping_repo'), + $app->orderStatuses(), + $s->get('shared.cron.repo'), + $s->get('shared.carrier_delivery_mappings.repo'), + $s->get('integrations.allegro.repo'), + $s->get('integrations.allegro.oauth'), + $s->get('integrations.allegro.api_client'), + $s->get('integrations.apaczka.repo'), + $s->get('integrations.apaczka.api_client'), + $s->get('shipments.polkurier.service') + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $c = static fn (string $m) => $services->lazy('integrations.shoppro.controller', $m); + $router->get('/settings/integrations/shoppro', $c('index'), [$auth]); + $router->post('/settings/integrations/shoppro/save', $c('save'), [$auth]); + $router->post('/settings/integrations/shoppro/test', $c('test'), [$auth]); + $router->post('/settings/integrations/shoppro/statuses/save', $c('saveStatusMappings'), [$auth]); + $router->post('/settings/integrations/shoppro/statuses/save-pull', $c('savePullStatusMappings'), [$auth]); + $router->post('/settings/integrations/shoppro/statuses/sync', $c('syncStatuses'), [$auth]); + $router->post('/settings/integrations/shoppro/delivery/save', $c('saveDeliveryMappings'), [$auth]); + } +} diff --git a/src/Modules/Settings/SmsplanetIntegrationModule.php b/src/Modules/Settings/SmsplanetIntegrationModule.php new file mode 100644 index 0000000..c5cfcdb --- /dev/null +++ b/src/Modules/Settings/SmsplanetIntegrationModule.php @@ -0,0 +1,38 @@ +config('app.integrations.secret', ''); + + $services->set('integrations.smsplanet.repo', static fn () => new SmsplanetIntegrationRepository($app->db(), $secret)); + $services->set('integrations.smsplanet.api_client', static fn () => new SmsplanetApiClient()); + + $services->set('integrations.smsplanet.controller', static fn (ServiceRegistry $s) => new SmsplanetIntegrationController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('integrations.smsplanet.repo'), + $s->get('integrations.smsplanet.api_client'), + $s->get('integrations.hub.repo') + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $c = static fn (string $m) => $services->lazy('integrations.smsplanet.controller', $m); + $router->get('/settings/integrations/smsplanet', $c('index'), [$auth]); + $router->post('/settings/integrations/smsplanet/save', $c('save'), [$auth]); + $router->post('/settings/integrations/smsplanet/test', $c('test'), [$auth]); + } +} diff --git a/src/Modules/Shipments/ShipmentsModule.php b/src/Modules/Shipments/ShipmentsModule.php new file mode 100644 index 0000000..eabf188 --- /dev/null +++ b/src/Modules/Shipments/ShipmentsModule.php @@ -0,0 +1,135 @@ +set('shared.shipment_packages.repo', static fn () => new ShipmentPackageRepository($app->db())); + $services->set('shared.carrier_delivery_mappings.repo', static fn () => new \App\Modules\Settings\CarrierDeliveryMethodMappingRepository($app->db())); + + $services->set('shipments.delivery_status.repo', static fn () => new DeliveryStatusRepository($app->db())); + $services->set('shipments.delivery_status_mappings.repo', static fn () => new DeliveryStatusMappingRepository($app->db())); + + // Provider services (one per carrier we own) + $services->set('shipments.allegro.service', static fn (ServiceRegistry $s) => new AllegroShipmentService( + $s->get('integrations.allegro.token_manager'), + new \App\Modules\Settings\AllegroApiClient(), + $s->get('shared.shipment_packages.repo'), + $s->get('shared.companies.repo'), + $app->orders() + )); + + $services->set('shipments.apaczka.service', static fn (ServiceRegistry $s) => new ApaczkaShipmentService( + $s->get('integrations.apaczka.repo'), + $s->get('integrations.apaczka.api_client'), + $s->get('shared.shipment_packages.repo'), + $s->get('shared.companies.repo'), + $app->orders() + )); + + $services->set('shipments.inpost.service', static fn (ServiceRegistry $s) => new InpostShipmentService( + $s->get('integrations.inpost.repo'), + $s->get('shared.shipment_packages.repo'), + $s->get('shared.companies.repo'), + $app->orders() + )); + + $services->set('shipments.polkurier.service', static fn (ServiceRegistry $s) => new PolkurierShipmentService( + $s->get('integrations.polkurier.repo'), + new \App\Modules\Settings\PolkurierApiClient(), + $s->get('shared.shipment_packages.repo'), + $s->get('shared.companies.repo'), + $app->orders() + )); + + $services->set('shipments.provider_registry', static fn (ServiceRegistry $s) => new ShipmentProviderRegistry([ + $s->get('shipments.allegro.service'), + $s->get('shipments.apaczka.service'), + $s->get('shipments.inpost.service'), + $s->get('shipments.polkurier.service'), + ])); + + $services->set('shipments.controller', static fn (ServiceRegistry $s) => new ShipmentController( + $app->template(), + $app->translator(), + $app->auth(), + $app->orders(), + $s->get('shared.companies.repo'), + $s->get('shipments.provider_registry'), + $s->get('shared.shipment_packages.repo'), + $s->get('automation.service'), + $app->basePath('storage'), + $s->get('shared.carrier_delivery_mappings.repo'), + $s->get('shared.print_jobs.repo'), + $s->get('integrations.erli.external_shipment_service') + )); + + $services->set('shipments.preset.repo', static fn () => new ShipmentPresetRepository($app->db())); + $services->set('shipments.preset.controller', static fn (ServiceRegistry $s) => new ShipmentPresetController( + $s->get('shipments.preset.repo') + )); + + $services->set('shipments.delivery_statuses.controller', static fn (ServiceRegistry $s) => new DeliveryStatusesController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('shipments.delivery_status.repo'), + $s->get('shipments.delivery_status_mappings.repo') + )); + + $services->set('shipments.delivery_status_mappings.controller', static fn (ServiceRegistry $s) => new DeliveryStatusMappingController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('shipments.delivery_status_mappings.repo'), + $s->get('shipments.delivery_status.repo') + )); + + // Side-effect: globalny rejestr w klasie DeliveryStatus (zgodnie ze stanem przed refaktorem). + DeliveryStatus::setRepository($services->get('shipments.delivery_status.repo')); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $sh = static fn (string $m) => $services->lazy('shipments.controller', $m); + $router->get('/orders/{id}/shipment/prepare', $sh('prepare'), [$auth]); + $router->post('/orders/{id}/shipment/create', $sh('create'), [$auth]); + $router->get('/orders/{id}/shipment/{packageId}/status', $sh('checkStatus'), [$auth]); + $router->post('/orders/{id}/shipment/{packageId}/label', $sh('label'), [$auth]); + $router->post('/orders/{id}/shipment/manual', $sh('createManual'), [$auth]); + $router->post('/orders/{id}/shipment/{packageId}/delete', $sh('delete'), [$auth]); + + $dsm = static fn (string $m) => $services->lazy('shipments.delivery_status_mappings.controller', $m); + $router->get('/settings/delivery-status-mappings', $dsm('index'), [$auth]); + $router->post('/settings/delivery-status-mappings/save', $dsm('save'), [$auth]); + $router->post('/settings/delivery-status-mappings/save-bulk', $dsm('saveBulk'), [$auth]); + $router->post('/settings/delivery-status-mappings/reset', $dsm('reset'), [$auth]); + $router->post('/settings/delivery-status-mappings/reset-all', $dsm('resetAll'), [$auth]); + + $ds = static fn (string $m) => $services->lazy('shipments.delivery_statuses.controller', $m); + $router->get('/settings/delivery-statuses', $ds('index'), [$auth]); + $router->get('/settings/delivery-statuses/new', $ds('create'), [$auth]); + $router->get('/settings/delivery-statuses/{id}/edit', $ds('edit'), [$auth]); + $router->post('/settings/delivery-statuses', $ds('store'), [$auth]); + $router->post('/settings/delivery-statuses/{id}/update', $ds('update'), [$auth]); + $router->post('/settings/delivery-statuses/{id}/delete', $ds('destroy'), [$auth]); + + $pr = static fn (string $m) => $services->lazy('shipments.preset.controller', $m); + $router->get('/api/shipment-presets', $pr('list'), [$auth]); + $router->post('/api/shipment-presets', $pr('store'), [$auth]); + $router->post('/api/shipment-presets/update', $pr('update'), [$auth]); + $router->post('/api/shipment-presets/delete', $pr('destroy'), [$auth]); + } +} diff --git a/src/Modules/Sms/SmsModule.php b/src/Modules/Sms/SmsModule.php new file mode 100644 index 0000000..15ba653 --- /dev/null +++ b/src/Modules/Sms/SmsModule.php @@ -0,0 +1,58 @@ +set('sms.messages.repo', static fn () => new SmsMessageRepository($app->db())); + $services->set('sms.templates.repo', static fn () => new SmsTemplateRepository($app->db())); + + $services->set('sms.variable_resolver', static fn (ServiceRegistry $s) => new SmsVariableResolver( + $s->get('shared.shipment_packages.repo') + )); + + $services->set('sms.conversation_service', static fn (ServiceRegistry $s) => new SmsConversationService( + $s->get('sms.messages.repo'), + $s->get('integrations.smsplanet.repo'), + new SmsplanetApiClient(), + $s->get('notifications.repo') + )); + + $services->set('sms.smsplanet_webhook.controller', static fn (ServiceRegistry $s) => new SmsplanetWebhookController( + $s->get('sms.conversation_service') + )); + + $services->set('sms.template.controller', static fn (ServiceRegistry $s) => new SmsTemplateController( + $app->template(), + $app->translator(), + $app->auth(), + $s->get('sms.templates.repo') + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $router->post('/webhooks/smsplanet/inbound', $services->lazy('sms.smsplanet_webhook.controller', 'inbound')); + $router->get('/webhooks/smsplanet/inbound', $services->lazy('sms.smsplanet_webhook.controller', 'inbound')); + + $t = static fn (string $m) => $services->lazy('sms.template.controller', $m); + $router->get('/settings/sms-templates', $t('index'), [$auth]); + $router->get('/settings/sms-templates/create', $t('create'), [$auth]); + $router->get('/settings/sms-templates/edit', $t('edit'), [$auth]); + $router->post('/settings/sms-templates/save', $t('save'), [$auth]); + $router->post('/settings/sms-templates/delete', $t('delete'), [$auth]); + $router->post('/settings/sms-templates/toggle', $t('toggleStatus'), [$auth]); + $router->get('/settings/sms-templates/variables', $t('getVariables'), [$auth]); + } +} diff --git a/src/Modules/Statistics/StatisticsModule.php b/src/Modules/Statistics/StatisticsModule.php new file mode 100644 index 0000000..29cae96 --- /dev/null +++ b/src/Modules/Statistics/StatisticsModule.php @@ -0,0 +1,29 @@ +set('statistics.controller', static fn () => new OrdersStatisticsController( + $app->template(), + $app->translator(), + $app->auth(), + new OrdersStatisticsRepository($app->db()) + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $router->get('/statistics/summary', $services->lazy('statistics.controller', 'summary'), [$auth]); + $router->get('/statistics/orders', $services->lazy('statistics.controller', 'index'), [$auth]); + } +} diff --git a/src/Modules/Users/UsersModule.php b/src/Modules/Users/UsersModule.php new file mode 100644 index 0000000..113c0db --- /dev/null +++ b/src/Modules/Users/UsersModule.php @@ -0,0 +1,33 @@ +set('users.controller', static fn () => new UsersController( + $app->template(), + $app->translator(), + $app->auth(), + $app->users() + )); + } + + public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth, Application $app): void + { + $router->get('/users', static fn (Request $request): Response => Response::redirect('/settings/users'), [$auth]); + $router->post('/users', $services->lazy('users.controller', 'store'), [$auth]); + $router->get('/settings/users', $services->lazy('users.controller', 'index'), [$auth]); + $router->post('/settings/users', $services->lazy('users.controller', 'store'), [$auth]); + } +} diff --git a/tests/Unit/Core/Routing/ServiceRegistryTest.php b/tests/Unit/Core/Routing/ServiceRegistryTest.php new file mode 100644 index 0000000..4b71c0c --- /dev/null +++ b/tests/Unit/Core/Routing/ServiceRegistryTest.php @@ -0,0 +1,126 @@ +set('foo', static function () { + $obj = new stdClass(); + $obj->value = 42; + return $obj; + }); + + $result = $registry->get('foo'); + + self::assertInstanceOf(stdClass::class, $result); + self::assertSame(42, $result->value); + } + + public function testGetMemoizesInstance(): void + { + $registry = new ServiceRegistry(); + $calls = 0; + $registry->set('foo', static function () use (&$calls) { + $calls++; + return new stdClass(); + }); + + $first = $registry->get('foo'); + $second = $registry->get('foo'); + + self::assertSame($first, $second); + self::assertSame(1, $calls); + } + + public function testHasReportsRegistration(): void + { + $registry = new ServiceRegistry(); + self::assertFalse($registry->has('foo')); + + $registry->set('foo', static fn () => new stdClass()); + + self::assertTrue($registry->has('foo')); + } + + public function testGetThrowsOnMissingService(): void + { + $registry = new ServiceRegistry(); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Service not registered: "missing"'); + + $registry->get('missing'); + } + + public function testFactoryReceivesRegistryForCrossLookup(): void + { + $registry = new ServiceRegistry(); + $registry->set('dep', static function () { + $dep = new stdClass(); + $dep->name = 'dep'; + return $dep; + }); + $registry->set('consumer', static function (ServiceRegistry $s) { + $obj = new stdClass(); + $obj->depName = $s->get('dep')->name; + return $obj; + }); + + $consumer = $registry->get('consumer'); + + self::assertSame('dep', $consumer->depName); + } + + public function testLazyDefersConstructionUntilInvocation(): void + { + $registry = new ServiceRegistry(); + $calls = 0; + $registry->set('ctrl', static function () use (&$calls) { + $calls++; + return new class { + public function show(Request $request): string + { + return 'shown:' . $request->path(); + } + }; + }); + + $handler = $registry->lazy('ctrl', 'show'); + self::assertSame(0, $calls, 'lazy() nie powinno konstruowac serwisu'); + + $request = new Request([], [], [], ['REQUEST_METHOD' => 'GET', 'REQUEST_URI' => '/foo']); + $result = $handler($request); + + self::assertSame('shown:/foo', $result); + self::assertSame(1, $calls); + } + + public function testSetOverwritesPreviousFactoryAndClearsInstance(): void + { + $registry = new ServiceRegistry(); + $registry->set('foo', static function () { + $obj = new stdClass(); + $obj->v = 'A'; + return $obj; + }); + self::assertSame('A', $registry->get('foo')->v); + + $registry->set('foo', static function () { + $obj = new stdClass(); + $obj->v = 'B'; + return $obj; + }); + + self::assertSame('B', $registry->get('foo')->v); + } +}