diff --git a/.paul/codebase/ARCHITECTURE.md b/.paul/codebase/ARCHITECTURE.md
new file mode 100644
index 0000000..4db9bc8
--- /dev/null
+++ b/.paul/codebase/ARCHITECTURE.md
@@ -0,0 +1,434 @@
+# Architecture
+
+**Analysis Date:** 2026-03-12
+
+---
+
+## 1. Directory Structure
+
+```
+orderPRO/ # Project root
+├── bin/ # CLI scripts (cron, migrations, data tools)
+├── bootstrap/
+│ └── app.php # Application bootstrap: autoload, env, config, DI wiring
+├── config/
+│ ├── app.php # Application config (name, debug, locale, paths, cron, secrets)
+│ └── database.php # DB connection params
+├── database/
+│ ├── migrations/ # SQL migration files (47 files, named YYYYMMDD_NNNNNN_description.sql)
+│ └── drafts/ # Non-active schema drafts
+├── DOCS/
+│ ├── ARCHITECTURE.md # Existing architecture notes (Polish, incremental)
+│ ├── DB_SCHEMA.md # Table-level schema reference
+│ ├── TECH_CHANGELOG.md # Chronological technical changelog
+│ └── todo.md # Work backlog
+├── public/
+│ ├── index.php # Single HTTP entry point
+│ └── assets/ # Compiled CSS, JS, images (served directly)
+├── resources/
+│ ├── lang/ # Translation files (PL locale)
+│ ├── modules/ # Frontend JS/CSS modules (e.g. jquery-alerts)
+│ ├── scss/ # SCSS source → compiled to public/assets/css/
+│ └── views/ # PHP template files (plain PHP, no engine)
+├── routes/
+│ └── web.php # All route definitions (returns closure)
+├── src/
+│ ├── Core/ # Framework kernel (router, DB, HTTP, security, i18n, views)
+│ └── Modules/ # Feature modules (Auth, Orders, Settings, Cron, Shipments, Users)
+├── storage/
+│ ├── cache/ # File cache
+│ ├── logs/ # app.log
+│ ├── sessions/ # PHP session files
+│ └── tmp/ # Temp files (e.g. downloaded labels)
+└── tests/
+ └── Unit/ # Unit tests (currently empty)
+```
+
+---
+
+## 2. Application Layers
+
+### Layer 1 — Kernel (`src/Core/`)
+
+Framework infrastructure. No business logic.
+
+| Class | File | Responsibility |
+|---|---|---|
+| `Application` | `src/Core/Application.php` | Root service locator. Wires all dependencies, boots session, registers error handlers, runs cron on web. |
+| `Router` | `src/Core/Routing/Router.php` | Pattern-matching router for GET/POST. Supports `{param}` segments and middleware pipeline. |
+| `Request` | `src/Core/Http/Request.php` | Immutable wrapper around `$_GET`, `$_POST`, `$_FILES`, `$_SERVER`. |
+| `Response` | `src/Core/Http/Response.php` | Fluent response builder (`html()`, `json()`, `redirect()`). |
+| `Template` | `src/Core/View/Template.php` | Output-buffer PHP view renderer. Injects `$e()` (XSS escape) and `$t()` (translation) helpers. Supports layout wrapping. |
+| `ConnectionFactory` | `src/Core/Database/ConnectionFactory.php` | Creates PDO connection from config array. |
+| `Migrator` | `src/Core/Database/Migrator.php` | Applies pending `.sql` files from `database/migrations/`. Uses DB lock (`GET_LOCK`) to prevent concurrent runs. |
+| `Csrf` | `src/Core/Security/Csrf.php` | Session-based CSRF token generate/validate using `hash_equals`. |
+| `Session` | `src/Core/Support/Session.php` | Thin wrapper around PHP sessions. |
+| `Flash` | `src/Core/Support/Flash.php` | Single-request flash messages stored in `$_SESSION`. |
+| `Logger` | `src/Core/Support/Logger.php` | Appends `[LEVEL] message {context_json}` lines to `storage/logs/app.log`. |
+| `Env` | `src/Core/Support/Env.php` | Parses `.env` file; `get()`, `bool()` helpers. |
+| `Translator` | `src/Core/I18n/Translator.php` | Loads language files from `resources/lang/`; `get($key, $replace)`. |
+
+### Layer 2 — Modules (`src/Modules/`)
+
+Feature slices. Each module owns its controllers, repositories and services.
+
+| Module | Path | Active Modules |
+|---|---|---|
+| Auth | `src/Modules/Auth/` | Login/logout, session-based auth |
+| Orders | `src/Modules/Orders/` | Order list/detail, status change, import |
+| Settings | `src/Modules/Settings/` | Statuses, integrations (Allegro/Apaczka/InPost/shopPRO), DB, cron, company |
+| Cron | `src/Modules/Cron/` | Job scheduling, execution, handlers per job type |
+| Shipments | `src/Modules/Shipments/` | Provider-agnostic shipment creation, label download |
+| Users | `src/Modules/Users/` | User management (list, create) |
+
+### Layer 3 — Views (`resources/views/`)
+
+Plain PHP templates. No Blade, no Twig.
+
+| Path | Renders for |
+|---|---|
+| `layouts/app.php` | Main authenticated shell (sidebar nav) |
+| `layouts/auth.php` | Login page shell |
+| `orders/list.php` | Order list page |
+| `orders/show.php` | Order detail page |
+| `settings/allegro.php` | Allegro integration settings |
+| `settings/apaczka.php` | Apaczka integration settings |
+| `settings/inpost.php` | InPost integration settings |
+| `settings/shoppro.php` | shopPRO integration settings (multi-instance) |
+| `settings/statuses.php` | Status groups & statuses management |
+| `settings/cron.php` | Cron scheduler UI |
+| `settings/integrations.php` | Integrations hub |
+| `settings/database.php` | Migrations UI |
+| `settings/company.php` | Company/sender settings |
+| `shipments/prepare.php` | Shipment creation form |
+| `users/index.php` | Users list |
+| `auth/login.php` | Login form |
+| `components/table-list.php` | Reusable paginated table component |
+| `components/order-status-panel.php` | Reusable order status panel |
+
+---
+
+## 3. Bootstrap and Request Flow
+
+### Bootstrap sequence
+
+```
+public/index.php
+ └─ bootstrap/app.php
+ ├─ autoload (vendor/autoload.php or spl_autoload from src/)
+ ├─ Env::load('.env')
+ ├─ load config/app.php + config/database.php
+ ├─ new Application($basePath, $config)
+ │ ├─ new Router
+ │ ├─ new Translator
+ │ ├─ new Template
+ │ ├─ ConnectionFactory::make → PDO
+ │ ├─ new UserRepository, OrdersRepository, OrderStatusRepository
+ │ ├─ new Migrator
+ │ └─ new AuthService, Logger
+ └─ $app->boot()
+ ├─ prepareDirectories (storage/*)
+ ├─ configureSession
+ ├─ registerErrorHandlers
+ └─ require routes/web.php → $routes($app)
+ (instantiates all controllers, repositories, services and registers routes)
+```
+
+### Per-request flow
+
+```
+HTTP request
+ → public/index.php
+ → $app->run()
+ ├─ Request::capture() (wraps $_GET/$_POST/$_FILES/$_SERVER)
+ ├─ maybeRunCronOnWeb() (if cron_run_on_web=1, throttled, DB-locked)
+ └─ Router::dispatch($request)
+ ├─ find route by method + path (exact match first, then {param} patterns)
+ ├─ attach URL params to $request via withAttributes()
+ ├─ build middleware pipeline (array_reduce, reversed)
+ │ └─ AuthMiddleware::handle() (most routes)
+ │ └─ redirect /login if not authenticated
+ └─ call controller method($request): Response
+ ├─ reads input via $request->input()
+ ├─ validates CSRF via Csrf::validate()
+ ├─ calls repository/service methods
+ └─ returns Response::html($template->render(...))
+ or Response::redirect(...)
+ or Response::json(...)
+ → Response::send() (sets headers, echoes body)
+```
+
+---
+
+## 4. Module Organization
+
+### Auth (`src/Modules/Auth/`)
+
+| Class | Responsibility |
+|---|---|
+| `AuthController` | `GET /login`, `POST /login`, `POST /logout` |
+| `AuthService` | `check()`, `user()`, `attempt(email, password)`, `logout()` backed by session |
+| `AuthMiddleware` | `handle(Request, callable $next)` — redirects to `/login` if session unauthenticated |
+
+### Orders (`src/Modules/Orders/`)
+
+| Class | Responsibility |
+|---|---|
+| `OrdersController` | `index` (list), `show` (detail), `updateStatus` — builds view data, delegates DB to repos |
+| `OrdersRepository` | `paginate()`, `findDetails()`, `statusCounts()`, `statusPanelConfig()`, `sourceOptions()`, `updateOrderStatus()`, `recordActivity()` |
+| `OrderImportRepository` | `upsertOrderAggregate()` — transactional upsert of the full order aggregate (order + addresses + items + payments + shipments + notes + status history) |
+
+### Settings (`src/Modules/Settings/`)
+
+Large module covering all configuration:
+
+**Controllers:**
+- `SettingsController` — statuses, status groups, DB migrations
+- `AllegroIntegrationController` — Allegro OAuth2, import, status mappings, delivery mappings
+- `ApaczkaIntegrationController` — Apaczka API config
+- `InpostIntegrationController` — InPost ShipX config
+- `ShopproIntegrationsController` — shopPRO multi-instance management (statuses, delivery, sync)
+- `IntegrationsHubController` — overview hub listing all provider instances
+- `CronSettingsController` — cron schedule UI
+- `CompanySettingsController` — company/sender details
+
+**Repositories:**
+- `OrderStatusRepository` — CRUD + reorder for `order_status_groups` and `order_statuses`
+- `AllegroIntegrationRepository` — reads/writes `allegro_integration_settings`; encrypts secrets via `IntegrationSecretCipher`
+- `AllegroStatusMappingRepository` — `allegro_order_status_mappings` CRUD
+- `AllegroOrderSyncStateRepository` — cursor state for Allegro sync (`integration_order_sync_state`)
+- `ApaczkaIntegrationRepository` — reads/writes `apaczka_integration_settings`
+- `InpostIntegrationRepository` — reads/writes `inpost_integration_settings`
+- `IntegrationsRepository` — base `integrations` table queries (hub listing)
+- `ShopproIntegrationsRepository` — multi-instance CRUD for `integrations` where `type=shoppro`
+- `ShopproStatusMappingRepository` — `order_status_mappings` per shopPRO integration
+- `ShopproDeliveryMethodMappingRepository` — `shoppro_delivery_method_mappings`
+- `CarrierDeliveryMethodMappingRepository` — `carrier_delivery_method_mappings` (unified, cross-source)
+- `CompanySettingsRepository` — `company_settings` single-row config
+- `AllegroDeliveryMethodMappingRepository` — Allegro-specific delivery method mappings (legacy, superseded by carrier table)
+
+**API Clients (HTTP adapters):**
+- `AllegroOAuthClient` — builds OAuth2 authorize URL, exchanges code for tokens, refreshes tokens
+- `AllegroApiClient` — calls Allegro REST API (`/order/checkout-forms`, `/sale/product-offers`, `/delivery-services`, etc.)
+- `ApaczkaApiClient` — calls Apaczka REST API (services, create shipment, label, points)
+- `ShopproApiClient` — calls shopPRO REST API (orders, products, dictionaries, statuses)
+
+**Services:**
+- `AllegroOrderImportService` — maps single Allegro checkout-form to neutral order aggregate, calls `OrderImportRepository::upsertOrderAggregate()`
+- `AllegroOrdersSyncService` — paginates Allegro checkout-forms list and imports new/changed orders; maintains sync cursor
+- `AllegroStatusSyncService` — syncs statuses between Allegro and orderPRO based on configured direction
+- `AllegroStatusDiscoveryService` — fetches live status list from Allegro API and seeds `allegro_order_status_mappings`
+- `ShopproOrdersSyncService` — fetches shopPRO orders, maps to neutral aggregate, handles paczkomat/pickup points, media
+- `ShopproStatusSyncService` — orchestrates shopPRO→orderPRO status synchronisation
+- `ShopproPaymentStatusSyncService` — polls shopPRO `paid` flag and updates `orders.payment_status`
+- `IntegrationSecretCipher` — AES-256-CBC encryption with HMAC-SHA256 authentication (`v1:base64(iv+mac+cipher)`)
+
+### Cron (`src/Modules/Cron/`)
+
+| Class | Responsibility |
+|---|---|
+| `CronRunner` | Dispatches due schedules to `cron_jobs` queue; processes pending jobs up to `$limit`; delegates to typed handlers |
+| `CronRepository` | `findDueSchedules()`, `claimNextPendingJob()`, `markJobCompleted()`, `markJobFailed()`, `enqueueJobFromSchedule()`, `getBoolSetting()`, `getIntSetting()` |
+| `AllegroTokenRefreshHandler` | `handle()` → refreshes Allegro OAuth token before expiry |
+| `AllegroOrdersImportHandler` | `handle()` → calls `AllegroOrdersSyncService::sync()` |
+| `AllegroStatusSyncHandler` | `handle()` → calls `AllegroStatusSyncService::sync()` |
+| `ShopproOrdersImportHandler` | `handle()` → calls `ShopproOrdersSyncService::syncAll()` |
+| `ShopproStatusSyncHandler` | `handle()` → calls `ShopproStatusSyncService::syncAll()` |
+| `ShopproPaymentStatusSyncHandler` | `handle()` → calls `ShopproPaymentStatusSyncService::syncAll()` |
+
+Cron is also triggerable via CLI (`bin/cron.php`) and optionally runs on each HTTP request when `cron_run_on_web=1` (throttled with session timer + DB advisory lock `orderpro_web_cron_lock`).
+
+### Shipments (`src/Modules/Shipments/`)
+
+Provider-agnostic shipment layer built around `ShipmentProviderInterface`:
+
+```php
+interface ShipmentProviderInterface {
+ public function code(): string;
+ public function getDeliveryServices(): array;
+ public function createShipment(int $orderId, array $formData): array;
+ public function checkCreationStatus(int $packageId): array;
+ public function downloadLabel(int $packageId, string $storagePath): array;
+}
+```
+
+| Class | Provider Code | Responsibility |
+|---|---|---|
+| `AllegroShipmentService` | `allegro_wza` | Allegro WZA carrier integration |
+| `ApaczkaShipmentService` | `apaczka` | Apaczka carrier API: wycena, create, label, points fallback for pickup addresses |
+| `ShipmentProviderRegistry` | — | Map of `code → ShipmentProviderInterface`; resolved by `ShipmentController` |
+| `ShipmentController` | — | `prepare` (form), `create`, `checkStatus`, `label` endpoints |
+| `ShipmentPackageRepository` | — | CRUD for `shipment_packages` table |
+
+### Users (`src/Modules/Users/`)
+
+| Class | Responsibility |
+|---|---|
+| `UsersController` | List users, create user |
+| `UserRepository` | Find by email, list, insert/update |
+
+---
+
+## 5. Core Domain Concepts
+
+### Order Aggregate
+
+The order domain is provider-neutral. All external orders (Allegro, shopPRO) are normalised into the same tables:
+
+```
+orders ← main record (source, integration_id, statuses, totals, flags)
+ ├─ order_addresses ← 1:N (type: customer | delivery | invoice)
+ ├─ order_items ← 1:N (products, quantities, media_url)
+ ├─ order_payments ← 1:N (payment method, amounts)
+ ├─ order_shipments ← 1:N (tracking numbers, provider info)
+ ├─ order_documents ← 1:N (invoices, etc.)
+ ├─ order_notes ← 1:N (internal notes)
+ ├─ order_status_history ← 1:N (status transitions with timestamps)
+ └─ order_activity_log ← 1:N (universal event log: status_change, payment, shipment, import, note, ...)
+```
+
+Key `orders` columns:
+- `source` — origin system (`allegro`, `shoppro`)
+- `source_order_id` — external ID in source system
+- `integration_id` — FK to `integrations.id`
+- `internal_order_number` — `OPXXXXXXXXX` (unique internal identifier, e.g. `OP000000001`)
+- `external_status_id` / `external_status_code` — last known status from source
+- `payment_status` — payment state (`0`=unpaid, `2`=paid, etc.)
+- `ordered_at` / `source_created_at` / `source_updated_at` / `fetched_at` — date fallback chain
+
+### Integration Registry (`integrations` table)
+
+Single base table for all integration instances. Specific settings are in satellite tables linked by `integration_id`:
+
+```
+integrations (type, name, is_active, api_key_encrypted, orders_fetch_enabled, ...)
+ ├─ allegro_integration_settings (1:1, environment, OAuth tokens, token_expires_at)
+ ├─ apaczka_integration_settings (1:1, app_id, app_secret_encrypted)
+ └─ inpost_integration_settings (1:1, api_token, organization_id, defaults)
+
+integrations (type=shoppro, multi-instance)
+ ├─ order_status_mappings (1:N, shoppro_status_code → orderpro_status_code)
+ └─ shoppro_delivery_method_mappings (1:N, delivery method → carrier service)
+```
+
+### Status Configuration
+
+```
+order_status_groups (name, code, color_hex, sort_order, is_active)
+ └─ order_statuses (1:N, name, code, sort_order, is_active)
+```
+
+Status codes in `orders` (`external_status_code`) are mapped to human labels via join with `order_statuses`. Fallback group "Pozostale" catches unmapped codes.
+
+External status mappings per provider:
+- `allegro_order_status_mappings` — `allegro_status_code → orderpro_status_code`
+- `order_status_mappings` — generic per-integration mapping (`shoppro_status_code → orderpro_status_code`)
+
+### Carrier / Delivery Method Mappings
+
+```
+carrier_delivery_method_mappings
+ source_system + source_integration_id + order_delivery_method
+ → provider + provider_service_id + provider_account_id
+```
+
+This is the unified resolution table used by `ShipmentController` to auto-select the carrier service when preparing a shipment.
+
+### Cron Scheduling Tables
+
+```
+cron_schedules (job_type, interval_seconds, priority, is_active, next_run_at)
+ → cron_jobs (job_type, payload, status, priority, scheduled_at, result_json, ...)
+```
+
+`CronRunner` reads due schedules, enqueues jobs, then claims and processes pending jobs up to the configured limit.
+
+---
+
+## 6. Dependency Injection Pattern
+
+The application does not use a DI container. All dependencies are wired manually in `routes/web.php` (for HTTP) and in `Application::maybeRunCronOnWeb()` (for web cron). This means:
+
+- Every controller receives its dependencies via constructor injection.
+- `Application` holds the core singletons (PDO, AuthService, OrdersRepository, etc.) and exposes them via typed getters (`$app->db()`, `$app->orders()`, `$app->auth()`, etc.).
+- `routes/web.php` is the composition root for the web context.
+
+---
+
+## 7. Security Architecture
+
+| Concern | Mechanism |
+|---|---|
+| Authentication | Session-based. `AuthService::check()` reads `$_SESSION`. `AuthMiddleware` guards all protected routes. |
+| CSRF | `Csrf::token()` / `Csrf::validate()` — single session token, validated on all POST handlers before any mutation. |
+| Secret storage | API keys and OAuth tokens encrypted at-rest using `IntegrationSecretCipher` (AES-256-CBC + HMAC-SHA256). Secret key comes from `INTEGRATIONS_SECRET` env var. |
+| XSS | Template helper `$e()` wraps `htmlspecialchars()` with `ENT_QUOTES`. |
+| SQL injection | All queries use PDO prepared statements (medoo-style named params). No raw string concatenation. |
+| DB locks | `GET_LOCK` used for migrations (`orderpro_migrations_lock`) and web cron (`orderpro_web_cron_lock`) to prevent concurrent execution. |
+
+---
+
+## 8. Database Design Summary
+
+**Migration system:** SQL files in `database/migrations/` named `YYYYMMDD_NNNNNN_description.sql`, sorted alphabetically and tracked in `migrations` table. Applied via `Migrator::runPending()` from UI or CLI (`bin/migrate.php`).
+
+**Core tables:**
+
+| Table | Purpose |
+|---|---|
+| `users` | Panel users (email, password_hash) |
+| `orders` | Master order record |
+| `order_addresses` | Typed addresses per order (customer/delivery/invoice) |
+| `order_items` | Order line items with media_url |
+| `order_payments` | Payment records per order |
+| `order_shipments` | Shipment/tracking records from source system |
+| `order_documents` | Document references (invoices etc.) |
+| `order_notes` | Internal notes |
+| `order_status_history` | Full status transition history |
+| `order_activity_log` | Universal event log (status_change, payment, shipment, import, note, document, message) |
+| `order_status_groups` | Configurable status groups (with color, sort) |
+| `order_statuses` | Statuses within groups |
+| `order_status_mappings` | External status → orderPRO status per integration (generic) |
+| `allegro_order_status_mappings` | Allegro-specific status mappings |
+| `integrations` | Base table for all integration instances (allegro, apaczka, inpost, shoppro) |
+| `allegro_integration_settings` | Allegro OAuth2 credentials and tokens (1:1 with integrations) |
+| `apaczka_integration_settings` | Apaczka app_id + secrets (1:1 with integrations) |
+| `inpost_integration_settings` | InPost ShipX config and defaults (1:1 with integrations) |
+| `integration_order_sync_state` | Import cursor per integration (last synced order, timestamps) |
+| `carrier_delivery_method_mappings` | Unified: order delivery method → shipment provider service |
+| `shoppro_delivery_method_mappings` | shopPRO-specific delivery mappings (legacy, superseded by carrier table) |
+| `shipment_packages` | Packages created via shipment providers (status, tracking, label path) |
+| `company_settings` | Single-row sender company config (address, contact_person, package defaults) |
+| `cron_schedules` | Named job schedules with intervals and priorities |
+| `cron_jobs` | Job queue: pending, running, completed, failed |
+| `app_settings` | Key-value settings store (cron_run_on_web, allegro_status_sync_direction, etc.) |
+| `migrations` | Applied migration filenames (Migrator tracking) |
+
+---
+
+## 9. CLI Scripts (`bin/`)
+
+| Script | Purpose |
+|---|---|
+| `bin/cron.php` | CLI cron runner; loads app and calls `CronRunner::run($limit)` |
+| `bin/migrate.php` | CLI migration runner; calls `Migrator::runPending()` |
+| `bin/deploy_and_seed_orders.php` | Applies order schema draft + seeds test data (`--count`, `--append`, `--profile=default\|realistic`) |
+| `bin/fix_status_codes.php` | Repairs status group/status codes (PL→ASCII transliteration, `--dry-run`) |
+| `bin/fill_order_item_images.php` | Backfills `order_items.media_url` from integration APIs |
+| `bin/randomize_order_statuses.php` | Dev utility to randomize order statuses |
+| `bin/fix_gs1_brand.php` | Fixes GS1 brand data |
+| `bin/test_gs1_api.php` | Tests GS1 API connectivity |
+
+---
+
+## 10. Frontend Architecture
+
+- **Styles:** SCSS source in `resources/scss/`, compiled to `public/assets/css/`.
+- **Scripts:** JS in `resources/modules/` (e.g. `jquery-alerts`), compiled to `public/assets/js/modules/`.
+- **Alerts/confirms:** `window.OrderProAlerts.confirm(...)` from `public/assets/js/modules/jquery-alerts.js`. Native `alert()`/`confirm()` are forbidden.
+- **Views:** Plain PHP; no JS framework. Inline JS kept minimal, page-level only in view files.
+- **Layout:** Single shell `resources/views/layouts/app.php` with sidebar nav. Sidebar `activeMenu` and `activeSettings` variables control highlighted state.
+
+---
+
+*Full architecture analysis: 2026-03-12*
diff --git a/.paul/codebase/CONCERNS.md b/.paul/codebase/CONCERNS.md
new file mode 100644
index 0000000..d332655
--- /dev/null
+++ b/.paul/codebase/CONCERNS.md
@@ -0,0 +1,384 @@
+# Codebase Concerns
+
+**Analysis Date:** 2026-03-12
+
+---
+
+## Tech Debt
+
+### [HIGH] Duplicated OAuth Token Refresh Logic — 4 copies
+
+- Issue: `resolveAccessToken()` and `forceRefreshToken()` are private methods duplicated verbatim in four separate classes. Any fix to token-refresh behavior must be applied in all four places.
+- Files:
+ - `src/Modules/Settings/AllegroOrderImportService.php` (lines 118–184)
+ - `src/Modules/Settings/AllegroOrdersSyncService.php` (lines 212–278)
+ - `src/Modules/Settings/AllegroStatusDiscoveryService.php` (lines 107–170)
+ - `src/Modules/Shipments/AllegroShipmentService.php` (lines 367–441)
+- Impact: Bug in token refresh (e.g., race condition, edge case) requires four coordinated fixes. High probability of drift.
+- Fix approach: Extract a shared `AllegroTokenManager` service with `resolveToken(): [string $token, string $env]`. Inject it into all four classes.
+
+---
+
+### [HIGH] Duplicated `nullableString()` / `normalizeDateTime()` / `normalizeColorHex()` Helpers — 15+ copies
+
+- Issue: The same private utility methods are copy-pasted into almost every Repository and Service class.
+ - `nullableString()` appears in 12+ classes.
+ - `normalizeDateTime()` appears in at least 3 classes.
+ - `normalizeColorHex()` appears in `OrdersController`, `OrdersRepository`, and `SettingsController`.
+- Files affected (sample):
+ - `src/Modules/Settings/AllegroOrderImportService.php`
+ - `src/Modules/Settings/AllegroOrdersSyncService.php`
+ - `src/Modules/Settings/AllegroOrderSyncStateRepository.php`
+ - `src/Modules/Settings/AllegroStatusMappingRepository.php`
+ - `src/Modules/Settings/CarrierDeliveryMethodMappingRepository.php`
+ - `src/Modules/Settings/CompanySettingsRepository.php`
+ - `src/Modules/Settings/InpostIntegrationRepository.php`
+ - `src/Modules/Settings/IntegrationsRepository.php`
+ - `src/Modules/Settings/ShopproIntegrationsRepository.php`
+ - `src/Modules/Settings/ShopproOrdersSyncService.php`
+ - `src/Modules/Orders/OrdersController.php`
+ - `src/Modules/Orders/OrdersRepository.php`
+- Impact: Violates project's anti-copy-paste rule. Inconsistent behavior if one copy diverges.
+- Fix approach: Create `src/Core/Support/StringHelper.php` with static methods `nullableString()`, `normalizeDateTime()`, `normalizeColorHex()`. Replace all copies.
+
+---
+
+### [HIGH] `ShopproOrdersSyncService` Uses `AllegroOrderSyncStateRepository`
+
+- Issue: The shopPRO sync service injects and depends on `AllegroOrderSyncStateRepository` to track its own sync cursor. This is a misplaced dependency — shopPRO's state is being written into the Allegro state table.
+- Files:
+ - `src/Modules/Settings/ShopproOrdersSyncService.php` (constructor, line 16)
+ - `src/Modules/Settings/AllegroOrderSyncStateRepository.php`
+- Impact: shopPRO sync cursor state is mixed with Allegro cursor state in the same repository. Adding multiple shopPRO integrations compounds this. Extremely fragile.
+- Fix approach: Create a dedicated `ShopproOrderSyncStateRepository` (can share the same DB table as long as `integration_id` scoping is correct, but must not use the Allegro-named class).
+
+---
+
+### [MEDIUM] `Application.php` Manually Instantiates Full Object Graph for Web Cron
+
+- Issue: `Application::maybeRunCronOnWeb()` (lines 271–354) contains 80+ lines of manual `new X($this->db)` construction for every cron handler. This duplicates the wiring already in `routes/web.php` and is vulnerable to getting out of sync.
+- File: `src/Core/Application.php`
+- Impact: Adding a new cron handler requires edits in two places. Already shows signs of drift vs. `routes/web.php`.
+- Fix approach: Extract a `CronHandlerFactory` or move cron wiring into `routes/web.php` so both web and CLI cron share the same configuration.
+
+---
+
+### [MEDIUM] CSRF Token Field Name Inconsistency
+
+- Issue: Two distinct CSRF field names are used across the application: `_token` (most controllers) and `_csrf_token` (orders and shipment controllers).
+- Files:
+ - `src/Modules/Orders/OrdersController.php` — reads `_csrf_token`
+ - `src/Modules/Shipments/ShipmentController.php` — reads `_csrf_token`
+ - All settings controllers and `AuthController` — read `_token`
+- Impact: Confusion during future form additions. Risk of accidentally using the wrong field name and breaking CSRF protection silently.
+- Fix approach: Standardize on one name (recommend `_token` to match the majority) and update the two outlier controllers and their views.
+
+---
+
+### [MEDIUM] 5 "ensure_*" Migrations Indicate Schema Drift
+
+- Issue: Five migrations with `ensure_` prefix were added to defensively re-create or re-add objects that should have existed from earlier migrations. This is a signal that schema was edited or deployed inconsistently.
+- Files:
+ - `database/migrations/20260308_000038_ensure_order_status_mappings_table.sql`
+ - `database/migrations/20260308_000039_ensure_integrations_fetch_columns.sql`
+ - `database/migrations/20260308_000040_ensure_shoppro_orders_import_schedule.sql`
+ - `database/migrations/20260308_000041_ensure_shoppro_status_sync_schedule_and_direction.sql`
+ - `database/migrations/20260308_000042_ensure_shoppro_payment_sync_schedule_and_columns.sql`
+- Impact: The real canonical schema is not in the migrations alone — some state is applied defensively in later migrations. Makes schema reconstruction from scratch unreliable.
+- Fix approach: Audit and document what each `ensure_*` migration fixes. Once all environments are converged, these can be annotated as "compensating migrations for env X".
+
+---
+
+### [MEDIUM] Direct `$_SESSION` Writes in Controllers Instead of `Flash`
+
+- Issue: `OrdersController` and `ShipmentController` write flash messages directly to `$_SESSION['order_flash_*']` and `$_SESSION['shipment_flash_*']` instead of using the existing `Flash` class in `src/Core/Support/Flash.php`.
+- Files:
+ - `src/Modules/Orders/OrdersController.php` (lines 162–220)
+ - `src/Modules/Shipments/ShipmentController.php` (lines 93–328)
+- Impact: Bypasses the abstraction layer, mixing direct session manipulation with the Flash utility. Inconsistent pattern across the codebase.
+- Fix approach: Replace `$_SESSION['*_flash_*']` with `Flash::set()`/`Flash::get()` using namespaced keys.
+
+---
+
+### [MEDIUM] SonarQube — 327 Open Issues (2026-03-12)
+
+Listed in `DOCS/todo.md`:
+- 95x `php:S112` — generic `new \Exception` instead of typed exceptions
+- 57x `php:S1142` — excessive `return` statements per method
+- 40x `php:S1192` — repeated string literals not extracted to constants
+- 31x `php:S3776` — high cognitive complexity
+- 11x `php:S1448` — classes with too many methods (god classes)
+- 4x `php:S138` — methods over the allowed line limit
+
+The most critical from a maintainability standpoint are the complexity and god-class violations.
+
+---
+
+### [LOW] Duplicate Migration Number `000014`
+
+- Issue: Two migration files share sequence number `000014`:
+ - `20260227_000014_create_product_integration_translations.sql`
+ - `20260301_000014_add_products_sku_format_setting.sql`
+- File: `database/migrations/`
+- Impact: Depending on how the migrator detects "already run" (by filename vs. sequence number), one of these may silently never execute or may fail.
+- Fix approach: Rename one to `000014b` or renumber all subsequent ones. Verify migrator uses full filename as key.
+
+---
+
+## Security Concerns
+
+### [HIGH] No SSL Verification on cURL Calls
+
+- Issue: None of the `AllegroApiClient`, `ShopproApiClient`, `ApaczkaApiClient`, or `AllegroOAuthClient` set `CURLOPT_SSL_VERIFYPEER` or `CURLOPT_SSL_VERIFYHOST`. PHP's default is to verify SSL, but explicitly not setting it means behavior depends on the server's `php.ini` `curl.cainfo` configuration.
+- Files:
+ - `src/Modules/Settings/AllegroApiClient.php`
+ - `src/Modules/Settings/AllegroOAuthClient.php`
+ - `src/Modules/Settings/ShopproApiClient.php`
+ - `src/Modules/Settings/ApaczkaApiClient.php`
+- Impact: On environments without a properly configured CA bundle (common on shared hosting / Windows XAMPP), all API calls may proceed without SSL verification, enabling MITM attacks on OAuth tokens and API keys.
+- Fix approach: Explicitly set `CURLOPT_SSL_VERIFYPEER => true` and configure `CURLOPT_CAINFO` pointing to a known CA bundle. Document this in `.env.example`.
+
+---
+
+### [MEDIUM] CSRF Token Is Never Rotated After Login
+
+- Issue: `Csrf::token()` in `src/Core/Security/Csrf.php` generates the token once per session and never regenerates it. The token is never rotated after a successful login, and it is never invalidated after a POST form is submitted.
+- File: `src/Core/Security/Csrf.php`
+- Impact: If a CSRF token leaks (e.g., via Referer header, browser history), it remains valid for the entire session lifetime.
+- Fix approach: Rotate the CSRF token on login. Optionally regenerate after each use (per-form tokens). At minimum, invalidate the session token on `AuthService::logout()`.
+
+---
+
+### [MEDIUM] Label File Served with `file_get_contents($fullPath)` — Partial Path Traversal Risk
+
+- Issue: In `ShipmentController::label()`, the file path is built from `$package['label_path']` retrieved from the database. While `basename()` is used for the filename in the response header, `$fullPath` itself is derived by concatenating `$storagePath . '/labels/' . $filename` from the shipment service. There is no `realpath()` check confirming the file is actually within the storage directory before serving it.
+- File: `src/Modules/Shipments/ShipmentController.php` (lines 292–316)
+- Impact: Low risk currently (path comes from DB, not user input directly), but if `label_path` column is ever injectable or incorrectly set, arbitrary file reads become possible.
+- Fix approach: Validate `realpath($fullPath)` starts with `realpath($storagePath)` before calling `file_get_contents()`.
+
+---
+
+### [LOW] Allegro OAuth Callback Endpoint Has No Auth Middleware
+
+- Issue: `GET /settings/integrations/allegro/oauth/callback` is registered without `$authMiddleware`. The OAuth state parameter is validated via session, which does provide CSRF protection for the OAuth flow, but any authenticated check is absent.
+- File: `routes/web.php` (line 239)
+- Impact: An unauthenticated actor with knowledge of the state parameter could complete an OAuth token exchange. In practice, the state must match the session, limiting exploitability to session-fixation scenarios.
+- Fix approach: Add `$authMiddleware` to the callback route. Handle the "not authenticated yet" edge case by storing the session state before redirect.
+
+---
+
+## Performance Concerns
+
+### [HIGH] N+1 Correlated Subqueries on Every Order List Row
+
+- Issue: The `OrdersRepository::paginate()` list SQL includes three correlated subqueries per row: `items_count`, `items_qty`, `shipments_count`, and `documents_count`. These fire once per order in the result set.
+- File: `src/Modules/Orders/OrdersRepository.php` (lines 124–127)
+- Impact: For a page of 50 orders: 4 × 50 = 200 additional subquery executions per page load. Degrades significantly as the `orders` table grows.
+- Fix approach: Replace correlated subqueries with LEFT JOIN + GROUP BY aggregates, or preload counts in a single separate query (`SELECT order_id, COUNT(*) ... GROUP BY order_id WHERE order_id IN (...)`).
+
+---
+
+### [HIGH] `canResolveMappedMedia()` Queries `information_schema.COLUMNS` on Every Request
+
+- Issue: `OrdersRepository::canResolveMappedMedia()` runs a query against `information_schema.COLUMNS` to check whether optional product mapping tables exist. It caches the result in a private property, but this property is instance-scoped — the check fires once per HTTP request that instantiates `OrdersRepository`.
+- File: `src/Modules/Orders/OrdersRepository.php` (lines 604–648)
+- Impact: `information_schema` queries are notoriously slow on MySQL. This fires on every order-related page load (list, detail, shipment).
+- Fix approach: Cache the result in a static class property or in the app-level cache/session so it survives across requests. Alternatively, remove the check entirely — if the schema is managed by migrations, the tables either exist or don't.
+
+---
+
+### [MEDIUM] AllegroStatusSyncService Fetches Up to 50 Orders Then Re-Imports Each One Fully
+
+- Issue: `AllegroStatusSyncService::findOrdersNeedingStatusSync()` returns up to 50 Allegro orders. For each, `importSingleOrder()` is called, which makes a full Allegro API call per order (checkout-form fetch + optional shipments fetch + optional offer image fetch per line item). This is 50+ API calls per cron run.
+- File: `src/Modules/Settings/AllegroStatusSyncService.php`
+- Impact: Rate-limit exhaustion on Allegro API. Slow cron runs.
+- Fix approach: Use a lighter-weight API call to fetch only status fields for multiple orders (e.g., `listCheckoutForms` with a filter) rather than a full re-import per order.
+
+---
+
+### [MEDIUM] No Database Indexes Verified for Key Queries
+
+- Issue: Queries frequently filter and sort on `orders.source`, `orders.external_status_id`, `orders.ordered_at`, `order_items.order_id`, and `allegro_order_status_mappings.allegro_status_code`. Whether indexes exist for these columns is not documented.
+- Files: `src/Modules/Orders/OrdersRepository.php`, `database/migrations/20260302_000018_create_orders_tables_and_schedule.sql`
+- Impact: Full table scans as data grows.
+- Fix approach: Audit the migration that creates `orders` and related tables for index definitions. Add missing indexes for `(source, external_status_id)`, `(ordered_at)`, and `allegro_status_code`.
+
+---
+
+## Architectural Concerns
+
+### [HIGH] `Settings` Module Is a God Module — Contains Unrelated Concerns
+
+- Issue: The `src/Modules/Settings/` directory contains 30+ classes covering: Allegro integration, Apaczka integration, InPost integration, shopPRO integration, company settings, cron settings, order statuses, carrier mappings, delivery method mappings, and OAuth. These are logically distinct domains crammed into a single module namespace.
+- Files: All files in `src/Modules/Settings/`
+- Impact: The module boundary is meaningless. New integrations will continue to pile into Settings. `AllegroIntegrationController` (923 lines) and `ShopproIntegrationsController` (901 lines) are god classes by SonarQube rules (S1448).
+- Fix approach: Gradually decompose into purpose-built modules: `src/Modules/Integrations/Allegro/`, `src/Modules/Integrations/Shoppro/`, `src/Modules/Integrations/Apaczka/`, `src/Modules/Integrations/Inpost/`.
+
+---
+
+### [HIGH] `AllegroIntegrationController` and `ShopproIntegrationsController` Are God Classes (900+ lines)
+
+- Issue: Each controller handles: displaying settings, saving credentials, OAuth flow, status mapping CRUD, delivery method mapping CRUD, and import triggering. Each is 900+ lines.
+- Files:
+ - `src/Modules/Settings/AllegroIntegrationController.php` (923 lines)
+ - `src/Modules/Settings/ShopproIntegrationsController.php` (901 lines)
+- Impact: Violates SRP. Methods average far more than 50 lines. Cognitive complexity is high (SonarQube S3776, 31 violations).
+- Fix approach: Split each into focused controllers per tab/concern: e.g., `AllegroCredentialsController`, `AllegroStatusMappingController`, `AllegroDeliveryMappingController`.
+
+---
+
+### [HIGH] `ShopproOrdersSyncService` Is 1192 Lines — Largest Single File
+
+- Issue: `ShopproOrdersSyncService` handles the full import lifecycle for shopPRO orders including: pagination, cursor management, order mapping, address building, item building, image resolution, payment mapping, shipment mapping, status history, and activity logging — all in one class.
+- File: `src/Modules/Settings/ShopproOrdersSyncService.php`
+- Impact: Highest complexity class in the codebase. Hard to test, hard to modify, prone to regression.
+- Fix approach: Extract a `ShopproOrderMapper` (mapping logic), a `ShopproImageResolver` (image fetch), and keep `ShopproOrdersSyncService` as the orchestrator. Mirror the pattern of `AllegroOrderImportService` which is better separated.
+
+---
+
+### [MEDIUM] No Dependency Injection Container — Full Object Graph in `routes/web.php`
+
+- Issue: `routes/web.php` manually constructs all services and controllers, including multiple separate instantiations of identical objects (e.g., `new AllegroApiClient()` appears 3 times, `new OrdersRepository($app->db())` appears 4 times).
+- File: `routes/web.php`
+- Impact: Multiple object instances where singletons are implied. Adding new dependencies requires editing this file, which is already 257 lines. Risk of silently passing the wrong instance.
+- Fix approach: Introduce a lightweight DI container or service locator. At minimum, hoist shared instances (`$allegroApiClient`, `$ordersRepository`) to named variables and reuse them.
+
+---
+
+### [MEDIUM] Flash Messages Implemented in Two Incompatible Ways
+
+- Issue: The project has `src/Core/Support/Flash.php` as the canonical flash mechanism (used in all Settings controllers). However, `OrdersController` and `ShipmentController` bypass Flash and write directly to `$_SESSION` with custom key names.
+- Files:
+ - `src/Core/Support/Flash.php`
+ - `src/Modules/Orders/OrdersController.php`
+ - `src/Modules/Shipments/ShipmentController.php`
+- Impact: Two incompatible patterns co-exist. New developers cannot determine which to follow.
+- Fix approach: See Tech Debt section — migrate both controllers to use `Flash::set()`/`Flash::get()`.
+
+---
+
+## Incomplete Features
+
+### [HIGH] 5 Stub Buttons with No Actions in Order Detail View
+
+- Issue: The order detail page (`resources/views/orders/show.php`, lines 48–53) contains 5 buttons that render in the UI but have no `href`, no form action, and no JavaScript handler: "Strefa klienta", "Platnosc", "Drukuj", "Pakuj", "Edytuj".
+- File: `resources/views/orders/show.php`
+- Impact: Users see clickable but non-functional UI elements. The "Pakuj" (pack) button is styled as the primary CTA (`btn--primary`).
+- Fix approach: Either implement the features or hide the buttons until they are ready.
+
+---
+
+### [HIGH] `orderpro_to_allegro` Status Sync Direction Not Implemented
+
+- Issue: The setting UI offers two sync directions: Allegro → orderPRO and orderPRO → Allegro. The second direction explicitly returns early with "Kierunek orderPRO -> Allegro nie jest jeszcze wdrozony" ("not yet implemented").
+- File: `src/Modules/Settings/AllegroStatusSyncService.php` (lines 38–45)
+- Impact: Users who configure the "push status to Allegro" direction get silent no-op behavior with no clear user-facing indication in the cron log.
+- Fix approach: Either implement the feature or disable the `orderpro_to_allegro` direction option in the UI until it is ready.
+
+---
+
+### [HIGH] TODO in `DOCS/todo.md` — UI Issues Open
+
+The following items from `DOCS/todo.md` are marked incomplete:
+- Item 12: Status change sync with Allegro after manual user change
+- Item 14: Input/select/textarea border color needs to be darker
+- Item 15: Source and ID display order should be reversed (source first, then ID)
+- Item 16: Order list statuses should be colored according to settings
+- Item 17: Source display should show specific integration name instead of "shopPRO"; ID label missing
+- Files: `resources/views/orders/list.php`, `resources/views/orders/show.php`
+
+---
+
+### [MEDIUM] InPost Integration — Settings Page Exists But No Shipment Provider
+
+- Issue: `InpostIntegrationController` and `InpostIntegrationRepository` exist and the settings page at `/settings/integrations/inpost` is functional. However, there is no `InpostShipmentService` implementing `ShipmentProviderInterface`. InPost labels are currently routed through `allegro_wza` as a workaround (`provider_code === 'inpost'` is remapped to `'allegro_wza'` in `ShipmentController::create()`).
+- Files:
+ - `src/Modules/Settings/InpostIntegrationController.php`
+ - `src/Modules/Settings/InpostIntegrationRepository.php`
+ - `src/Modules/Shipments/ShipmentController.php` (line 165–166)
+- Impact: InPost credentials are stored but not used by their own provider. The remap to `allegro_wza` means InPost shipments are actually created via Allegro WZA. This breaks if the user has only InPost credentials and no Allegro integration.
+- Fix approach: Implement `InpostShipmentService implements ShipmentProviderInterface` using the stored InPost API token.
+
+---
+
+### [MEDIUM] `database/drafts/` Contains Uncommitted Schema SQL
+
+- Issue: `database/drafts/20260302_orders_schema_v1.sql` exists in the `drafts` subdirectory. This is not a migration — it will not be executed by the migrator.
+- File: `database/drafts/20260302_orders_schema_v1.sql`
+- Impact: Unclear whether this represents schema that has not yet been migrated or schema that was migrated by other means. Creates confusion about source of truth.
+- Fix approach: Either move to `database/migrations/` with a proper sequence number, or delete it if already superseded.
+
+---
+
+## Known Bugs
+
+### [HIGH] ZPL Label Page Size: Dead Conditional `'ZPL' ? 'A6' : 'A6'`
+
+- Issue: In `AllegroShipmentService::downloadLabel()`, the page size for label requests is determined by: `$pageSize = $labelFormat === 'ZPL' ? 'A6' : 'A6'`. Both branches return `'A6'` — the ZPL branch should return `'A7'` (or the appropriate ZPL page size constant).
+- File: `src/Modules/Shipments/AllegroShipmentService.php` (line 255)
+- Impact: ZPL labels are always requested with page size A6, which is incorrect for ZPL/thermal printer format.
+- Fix approach: Determine the correct Allegro API page size constant for ZPL (check Allegro documentation) and fix the ternary.
+
+---
+
+### [MEDIUM] `AllegroStatusSyncService::findOrdersNeedingStatusSync()` — No Time-Based Cursor
+
+- Issue: The query selects the 50 most recently updated Allegro orders that are not in a final status, ordered by `source_updated_at DESC`. There is no cursor or marker for what was last checked. On every cron run, the same 50 active orders are returned and re-imported, even if no status change occurred.
+- File: `src/Modules/Settings/AllegroStatusSyncService.php` (lines 82–102)
+- Impact: Unnecessary API calls on every cron tick. For a shop with many active orders, the same orders are re-imported over and over.
+- Fix approach: Add a `last_status_checked_at` column to `orders` or use a cron cursor table. Only re-check orders where `source_updated_at > last_status_checked_at`.
+
+---
+
+## Test Coverage Gaps
+
+### [HIGH] No Integration or Functional Tests Exist
+
+- Issue: The `tests/` directory contains only `bootstrap.php`. No test files exist. The `archive/` directory contains unit tests from a previous reset, but they are not in active use.
+- Files: `tests/bootstrap.php`, `archive/2026-03-02_users-only-reset/tests/`
+- Risk: Zero automated coverage for any critical path: order import, shipment creation, OAuth flow, cron sync, status updates.
+- Priority: High
+
+---
+
+### [HIGH] Allegro OAuth Token Refresh Logic Has No Tests
+
+- Issue: The token refresh logic is both duplicated (4 copies) and untested. It includes complex edge cases: token expiry within 5-minute window, empty refresh token fallback, write-then-re-read pattern.
+- Files: See "Duplicated OAuth Token Refresh Logic" section above.
+- Risk: Token refresh failures cause complete import failure. Silent breakage possible if one copy diverges.
+- Priority: High
+
+---
+
+### [MEDIUM] No Error Path Tests for Order Import
+
+- Issue: `AllegroOrderImportService::importSingleOrder()` and `ShopproOrdersSyncService::sync()` have complex error handling (401 retry, silent Throwable catch blocks). None of this is tested.
+- Files:
+ - `src/Modules/Settings/AllegroOrderImportService.php`
+ - `src/Modules/Settings/ShopproOrdersSyncService.php`
+- Risk: Error paths that swallow exceptions silently (multiple `catch (Throwable) {}` blocks in `OrdersRepository`) may hide data integrity issues.
+- Priority: Medium
+
+---
+
+## Fragile Areas
+
+### [HIGH] `AllegroStatusSyncService` — `orderpro_to_allegro` Silent No-Op
+
+- Files: `src/Modules/Settings/AllegroStatusSyncService.php`
+- Why fragile: Returns `ok: true` when direction is `orderpro_to_allegro` with zero processing, no log, no user feedback. Cron logs will show "success" even though nothing happened.
+- Safe modification: Do not add code after the early return without understanding why it was left as a no-op. Consider returning `ok: false` or a warning until the feature is implemented.
+- Test coverage: None.
+
+---
+
+### [MEDIUM] Web Cron Throttling Uses Session Timestamp (`$_SESSION['cron_web_last_run_at']`)
+
+- Issue: `Application::isWebCronThrottled()` reads and writes a cron timestamp from `$_SESSION`. In stateless or load-balanced deployments, different users hit different sessions, meaning the throttle is per-session (per-user), not global.
+- File: `src/Core/Application.php` (lines 363–375)
+- Why fragile: With two active sessions, the cron may run twice within the throttle window. The DB-level `GET_LOCK` is the real protection, but this creates confusing interactions.
+- Fix approach: Move last-run timestamp to the database (e.g., an `app_settings` key) instead of the session.
+
+---
+
+*Concerns audit: 2026-03-12*
diff --git a/.paul/codebase/CONVENTIONS.md b/.paul/codebase/CONVENTIONS.md
new file mode 100644
index 0000000..40ae074
--- /dev/null
+++ b/.paul/codebase/CONVENTIONS.md
@@ -0,0 +1,603 @@
+# Coding Conventions
+
+**Analysis Date:** 2026-03-12
+
+---
+
+## 1. Documented Rules (AGENTS.md / CLAUDE.md)
+
+These rules are canonical and enforced across the project:
+
+- Code must be readable by a stranger — clear names, no magic
+- No logic in views, no copy-paste, no random helpers without coherent design
+- Each function/class has **one responsibility**, typically 30–50 lines; split if longer
+- Max **3 levels of nesting** (if/foreach); move deeper logic to separate methods
+- Comments only where they explain **why**, never **what**
+- DB: Medoo is mentioned in docs but the codebase uses **raw PDO** with prepared statements — no string-concatenated SQL for user input
+- XSS: escape all output in views with the `e()` helper
+- CSRF token required on all POST forms, validated via `Csrf::validate()`
+- All styles live in `resources/scss/`, never inline in view files
+- Never use native `alert()` / `confirm()` — use `window.OrderProAlerts.confirm()`
+- Reusable UI blocks go in `resources/views/components/`
+
+---
+
+## 2. Naming Conventions
+
+### PHP
+
+| Element | Convention | Example |
+|---|---|---|
+| Classes | PascalCase | `OrdersRepository`, `ShipmentProviderRegistry` |
+| Methods | camelCase | `findDetails()`, `updateOrderStatus()` |
+| Variables | camelCase | `$orderId`, `$statusLabelMap` |
+| Constants | UPPER_SNAKE_CASE | `SESSION_KEY` (in `Csrf`) |
+| Namespaces | PascalCase, PSR-4 | `App\Modules\Orders`, `App\Core\Http` |
+| Interfaces | PascalCase + `Interface` suffix | `ShipmentProviderInterface` |
+| Test classes | PascalCase + `Test` suffix | `CronJobTypeTest` |
+
+No abbreviations in names. `$orderId` not `$oid`. Loop variables in short 2–3 line loops are the only exception.
+
+### Files
+
+- One class per file, filename matches class name exactly
+- `PascalCase.php` for all PHP class files
+- `kebab-case` or `snake_case` not used in PHP
+
+### CSS / SCSS
+
+- BEM-style class naming: `block__element--modifier`
+- Examples: `.sidebar__brand`, `.orders-ref__meta`, `.btn--primary`, `.is-active`
+- State modifier classes: `.is-active`, `.is-collapsed`, `.is-open`, `.is-disabled`
+- JavaScript hook classes: `.js-` prefix (e.g. `.js-sidebar-collapse`, `.js-filter-toggle-btn`)
+
+---
+
+## 3. PHP Code Style
+
+### Strict Types
+
+Every PHP file starts with:
+```php
+>, total:int, page:int, per_page:int, error:string}
+ */
+public function paginate(array $filters): array
+```
+
+### Defensive Casting
+
+All values from external sources (request input, DB rows) are cast explicitly before use:
+```php
+$orderId = max(0, (int) $request->input('id', 0));
+$search = trim((string) ($filters['search'] ?? ''));
+$totalPaid = $row['total_paid'] !== null ? (float) $row['total_paid'] : null;
+```
+
+---
+
+## 4. Design Patterns
+
+### Service Layer Pattern
+
+Business logic lives in dedicated `*Service` classes, not controllers or repositories:
+- `src/Modules/Settings/AllegroOrderImportService.php` — order import logic
+- `src/Modules/Settings/ShopproStatusSyncService.php` — status sync logic
+- `src/Modules/Settings/ShopproPaymentStatusSyncService.php`
+
+Controllers delegate to services, which delegate to repositories.
+
+### Repository Pattern
+
+All database access goes through `*Repository` classes. No direct PDO calls in controllers or services beyond what repositories expose:
+- `src/Modules/Orders/OrdersRepository.php`
+- `src/Modules/Settings/AllegroIntegrationRepository.php`
+- `src/Modules/Shipments/ShipmentPackageRepository.php`
+
+### Interface + Registry Pattern
+
+Provider integrations use an interface + registry:
+```php
+// src/Modules/Shipments/ShipmentProviderInterface.php
+interface ShipmentProviderInterface
+{
+ public function code(): string;
+ public function createShipment(int $orderId, array $formData): array;
+ // ...
+}
+
+// src/Modules/Shipments/ShipmentProviderRegistry.php
+final class ShipmentProviderRegistry
+{
+ public function get(string $code): ?ShipmentProviderInterface { ... }
+ public function all(): array { ... }
+}
+```
+Registered at boot time in `routes/web.php`:
+```php
+$shipmentProviderRegistry = new ShipmentProviderRegistry([
+ $shipmentService, // AllegroShipmentService
+ $apaczkaShipmentService,
+]);
+```
+
+### Cron Handler Pattern
+
+Cron jobs follow a handler contract. Each job type has a dedicated handler class with a `handle(array $payload): array` method:
+- `src/Modules/Cron/AllegroOrdersImportHandler.php`
+- `src/Modules/Cron/ShopproStatusSyncHandler.php`
+- `src/Modules/Cron/AllegroTokenRefreshHandler.php`
+
+Handlers are registered as a string-keyed array in `CronRunner`:
+```php
+$runner = new CronRunner($repository, $logger, [
+ 'allegro_orders_import' => new AllegroOrdersImportHandler($ordersSyncService),
+ 'shoppro_order_status_sync' => new ShopproStatusSyncHandler($shopproStatusSyncService),
+]);
+```
+
+### Manual Dependency Injection (No Container)
+
+There is no DI container. All dependencies are wired manually in `routes/web.php` and `Application.php`. The Application class acts as a **Service Locator** for top-level objects (`$app->db()`, `$app->orders()`, `$app->logger()`), but controllers and services receive dependencies through constructor injection.
+
+### Middleware Pipeline
+
+Middleware uses a `handle(Request $request, callable $next): Response` or `__invoke` signature. The router builds a pipeline with `array_reduce`:
+```php
+// src/Core/Routing/Router.php
+$pipeline = array_reduce(
+ array_reverse($middlewares),
+ fn (callable $next, callable|object $middleware): callable => $this->wrapMiddleware($middleware, $next),
+ fn (Request $req): mixed => $handler($req)
+);
+```
+
+---
+
+## 5. Database Access
+
+### Driver
+
+Raw **PDO** with MySQL (`src/Core/Database/ConnectionFactory.php`). **No ORM**, no query builder (Medoo is referenced in docs but not in actual code). PDO is configured with:
+```php
+PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+PDO::ATTR_EMULATE_PREPARES => false,
+```
+
+### Prepared Statements — Mandatory
+
+**All** user-supplied data goes through prepared statements with named parameters:
+```php
+$stmt = $this->pdo->prepare(
+ 'UPDATE orders SET external_status_id = :status, updated_at = NOW() WHERE id = :id'
+);
+$stmt->execute(['status' => $newStatusCode, 'id' => $orderId]);
+```
+
+For `IN (...)` clauses with a dynamic list, positional `?` placeholders are used and values are bound with `bindValue`:
+```php
+$placeholders = implode(',', array_fill(0, count($cleanIds), '?'));
+$stmt = $this->pdo->prepare('SELECT ... WHERE oi.order_id IN (' . $placeholders . ')');
+foreach ($cleanIds as $index => $orderId) {
+ $stmt->bindValue($index + 1, $orderId, PDO::PARAM_INT);
+}
+```
+
+Dynamic `ORDER BY` columns are resolved with `match` expressions against a hard-coded allowlist — never interpolated from user input:
+```php
+$sortColumn = match ($sort) {
+ 'source_order_id' => 'o.source_order_id',
+ 'total_with_tax' => 'o.total_with_tax',
+ default => $effectiveOrderedAtSql,
+};
+```
+
+### SQL Style
+
+Multi-line SQL strings use string concatenation only for safe, code-generated SQL fragments (not user data). Named SQL helper methods extract reusable SQL snippets:
+```php
+private function effectiveStatusSql(string $orderAlias, string $mappingAlias): string
+{
+ return 'CASE WHEN ' . $orderAlias . '.source = "allegro" AND ...';
+}
+
+private function effectiveOrderedAtSql(string $orderAlias): string
+{
+ return 'COALESCE(' . $orderAlias . '.ordered_at, ' . $orderAlias . '.source_created_at, ...)';
+}
+```
+
+### Error Handling in DB Layer
+
+Repository methods catch `Throwable` and return safe defaults (empty array, `null`, or `false`) rather than letting exceptions propagate to the controller:
+```php
+try {
+ $rows = $this->pdo->query('SELECT ...')->fetchAll(PDO::FETCH_COLUMN);
+} catch (Throwable) {
+ return [];
+}
+```
+
+---
+
+## 6. Frontend Patterns
+
+### View System
+
+Plain PHP views rendered via `src/Core/View/Template.php`. The `render()` method:
+1. Renders the view file with `extract($data)` — data keys become local variables
+2. Wraps it in a layout via a second `renderFile()` call, injecting `$content`
+
+```php
+$html = $this->template->render('orders/list', $data, 'layouts/app');
+```
+
+Two helpers are injected into every view scope:
+```php
+$e = static fn (mixed $value): string => htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
+$t = static fn (string $key, array $replace = []): string => $translator->get($key, $replace);
+```
+
+**Always use `$e()` for output.** Use `= (string) $rawHtml ?>` (raw echo) only when the value is pre-escaped HTML built in the controller.
+
+### View Structure
+
+```
+resources/views/
+├── layouts/
+│ ├── app.php # Main authenticated layout (sidebar + topbar)
+│ └── auth.php # Login layout
+├── components/
+│ ├── table-list.php # Generic paginated table with filters, sorting, column toggle
+│ └── order-status-panel.php
+├── orders/
+│ ├── list.php
+│ └── show.php
+├── settings/
+│ └── *.php # One view per settings section
+└── shipments/
+ └── prepare.php
+```
+
+Views are **data receivers**, not logic writers. All HTML fragment generation for complex cells (status badges, product previews) happens in controller methods and is passed as pre-built HTML strings with `raw: true` in the table config.
+
+### Reusable Components
+
+The `resources/views/components/table-list.php` component accepts a `$tableList` config array and renders a full paginated table with filters, column toggles, and action forms. Include it with:
+```php
+resolve('components/table-list') ?>
+// or via the view data key 'tableList'
+```
+
+The component reads its own config defensively:
+```php
+$config = is_array($tableList ?? null) ? $tableList : [];
+$rows = is_array($config['rows'] ?? null) ? $config['rows'] : [];
+```
+
+### SCSS / CSS
+
+Source: `resources/scss/`
+Compiled output: `public/assets/css/`
+Build command: `npm run build:assets` (uses Dart Sass)
+
+```
+resources/scss/
+├── app.scss # Main panel stylesheet
+├── login.scss # Auth page stylesheet
+└── shared/
+ └── _ui-components.scss # Shared UI primitives (imported with @use)
+```
+
+**Never put styles in view files.** All styles go in SCSS. Cache-busting on the compiled CSS is done via `filemtime()` in the layout:
+```php
+
+```
+
+### JavaScript
+
+No bundler, no TypeScript. Vanilla JavaScript only (ES5 style with `var` in components, ES6 `const/let` allowed in module files).
+
+**Module pattern:** All JS in views is wrapped in an IIFE to avoid global scope pollution:
+```js
+(function() {
+ // component code
+})();
+```
+
+**UI Alerts and Confirmations:** The custom `window.OrderProAlerts` module (built from `resources/modules/jquery-alerts/`) is used for all confirmations. Never use `window.confirm()` directly. The pattern in table-list:
+```js
+if (window.OrderProAlerts && typeof window.OrderProAlerts.confirm === 'function') {
+ window.OrderProAlerts.confirm({ title, message, ... }).then(function(accepted) {
+ if (!accepted) return;
+ form.setAttribute('data-confirmed', '1');
+ form.submit();
+ });
+}
+```
+
+**DOM targeting:** Use `data-` attributes and `js-` prefixed class names for JS hooks. Never use presentation class names (`.btn--primary`) as JS selectors.
+
+**localStorage:** Filter state, column visibility, and sidebar collapsed state are persisted in `localStorage` with namespaced keys (e.g. `tableList_/orders/list_orders_filters_open`).
+
+---
+
+## 7. Security
+
+### CSRF
+
+All POST forms include a hidden token field:
+```html
+
+```
+
+Controllers validate it before processing:
+```php
+$csrfToken = (string) $request->input('_csrf_token', '');
+if (!Csrf::validate($csrfToken)) {
+ $_SESSION['order_flash_error'] = $this->translator->get('auth.errors.csrf_expired');
+ return Response::redirect('/orders/' . $orderId);
+}
+```
+
+`Csrf::validate()` uses `hash_equals()` to prevent timing attacks (`src/Core/Security/Csrf.php`).
+
+### XSS
+
+Every dynamic value echoed in a view must go through `$e()`:
+```php
+= $e($buyerName) ?>
+= $e($t('navigation.orders')) ?>
+```
+
+Pre-built HTML fragments (e.g. status badge HTML assembled in controller) are output raw and must already be escaped internally using `htmlspecialchars(..., ENT_QUOTES, 'UTF-8')`.
+
+### Secrets Encryption
+
+Integration credentials (API keys, tokens) are encrypted at rest using AES-256-CBC with HMAC-SHA256 authentication (`src/Modules/Settings/IntegrationSecretCipher.php`). The cipher uses a key derived from `INTEGRATIONS_SECRET` env var. Encrypted values are prefixed with `v1:`.
+
+---
+
+## 8. Error Handling
+
+### Global Handler
+
+Unhandled exceptions are caught in `Application::registerErrorHandlers()`:
+```php
+set_exception_handler(function (Throwable $exception) use ($debug): void {
+ $this->logger->error('Unhandled exception', [
+ 'message' => $exception->getMessage(),
+ 'file' => $exception->getFile(),
+ 'line' => $exception->getLine(),
+ ]);
+
+ $message = $debug ? $exception->getMessage() : 'Internal server error';
+ Response::html($message, 500)->send();
+});
+```
+
+### Repository Layer
+
+Repositories return safe fallback values on error and never expose raw exception messages to callers:
+```php
+try {
+ // ...
+} catch (Throwable $exception) {
+ return [
+ 'items' => [],
+ 'total' => 0,
+ 'error' => $exception->getMessage(), // returned in the data, not thrown
+ ];
+}
+```
+
+For methods returning `bool`, failure is signaled by returning `false`:
+```php
+public function updateOrderStatus(...): bool
+{
+ try { /* ... */ return true; }
+ catch (Throwable) { return false; }
+}
+```
+
+For methods returning `?array`, failure returns `null`.
+
+### Cron Layer
+
+The `CronRunner` catches per-job exceptions, logs them, marks the job as failed, and continues processing the next job:
+```php
+} catch (Throwable $exception) {
+ $this->repository->markJobFailed($jobId, $exception->getMessage(), ...);
+ $this->logger->error('Cron job failed', [
+ 'job_id' => $jobId,
+ 'job_type' => $jobType,
+ 'error' => $exception->getMessage(),
+ ]);
+ $failed++;
+}
+```
+
+### Logger
+
+`src/Core/Support/Logger.php` — custom file logger writing to `storage/logs/`. Format:
+```
+[2026-03-12 14:22:01] ERROR Cron job failed {"job_id":42,"job_type":"allegro_orders_import","error":"..."}
+```
+
+Methods: `$logger->error(string $message, array $context = [])` and `$logger->info(...)`. Context is encoded as JSON on the same line. Log path is configured via `app.log_path` in `config/app.php`.
+
+---
+
+## 9. Testing
+
+### Framework
+
+PHPUnit 11.x. Config: `phpunit.xml` at project root.
+
+```bash
+composer test # vendor/bin/phpunit -c phpunit.xml --testdox
+```
+
+Settings:
+- `failOnWarning="true"` — warnings are test failures
+- `failOnRisky="true"` — risky tests fail
+- `executionOrder="depends,defects"` — dependency-aware ordering
+
+### Test File Location
+
+Tests live in `tests/` (PSR-4 namespace `Tests\`), mirroring the `src/` structure:
+```
+tests/
+├── bootstrap.php
+└── Unit/
+ ├── Cron/
+ │ └── CronJobTypeTest.php
+ └── Settings/
+ └── OrderStatusMappingRepositoryTest.php
+```
+
+**Note:** The active `tests/` directory currently contains only `bootstrap.php`. The working test files exist in `archive/2026-03-02_users-only-reset/tests/` — these are the reference patterns to follow when adding new tests.
+
+### Test Class Pattern
+
+```php
+pdo = new PDO('sqlite::memory:');
+ $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+ $this->pdo->exec('CREATE TABLE ...'); // schema DDL
+ $this->repository = new OrderStatusMappingRepository($this->pdo);
+ }
+
+ public function testReplaceAndReadMappingsForIntegration(): void
+ {
+ $this->repository->replaceForIntegration(10, [...]);
+ $rows = $this->repository->listByIntegration(10);
+ self::assertArrayHasKey('new', $rows);
+ self::assertSame('completed', $rows['paid']['orderpro_status_code']);
+ }
+}
+```
+
+Key patterns:
+- Use `#[CoversClass(ClassName::class)]` PHP 8 attribute on every test class
+- SQLite in-memory database for repository tests (no real DB connection needed)
+- Test method names start with `test` and describe behavior: `testReplaceAndReadMappingsForIntegration`
+- Use `self::assertSame()` (strict) not `assertEquals()`
+- Classes and methods are `final`
+- No mocking framework observed — tests use real classes with SQLite in-memory
+
+### What to Test
+
+Repository tests test read/write behavior against an in-memory SQLite DB. Value object / enum tests verify constants and helper methods. No integration tests against the real MySQL database.
+
+---
+
+## 10. Code Quality Tools
+
+### SonarQube
+
+Configured via `sonar-project.properties`:
+```
+sonar.sources=src,resources/views,routes
+sonar.tests=tests
+sonar.exclusions=archive/**,node_modules/**,vendor/**,public/assets/**,storage/**
+sonar.php.version=8.1
+```
+Runs against an external SonarQube server at `https://sonar.project-pro.pl`.
+
+### PHPUnit (Coverage Source)
+
+`phpunit.xml` configures `src/` as the source for coverage analysis.
+
+### No Local Static Analysis Config
+
+No `phpstan.neon`, `.phpcs.xml`, or `psalm.xml` are present. SonarQube is the sole static analysis tool. PHP_CS_Fixer and PHPStan are not in `composer.json`.
+
+---
+
+## 11. Import / Use Statement Organization
+
+Use statements are grouped and ordered:
+1. Internal `App\Core\*` namespaces
+2. Internal `App\Modules\*` namespaces
+3. PHP built-in classes (`PDO`, `DateTimeImmutable`, `RuntimeException`, `Throwable`)
+
+Each `use` on its own line. No `use` grouping with `{}`.
+
+---
+
+## 12. Configuration
+
+App config is split by concern in `config/`:
+- `config/app.php` — application settings (paths, locale, session, logging, cron)
+- `config/database.php` — DB connection parameters
+
+The `Application::config(string $key, mixed $default)` method resolves dot-notation keys:
+```php
+$app->config('app.integrations.secret', '')
+$app->config('database.host')
+```
+
+Environment variables are loaded via `src/Core/Support/Env.php` and read in config files.
+
+---
+
+*Convention analysis: 2026-03-12*
diff --git a/.paul/codebase/INDEX.md b/.paul/codebase/INDEX.md
new file mode 100644
index 0000000..cf84834
--- /dev/null
+++ b/.paul/codebase/INDEX.md
@@ -0,0 +1,73 @@
+# Codebase Map — orderPRO
+
+> Generated: 2026-03-12
+> Branch: main
+
+## What is this project?
+
+**orderPRO** is a custom PHP 8.4 order management system (OMS) that aggregates and manages orders from multiple sales channels (Allegro marketplace, shopPRO e-commerce platform). It handles order import, status synchronization, shipment management, and product data enrichment. The system is entirely self-contained — no Composer runtime dependencies — built on a hand-rolled micro-framework.
+
+---
+
+## Documents
+
+| Document | Contents |
+|----------|----------|
+| [STACK.md](./STACK.md) | Language, framework, DB, integrations, build tools, dependencies |
+| [ARCHITECTURE.md](./ARCHITECTURE.md) | Directory layout, layers, request flow, domain model, all 24 DB tables |
+| [CONVENTIONS.md](./CONVENTIONS.md) | Naming rules, design patterns, DB access style, frontend conventions, testing |
+| [CONCERNS.md](./CONCERNS.md) | Tech debt, bugs, security gaps, incomplete features — with severity levels |
+
+---
+
+## Quick Reference
+
+### Stack
+- **PHP 8.4**, zero Composer runtime deps, hand-rolled framework in `src/Core/`
+- **MySQL** via raw PDO prepared statements (no ORM)
+- **Frontend**: plain PHP views, SCSS → CSS via Dart Sass, jQuery + custom alert module
+- **Tests**: PHPUnit 11 (currently zero active test coverage)
+
+### External Integrations
+| Integration | Purpose |
+|-------------|---------|
+| **Allegro** | OAuth 2.0, order import, status sync, shipment management |
+| **shopPRO** | API key auth, order/product import |
+| **Apaczka** | HMAC-signed REST, multi-carrier shipping aggregator |
+| **InPost ShipX** | Bearer token REST, parcel lockers (settings only — shipments route via Allegro WZA workaround) |
+| **GS1** | Barcode/product data (bin scripts) |
+
+### Architecture in One Sentence
+`public/index.php` → `Application` → `Router` → middleware pipeline → controller (in `src/Modules/`) → service → repository (raw PDO) → plain PHP view.
+
+### Top Concerns to Address
+1. **OAuth refresh duplicated** across 4 Allegro services → extract `AllegroTokenRefreshService`
+2. **Utility methods duplicated** 15+ times across repositories → shared trait or utility class
+3. **Zero test coverage** — `tests/` directory is empty
+4. **Known bug**: `AllegroShipmentService.php:255` — ZPL branch always returns `'A6'` (dead conditional)
+5. **InPost shipment provider stub** — settings exist, no `ShipmentProviderInterface` implementation
+6. **No `CURLOPT_SSL_VERIFYPEER`** set in any cURL client — implicit trust of TLS certs
+
+---
+
+## Module Overview
+
+| Module | Path | Responsibility |
+|--------|------|----------------|
+| Auth | `src/Modules/Auth/` | Login, session management |
+| Orders | `src/Modules/Orders/` | Order list, detail, filters, status updates |
+| Settings | `src/Modules/Settings/` | Integration config, carrier mapping, status groups |
+| Shipments | `src/Modules/Shipments/` | Shipment creation, label printing, provider dispatch |
+| Cron | `src/Modules/Cron/` | Cron job runner, schedule management |
+| Users | `src/Modules/Users/` | User management |
+
+---
+
+## Entry Points
+
+| Entry | Path |
+|-------|------|
+| Web app | `public/index.php` |
+| Route definitions | `routes/web.php` |
+| CLI scripts | `bin/` (8 scripts) |
+| DB migrations | `database/migrations/` |
diff --git a/.paul/codebase/STACK.md b/.paul/codebase/STACK.md
new file mode 100644
index 0000000..b4d965f
--- /dev/null
+++ b/.paul/codebase/STACK.md
@@ -0,0 +1,286 @@
+# Technology Stack
+
+**Analysis Date:** 2026-03-12
+
+---
+
+## Languages
+
+**Primary:**
+- PHP 8.4 — all backend logic, routing, controllers, repositories, CLI scripts
+- SQL — raw migration files, all database queries via PDO prepared statements
+
+**Secondary:**
+- SCSS — compiled to CSS for frontend styling (`resources/scss/`)
+- JavaScript — custom jQuery-based UI alert/confirm module (`resources/modules/jquery-alerts/`)
+
+---
+
+## Runtime & Server
+
+**PHP:**
+- Version: `^8.4` (strict, declared via `declare(strict_types=1)` in every file)
+- Extensions required: `pdo_mysql`, `openssl`, `curl`, `hash`, `mbstring`, `session`
+- Dev server: `php -S localhost:8000 -t public public/index.php` (via `composer serve`)
+- Production: any standard PHP-FPM + Apache/Nginx setup targeting `public/` as document root
+
+**Entry point:**
+- `public/index.php` — bootstraps the application
+- `index.php` (root) — likely a redirect or secondary entry
+
+**CLI scripts (in `bin/`):**
+- `bin/migrate.php` — runs database migrations (`composer migrate`)
+- `bin/cron.php` — executes scheduled cron jobs (`composer cron`)
+
+---
+
+## Framework
+
+**No third-party framework.** The application uses a fully custom, hand-rolled framework located in `src/Core/`:
+
+| Component | Class | File |
+|---|---|---|
+| Application container | `App\Core\Application` | `src/Core/Application.php` |
+| HTTP Router | `App\Core\Routing\Router` | `src/Core/Routing/Router.php` |
+| HTTP Request | `App\Core\Http\Request` | `src/Core/Http/Request.php` |
+| HTTP Response | `App\Core\Http\Response` | `src/Core/Http/Response.php` |
+| Template engine | `App\Core\View\Template` | `src/Core/View/Template.php` |
+| Database factory | `App\Core\Database\ConnectionFactory` | `src/Core/Database/ConnectionFactory.php` |
+| Migrator | `App\Core\Database\Migrator` | `src/Core/Database/Migrator.php` |
+| Session | `App\Core\Support\Session` | `src/Core/Support/Session.php` |
+| Logger | `App\Core\Support\Logger` | `src/Core/Support/Logger.php` |
+| CSRF protection | `App\Core\Security\Csrf` | `src/Core/Security/Csrf.php` |
+| Translator (i18n) | `App\Core\I18n\Translator` | `src/Core/I18n/Translator.php` |
+| Env helper | `App\Core\Support\Env` | `src/Core/Support/Env.php` |
+
+**Routing style:** `GET`/`POST` registration with path-param support (`{id}`) and per-route middleware pipeline. Routes defined in `routes/web.php`.
+
+**Template engine:** PHP native `require` with `ob_start()`/`ob_get_clean()`. Views live in `resources/views/`. Layout support via a second render pass. XSS helper `$e()` exposed to every view via `htmlspecialchars`. Translation helper `$t()` also injected.
+
+**i18n:** Single-locale PHP array file. Currently Polish only: `resources/lang/pl.php`.
+
+---
+
+## Database
+
+**Engine:** MySQL (only supported driver — enforced with `RuntimeException` in `ConnectionFactory`)
+**Default port:** 3306
+**Charset:** `utf8mb4`
+**Client:** PHP PDO with `ERRMODE_EXCEPTION`, `FETCH_ASSOC`, `EMULATE_PREPARES = false`
+**ORM:** None — raw PDO prepared statements everywhere
+
+**Migrations:**
+- Plain SQL files in `database/migrations/`, named `YYYYMMDD_NNNNNN_description.sql`
+- Run via `bin/migrate.php` / `composer migrate`
+- 46 migrations present (as of analysis date), dating from 2026-02-21
+
+**Config file:** `config/database.php`
+
+---
+
+## Package Manager & Build Tools
+
+**PHP dependencies (Composer):**
+- `composer.json` — PSR-4 autoload: `App\` → `src/`, `Tests\` → `tests/`
+- No runtime Composer packages — zero third-party PHP libraries
+- Dev only: `phpunit/phpunit ^11.5`
+- Lockfile: `composer.lock` (present)
+
+**Node / Frontend (npm):**
+- `package.json` — `devDependencies` only
+- `sass ^1.97.3` — SCSS compiler (Dart Sass CLI)
+- Lockfile: `package-lock.json` (present)
+
+**Build commands (npm scripts):**
+
+```bash
+npm run build:css # Compile app.scss + login.scss → public/assets/css/
+npm run build:modules # Compile jquery-alerts.scss + copy jquery-alerts.js → public/assets/
+npm run build:assets # Full build (build:css + build:modules)
+npm run watch:css # Watch mode for SCSS development
+```
+
+**Output paths:**
+- `public/assets/css/app.css` — main application styles
+- `public/assets/css/login.css` — login page styles
+- `public/assets/css/modules/jquery-alerts.css` — alert/confirm modal styles
+- `public/assets/js/modules/jquery-alerts.js` — alert/confirm module JS
+
+**SCSS sources:**
+- `resources/scss/app.scss` — main stylesheet
+- `resources/scss/login.scss` — login page stylesheet
+- `resources/scss/shared/_ui-components.scss` — shared component partials
+- `resources/modules/jquery-alerts/jquery-alerts.scss` — module source
+
+---
+
+## Testing
+
+**Framework:** PHPUnit `^11.5`
+**Config:** `phpunit.xml`
+**Run command:** `composer test` → `vendor/bin/phpunit -c phpunit.xml --testdox`
+**Test root:** `tests/` (`Tests\` namespace)
+**Bootstrap:** `tests/bootstrap.php`
+**Cache dir:** `storage/cache/phpunit`
+**Coverage source:** `src/`
+
+---
+
+## Security
+
+**CSRF:** Session-based token via `App\Core\Security\Csrf` (`src/Core/Security/Csrf.php`) — `hash_equals()` comparison
+
+**Credential encryption (integration API keys):** AES-256-CBC + HMAC-SHA256 via `App\Modules\Settings\IntegrationSecretCipher` (`src/Modules/Settings/IntegrationSecretCipher.php`)
+- Encryption key and HMAC key are both derived from `INTEGRATIONS_SECRET` env var using `hash('sha256', 'enc|...')` / `hash('sha256', 'auth|...')`
+- Encrypted values stored in DB as `v1:` format
+
+**XSS:** `htmlspecialchars()` via `$e()` helper injected into every view by `Template::renderFile()`
+
+**SQL injection:** PDO prepared statements enforced throughout all repositories
+
+---
+
+## External Integrations
+
+### Allegro (Polish marketplace)
+
+- **OAuth 2.0 client:** `App\Modules\Settings\AllegroOAuthClient` (`src/Modules/Settings/AllegroOAuthClient.php`)
+ - Authorization code + refresh token flow
+ - Environments: `production` (`https://allegro.pl`) and `sandbox` (`https://allegro.pl.allegrosandbox.pl`)
+ - Scopes: `allegro:api:orders:read`, `allegro:api:sale:offers:read`, `allegro:api:shipments:read`, `allegro:api:shipments:write`
+- **REST API client:** `App\Modules\Settings\AllegroApiClient` (`src/Modules/Settings/AllegroApiClient.php`)
+ - Endpoints used: checkout forms (order list/detail), shipments, delivery services, product offers, shipment labels (binary PDF)
+ - Accept header: `application/vnd.allegro.public.v1+json`
+ - HTTP transport: native PHP `curl`
+- **Sync services:**
+ - `AllegroOrdersSyncService` — order import from Allegro
+ - `AllegroStatusSyncService` — order status synchronization
+ - `AllegroOrderImportService` — individual order import
+- **Cron handlers:** `AllegroOrdersImportHandler`, `AllegroStatusSyncHandler`, `AllegroTokenRefreshHandler`
+- **Credentials stored:** in DB `integrations` table, API key encrypted with `IntegrationSecretCipher`
+
+### shopPRO (Polish e-commerce platform)
+
+- **REST API client:** `App\Modules\Settings\ShopproApiClient` (`src/Modules/Settings/ShopproApiClient.php`)
+ - Endpoint pattern: `{baseUrl}/api.php?endpoint=orders&action=list`
+ - Auth: `X-Api-Key` HTTP header
+ - Supports: order list (paginated), order by ID, product by ID
+- **Sync services:**
+ - `ShopproOrdersSyncService` — order import from shopPRO
+ - `ShopproStatusSyncService` — order status sync
+ - `ShopproPaymentStatusSyncService` — payment status sync
+- **Cron handlers:** `ShopproOrdersImportHandler`, `ShopproStatusSyncHandler`, `ShopproPaymentStatusSyncHandler`
+
+### Apaczka (Polish shipping aggregator)
+
+- **REST API client:** `App\Modules\Settings\ApaczkaApiClient` (`src/Modules/Settings/ApaczkaApiClient.php`)
+ - Base URL: `https://www.apaczka.pl/api/v2`
+ - Auth: HMAC-SHA256 signature (`app_id:route:requestJson:expires` + `appSecret`)
+ - Signature variant fallback: tries multiple formats to handle Apaczka API quirks
+ - Endpoints: `service_structure`, `order_send`, `order/{id}`, `waybill/{id}`, `points/{type}`
+- **Shipment provider:** `App\Modules\Shipments\ApaczkaShipmentService` (`src/Modules/Shipments/ApaczkaShipmentService.php`)
+- **Settings repo:** `ApaczkaIntegrationRepository`, `ApaczkaIntegrationController`
+
+### InPost ShipX
+
+- **API base URL:** `https://api-shipx-pl.easypack24.net`
+- **Auth:** Bearer token (stored encrypted in DB)
+- **Settings repo:** `App\Modules\Settings\InpostIntegrationRepository` (`src/Modules/Settings/InpostIntegrationRepository.php`)
+- **Shipment provider:** `App\Modules\Shipments\AllegroShipmentService` also covers InPost services through Allegro's shipment management API
+- **Config stored:** `inpost_integration_settings` DB table
+- **Features:** sandbox/production toggle, dispatch method, locker size, dimensions, label format (PDF/ZPL/EPL), weekend delivery, multi-parcel
+
+### GS1 (product barcode data)
+
+- Referenced in `bin/test_gs1_api.php` and `bin/fix_gs1_brand.php`
+- Settings stored in `app_settings` table (GS1 API key, GLN, etc.)
+
+---
+
+## Shipment Provider Abstraction
+
+Interface: `App\Modules\Shipments\ShipmentProviderInterface` (`src/Modules/Shipments/ShipmentProviderInterface.php`)
+
+Methods:
+- `code(): string`
+- `getDeliveryServices(): array`
+- `createShipment(int $orderId, array $formData): array`
+- `checkCreationStatus(int $packageId): array`
+- `downloadLabel(int $packageId, string $storagePath): array`
+
+Registry: `App\Modules\Shipments\ShipmentProviderRegistry` (`src/Modules/Shipments/ShipmentProviderRegistry.php`)
+
+Implementations:
+- `AllegroShipmentService` — creates shipments via Allegro shipment management API
+- `ApaczkaShipmentService` — creates shipments via Apaczka API
+
+---
+
+## Cron System
+
+**Runner:** `App\Modules\Cron\CronRunner` (`src/Modules/Cron/CronRunner.php`)
+**Repository:** `App\Modules\Cron\CronRepository` (`src/Modules/Cron/CronRepository.php`)
+**CLI entry:** `bin/cron.php`
+**Web trigger (optional):** Cron can run on each web request (throttled, DB-locked) when `CRON_RUN_ON_WEB=true`
+
+**Registered jobs:**
+
+| Job key | Handler | Purpose |
+|---|---|---|
+| `allegro_token_refresh` | `AllegroTokenRefreshHandler` | Refresh Allegro OAuth tokens |
+| `allegro_orders_import` | `AllegroOrdersImportHandler` | Import orders from Allegro |
+| `allegro_status_sync` | `AllegroStatusSyncHandler` | Sync order statuses to Allegro |
+| `shoppro_orders_import` | `ShopproOrdersImportHandler` | Import orders from shopPRO |
+| `shoppro_order_status_sync` | `ShopproStatusSyncHandler` | Sync order statuses to shopPRO |
+| `shoppro_payment_status_sync` | `ShopproPaymentStatusSyncHandler` | Sync payment statuses from shopPRO |
+
+---
+
+## Environment Configuration
+
+**File:** `.env` (not committed) — see `.env.example` for all required vars
+
+| Variable | Purpose | Default |
+|---|---|---|
+| `APP_NAME` | Application display name | `orderPRO` |
+| `APP_ENV` | Environment (`local`/`production`) | `local` |
+| `APP_DEBUG` | Debug mode (display errors) | `true` |
+| `APP_URL` | Application base URL | `http://localhost:8000` |
+| `SESSION_NAME` | PHP session cookie name | `orderpro_session` |
+| `INTEGRATIONS_SECRET` | Master key for encrypting API credentials | *(required, no default)* |
+| `CRON_RUN_ON_WEB` | Enable web-triggered cron | `false` |
+| `CRON_WEB_LIMIT` | Max cron jobs per web request | `5` |
+| `DB_CONNECTION` | DB driver (only `mysql` supported) | `mysql` |
+| `DB_HOST` | MySQL host (runtime) | `127.0.0.1` |
+| `DB_HOST_REMOTE` | MySQL host for agent/migration use only — **not for runtime** | *(empty)* |
+| `DB_PORT` | MySQL port | `3306` |
+| `DB_DATABASE` | Database name | `orderpro` |
+| `DB_USERNAME` | Database user | `root` |
+| `DB_PASSWORD` | Database password | *(empty)* |
+| `DB_CHARSET` | Connection charset | `utf8mb4` |
+
+**Config files:** `config/app.php`, `config/database.php`
+
+---
+
+## Storage Directories (auto-created at boot)
+
+| Path | Purpose |
+|---|---|
+| `storage/logs/` | Application log file (`app.log`) |
+| `storage/sessions/` | PHP session files |
+| `storage/cache/` | PHPUnit cache |
+| `storage/tmp/` | Temporary files (e.g. label downloads) |
+
+---
+
+## Platform Requirements (Development)
+
+- PHP 8.4+ with extensions: `pdo_mysql`, `openssl`, `curl`, `mbstring`
+- MySQL 5.7+ or MySQL 8.x
+- Node.js + npm (for SCSS compilation only)
+- XAMPP on Windows: `C:\xampp\php\php.exe` (add to `PATH`)
+
+---
+
+*Stack analysis: 2026-03-12*