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>
17 KiB
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 |
| 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 afterDOMContentLoaded. - Keeps the original select in the form, synchronizes option
selectedstate, and preserves native GET/POST names such aschannels[]andstatus_groups[]. - Used by
/statistics/ordersand/statistics/summaryfilters 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.phpafter Chart.js 4.4.8 CDN; activates only when#js-statistics-summary-dataexists. - 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
Razemline. - Chart 2 displays monthly gross order values per selected integration plus a
Razemline. - 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/unreadevery 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
- Import — Cron handler → API client →
OrderImportService→OrdersRepository::insertOrder()→AutomationService::executeForNewOrder() - Status update —
OrdersController::updateStatus()→OrdersRepository::updateStatus()→ automation check - Status sync — Cron →
AllegroStatusSyncService/ShopproStatusSyncService→ carrier API
Statistics Summary
- Request —
/statistics/summary→OrdersStatisticsController::summary() - Filters — controller reuses statistics filter semantics: date range,
channels[],status_groups[], default status groups excluding cancelled; default history starts at2026-04-01. - Aggregation —
OrdersStatisticsRepository::aggregateByMonth()groups existingordersrows byYYYY-MMand channel key, using the same effective date/channel/status/gross amount SQL helpers as the daily report. - View model — controller builds per-integration series and total series for order count and gross value charts.
- Render —
resources/views/statistics/summary.phprenders filters, chart JSON, two canvas targets, and table fallbacks.
Shipment Flow
- Create —
ShipmentController::create()→ShipmentProviderRegistry→ carrierShipmentService::createShipment()→ShipmentPackageRepository::insert() - Track — Cron
ShipmentTrackingHandler→ShipmentTrackingRegistry→ carrier tracking API →ShipmentPackageRepository::updateDeliveryStatus()
Receipt / Invoice
- Generate —
ReceiptController::store()→ReceiptService::generateReceipt()→ReceiptRepository::insert()+ Dompdf PDF - Email —
EmailSendingService::send()→VariableResolver::resolve()→AttachmentGenerator::generatePdf()→ PHPMailer SMTP
SMSPLANET Conversation
- Settings —
/settings/integrations/smsplanetstores auth, text sender,sender_mode, optional 2WAYsender_phone, and optional globaldefault_footer. - Outbound from order —
/orders/{id}/sms/send→OrdersController::sendSms()→SmsConversationService::sendFromOrder()appendsdefault_footerwhen configured, validates the final body against 918 characters, sends throughSmsplanetApiClient::sendSms(), and stores the final sent body insms_messages. - Inbound webhook — public
/webhooks/smsplanet/inboundaccepts SMSPLANET 2WAYPOST application/x-www-form-urlencodedwithmessage=<JSON>, plus fallback POST/GET payloads →SmsplanetWebhookController::inbound()→SmsConversationService::receiveSmsplanetWebhook(); successful 2WAY receipt returns plainOK. - Order matching — inbound sender phone is normalized and matched to the latest order by
order_addresses.phone. - Notification — inbound SMS creates
notifications.type='sms_inbound'with a target URL to the order SMS tab when an order was matched.
Automation Rules
- Setup —
AutomationController→AutomationRepository::insertRule() - Trigger —
AutomationService::executeForOrder()→ evaluates trigger (order_status_changed,order_status_aged) → runs action (send email, update status) - Log —
AutomationExecutionLogRepositorytracks 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 wpisemintegrationstypuhostedsms. - Szyfruje haslo przez
IntegrationSecretCipher; formularz widzi tylko flagehas_password. - Udostepnia
getCredentials()dla kontrolera testowej wysylki SMS.
HostedSmsApiClient (src/Modules/Settings/HostedSmsApiClient.php)
- Wykonuje
POST https://api.hostedsms.pl/SimpleApijakoapplication/x-www-form-urlencoded. - Wysyla
UserEmail,Password,Sender,Phone,Messageoraz opcjonalnieConvertMessageToGSM7. - Traktuje
MessageIdjako sukces, aErrorMessagejako 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. testrealnie wysyla SMS z edytowalna trescia i zapisuje wynik wintegrations.last_test_*.
IntegrationsHubController
- Dodaje wiersz HostedSMS do
/settings/integrationsze 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 wpisemintegrationstypusmsplanet. - Obsluguje dwie metody autoryzacji: Bearer token oraz
key+password. - Szyfruje token, klucz API i haslo przez
IntegrationSecretCipher; formularz widzi tylko flagihas_api_token,has_api_keyihas_api_password. - Udostepnia
getCredentials()tylko dla kompletnej i aktywnej konfiguracji testowej wysylki SMS, razem z opcjonalnadefault_footer.
SmsplanetApiClient (src/Modules/Settings/SmsplanetApiClient.php)
- Wykonuje
POST https://api2.smsplanet.pl/smsjakoapplication/x-www-form-urlencoded. - Dla Bearer token wysyla naglowek
Authorization: Bearer ...; dlakey_passwordwysyla parametrykeyipassword. - Wysyla
from,to,msgoraz opcjonalnieclear_polishitransactional; test nie ustawiatest=1, wiec wysyla realny SMS. - Traktuje
messageIdjako sukces, aerrorMsg/errorCodejako blad biznesowy.
SmsplanetIntegrationController (src/Modules/Settings/SmsplanetIntegrationController.php)
- Endpointy:
GET /settings/integrations/smsplanet,POST /settings/integrations/smsplanet/save,POST /settings/integrations/smsplanet/test. testrealnie wysyla SMS z edytowalna trescia i zapisuje wynik wintegrations.last_test_*.- Testowa wysylka dopisuje
default_footerprzed wywolaniem SMSPLANET i waliduje finalna tresc w limicie 918 znakow.
IntegrationsHubController
- Dodaje wiersz SMSPLANET do
/settings/integrationsze 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 wpisemintegrations.type='fakturownia'. getSettings()zwraca dane formularza, flagihas_api_token, aktywnosc i wynik ostatniego testu.saveSettings()aktualizuje globalna konfiguracje; pustyapi_tokenzachowuje zapisany sekret.findAll()zostaje jako kompatybilny wrapper zwracajacy liste z jednym elementem dla starszych wywolan.getIntegrationId()jest zrodlem prawdy dlainvoice_configs.integration_idprzy delegacji faktur.
FakturowniaIntegrationController
- Endpointy aktywne:
GET /settings/integrations/fakturownia,POST /settings/integrations/fakturownia/save,POST /settings/integrations/fakturownia/test. - Legacy
/newi/editprzekierowuja na globalna konfiguracje; delete z UI nie jest oferowany. - Widok
resources/views/settings/fakturownia.phppokazuje jeden formularz konfiguracji oraz panel testu polaczenia.
InvoiceConfigRepository + InvoiceConfigController
- Przy
is_delegated=1zapis konfiguracji ignoruje wieloinstancyjny wybor konta i ustawiaintegration_idna globalny Fakturownia id. - Kolumna
invoice_configs.integration_idzostaje dla kompatybilnosci zInvoiceServicei 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_idna zachowana instancje i zerujeintegration_iddla lokalnych konfiguracji. - Usuwa nadmiarowe rekordy
fakturownia_integration_settingsiintegrations.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_mappingslubshipment_packages
DeliveryStatusesController (src/Modules/Settings/DeliveryStatusesController.php)
- Panel
/settings/delivery-statuses - Dwie zakładki via
?tab=param:statuses(CRUD) imapping(embed mapowania) - Wstrzykuje
DeliveryStatusRepositoryiDeliveryStatusMappingRepository
DeliveryStatus::setRepository() (dynamic loading)
- Wywoływane raz w
routes/web.phppo 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' => ...]]zDeliveryStatus::getAllOptions()(DB-driven)- Walidacja
shipment_statuswarunku iupdate_shipment_statusakcji wparseConditionValue()/parseActionConfig()używaDeliveryStatus::getAllStatuses() AutomationService::evaluateShipmentStatusCondition()— bezpośrednie porównanie kluczy DB (usunięto mapping grupowySHIPMENT_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