- Nowa tabela sms_templates (name + body + is_active) + minimalny CRUD.
- /settings/sms-templates: lista + formularz z paleta zmiennych (pill chips).
- Wydzielono Sms\SmsVariableResolver ze wspolna logika placeholderow;
Email\VariableResolver staje sie cienka fasada — EmailSendingService bez zmian.
- Dropdown "Wybierz szablon" w zakladce SMS na /orders/{id} z fetch
GET /orders/{id}/sms/template + OrderProAlerts.confirm przy nadpisaniu.
- Stopka SMSPLANET dalej doklejana wylacznie przez SmsConversationService
(Phase 122 contract preserved).
- Sidebar Ustawien: nowy link "Szablony SMS".
Migration: 20260512_000112_create_sms_templates.sql (CREATE TABLE).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
38 KiB
38 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 (581 lines) |
All ~80 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 | 54+ | Integration controllers, OAuth clients, API clients (Fakturownia incl.), mappers | Allegro/shopPRO/Apaczka/InPost/Fakturownia 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.
Key Data Flows
Order Lifecycle
- Import — Cron handler → API client →
OrderImportService→OrdersRepository::insertOrder()→AutomationService::executeForNewOrder() - Re-import (Phase 111 + 112 + 119) —
OrderImportRepository::upsertOrderAggregatewykrywa tranzycjepayment_statusz 0/1 na 2 i zwracapayment_transition=true.AllegroOrderImportServiceiShopproOrdersSyncServicena tej fladze emitujapayment.status_changed, co przez chain reguly automatyzacji #7 zmieniastatus_codenaw_realizacji. Logika preservacjistatus_codez Phase 62 pozostaje rozdzielona (statusOverwriteAllowed=currentStatus='nieoplacone' && newPaymentStatus===2). Phase 112-01 (delta-only re-import): przycreated=falserepo nie wywolujereplaceAddresses/replaceItems/replaceNotes/replaceShipments/replaceStatusHistory—order_items.idi flagi lokalne (np.project_generatedz Phase 97) pozostaja stabilne.updateOrderDelta()aktualizuje wylaczniestatus_code(warunkowo, z propagacja anulowania),payment_status,total_paid,is_canceled_by_buyer,source_updated_at,payload_json,fetched_at,updated_at. Anulowanie ze zrodla (is_canceled_by_buyer=1lub zmapowany pullstatus_code='anulowane') nadpisuje preservacje statusu. Identical-payload guard (normalizePayloadJson) pomija UPDATE gdy znormalizowany payload nie rozni sie od DB i brak innych tranzycji. Phase 119-01 (total_paid protection): gdypaymentStatusUnchanged=true(oldPaymentStatus === newPaymentStatus),updateOrderDelta()nie dolaczatotal_paiddo UPDATE — chroni reczne korekty kwoty (np. zwroty czesciowe).is_canceled_by_buyerjest pomijane analogicznie, chyba zecancelledBySource=true(cancel propagation ze zrodla zawsze wymusza wpis flagi). Pozostale pola (status_code,payment_status,source_updated_at,payload_json,fetched_at,updated_at) zachowuja niezmieniony kontrakt z Phase 112-01. - 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
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 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
Phase 113 — Fakturownia Integration Foundation
Schema (Plan 113-01)
- Tabele
invoice_configs,invoices,invoice_number_counters(mirrorreceipt_configs/receipts/receipt_number_countersplus delegation fields:invoice_configs.integration_id,is_delegated;invoices.external_invoice_id,external_pdf_url). - Tabela
fakturownia_integration_settings(multi-account:integration_id INT UNSIGNED NOT NULL UNIQUEFK ->integrations(id)). orders.invoice_requested TINYINT(1) NOT NULL DEFAULT 0z indexemidx_orders_invoice_requested.
FakturowniaIntegrationRepository (src/Modules/Settings/FakturowniaIntegrationRepository.php)
findAll()JOINintegrations+fakturownia_integration_settingszwraca listę kont Fakturowni.findByIntegrationId(int)zwraca jedno konto (z resolvedapi_token_encryptedzintegrations.api_key_encryptedz fallbackiem na settings).save(?int $integrationId, array $payload)- upsert (insert dointegrationsprzezIntegrationsRepository::ensureIntegrationgdy$integrationId=null; w przeciwnym razie update name/is_active). Token szyfrowany przezIntegrationSecretCipheri zapisywany dointegrations.api_key_encrypted(źródło prawdy) oraz settings.api_token_encrypted (cache).delete(int $integrationId)— blokuje usunięcie gdyinvoice_configs.integration_id = X(FK SET NULL chroniony aplikacyjnie przezIntegrationConfigException).getDecryptedToken(int $integrationId)— dla użycia w przyszłych planach (createInvoice/downloadPdf).
FakturowniaApiClient (src/Modules/Settings/FakturowniaApiClient.php)
testConnection(string $prefix, string $apiToken): array— GEThttps://{prefix}.fakturownia.pl/account.json?api_token=...z cURL +SslCertificateResolver::resolve(). Zwraca['ok' => bool, 'http_code' => int, 'message' => string].createInvoice()idownloadPdf()— STUB-y rzucająceRuntimeExceptiondo implementacji w kolejnym planie.
IntegrationsRepository::updateTestResult()
- Nowa metoda zapisująca
last_test_status / last_test_http_code / last_test_message / last_test_atpo wywołaniu API test. Używana przezFakturowniaIntegrationController::test()(i będzie reuse'owana w przyszłych integracjach).
FakturowniaIntegrationController (src/Modules/Settings/FakturowniaIntegrationController.php)
- Routy
/settings/integrations/fakturownia(lista),.../edit,.../save,.../test,.../delete(POST z_tokenCSRF). - Wykorzystuje
Flash::set('fakturownia.save'|'fakturownia.test'|'fakturownia.error')iRedirectPathResolver.
IntegrationsHubController
- Nowy parametr konstruktora
FakturowniaIntegrationRepository $fakturowniai nowa metodabuildFakturowniaRow()agregująca status wszystkich kont (count instancji, configured/active counts, ostatni test).
Phase 118 — Fakturownia Single Instance
FakturowniaIntegrationRepository
- Zarzadza jedna globalna konfiguracja
fakturownia_integration_settings.id=1i jednym rekordemintegrations.type='fakturownia'. getSettings()zasila formularz i hub integracji;saveSettings()zapisuje prefix, token, department/defaults i aktywnosc.getIntegrationId()jest zrodlem prawdy dla delegowanychinvoice_configs.integration_id.findAll()zostaje kompatybilnym wrapperem zwracajacym liste z jednym elementem.
FakturowniaIntegrationController + UI
/settings/integrations/fakturowniapokazuje jeden formularz i test polaczenia.- Legacy
/newi/editprzekierowuja do globalnej konfiguracji; delete nie jest oferowany w UI. - Hub integracji pokazuje jedna instancje Fakturowni, bez licznika kont.
Invoice Config Delegation
InvoiceConfigRepository::save()przyis_delegated=1ignoruje wieloinstancyjny wybor i ustawia globalny Fakturownia integration id.- UI konfiguracji faktury pokazuje status globalnej konfiguracji zamiast selecta kont.
invoice_configs.integration_idzostaje dla kompatybilnosci zInvoiceServicei istniejaca historia faktur.
Migration 20260512_000109
- Wybiera aktywna instancje Fakturowni; fallback: uzywana w
invoice_configs, potem najnizsze id. - Przepina delegowane
invoice_configs.integration_idna zachowany rekord i usuwa nadmiarowe rekordy Fakturowni po przepieciu zaleznosci.
Phase 115 — Wystawianie faktury z zamowienia
InvoiceService (src/Modules/Accounting/InvoiceService.php)
issue(array $params): array— orchestrator. Walidacja config (active), order details fetch, build snapshots (seller zcompany_settings, buyer merged z payload_json+addresses+manual override, items z VAT-aware netto/brutto split), routing doissueLocal()lubissueDelegated()zaleznie odinvoice_configs.is_delegated.issueLocal()—InvoiceRepository::nextLocalNumber()(atomowy counter zinvoice_number_counters) ->insertLocal()-> zwraca{invoice_id, invoice_number, total_gross, mode='local'}.issueDelegated()—FakturowniaApiClient::createInvoice()PRZED INSERT lokalnym; on success zapisexternal_invoice_id/external_pdf_url/invoice_numberz odpowiedzi API; on failure rzucaInvoiceIssueException(zaden wiersz winvoices).invoice_number_countersNIE jest dotykany dla delegated.- Static
extractBuyerTaxNumber($order, $buyerAddress)— parsuje NIP z payload_json sciezki:invoice.address.taxId(Allegro),invoice.taxId/nip,buyer.tax_number/nip,client.nip/tax_number, top-levelnip/tax_number. Fallback naorder_addresses.company_tax_number.
InvoiceRepository (src/Modules/Accounting/InvoiceRepository.php)
findByOrderId/findById— JOINinvoices+invoice_configs+integrations(type='fakturownia') +fakturownia_integration_settings(LEFT JOIN dlaaccount_prefix).insertLocal/insertDelegated— wspolny prywatnyinsert()z roznymi NULL-amizacjamiexternal_*pol.nextLocalNumber()—INSERT ... ON DUPLICATE KEY UPDATE last_number = last_number + 1nainvoice_number_counters, mirrorReceiptRepository::getNextNumber.paginate()— filtry:search(numer/order ref),config_id,mode(local/delegated rozroznia poexternal_invoice_id IS NULL),date_from/date_to.
FakturowniaApiClient (rozszerzony)
createInvoice(array $settings, array $invoice)— POSThttps://{prefix}.fakturownia.pl/invoices.jsonz body{api_token, invoice}. cURL zSslCertificateResolver, timeout$timeoutSeconds. On 2xx parsuje JSON na{id, number, view_url, pdf_url, raw}. On non-2xx rzucaRuntimeException("HTTP {code}: {error}").buildPdfUrl(prefix, invoiceId, apiToken)— string-builder dlahttps://{prefix}.fakturownia.pl/invoices/{id}.pdf?api_token=.... Bez fetcha; uzywany w redirect 302.- Dodany
httpPostJson()(private) odpowiednik istniejacegohttpGet().
InvoiceController (src/Modules/Accounting/InvoiceController.php)
create(Request)— GET/orders/{id}/invoice/create. Walidacjaorders.invoice_requested=1(przekierowanie z flash error gdy 0). Active configs (filteris_active=1). NIP auto-prefill viaInvoiceService::extractBuyerTaxNumber(). Renderujeaccounting/invoice_form.store(Request)— POST/orders/{id}/invoice/store. CSRF,config_idwalidacja. WywolujeInvoiceService::issue()z buyer overrides z formularza. On success:OrdersRepository::recordActivity('invoice_issued'), flash success, redirect na/orders/{id}/invoice/{invoiceId}. OnInvoiceIssueException: flash doinvoice.error, redirect z powrotem na form.show(Request)— GET/orders/{id}/invoice/{invoiceId}. HTML preview z snapshotow.pdf(Request)— GET/orders/{id}/invoice/{invoiceId}/pdf. Gdyexternal_pdf_urlistnieje -> redirect 302; inaczej Dompdf inline z templatuaccounting/invoice_pdf.issuedList(Request)— GET/settings/accounting/invoices/issued. Filtry GET, paginacja 50/strona.
orders.invoice_requested toggle
OrdersRepository::setInvoiceRequested(int, bool)— UPDATE zupdated_at = NOW().OrdersController::toggleInvoiceRequested— POST/orders/{id}/invoice-requested/toggle. CSRF, JSON response{success, invoice_requested}. Logujeorder_activity_logzevent_type='invoice_requested_changed'.public/assets/js/modules/invoice-requested-toggle.js— vanilla JS, idempotent guarddataset.bound='1'. AJAX POST przychange, optimistic show/hide[data-invoice-button-wrap]. Rollback checkbox przy HTTP/network blad.
Auto-import flagi invoice_requested
AllegroOrderImportService::importSingleOrder— przywasCreated=truejezelipayload.invoice.requiredtruthy ->setInvoiceRequested(true). Tylko pierwszy import (delta-only re-import nie nadpisuje manualnej zmiany).ShopproOrdersSyncService::shouldRequestInvoice($rawOrder)— flexible parser sprawdzajacywants_invoice,invoice_required,invoice.required,buyer.wants_invoice,buyer.invoice(akceptuje true/1/'1'/'true'/'yes'/'tak'). Wywolany tylko przywasCreated=true.
View hierarchy
accounting/invoice_form.php— formularz wystawiania.accounting/invoice_preview.php— HTML preview po wystawieniu.accounting/invoice_pdf.php— template Dompdf, mirrorreceipts/print.phpz dodatkowymi polami faktury VAT (parties, netto/VAT/brutto per stawka, termin platnosci).accounting/invoices_issued_list.php— lista pod/settings/accounting/invoices/issued.orders/show.php— checkbox toggle + warunkowy przycisk "Wystaw fakture" + sekcja "Faktury" w tabie documents.
DI wiring (routes/web.php)
$invoiceRepository = new InvoiceRepository($app->db());(poInvoiceConfigRepository).$invoiceService = new InvoiceService($invoiceRepository, $invoiceConfigRepository, $companySettingsRepository, new OrdersRepository(...), $fakturowniaIntegrationRepository, $fakturowniaApiClient);$invoiceController = new InvoiceController($template, $translator, $auth, $invoiceRepository, $invoiceConfigRepository, $companySettingsRepository, new OrdersRepository(...), $invoiceService);$ordersControllerrozszerzony o 2 trailing params:$invoiceRepository,$invoiceConfigRepository.
BREAKING / migration
- Zadnych nowych migracji — Phase 113-01 dostarczyla
orders.invoice_requested,invoice_configs/invoices/invoice_number_countersifakturownia_integration_settings. OrdersControllerctor dostal 2 NEW optional params (default null) — backwards compatible.
Edge cases / known limits
- INVOICE-IDEMP-115 (
.paul/codebase/todo.md) — brak idempotencji przy double-POST do Fakturowni gdy odpowiedz nie dotrze; operator musi recznie zweryfikowac w panelu. - Brak
invoice.createdevent automatyzacji (per Phase 113 decision). - Brak download+cache PDF z Fakturowni — tylko redirect 302 (kazdy klik na PDF dla delegated faktury fetchuje PDF z Fakturowni).
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, dopisujedefault_footergdy jest skonfigurowana i zapisuje wynik wintegrations.last_test_*.
IntegrationsHubController
- Dodaje wiersz SMSPLANET do
/settings/integrationsze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu.
Phase 121 - SMSPLANET Conversation + Notifications
SmsConversationService (src/Modules/Sms/SmsConversationService.php)
- Wysyla SMS z poziomu zamowienia przez
SmsplanetApiClient, dopisujedefault_footergdy jest skonfigurowana, zapisuje finalna tresc wsms_messagesi uzywasender_modedo wyboru nadpisu albo numeru 2WAY. - Parsuje publiczny webhook
/webhooks/smsplanet/inbound, normalizuje telefony i dopasowuje przychodzacy SMS do najnowszego zamowienia po telefonie klienta/adresu. - Endpoint inbound akceptuje POST i GET; format 2WAY
message=<JSON>jest dekodowany, sukces zwraca plainOK, a dopasowanie zamowienia korzysta zorder_addresses.phone. - Tworzy
notifications.type='sms_inbound'z linkiem do/orders/{id}?tab=sms.
Notifications module
/notificationspokazuje historie powiadomien i pozwala oznaczac wpisy jako przeczytane./api/notifications/unreadzasila topbar badge orazpublic/assets/js/modules/notifications.js.- Browser Notification API jest progresywne: brak zgody nie blokuje strony ani pollingu.
Phase 123 — Receipts Export VAT Breakdown
ReceiptService::buildItemsSnapshot (src/Modules/Accounting/ReceiptService.php)
- Snapshot pozycji w
receipts.items_jsonma teraz polevat(procent jako float). Zrodlo:order_items.tax_rate(fallbackitem.vat, ostatecznie 23.0). - Pozycja "Koszt wysylki" (gdy
delivery_price > 0) dostajevat = 23.0. - Stary kontrakt (
name,quantity,price,total,sku,ean) zachowany — tylko dodatek polavat. Widoki paragonu (print/preview) nie wymagaja zmian.
AccountingController::export (src/Modules/Accounting/AccountingController.php)
- Naglowki XLSX:
Numer | Data wystawienia | Kwota brutto | Kwota netto | Stawka VAT | Kwota VAT. Usunieto: Data sprzedazy, Konfiguracja, Nr zamowienia, Nr referencyjny. buildVatBreakdown(itemsJson, totalNet, totalGross)grupuje pozycjeitems_jsonpovat, oblicza per-grupanet = round(gross / (1 + rate/100), 2)ivat = gross - net. Zwraca liste[{rate_label, net, vat}, ...]posortowana malejaco po stawce.- Legacy fallback: gdy zaden item nie ma klucza
vat, zwraca pojedynczy wiersz[{rate_label: '23%', net: total_net, vat: total_gross - total_net}]. - Multi-rate paragon = wiele wierszy w XLSX (ten sam Numer, Data wystawienia i Kwota brutto powtarzane).
- Helper
formatVatRate()formatuje stawke (23.0 -> "23%", 7.5 -> "7.5%").
Phase 120 — Alert Component Unification
Alert component (resources/views/components/alert.php)
- Reusable alert renderer with params:
$type(info|success|warning|danger; fallback 'info'),$message(escaped) lub$messageHtml(trusted),$dismissible(default true),$role('alert'|'status'). - Renders inline SVG icon per type + body + optional dismiss button. Markup:
<div class="alert alert--TYPE" data-alert>...<button data-alert-dismiss>...</button></div>. - Used via
include __DIR__ . '/../components/alert.php'po ustawieniu lokalnych$type/$message/$dismissible.
SCSS — .alert w resources/scss/shared/_ui-components.scss
.alertjest teraz flex (icon + body + dismiss). Dodane:.alert__icon,.alert__body,.alert__dismiss.- Nowy wariant
.alert--info(blue: border #bfdbfe, bg #eff6ff, color #1e3a8a) — wczesniej brakowal i renderowal sie jako czarny tekst na bialym tle. - Wariantow
--success/--warning/--dangernie zmieniono kolorystycznie. - Wrapper
.alerts-stack(gap 8px) do stackowania wielu alertow z layoutu.
JS — public/assets/js/modules/alert-dismiss.js
- Vanilla JS, idempotent guard (
window.__alertDismissBound). - Delegated click handler na
[data-alert-dismiss]— usuwa najblizszy[data-alert]z DOM bez przeladowania. - Ladowany globalnie w
layouts/app.php,layouts/auth.php,layouts/public.php.
Flash — App\Core\Support\Flash rozszerzenie
- Nowa kolejka typowana
$_SESSION['_flash_queue']z entries{type, message}. Flash::push(string $type, string $message): void— append do kolejki (whitelist info/success/warning/danger, fallback info).Flash::all(): array— zwraca i czysci kolejke + skanuje legacy_flash(heurystyka klucza:error/fail/danger→ danger,warning→ warning,success/.save/.created/.deleted/.toggled→ success, reszta → info). BC zachowany:Flash::set/getdziala bez zmian.
Centralny renderer flash w layoutach
layouts/app.php,layouts/auth.php,layouts/public.phpna poczatku glownego content area iterujaFlash::all()i wlaczaja komponentalert.phpper wpis (wrap.alerts-stack).- Kontrolery NIE wymagaly zmian — pre-fetched
Flash::get('module.key', '')przekazany do widoku jako lokalna zmienna jest dalej renderowany inline przez widok (przez ten sam komponent). Centralny renderer przejmuje wpisyFlash::push(...)oraz nieskonsumowane legacy entries.
Migracja widokow
- Wszystkie inline
<div class="alert alert--TYPE">...</div>w widokach (36 plikow razem zeshipments/prepare.phpiorders/show.php) zastapione przez<?php $type=...; $message=...; $dismissible=...; include dirname(__DIR__) . '/components/alert.php'; ?>. .flash--error/.flash--successworders/show.phpishipments/prepare.phpzastapione komponentem (klasa.flash--*w SCSS pozostaje bez uzycia, deferred cleanup).- Wyjatek:
settings/email-mailboxes.phpma JS-generowane alerty (resultDiv.className = 'mt-12 alert alert--success') z dynamicznej odpowiedzi AJAX test polaczenia SMTP — uzywaja klas SCSS bez markupu komponentu (out of scope dla tej fazy).
Phase 114 — Accounting Configs Refactor
Sekcja Ksiegowosc — struktura URL
/settings/accounting— hub-rozdroze z 2 kartami: "Paragony" i "Faktury".ReceiptConfigController::hub()./settings/accounting/receipts— lista konfiguracji paragonow.ReceiptConfigController::list()./settings/accounting/receipts/new,/edit?id=N— formularz na osobnej podstronie.ReceiptConfigController::edit()./settings/accounting/receipts/save|toggle|delete— POST actions.- Legacy aliasy:
/settings/accounting/save|toggle|delete(POST) zostaja jako duplicate routes (wsteczna kompatybilnosc z<form action>w starszych szablonach/bookmarkach). /settings/accounting/invoices+/new,/edit,/save,/toggle,/delete— analogicznie dlainvoice_configs.InvoiceConfigController.
InvoiceConfigRepository (src/Modules/Settings/InvoiceConfigRepository.php)
listAll()JOINinvoice_configs LEFT JOIN integrations(type='fakturownia') — zwracaintegration_namegdyis_delegated=1.save(array $data): int— walidacja serwerowa wszystkich pol. Krytyczna regula: gdyis_delegated=1musi bycintegration_id > 0wskazujacy naintegrations.type='fakturownia', inaczej rzucaIntegrationConfigException. Gdyis_delegated=0, ignorujeintegration_id(NULL).toggleStatus(int $id)przezToggleableRepositoryTrait::toggleActive().delete(int $id)— pre-checkSELECT 1 FROM invoices WHERE config_idzeby zwrocic czytelny PL komunikat zamiast brzydkiego SQLSTATE z FK RESTRICT.
Seed
- Migracja
20260511_000107_seed_default_invoice_config.sql— idempotentny insertDomyslny VAT(NOT EXISTS guard,invoice_configs.namenie jest UNIQUE).
invoice-config-form.js (public/assets/js/modules/invoice-config-form.js)
- Vanilla JS modul ladowany globalnie przez
layouts/app.php. - Toggle widocznosci
[data-invoice-delegation]wrappera w zaleznosci od stanu[data-invoice-delegated]checkboxa. - Ustawia
select[name=integration_id].requiredzgodnie ze stanem checkboxa; przy unchecked czyscivalue.
Ujednolicony wyglad list paragonow/faktur
- Tabela
table.tablewtable-wrap, badgebadge--{success,muted}na statusy. - Edycja przez
<a href=".../edit?id=N">, toggle/delete przez<form>z_tokenijs-confirm-delete. - Wspolny pattern miedzy
accounting-receipts.phpiaccounting-invoices.php(faktury maja dodatkowe kolumny: Tryb, Konto Fakturowni).
Phase 124 — SMS Templates
SmsTemplateRepository (src/Modules/Sms/SmsTemplateRepository.php)
- CRUD na
sms_templates(PDO prepared statements, ToggleableRepositoryTrait). listAll()(cala lista alfabetycznie poname),listActive()(tylko is_active=1, kolumnyid|name|bodydo dropdownu w UI).save(array): intwaliduje wymaganename+body(rzucaRuntimeExceptiongdy puste); wykonuje INSERT albo UPDATE wg obecnosciidw payloadzie; zwraca id rekordu.delete(int),toggleStatus(int)przeztoggleActive('sms_templates', $id).
SmsVariableResolver (src/Modules/Sms/SmsVariableResolver.php)
- Wydzielony z
Email\VariableResolver— wspolna logika zmiennych dla Email i SMS. buildVariableMap(order, addresses, companySettings)zwraca mape placeholderow:zamowienie.*,kupujacy.*,adres.*,firma.*,przesylka.*(przesylka.numer/przesylka.link_sledzeniaz najnowszej paczki przezShipmentPackageRepository::findLatestByOrderId+DeliveryStatus::trackingUrl).resolve(template, variableMap)zastepuje{{group.var}}wartoscia z mapy (puste gdy brak klucza).
Email\VariableResolver (refaktor)
- Pozostaje final class z tym samym API publicznym (
buildVariableMap/resolve) —EmailSendingServiceniezmieniony. - Konstruktor:
(ShipmentPackageRepository $repo, ?SmsVariableResolver $inner = null). Gdy$innernie podany, sam tworzy SmsVariableResolver — backward compat dla starego wiringu. - Metody publiczne deleguja do
$this->inner— zero duplikacji logiki zmiennych.
SmsTemplateController (src/Modules/Settings/SmsTemplateController.php)
- Mirror
EmailTemplateControllerbez Quill/skrzynki/zalacznika/duplikacji. - Akcje:
index(lista),create/edit/save(form CRUD),delete,toggleStatus(AJAX JSON),getVariables(JSON paleta dla ewentualnego dynamic palette). VARIABLE_GROUPSjako stala klasy — pelne 5 grup (zamowienie/kupujacy/adres/firma/przesylka) zgodnie ze wspolnym SmsVariableResolver.- Routy:
/settings/sms-templates,/create,/edit,/save,/delete,/toggle,/variables. CSRF_tokenna POST. Flashsettings.sms_templates.success|error.
OrdersController (rozszerzenie)
- Dodane optional params konstruktora:
?SmsTemplateRepository $smsTemplates,?SmsVariableResolver $smsVariableResolver,?CompanySettingsRepository $companySettingsRepo(po istniejacych SMS params; default null = backward compat). show()przekazuje$smsTemplates(list active) do widoku jakosmsTemplates.- Nowa metoda
smsTemplate(Request)->GET /orders/{id}/sms/template?template_id=N-> JSON{ok, body, name}z rozwinietymi zmiennymi. 400/404/500 dla nieprawidlowych parametrow/braku rekordu.
Widok orders/show.php
- Nad textarea
name="message"(#js-sms-message) dodany conditional<select data-sms-template-picker data-order-id data-message-target="js-sms-message">z opcja domyslna + aktywne szablony (renderowany tylko gdy$smsTemplatesList !== []). - Textarea ma teraz
id="js-sms-message"— JS target.
Frontend module public/assets/js/modules/sms-template-picker.js
- Vanilla JS, idempotent guard
window.__smsTemplatePickerBound+ per-elementdataset.smsPickerBound. - Na
changeselecta: fetch/orders/{id}/sms/template?template_id=N, podstaw body do textarea, fireinputevent. - Gdy textarea ma juz tresc ->
OrderProAlerts.confirm({...})options-object API (Phase 114 pattern). Po zatwierdzeniu nadpisuje, po anulowaniu resetuje select. Fallback na natywnyconfirm(). - Ladowany globalnie z
layouts/app.php(linia ponotifications.js).
Wspolny resolver — wiring DI (routes/web.php)
$smsVariableResolver = new SmsVariableResolver($shipmentPackageRepositoryForOrders);$variableResolver = new VariableResolver($shipmentPackageRepositoryForOrders, $smsVariableResolver);(drugi argument opcjonalny dla BC).$smsTemplateRepository = new SmsTemplateRepository($app->db());$smsTemplateController = new SmsTemplateController($template, $translator, $auth, $smsTemplateRepository);$ordersControllerrozszerzony o 3 trailing params (smsTemplateRepository, smsVariableResolver, companySettingsRepository).
SCSS — _sms-templates.scss
- Nowy partial
resources/scss/modules/_sms-templates.scssz klasami.sms-template-*(active label, counter, body grid) oraz.sms-var-panel/.sms-var-group/.sms-var-itemdla palety zmiennych. - Import w
app.scsspocustomer-risk-alert.
Stopka — preserved Phase 122 contract
- Szablony SMS NIE zawieraja
default_footer— operator wpisuje sama tresc. SmsConversationService::buildFinalOutboundBody()dokleja stopke raz przysendFromOrder()(po wstawieniu szablonu i ewentualnej edycji przez operatora). WalidacjaMAX_SMS_LENGTH = 918obowiazuje na finalnej tresci.
BREAKING / migration
- Migracja
20260512_000112_create_sms_templates.sql—CREATE TABLE IF NOT EXISTS sms_templates(DDL, brak SELECT no-op). - Brak innych zmian schematu.
OrdersControllerctor: 3 NEW optional params (default null) — backwards compatible.