Files
orderPRO/DOCS/ARCHITECTURE.md
Jacek Pyziak 7972bb9fa4 feat(129): erli status mapping sync
Phase 129 complete:
- Add Erli pull/push status mapping tables, seeds and repositories
- Wire Erli status sync cron for inbox pull and manual-only push
- Add tabbed Erli settings UI, tests and documentation

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-16 00:27:28 +02:00

23 KiB

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 → OrderImportServiceOrdersRepository::insertOrder()AutomationService::executeForNewOrder()
  2. Status updateOrdersController::updateStatus()OrdersRepository::updateStatus() → automation check
  3. Status sync — Cron → AllegroStatusSyncService / ShopproStatusSyncService / ErliStatusSyncService → marketplace API

Statistics Summary

  1. Request/statistics/summaryOrdersStatisticsController::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. AggregationOrdersStatisticsRepository::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. Renderresources/views/statistics/summary.php renders filters, chart JSON, two canvas targets, and table fallbacks.

Shipment Flow

  1. CreateShipmentController::create()ShipmentProviderRegistry → carrier ShipmentService::createShipment()ShipmentPackageRepository::insert()
  2. Track — Cron ShipmentTrackingHandlerShipmentTrackingRegistry → carrier tracking API → ShipmentPackageRepository::updateDeliveryStatus()

Receipt / Invoice

  1. GenerateReceiptController::store()ReceiptService::generateReceipt()ReceiptRepository::insert() + Dompdf PDF
  2. EmailEmailSendingService::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/sendOrdersController::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. SetupAutomationControllerAutomationRepository::insertRule()
  2. TriggerAutomationService::executeForOrder() → evaluates trigger (order_status_changed, order_status_aged) → runs action (send email, update status)
  3. LogAutomationExecutionLogRepository 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/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. Deferred - Label generation, shipment creation, and tracking are planned for v3.8 Phases 130-131.

Dependency Injection

Manual constructor injection in routes/web.php — no DI container library. Example:

$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.
  • 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.
  • 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.

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