Files
orderPRO/DOCS/ARCHITECTURE.md
Jacek Pyziak 360eef128d feat(121+122): smsplanet conversation, notifications, default footer
Phase 121 — SMSPLANET Conversation + Notifications:
- migration 20260512_000110 adds smsplanet conversation + notifications tables
- src/Modules/Sms (SmsConversationService, SmsMessageRepository, SmsplanetWebhookController)
- src/Modules/Notifications (Repository, Controller, ApiController)
- order SMS tab, notification center, sender mode, inbound webhook
- public notifications.js + layouts/app.php integration

Phase 122 — SMSPLANET Default SMS Footer:
- migration 20260512_000111 adds smsplanet_integration_settings.default_footer
- footer appended to test SMS and order SMS, validated against 918 char limit
- settings textarea + compact order SMS note when footer configured

Bundled (could not split per-phase without hunk staging):
- routes/web.php (also carries Phase 118 fakturownia redirects)
- DOCS/{ARCHITECTURE,DB_SCHEMA,TECH_CHANGELOG}.md (118 + 121 + 122 entries)
- .paul/codebase/{architecture,db_schema,tech_changelog}.md (118 + 121 + 122)
- .paul/STATE.md, ROADMAP.md, changelog/2026-05-12.md (UNIFY closure)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-12 20:37:41 +02:00

17 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 51+ Integration controllers, OAuth clients, API clients, mappers Allegro/shopPRO/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 → carrier 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
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

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