# Architecture ## Request Flow ``` HTTP Request → public/index.php → bootstrap/app.php (loads config, registers PDO, services) → Application::boot() (loads routes/web.php) → Router::dispatch(Request) (matches URL, runs middleware pipeline) → [Middleware] (AuthMiddleware, ApiKeyMiddleware) → Controller::method() (parse input → call repository/service → render) → Template::render() (PHP native, layout composition) → Response::send() ``` ## Layer Map | Layer | Location | Responsibility | |-------|----------|----------------| | Entry | `public/index.php` | Bootstrap only | | Routes | `routes/web.php` (581 lines) | All ~80 routes; manual DI wiring | | Core | `src/Core/` (25 files) | Framework infrastructure | | Controllers | `src/Modules/*/Controller.php` | Request parsing → response | | Services | `src/Modules/*/Service.php` | Business logic | | Repositories | `src/Modules/*/Repository.php` | PDO data access (34+ repos) | | Views | `resources/views/` | PHP templates with `$e()` / `$t()` | | Components | `resources/views/components/` | Reusable UI blocks | | Frontend modules | `public/assets/js/modules/` | Small vanilla JS enhancements loaded by layout | ## Module Inventory (`src/Modules/`) | Module | Files | Key Classes | Purpose | |--------|-------|-------------|---------| | **Auth** | 3 | `AuthController`, `AuthMiddleware`, `AuthService` | Login/logout, session | | **Users** | 2 | `UserController`, `UserRepository` | User CRUD | | **Orders** | 3 | `OrdersController` (1187 LOC), `OrdersRepository` (1221 LOC) | Order list, detail, status, payment, correlated subquery for return-risk | | **Shipments** | 17 | `ShipmentController`, provider services + tracking services | Shipment creation, label download, tracking polling | | **Accounting** | 5 | `AccountingController`, `ReceiptService`, `ReceiptRepository` | Receipts, invoices, PDF, Excel export | | **Email** | 3 | `EmailSendingService`, `VariableResolver`, `AttachmentGenerator` | Template-based email with PDF attachments | | **Automation** | 6 | `AutomationService` (834 LOC), `AutomationRepository`, `AutomationExecutionLogRepository` | Event→condition→action rules, email triggers | | **Settings** | 54+ | Integration controllers, OAuth clients, API clients (Fakturownia incl.), mappers | Allegro/shopPRO/Apaczka/InPost/Fakturownia config, status mappings | | **Sms** | 3 | `SmsMessageRepository`, `SmsConversationService`, `SmsplanetWebhookController` | SMSPLANET outbound order SMS, inbound webhook parsing, order matching | | **Notifications** | 3 | `NotificationRepository`, `NotificationController`, `NotificationApiController` | Global notification history, unread polling API, mark-read actions | | **Cron** | 12 | `CronRepository`, `CronHandlerFactory`, handler classes | Scheduled imports, syncs, token refresh | | **Printing** | 4 | `PrintApiController`, `PrintJobRepository`, `ApiKeyMiddleware` | REST API for Windows print client | | **Statistics** | 3 | `OrdersStatisticsController`, `OrdersStatisticsRepository`, `statistics-summary-charts.js` | Daily order statistics and monthly summary charts | | **Info** | 1 | `InfoController` | Health check | ## Frontend Enhancement Modules ### Checkbox Multiselect (`public/assets/js/modules/checkbox-multiselect.js`) - Loaded globally from `resources/views/layouts/app.php`. - Enhances native `` (lista uslug z `available_carriers`). - `` synchronizowany z polkurier select przez `syncPolkurierFields()`. - Brak dedykowanego selektora punktu odbioru — operator wpisuje `receiver_point_id` w istniejacy text input w sekcji Adres odbiorcy (np. `POP-RZE54`). Format string `POP-RZE54 | Lukasiewicza 78, 35-604 Rzeszow` z importu zamowienia nie jest parsowany — operator skraca recznie. - JS toggle widocznosci paneli rozszerzony o polkurier; `clearHiddenFields()` czysci `service_code`; `showPanel('polkurier')` ustawia `provider_code='polkurier'`. ### Rozmiar etykiety A4 vs A6 - API polkurier nie udostepnia parametru sterowania rozmiarem etykiety w `get_label` ani `create_order` (zweryfikowane w PDF v1.11). - Domyslny rozmiar ustawiany jest w **panelu klienta polkurier.pl → Ustawienia konta → Preferencje etykiet** (per-konto, globalnie dla wszystkich `get_label` calli). - `polkurier_integration_settings.default_label_format` (PDF/ZPL/EPL) sluzy tylko typowi pliku, NIE rozmiarowi. ### Seed delivery_status_mappings (`20260514_000115_seed_polkurier_delivery_status_mappings.sql`) - 7 wpisow `provider='polkurier'` (kody z oficjalnej tabeli ORDER_STATUS w PDF v1.11): - `O` → `created` (Oczekuje na platnosc) - `P` → `confirmed` (Potwierdzone, list wygenerowany) - `A` → `cancelled` (Anulowane) - `WP` → `in_transit` (W przewozie) - `D` → `delivered` (Dostarczona) - `Z` → `returned` (Zwrot do nadawcy) - `W` → `problem` (Wyjatek) - Idempotentne: `ON DUPLICATE KEY UPDATE normalized_status / description / updated_at`. ### Boundaries / co NIE zostalo zmienione - Apaczka (`ApaczkaShipmentService`, `ApaczkaTrackingService`, `apaczka_integration_settings`) niezalezna, dziala obok polkuriera. - `ShipmentProviderInterface` i `ShipmentTrackingInterface` kontrakty niezmienione. - `getInpostParcelMachines`/`getCourierPoints` w API client zaimplementowane ale nieuzywane przez UI w Phase 128 (operator wpisuje punkt recznie). - `cancelOrder` zaimplementowane w API client ale nie wywolywane z UI/cron — operator anuluje w panelu polkuriera. - Brak presetow przesylek dla polkuriera (`shipment_presets.provider_code='polkurier'`) — kolejna faza. ## Phase 121 - SMSPLANET Conversation + Notifications ### SmsConversationService (`src/Modules/Sms/SmsConversationService.php`) - Wysyla SMS z poziomu zamowienia przez `SmsplanetApiClient`, dopisuje `default_footer` gdy jest skonfigurowana, zapisuje finalna tresc w `sms_messages` i uzywa `sender_mode` do wyboru nadpisu albo numeru 2WAY. - Parsuje publiczny webhook `/webhooks/smsplanet/inbound`, normalizuje telefony i dopasowuje przychodzacy SMS do najnowszego zamowienia po telefonie klienta/adresu. - Endpoint inbound akceptuje POST i GET; format 2WAY `message=` jest dekodowany, sukces zwraca plain `OK`, a dopasowanie zamowienia korzysta z `order_addresses.phone`. - Tworzy `notifications.type='sms_inbound'` z linkiem do `/orders/{id}?tab=sms`. ### Notifications module - `/notifications` pokazuje historie powiadomien i pozwala oznaczac wpisy jako przeczytane. - `/api/notifications/unread` zasila topbar badge oraz `public/assets/js/modules/notifications.js`. - Browser Notification API jest progresywne: brak zgody nie blokuje strony ani pollingu. ## Phase 123 — Receipts Export VAT Breakdown ### ReceiptService::buildItemsSnapshot (`src/Modules/Accounting/ReceiptService.php`) - Snapshot pozycji w `receipts.items_json` ma teraz pole `vat` (procent jako float). Zrodlo: `order_items.tax_rate` (fallback `item.vat`, ostatecznie 23.0). - Pozycja "Koszt wysylki" (gdy `delivery_price > 0`) dostaje `vat = 23.0`. - Stary kontrakt (`name`, `quantity`, `price`, `total`, `sku`, `ean`) zachowany — tylko dodatek pola `vat`. Widoki paragonu (print/preview) nie wymagaja zmian. ### AccountingController::export (`src/Modules/Accounting/AccountingController.php`) - Naglowki XLSX: `Numer | Data wystawienia | Kwota brutto | Kwota netto | Stawka VAT | Kwota VAT`. Usunieto: Data sprzedazy, Konfiguracja, Nr zamowienia, Nr referencyjny. - `buildVatBreakdown(itemsJson, totalNet, totalGross)` grupuje pozycje `items_json` po `vat`, oblicza per-grupa `net = round(gross / (1 + rate/100), 2)` i `vat = gross - net`. Zwraca liste `[{rate_label, net, vat}, ...]` posortowana malejaco po stawce. - Legacy fallback: gdy zaden item nie ma klucza `vat`, zwraca pojedynczy wiersz `[{rate_label: '23%', net: total_net, vat: total_gross - total_net}]`. - Multi-rate paragon = wiele wierszy w XLSX (ten sam Numer, Data wystawienia i Kwota brutto powtarzane). - Helper `formatVatRate()` formatuje stawke (23.0 -> "23%", 7.5 -> "7.5%"). ## Phase 120 — Alert Component Unification ### Alert component (`resources/views/components/alert.php`) - Reusable alert renderer with params: `$type` (info|success|warning|danger; fallback 'info'), `$message` (escaped) lub `$messageHtml` (trusted), `$dismissible` (default true), `$role` ('alert'|'status'). - Renders inline SVG icon per type + body + optional dismiss button. Markup: `
...
`. - Used via `include __DIR__ . '/../components/alert.php'` po ustawieniu lokalnych `$type/$message/$dismissible`. ### SCSS — `.alert` w `resources/scss/shared/_ui-components.scss` - `.alert` jest teraz flex (icon + body + dismiss). Dodane: `.alert__icon`, `.alert__body`, `.alert__dismiss`. - Nowy wariant `.alert--info` (blue: border #bfdbfe, bg #eff6ff, color #1e3a8a) — wczesniej brakowal i renderowal sie jako czarny tekst na bialym tle. - Wariantow `--success/--warning/--danger` nie zmieniono kolorystycznie. - Wrapper `.alerts-stack` (gap 8px) do stackowania wielu alertow z layoutu. ### JS — `public/assets/js/modules/alert-dismiss.js` - Vanilla JS, idempotent guard (`window.__alertDismissBound`). - Delegated click handler na `[data-alert-dismiss]` — usuwa najblizszy `[data-alert]` z DOM bez przeladowania. - Ladowany globalnie w `layouts/app.php`, `layouts/auth.php`, `layouts/public.php`. ### Flash — `App\Core\Support\Flash` rozszerzenie - Nowa kolejka typowana `$_SESSION['_flash_queue']` z entries `{type, message}`. - `Flash::push(string $type, string $message): void` — append do kolejki (whitelist info/success/warning/danger, fallback info). - `Flash::all(): array` — zwraca i czysci kolejke + skanuje legacy `_flash` (heurystyka klucza: `error/fail/danger` → danger, `warning` → warning, `success/.save/.created/.deleted/.toggled` → success, reszta → info). BC zachowany: `Flash::set/get` dziala bez zmian. ### Centralny renderer flash w layoutach - `layouts/app.php`, `layouts/auth.php`, `layouts/public.php` na poczatku glownego content area iteruja `Flash::all()` i wlaczaja komponent `alert.php` per wpis (wrap `.alerts-stack`). - Kontrolery NIE wymagaly zmian — pre-fetched `Flash::get('module.key', '')` przekazany do widoku jako lokalna zmienna jest dalej renderowany inline przez widok (przez ten sam komponent). Centralny renderer przejmuje wpisy `Flash::push(...)` oraz nieskonsumowane legacy entries. ### Migracja widokow - Wszystkie inline `
...
` w widokach (36 plikow razem ze `shipments/prepare.php` i `orders/show.php`) zastapione przez ``. - `.flash--error` / `.flash--success` w `orders/show.php` i `shipments/prepare.php` zastapione komponentem (klasa `.flash--*` w SCSS pozostaje bez uzycia, deferred cleanup). - Wyjatek: `settings/email-mailboxes.php` ma JS-generowane alerty (`resultDiv.className = 'mt-12 alert alert--success'`) z dynamicznej odpowiedzi AJAX test polaczenia SMTP — uzywaja klas SCSS bez markupu komponentu (out of scope dla tej fazy). ## Phase 114 — Accounting Configs Refactor ### Sekcja Ksiegowosc — struktura URL - `/settings/accounting` — hub-rozdroze z 2 kartami: "Paragony" i "Faktury". `ReceiptConfigController::hub()`. - `/settings/accounting/receipts` — lista konfiguracji paragonow. `ReceiptConfigController::list()`. - `/settings/accounting/receipts/new`, `/edit?id=N` — formularz na osobnej podstronie. `ReceiptConfigController::edit()`. - `/settings/accounting/receipts/save|toggle|delete` — POST actions. - **Legacy aliasy:** `/settings/accounting/save|toggle|delete` (POST) zostaja jako duplicate routes (wsteczna kompatybilnosc z `
` w starszych szablonach/bookmarkach). - `/settings/accounting/invoices` + `/new`, `/edit`, `/save`, `/toggle`, `/delete` — analogicznie dla `invoice_configs`. `InvoiceConfigController`. ### InvoiceConfigRepository (`src/Modules/Settings/InvoiceConfigRepository.php`) - `listAll()` JOIN `invoice_configs LEFT JOIN integrations` (`type='fakturownia'`) — zwraca `integration_name` gdy `is_delegated=1`. - `save(array $data): int` — walidacja serwerowa wszystkich pol. Krytyczna regula: gdy `is_delegated=1` musi byc `integration_id > 0` wskazujacy na `integrations.type='fakturownia'`, inaczej rzuca `IntegrationConfigException`. Gdy `is_delegated=0`, ignoruje `integration_id` (NULL). - `toggleStatus(int $id)` przez `ToggleableRepositoryTrait::toggleActive()`. - `delete(int $id)` — pre-check `SELECT 1 FROM invoices WHERE config_id` zeby zwrocic czytelny PL komunikat zamiast brzydkiego SQLSTATE z FK RESTRICT. ### Seed - Migracja `20260511_000107_seed_default_invoice_config.sql` — idempotentny insert `Domyslny VAT` (NOT EXISTS guard, `invoice_configs.name` nie jest UNIQUE). ### invoice-config-form.js (`public/assets/js/modules/invoice-config-form.js`) - Vanilla JS modul ladowany globalnie przez `layouts/app.php`. - Toggle widocznosci `[data-invoice-delegation]` wrappera w zaleznosci od stanu `[data-invoice-delegated]` checkboxa. - Ustawia `select[name=integration_id].required` zgodnie ze stanem checkboxa; przy unchecked czysci `value`. ### Ujednolicony wyglad list paragonow/faktur - Tabela `table.table` w `table-wrap`, badge `badge--{success,muted}` na statusy. - Edycja przez ``, toggle/delete przez `` z `_token` i `js-confirm-delete`. - Wspolny pattern miedzy `accounting-receipts.php` i `accounting-invoices.php` (faktury maja dodatkowe kolumny: Tryb, Konto Fakturowni). ## Phase 124 — SMS Templates ### SmsTemplateRepository (`src/Modules/Sms/SmsTemplateRepository.php`) - CRUD na `sms_templates` (PDO prepared statements, ToggleableRepositoryTrait). - `listAll()` (cala lista alfabetycznie po `name`), `listActive()` (tylko is_active=1, kolumny `id|name|body` do dropdownu w UI). - `save(array): int` waliduje wymagane `name` + `body` (rzuca `RuntimeException` gdy puste); wykonuje INSERT albo UPDATE wg obecnosci `id` w payloadzie; zwraca id rekordu. - `delete(int)`, `toggleStatus(int)` przez `toggleActive('sms_templates', $id)`. ### SmsVariableResolver (`src/Modules/Sms/SmsVariableResolver.php`) - Wydzielony z `Email\VariableResolver` — wspolna logika zmiennych dla Email i SMS. - `buildVariableMap(order, addresses, companySettings)` zwraca mape placeholderow: `zamowienie.*`, `kupujacy.*`, `adres.*`, `firma.*`, `przesylka.*` (`przesylka.numer`/`przesylka.link_sledzenia` z najnowszej paczki przez `ShipmentPackageRepository::findLatestByOrderId` + `DeliveryStatus::trackingUrl`). - `resolve(template, variableMap)` zastepuje `{{group.var}}` wartoscia z mapy (puste gdy brak klucza). ### Email\VariableResolver (refaktor) - Pozostaje final class z tym samym API publicznym (`buildVariableMap`/`resolve`) — `EmailSendingService` niezmieniony. - Konstruktor: `(ShipmentPackageRepository $repo, ?SmsVariableResolver $inner = null)`. Gdy `$inner` nie podany, sam tworzy SmsVariableResolver — backward compat dla starego wiringu. - Metody publiczne deleguja do `$this->inner` — zero duplikacji logiki zmiennych. ### SmsTemplateController (`src/Modules/Settings/SmsTemplateController.php`) - Mirror `EmailTemplateController` bez Quill/skrzynki/zalacznika/duplikacji. - Akcje: `index` (lista), `create`/`edit`/`save` (form CRUD), `delete`, `toggleStatus` (AJAX JSON), `getVariables` (JSON paleta dla ewentualnego dynamic palette). - `VARIABLE_GROUPS` jako stala klasy — pelne 5 grup (zamowienie/kupujacy/adres/firma/przesylka) zgodnie ze wspolnym SmsVariableResolver. - Routy: `/settings/sms-templates`, `/create`, `/edit`, `/save`, `/delete`, `/toggle`, `/variables`. CSRF `_token` na POST. Flash `settings.sms_templates.success|error`. ### OrdersController (rozszerzenie) - Dodane optional params konstruktora: `?SmsTemplateRepository $smsTemplates`, `?SmsVariableResolver $smsVariableResolver`, `?CompanySettingsRepository $companySettingsRepo` (po istniejacych SMS params; default null = backward compat). - `show()` przekazuje `$smsTemplates` (list active) do widoku jako `smsTemplates`. - Nowa metoda `smsTemplate(Request)` -> `GET /orders/{id}/sms/template?template_id=N` -> JSON `{ok, body, name}` z rozwinietymi zmiennymi. 400/404/500 dla nieprawidlowych parametrow/braku rekordu. ### Widok `orders/show.php` - Nad textarea `name="message"` (`#js-sms-message`) dodany conditional `