refactor(routing): module providers + lazy ServiceRegistry

Rozbicie routes/web.php (859 lin.) na 24 klasy <Modul>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) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 21:25:07 +02:00
parent 2df4638365
commit e77b0f12a2
37 changed files with 2849 additions and 854 deletions

View File

@@ -4,7 +4,8 @@
**Ostatnia aktualizacja:** 2026-05-19 **Ostatnia aktualizacja:** 2026-05-19
## Aktywna praca ## 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 `<Modul>Module.php`).
## Kontekst sesji ## Kontekst sesji
- Galaz: `main` (czysta). - Galaz: `main` (czysta).

View File

@@ -4,7 +4,7 @@
## Przeglad ## Przeglad
Monolityczna aplikacja PHP w stylu **modular monolith**: warstwa rdzeniowa (`src/Core/`) + moduly domenowe (`src/Modules/<Modul>/`). 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/<Modul>/`). Brak DI containera w sensie autowire/refleksji — zaleznosci montowane jawnie w klasach `<Modul>Module.php` (kompozycja w stylu "poor man's DI", od 2026-05-19 z leniwa rejestracja przez `ServiceRegistry`).
## Punkty wejscia ## 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 migrate | `bin/migrate.php` -> `App\Core\Database\Migrator` | migracje SQL |
| CLI backfill/utils | `bin/backfill_*.php`, `bin/fix_*.php`, `bin/deploy_*.php` | operacje serwisowe | | 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/<Modul>/<Modul>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 ## 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`). 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). 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. 4. **Repositories**`src/Modules/*/...*Repository.php` (47 klas). PDO + prepared statements. Brak ORM.

View File

@@ -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/Shipments/DeliveryStatus.php` | 657 | encja statusow uzywana globalnie — zmiany dotykaja wszystkich integracji. |
| `src/Modules/Settings/AllegroIntegrationController.php` | 653 | | | `src/Modules/Settings/AllegroIntegrationController.php` | 653 | |
| `src/Modules/Statistics/OrdersStatisticsController.php` | 640 | | | `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 `<Modul>Module.php`. Patrz `.paul/plans/20260519-1200-refactor-routes-web/SUMMARY.md`. |
## Luki testowe (krytyczne) ## 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). 1. **Test coverage** dla Shoppro i InPost (high blast radius).
2. **Dekompozycja `OrdersController`** (1490 lin.) — minimum 3 sub-kontrolery. 2. **Dekompozycja `OrdersController`** (1490 lin.) — minimum 3 sub-kontrolery.
3. **Bazowy `IntegrationController`** lub trait — eliminacja powtarzajacej sie tresci `index`/`save`/`test`. 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. 5. **Wiecej komponentow widokow** — wyodrebnic powtarzajace sie sekcje.

View File

@@ -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 `<Modul>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 `<Modul>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".

View File

@@ -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/<Modul>/<Modul>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 `<Modul>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/` + `<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
<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 `<Modul>Module.php` w kazdym `src/Modules/<Modul>/`.
- 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).
</impact_scan>
## 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 `<Modul>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 `<Provider>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/<Modul>/*Controller.php`, `*Service.php`, `*Repository.php` nie zmienia konstruktora ani metod publicznych. Refaktor ogranicza sie do dodania `<Modul>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
<?php
declare(strict_types=1);
namespace App\Core\Routing;
use Closure;
use RuntimeException;
final class ServiceRegistry
{
/** @var array<string, Closure> */
private array $factories = [];
/** @var array<string, mixed> */
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
<?php
declare(strict_types=1);
namespace App\Core\Routing;
use App\Core\Application;
use App\Core\Routing\Router;
use App\Modules\Auth\AuthMiddleware;
interface ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void;
public function routes(Router $router, ServiceRegistry $services, AuthMiddleware $auth): void;
}
```
### Konwencja kluczy w ServiceRegistry
- `'orders.controller'`, `'orders.repository'`, `'orders.notes_service'`
- `'integrations.allegro.repository'`, `'integrations.allegro.api_client'`, `'integrations.allegro.oauth'`, `'integrations.allegro.token_manager'`, `'integrations.allegro.controller'`
- `'shared.companies.repository'`, `'shared.cron.repository'`, `'shared.shipment_packages.repository'`, `'shared.carrier_delivery_mappings.repository'` — wspoldzielone (rejestrowane przez `SettingsModule` lub `CronModule`/`ShipmentsModule` zaleznie od domeny "ownerskej")
- `'mw.auth'`, `'mw.api_key'` — middleware
- Klucz w stylu `domain.role` (kropka jako separator).
### Przyklad: `OrdersModule`
```php
<?php
declare(strict_types=1);
namespace App\Modules\Orders;
use App\Core\Application;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
use App\Modules\Shipments\ShipmentPackageRepository;
final class OrdersModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$services->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
<?php
declare(strict_types=1);
use App\Core\Application;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Accounting\AccountingModule;
use App\Modules\Auth\AuthMiddleware;
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\SettingsModule;
use App\Modules\Settings\Integrations\AllegroIntegrationModule;
use App\Modules\Settings\Integrations\ApaczkaIntegrationModule;
use App\Modules\Settings\Integrations\ErliIntegrationModule;
use App\Modules\Settings\Integrations\FakturowniaIntegrationModule;
use App\Modules\Settings\Integrations\HostedSmsIntegrationModule;
use App\Modules\Settings\Integrations\InpostIntegrationModule;
use App\Modules\Settings\Integrations\IntegrationsHubModule;
use App\Modules\Settings\Integrations\PolkurierIntegrationModule;
use App\Modules\Settings\Integrations\ShopproIntegrationModule;
use App\Modules\Settings\Integrations\SmsplanetIntegrationModule;
use App\Modules\Shipments\ShipmentsModule;
use App\Modules\Sms\SmsModule;
use App\Modules\Statistics\StatisticsModule;
use App\Modules\Users\UsersModule;
return static function (Application $app): void {
$modules = [
new InfoModule(),
new AuthModule(),
new UsersModule(),
new SettingsModule(),
new CronModule(),
new ShipmentsModule(),
new AccountingModule(),
new EmailModule(),
new SmsModule(),
new NotificationsModule(),
new AutomationModule(),
new OrdersModule(),
new StatisticsModule(),
new PrintingModule(),
new IntegrationsHubModule(),
new AllegroIntegrationModule(),
new ApaczkaIntegrationModule(),
new InpostIntegrationModule(),
new ShopproIntegrationModule(),
new ErliIntegrationModule(),
new PolkurierIntegrationModule(),
new FakturowniaIntegrationModule(),
new HostedSmsIntegrationModule(),
new SmsplanetIntegrationModule(),
];
$services = new ServiceRegistry();
foreach ($modules as $m) {
$m->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 `<Modul>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/<dostawca>/*`.
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 `<Modul>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/<Dostawca>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.

View File

@@ -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 `<Modul>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).

View File

@@ -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'

View File

@@ -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'

82
bin/smoke_routes.php Normal file
View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use App\Core\Application;
use App\Core\Routing\ServiceRegistry;
// Stubowany Application — tylko config() i basePath() musza dzialac;
// register() w wiekszosci modulow nie dotyka DB, bo factory sa leniwe.
$basePath = realpath(__DIR__ . '/..');
$mergedConfig = [
'app' => 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;

View File

@@ -2,858 +2,77 @@
declare(strict_types=1); declare(strict_types=1);
use App\Core\Application; use App\Core\Application;
use App\Core\Http\Request; use App\Core\Routing\ServiceRegistry;
use App\Core\Http\Response; use App\Modules\Accounting\AccountingModule;
use App\Modules\Auth\AuthController;
use App\Modules\Auth\AuthMiddleware; use App\Modules\Auth\AuthMiddleware;
use App\Modules\Cron\CronHandlerFactory; use App\Modules\Auth\AuthModule;
use App\Modules\Cron\CronRepository; use App\Modules\Automation\AutomationModule;
use App\Modules\Orders\OrdersController; use App\Modules\Cron\CronModule;
use App\Modules\Orders\OrderImportRepository; use App\Modules\Email\EmailModule;
use App\Modules\Orders\OrderNotesService; use App\Modules\Info\InfoModule;
use App\Modules\Orders\OrdersRepository; use App\Modules\Notifications\NotificationsModule;
use App\Modules\Statistics\OrdersStatisticsController; use App\Modules\Orders\OrdersModule;
use App\Modules\Statistics\OrdersStatisticsRepository; use App\Modules\Printing\PrintingModule;
use App\Modules\Settings\AllegroApiClient; use App\Modules\Settings\AllegroIntegrationModule;
use App\Modules\Settings\AllegroDeliveryMappingController; use App\Modules\Settings\ApaczkaIntegrationModule;
use App\Modules\Settings\AllegroIntegrationController; use App\Modules\Settings\ErliIntegrationModule;
use App\Modules\Settings\AllegroIntegrationRepository; use App\Modules\Settings\FakturowniaIntegrationModule;
use App\Modules\Settings\AllegroOAuthClient; use App\Modules\Settings\HostedSmsIntegrationModule;
use App\Modules\Settings\AllegroOrderImportService; use App\Modules\Settings\InpostIntegrationModule;
use App\Modules\Settings\AllegroPullStatusMappingRepository; use App\Modules\Settings\IntegrationsHubModule;
use App\Modules\Settings\AllegroStatusDiscoveryService; use App\Modules\Settings\PolkurierIntegrationModule;
use App\Modules\Settings\AllegroStatusMappingController; use App\Modules\Settings\SettingsModule;
use App\Modules\Settings\AllegroTokenManager; use App\Modules\Settings\ShopproIntegrationModule;
use App\Modules\Settings\AllegroStatusMappingRepository; use App\Modules\Settings\SmsplanetIntegrationModule;
use App\Modules\Settings\OrderStatusRepository; use App\Modules\Shipments\ShipmentsModule;
use App\Modules\Settings\ApaczkaApiClient; use App\Modules\Sms\SmsModule;
use App\Modules\Settings\ApaczkaIntegrationController; use App\Modules\Statistics\StatisticsModule;
use App\Modules\Settings\ApaczkaIntegrationRepository; use App\Modules\Users\UsersModule;
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;
/**
* Modular routing entrypoint.
*
* Kazdy modul (src/Modules/<Modul>/<Modul>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 { return static function (Application $app): void {
$router = $app->router(); $modules = [
$template = $app->template(); new InfoModule(),
$auth = $app->auth(); new AuthModule(),
$translator = $app->translator(); 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); $services = new ServiceRegistry();
$usersController = new UsersController($template, $translator, $auth, $app->users()); foreach ($modules as $module) {
$shipmentPackageRepositoryForOrders = new ShipmentPackageRepository($app->db()); $module->register($services, $app);
$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);
$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', '')); $authMiddleware = new AuthMiddleware($app->auth());
if ($expectedToken === '' || $token === '' || !hash_equals($expectedToken, $token)) { foreach ($modules as $module) {
return Response::json([ $module->routes($app->router(), $services, $authMiddleware, $app);
'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]);
}; };

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Core\Routing;
use App\Core\Application;
use App\Modules\Auth\AuthMiddleware;
/**
* Modularny dostawca: zglasza swoje serwisy do ServiceRegistry i rejestruje
* route'y w Routerze. Implementacja zyje w katalogu modulu domenowego
* (src/Modules/<Modul>/<Modul>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;
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Core\Routing;
use App\Core\Http\Request;
use Closure;
use RuntimeException;
/**
* Lekki rejestr uslug z leniwa konstrukcja (memoizacja per request).
*
* Cel: zastapic recznie montowane zmienne w routes/web.php tak, aby
* kontrolery i serwisy budowaly sie tylko gdy router trafi w handler.
*
* Nie wprowadza autowire ani refleksji - klucze i factory sa jawne.
*/
final class ServiceRegistry
{
/** @var array<string, Closure> */
private array $factories = [];
/** @var array<string, mixed> */
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);
};
}
}

View File

@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace App\Modules\Accounting;
use App\Core\Application;
use App\Core\Http\MfWhitelistApiClient;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
use App\Modules\Settings\InvoiceConfigController;
use App\Modules\Settings\InvoiceConfigRepository;
use App\Modules\Settings\ReceiptConfigController;
use App\Modules\Settings\ReceiptConfigRepository;
final class AccountingModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
// Receipts
$services->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]);
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Modules\Auth;
use App\Core\Application;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
final class AuthModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$services->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]);
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Modules\Automation;
use App\Core\Application;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
final class AutomationModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$services->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]);
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace App\Modules\Cron;
use App\Core\Application;
use App\Core\Http\Request;
use App\Core\Http\Response;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
use App\Modules\Settings\CronSettingsController;
use Throwable;
final class CronModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$services->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);
}
}

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace App\Modules\Email;
use App\Core\Application;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
use App\Modules\Settings\EmailMailboxController;
use App\Modules\Settings\EmailMailboxRepository;
use App\Modules\Settings\EmailTemplateController;
use App\Modules\Settings\EmailTemplateRepository;
use App\Modules\Settings\IntegrationSecretCipher;
use App\Modules\Settings\SmtpSecurityContextFactory;
final class EmailModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$secret = (string) $app->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]);
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Modules\Info;
use App\Core\Application;
use App\Core\Http\Request;
use App\Core\Http\Response;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
final class InfoModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$services->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'));
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Modules\Notifications;
use App\Core\Application;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
final class NotificationsModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$services->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]);
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace App\Modules\Orders;
use App\Core\Application;
use App\Core\Http\Request;
use App\Core\Http\Response;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
final class OrdersModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$services->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]);
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace App\Modules\Printing;
use App\Core\Application;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
use App\Modules\Settings\PrintSettingsController;
use App\Modules\Settings\ProjectMappingController;
use App\Modules\Settings\ProjectMappingRepository;
final class PrintingModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$services->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]);
}
}

View File

@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use App\Core\Application;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
use App\Modules\Orders\OrderImportRepository;
final class AllegroIntegrationModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$secret = (string) $app->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'));
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use App\Core\Application;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
final class ApaczkaIntegrationModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$secret = (string) $app->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]);
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use App\Core\Application;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
use App\Modules\Orders\OrderImportRepository;
final class ErliIntegrationModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$secret = (string) $app->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]);
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use App\Core\Application;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
final class FakturowniaIntegrationModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$secret = (string) $app->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]);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use App\Core\Application;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
final class HostedSmsIntegrationModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$secret = (string) $app->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]);
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use App\Core\Application;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
final class InpostIntegrationModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$secret = (string) $app->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]);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use App\Core\Application;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
final class IntegrationsHubModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$services->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]);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use App\Core\Application;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
final class PolkurierIntegrationModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$secret = (string) $app->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]);
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use App\Core\Application;
use App\Core\Http\Request;
use App\Core\Http\Response;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
/**
* Wspolne ustawienia: database, statuses, status-groups, company, redirect /settings.
* Routes podzielone tematycznie tylko dla czytelnosci.
*/
final class SettingsModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$services->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]);
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use App\Core\Application;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
final class ShopproIntegrationModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$secret = (string) $app->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]);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use App\Core\Application;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
final class SmsplanetIntegrationModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$secret = (string) $app->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]);
}
}

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace App\Modules\Shipments;
use App\Core\Application;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
use App\Modules\Settings\DeliveryStatusesController;
use App\Modules\Settings\DeliveryStatusMappingController;
final class ShipmentsModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
// Shared repos owned by Shipments domain
$services->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]);
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Modules\Sms;
use App\Core\Application;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
use App\Modules\Settings\SmsplanetApiClient;
use App\Modules\Settings\SmsTemplateController;
final class SmsModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$services->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]);
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Modules\Statistics;
use App\Core\Application;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
final class StatisticsModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$services->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]);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Modules\Users;
use App\Core\Application;
use App\Core\Http\Request;
use App\Core\Http\Response;
use App\Core\Routing\ModuleProvider;
use App\Core\Routing\Router;
use App\Core\Routing\ServiceRegistry;
use App\Modules\Auth\AuthMiddleware;
final class UsersModule implements ModuleProvider
{
public function register(ServiceRegistry $services, Application $app): void
{
$services->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]);
}
}

View File

@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Core\Routing;
use App\Core\Http\Request;
use App\Core\Routing\ServiceRegistry;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use stdClass;
final class ServiceRegistryTest extends TestCase
{
public function testSetAndGetReturnsBuiltInstance(): void
{
$registry = new ServiceRegistry();
$registry->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);
}
}