Files
orderPRO/DOCS/ARCHITECTURE.md

325 lines
25 KiB
Markdown

# 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` | All 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** | 60+ | Integration controllers, OAuth clients, API clients, mappers | Allegro/shopPRO/Erli/Apaczka/InPost 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 `<select multiple data-checkbox-multiselect>` controls after `DOMContentLoaded`.
- Keeps the original select in the form, synchronizes option `selected` state, and preserves native GET/POST names such as `channels[]` and `status_groups[]`.
- Used by `/statistics/orders` and `/statistics/summary` filters to display a compact trigger, checkbox dropdown, "Wszystkie" bulk toggle, and selected count.
- Progressive enhancement: if JavaScript fails, the native multi-select remains visible.
### Statistics Summary Charts (`public/assets/js/modules/statistics-summary-charts.js`)
- Loaded globally from `resources/views/layouts/app.php` after Chart.js 4.4.8 CDN; activates only when `#js-statistics-summary-data` exists.
- Reads JSON produced by `OrdersStatisticsController::summary()` and renders two interactive Chart.js line charts on `/statistics/summary`.
- Chart 1 displays monthly order counts per selected integration plus a `Razem` line.
- Chart 2 displays monthly gross order values per selected integration plus a `Razem` line.
- The PHP view keeps table fallbacks under both charts, so the data remains visible if JavaScript fails.
### Notifications (`public/assets/js/modules/notifications.js`)
- Loaded globally from `resources/views/layouts/app.php`; activates only when the topbar notification button exists.
- Polls `/api/notifications/unread` every 30 seconds and updates the unread badge.
- Requests browser Notification API permission only after user interaction with the notification button.
- Shows native browser notifications for newly seen unread items when permission is granted; click navigates to `target_url`.
## Key Data Flows
### Order Lifecycle
1. **Import** — Cron handler → API client → `OrderImportService``OrdersRepository::insertOrder()``AutomationService::executeForNewOrder()`
2. **Status update**`OrdersController::updateStatus()``OrdersRepository::updateStatus()` → automation check
3. **Status sync** — Cron → `AllegroStatusSyncService` / `ShopproStatusSyncService` / `ErliStatusSyncService` → marketplace API
### Statistics Summary
1. **Request**`/statistics/summary``OrdersStatisticsController::summary()`
2. **Filters** — controller reuses statistics filter semantics: date range, `channels[]`, `status_groups[]`, default status groups excluding cancelled; default history starts at `2026-04-01`.
3. **Aggregation**`OrdersStatisticsRepository::aggregateByMonth()` groups existing `orders` rows by `YYYY-MM` and channel key, using the same effective date/channel/status/gross amount SQL helpers as the daily report.
4. **View model** — controller builds per-integration series and total series for order count and gross value charts.
5. **Render**`resources/views/statistics/summary.php` renders filters, chart JSON, two canvas targets, and table fallbacks.
### Shipment Flow
Phase 130 adds an Erli-specific post-label step: for `orders.source='erli'`, `ShipmentController` calls `ErliExternalShipmentService::syncPackage()` after a local provider has a tracking number. The service posts `vendor/status/trackingNumber/orderId` to Erli `POST /shipping/external` and stores the response in `shipment_packages.payload_json.erli_external_parcel`; failures are activity-log warnings and do not block local labels.
1. **Create**`ShipmentController::create()``ShipmentProviderRegistry` → carrier `ShipmentService::createShipment()``ShipmentPackageRepository::insert()`
2. **Track** — Cron `ShipmentTrackingHandler``ShipmentTrackingRegistry` → carrier tracking API → `ShipmentPackageRepository::updateDeliveryStatus()`
### Receipt / Invoice
1. **Generate**`ReceiptController::store()``ReceiptService::generateReceipt()``ReceiptRepository::insert()` + Dompdf PDF
2. **Email**`EmailSendingService::send()``VariableResolver::resolve()``AttachmentGenerator::generatePdf()` → PHPMailer SMTP
### SMSPLANET Conversation
1. **Settings**`/settings/integrations/smsplanet` stores auth, text sender, `sender_mode`, optional 2WAY `sender_phone`, and optional global `default_footer`.
2. **Outbound from order**`/orders/{id}/sms/send``OrdersController::sendSms()``SmsConversationService::sendFromOrder()` appends `default_footer` when configured, validates the final body against 918 characters, sends through `SmsplanetApiClient::sendSms()`, and stores the final sent body in `sms_messages`.
3. **Inbound webhook** — public `/webhooks/smsplanet/inbound` accepts SMSPLANET 2WAY `POST application/x-www-form-urlencoded` with `message=<JSON>`, plus fallback POST/GET payloads → `SmsplanetWebhookController::inbound()``SmsConversationService::receiveSmsplanetWebhook()`; successful 2WAY receipt returns plain `OK`.
4. **Order matching** — inbound sender phone is normalized and matched to the latest order by `order_addresses.phone`.
5. **Notification** — inbound SMS creates `notifications.type='sms_inbound'` with a target URL to the order SMS tab when an order was matched.
### Automation Rules
1. **Setup**`AutomationController``AutomationRepository::insertRule()`
2. **Trigger**`AutomationService::executeForOrder()` → evaluates trigger (`order_status_changed`, `order_status_aged`) → runs action (send email, update status)
3. **Log**`AutomationExecutionLogRepository` tracks every run
### Cron Jobs
| Handler | Task |
|---------|------|
| `AllegroOrdersImportHandler` | Fetch new Allegro orders |
| `AllegroStatusSyncHandler` | Push status changes to Allegro |
| `AllegroTokenRefreshHandler` | OAuth token refresh (24h expiry) |
| `ShopproOrdersImportHandler` | Fetch new shopPRO orders |
| `ErliOrdersImportHandler` | Fetch unread Erli inbox order events |
| `ErliStatusSyncHandler` | Pull Erli status events via inbox or push manual local status changes to Erli |
| `ShopproStatusSyncHandler` | Push status to shopPRO |
| `ShopproPaymentStatusSyncHandler` | Sync payment statuses |
| `ShipmentTrackingHandler` | Poll carrier tracking APIs |
| `OrderStatusAgedHandler` | Trigger automation for stuck statuses |
| `AutomationHistoryCleanupHandler` | Purge old automation logs |
### Erli Integration Foundation
1. **Settings** - `/settings/integrations/erli` renders tabbed integration/status/delivery/settings panels and stores one global Erli API key encrypted via `IntegrationSecretCipher`, an optional account label, active flag, and last connection-test result.
2. **Connection test** - `ErliIntegrationController::test()` loads active credentials, calls `ErliApiClient::testConnection()`, performs a real authenticated `GET https://erli.pl/svc/shop-api/inbox`, and stores the result in `integrations.last_test_*`.
3. **Hub** - `IntegrationsHubController::buildErliRow()` adds Erli to `/settings/integrations` with configured/missing secret status, active status, last test timestamp, and configure URL.
4. **Order import** - Phase 128 adds `/settings/integrations/erli/import` and cron `erli_orders_import`. Both call `ErliOrdersSyncService`, which fetches unread `/inbox` messages, maps supported order events through `ErliOrderMapper`, persists via `OrderImportRepository::upsertOrderAggregate()`, emits existing automation events, and acknowledges `POST /inbox/mark-read` only after a zero-failure batch.
5. **Status mapping/sync** - Phase 129 adds pull/push status mapping tables, status controls in Erli settings, and cron `erli_status_sync`. Pull reuses inbox import; push sends manual orderPRO status changes to `PATCH /orders/{id}/status`.
6. **Delivery mapping and labels** - Phase 130 adds `ErliDeliveryMappingController` and a Delivery tab. It maps imported Erli delivery method labels to local shipment providers (`inpost`/`apaczka`) and stores Erli `source_vendor_code` for external parcel registration. Label files are still produced by local providers, not by Erli carrier-contract parcels.
7. **External shipment sync** - Phase 130 extends `ErliApiClient` with shipping dictionary calls and `createExternalParcel()` (`POST /shipping/external`). `ErliExternalShipmentService` registers a local package in Erli only after `shipment_packages.tracking_number` exists; failures are activity-log warnings and do not block local labels.
8. **Deferred** - Carrier tracking automation and broader delivery-status hooks are planned for v3.8 Phase 131.
## Dependency Injection
Manual constructor injection in `routes/web.php` — no DI container library. Example:
```php
$ordersController = new OrdersController(
$template, $translator, $auth,
$app->orders(), $shipmentPackageRepository,
$receiptRepository, $receiptConfigRepository, ...
);
```
All production classes are `final` — prevents accidental inheritance.
## Directory Structure
```
bootstrap/ app.php (service wiring, config loading)
bin/ migrate.php, cron.php (CLI entry points)
config/ app.php, database.php
database/
migrations/ 84 SQL files (YYYYMMDD_NNNNNN_description.sql)
drafts/ WIP migrations
public/
index.php HTTP entry point
.htaccess Apache rewrite rules
assets/css/ Compiled CSS (app.css, login.css, modules/)
assets/js/ jquery-alerts.js, global-search.js, automation-form.js
resources/
views/ PHP templates by module + components/ layouts/
scss/ SCSS sources (app.scss, login.scss, modules/_*.scss)
modules/ jquery-alerts JS+SCSS source
lang/pl/ Polish translations
routes/
web.php All routes (581 lines)
src/
Core/ Framework (25 files)
Modules/ 13 feature modules (~200+ PHP files)
storage/
logs/ app.log
sessions/ PHP session files
cache/ PHPUnit cache, etc.
tests/
Unit/ PHPUnit tests (7+ service test files)
bootstrap.php PSR-4 autoloader for tests
```
## Phase 127 - Erli Integration Foundation
### ErliIntegrationRepository (`src/Modules/Settings/ErliIntegrationRepository.php`)
- Zarzadza pojedynczym rekordem `erli_integration_settings` (`id=1`) i bazowym wpisem `integrations.type='erli'`.
- Szyfruje klucz API przez `IntegrationSecretCipher`; formularz widzi tylko flage `has_api_key`.
- `getCredentials()` zwraca aktywna konfiguracje z `base_url='https://erli.pl/svc/shop-api'` i odszyfrowanym Bearer API key.
- Pusty input `api_key` podczas edycji zachowuje zapisany sekret.
### ErliApiClient (`src/Modules/Settings/ErliApiClient.php`)
- `testConnection()` wykonuje realny `GET /inbox` do Erli z naglowkiem `Authorization: Bearer ...`.
- Phase 128: `fetchInbox()` pobiera do 500 nieprzeczytanych wiadomosci; `markInboxRead()` potwierdza `POST /inbox/mark-read` z `lastMessageId` dopiero po udanym batchu.
- Phase 129: `updateOrderStatus()` wysyla `PATCH /orders/{id}/status` z body `{"status": "..."}` dla recznych zmian statusu orderPRO mapowanych na status Erli.
- Phase 130: `getShippingMethods()`, `getDeliveryMethods()`, `getDeliveryVendors()`, `getPriceLists()`, `getPriceListsDetails()` i `createExternalParcel()` obsluguja natywne shipping API Erli bez wymuszania nadawania przez umowe Erli.
- Wysyla `Accept: application/json` i `User-Agent: orderPRO/1.0 (erli-integration)`.
- Traktuje HTTP 2xx jako sukces; 401/403 jako blad autoryzacji, 429 jako limit zapytan, pozostale bledy jako czytelny komunikat z odpowiedzi.
- Uzywa `SslCertificateResolver` i nie wywoluje `curl_close()` (PHP 8.5 compatible).
### ErliIntegrationController (`src/Modules/Settings/ErliIntegrationController.php`)
- Endpointy: `GET /settings/integrations/erli`, `POST /settings/integrations/erli/save`, `POST /settings/integrations/erli/test`, `POST /settings/integrations/erli/import`, `POST /settings/integrations/erli/statuses/save-pull`, `POST /settings/integrations/erli/statuses/save-push`, `POST /settings/integrations/erli/delivery/save`.
- `save` zapisuje label, aktywnosc, sekret, ustawienia importu (`orders_fetch_enabled`, `orders_fetch_start_date`, interwal crona) oraz kierunek/interwal `erli_status_sync`; `test` wykonuje realny test API i zapisuje wynik przez `IntegrationsRepository::updateTestResult()`.
- `importNow()` uruchamia reczny import Erli z pominieciem flagi cron enable, ale nadal wymaga aktywnych credentials.
### ErliDeliveryMappingController (`src/Modules/Settings/ErliDeliveryMappingController.php`)
- Buduje dane zakladki Dostawy: metody z `orders.external_carrier_id` dla `source='erli'`, aktualne `carrier_delivery_method_mappings`, slowniki vendorow/metod z Erli oraz lokalne uslugi InPost/Apaczka.
- `saveDeliveryMappings()` zapisuje mapowanie globalne `source_system='erli'`, `source_integration_id=0` z lokalnym providerem oraz `source_vendor_code` wymaganym przez Erli `POST /shipping/external`.
### ErliExternalShipmentService (`src/Modules/Settings/ErliExternalShipmentService.php`)
- `syncPackage(int $packageId)` sprawdza, czy paczka nalezy do zamowienia Erli i ma tracking number.
- Pobiera vendor Erli z mapowania dostawy albo proboje go wywnioskowac z lokalnego providera/carrier id.
- Wysyla `POST /shipping/external` z `orderId`, `vendor`, `status='sent'`, `trackingNumber`; sukces zapisuje w `shipment_packages.payload_json.erli_external_parcel`, blad trafia do activity log jako niekrytyczny.
### ErliOrdersSyncService / ErliOrderMapper (`src/Modules/Settings/`)
- `ErliOrdersSyncService::sync()` jest wspolnym entrypointem dla crona i importu recznego. Zwraca liczniki `processed`, `imported_created`, `imported_updated`, `failed`, `skipped`, `acknowledged`.
- Obsluguje tylko zdarzenia order inbox (`orderCreated`, `orderStatusChanged`, `orderSellerStatusChanged`); wiadomosci produktowe sa pomijane do przyszlych faz.
- `ErliOrderMapper` mapuje statusy przez `ErliPullStatusMappingRepository` gdy istnieje konfiguracja; w przeciwnym razie zachowuje fallbacki `pending -> nieoplacone`, `purchased -> nowe`, `cancelled/returned -> anulowane`.
- `ErliOrdersSyncService` odkrywa surowe statusy Erli z inboxa i dopisuje je do `erli_order_status_pull_mappings`, zeby operator mogl je zmapowac w UI.
- Nowe zamowienia z invoice/company/tax id ustawiają `orders.invoice_requested=1`; re-import korzysta z istniejacego delta-only kontraktu `OrderImportRepository`.
- Automatyzacje: `order.imported` dla nowych zamowien i `payment.status_changed` przy tranzycji platnosci na re-imporcie.
### ErliOrdersImportHandler (`src/Modules/Cron/ErliOrdersImportHandler.php`)
- Handler crona `erli_orders_import`, domyslnie seedowany jako disabled. Operator wlacza go z ustawien Erli.
### ErliStatusSyncService / ErliStatusSyncHandler (`src/Modules/Settings/`, `src/Modules/Cron/`)
- Kierunek `erli_to_orderpro` wywoluje `ErliOrdersSyncService::sync()` z `ignore_orders_fetch_enabled=true`, czyli statusy przychodzace z Erli przechodza tym samym bezpiecznym `/inbox` + ACK flow co import.
- Kierunek `orderpro_to_erli` wybiera tylko zamowienia `source='erli'` z reczna zmiana statusu (`order_status_history.change_source='manual'`) po `integration_order_sync_state.last_status_pushed_at`.
- Push korzysta z `erli_order_status_mappings` i `ErliApiClient::updateOrderStatus()`. Brak mapowania powoduje `skipped`; blad API powoduje `failed` i nie przesuwa kursora poza ostatni udany timestamp.
- `erli_status_sync` jest seedowany jako disabled; zapis ustawien Erli aktualizuje interwal, kierunek i wlaczenie harmonogramu zgodnie z aktywnoscia integracji.
### IntegrationsHubController
- Dodaje wiersz Erli do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu.
### Scope Boundary
- Phase 127 nie dodaje importu zamowien, mapowania/synchronizacji statusow, etykiet ani trackingu Erli. Te obszary sa odlozone do Phases 128-131.
## Phase 116 - HostedSMS Integration Settings
### HostedSmsIntegrationRepository (`src/Modules/Settings/HostedSmsIntegrationRepository.php`)
- Zarzadza pojedynczym rekordem `hostedsms_integration_settings` (`id=1`) i bazowym wpisem `integrations` typu `hostedsms`.
- Szyfruje haslo przez `IntegrationSecretCipher`; formularz widzi tylko flage `has_password`.
- Udostepnia `getCredentials()` dla kontrolera testowej wysylki SMS.
### HostedSmsApiClient (`src/Modules/Settings/HostedSmsApiClient.php`)
- Wykonuje `POST https://api.hostedsms.pl/SimpleApi` jako `application/x-www-form-urlencoded`.
- Wysyla `UserEmail`, `Password`, `Sender`, `Phone`, `Message` oraz opcjonalnie `ConvertMessageToGSM7`.
- Traktuje `MessageId` jako sukces, a `ErrorMessage` jako blad biznesowy nawet przy HTTP 200.
### HostedSmsIntegrationController (`src/Modules/Settings/HostedSmsIntegrationController.php`)
- Endpointy: `GET /settings/integrations/hostedsms`, `POST /settings/integrations/hostedsms/save`, `POST /settings/integrations/hostedsms/test`.
- `test` realnie wysyla SMS z edytowalna trescia i zapisuje wynik w `integrations.last_test_*`.
### IntegrationsHubController
- Dodaje wiersz HostedSMS do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu.
## Phase 117 - SMSPLANET Integration Settings
### SmsplanetIntegrationRepository (`src/Modules/Settings/SmsplanetIntegrationRepository.php`)
- Zarzadza pojedynczym rekordem `smsplanet_integration_settings` (`id=1`) i bazowym wpisem `integrations` typu `smsplanet`.
- Obsluguje dwie metody autoryzacji: Bearer token oraz `key` + `password`.
- Szyfruje token, klucz API i haslo przez `IntegrationSecretCipher`; formularz widzi tylko flagi `has_api_token`, `has_api_key` i `has_api_password`.
- Udostepnia `getCredentials()` tylko dla kompletnej i aktywnej konfiguracji testowej wysylki SMS, razem z opcjonalna `default_footer`.
### SmsplanetApiClient (`src/Modules/Settings/SmsplanetApiClient.php`)
- Wykonuje `POST https://api2.smsplanet.pl/sms` jako `application/x-www-form-urlencoded`.
- Dla Bearer token wysyla naglowek `Authorization: Bearer ...`; dla `key_password` wysyla parametry `key` i `password`.
- Wysyla `from`, `to`, `msg` oraz opcjonalnie `clear_polish` i `transactional`; test nie ustawia `test=1`, wiec wysyla realny SMS.
- Traktuje `messageId` jako sukces, a `errorMsg`/`errorCode` jako blad biznesowy.
### SmsplanetIntegrationController (`src/Modules/Settings/SmsplanetIntegrationController.php`)
- Endpointy: `GET /settings/integrations/smsplanet`, `POST /settings/integrations/smsplanet/save`, `POST /settings/integrations/smsplanet/test`.
- `test` realnie wysyla SMS z edytowalna trescia i zapisuje wynik w `integrations.last_test_*`.
- Testowa wysylka dopisuje `default_footer` przed wywolaniem SMSPLANET i waliduje finalna tresc w limicie 918 znakow.
### IntegrationsHubController
- Dodaje wiersz SMSPLANET do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu.
## Phase 118 - Fakturownia Single Instance
### FakturowniaIntegrationRepository (`src/Modules/Settings/FakturowniaIntegrationRepository.php`)
- Zarzadza pojedynczym globalnym rekordem `fakturownia_integration_settings` (`id=1`) i jednym bazowym wpisem `integrations.type='fakturownia'`.
- `getSettings()` zwraca dane formularza, flagi `has_api_token`, aktywnosc i wynik ostatniego testu.
- `saveSettings()` aktualizuje globalna konfiguracje; pusty `api_token` zachowuje zapisany sekret.
- `findAll()` zostaje jako kompatybilny wrapper zwracajacy liste z jednym elementem dla starszych wywolan.
- `getIntegrationId()` jest zrodlem prawdy dla `invoice_configs.integration_id` przy delegacji faktur.
### FakturowniaIntegrationController
- Endpointy aktywne: `GET /settings/integrations/fakturownia`, `POST /settings/integrations/fakturownia/save`, `POST /settings/integrations/fakturownia/test`.
- Legacy `/new` i `/edit` przekierowuja na globalna konfiguracje; delete z UI nie jest oferowany.
- Widok `resources/views/settings/fakturownia.php` pokazuje jeden formularz konfiguracji oraz panel testu polaczenia.
### InvoiceConfigRepository + InvoiceConfigController
- Przy `is_delegated=1` zapis konfiguracji ignoruje wieloinstancyjny wybor konta i ustawia `integration_id` na globalny Fakturownia id.
- Kolumna `invoice_configs.integration_id` zostaje dla kompatybilnosci z `InvoiceService` i historia wystawionych faktur.
- Widok konfiguracji faktury pokazuje status globalnej Fakturowni zamiast selecta kont.
### Migration 20260512_000109
- Wybiera aktywna instancje Fakturowni jako zachowana; fallback: najczesciej uzywana w `invoice_configs`, potem najnizsze id.
- Przepina delegowane `invoice_configs.integration_id` na zachowana instancje i zeruje `integration_id` dla lokalnych konfiguracji.
- Usuwa nadmiarowe rekordy `fakturownia_integration_settings` i `integrations.type='fakturownia'` po przepieciu zaleznosci.
## Phase 108 — Delivery Status Management
### DeliveryStatusRepository (`src/Modules/Shipments/DeliveryStatusRepository.php`)
- CRUD dla tabeli `delivery_statuses`
- Per-request static cache (`private static ?array $cache`)
- Blokuje edycję/usunięcie statusów systemowych (`is_system=1`)
- Blokuje usunięcie statusów używanych w `delivery_status_mappings` lub `shipment_packages`
### DeliveryStatusesController (`src/Modules/Settings/DeliveryStatusesController.php`)
- Panel `/settings/delivery-statuses`
- Dwie zakładki via `?tab=` param: `statuses` (CRUD) i `mapping` (embed mapowania)
- Wstrzykuje `DeliveryStatusRepository` i `DeliveryStatusMappingRepository`
### DeliveryStatus::setRepository() (dynamic loading)
- Wywoływane raz w `routes/web.php` po bootstrap
- `label()`, `getAllOptions()`, `getAllStatuses()`, `getColor()` ładują z DB gdy repo ustawione
- Fallback na hardcoded stałe gdy repo niedostępne
### AutomationController + AutomationService (Phase 108 Plan 02)
- `AutomationController::buildShipmentStatusOptions()` — buduje listę opcji `[key => ['label' => ...]]` z `DeliveryStatus::getAllOptions()` (DB-driven)
- Walidacja `shipment_status` warunku i `update_shipment_status` akcji w `parseConditionValue()`/`parseActionConfig()` używa `DeliveryStatus::getAllStatuses()`
- `AutomationService::evaluateShipmentStatusCondition()` — bezpośrednie porównanie kluczy DB (usunięto mapping grupowy `SHIPMENT_STATUS_OPTION_MAP`)
- `AutomationService::resolveStatusFromActionKey()` — bezpośredni klucz statusu z DB jako target
- BREAKING: stare reguły z grupowymi kluczami (`registered`, `courier_pickup`, `dropped_at_point`, `unclaimed`, `picked_up_return`) nie matchują się — operator musi je odtworzyć przy użyciu nowych kluczy DB