Files
orderPRO/.paul/codebase/architecture.md
Jacek Pyziak 0227f2d072 feat(123): receipts export xlsx VAT breakdown
- AccountingController::export(): new headers (Numer | Data wystawienia | Kwota brutto | Kwota netto | Stawka VAT | Kwota VAT), removed Data sprzedazy/Konfiguracja/Nr zamowienia/Nr referencyjny
- buildVatBreakdown() helper groups items_json by vat rate, emits one XLSX row per (receipt x rate); legacy receipts (no `vat` in snapshot) fallback to net=brutto/1.23
- ReceiptService::buildItemsSnapshot(): writes `vat` per item from order_items.tax_rate (fallback 23.0); shipping cost item gets vat=23.0
- RECEIPT-NET-FIX deferred (.paul/codebase/todo.md): ReceiptService::issue() still saves total_net=total_gross

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:06:53 +02:00

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

Key Data Flows

Order Lifecycle

  1. Import — Cron handler → API client → OrderImportServiceOrdersRepository::insertOrder()AutomationService::executeForNewOrder()
  2. Re-import (Phase 111 + 112 + 119)OrderImportRepository::upsertOrderAggregate wykrywa tranzycje payment_status z 0/1 na 2 i zwraca payment_transition=true. AllegroOrderImportService i ShopproOrdersSyncService na tej fladze emituja payment.status_changed, co przez chain reguly automatyzacji #7 zmienia status_code na w_realizacji. Logika preservacji status_code z Phase 62 pozostaje rozdzielona (statusOverwriteAllowed = currentStatus='nieoplacone' && newPaymentStatus===2). Phase 112-01 (delta-only re-import): przy created=false repo nie wywoluje replaceAddresses/replaceItems/replaceNotes/replaceShipments/replaceStatusHistoryorder_items.id i flagi lokalne (np. project_generated z Phase 97) pozostaja stabilne. updateOrderDelta() aktualizuje wylacznie status_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=1 lub zmapowany pull status_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): gdy paymentStatusUnchanged=true (oldPaymentStatus === newPaymentStatus), updateOrderDelta() nie dolacza total_paid do UPDATE — chroni reczne korekty kwoty (np. zwroty czesciowe). is_canceled_by_buyer jest pomijane analogicznie, chyba ze cancelledBySource=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.
  3. Status updateOrdersController::updateStatus()OrdersRepository::updateStatus() → automation check
  4. 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

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

Phase 113 — Fakturownia Integration Foundation

Schema (Plan 113-01)

  • Tabele invoice_configs, invoices, invoice_number_counters (mirror receipt_configs/receipts/receipt_number_counters plus 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 UNIQUE FK -> integrations(id)).
  • orders.invoice_requested TINYINT(1) NOT NULL DEFAULT 0 z indexem idx_orders_invoice_requested.

FakturowniaIntegrationRepository (src/Modules/Settings/FakturowniaIntegrationRepository.php)

  • findAll() JOIN integrations + fakturownia_integration_settings zwraca listę kont Fakturowni.
  • findByIntegrationId(int) zwraca jedno konto (z resolved api_token_encrypted z integrations.api_key_encrypted z fallbackiem na settings).
  • save(?int $integrationId, array $payload) - upsert (insert do integrations przez IntegrationsRepository::ensureIntegration gdy $integrationId=null; w przeciwnym razie update name/is_active). Token szyfrowany przez IntegrationSecretCipher i zapisywany do integrations.api_key_encrypted (źródło prawdy) oraz settings.api_token_encrypted (cache).
  • delete(int $integrationId) — blokuje usunięcie gdy invoice_configs.integration_id = X (FK SET NULL chroniony aplikacyjnie przez IntegrationConfigException).
  • getDecryptedToken(int $integrationId) — dla użycia w przyszłych planach (createInvoice/downloadPdf).

FakturowniaApiClient (src/Modules/Settings/FakturowniaApiClient.php)

  • testConnection(string $prefix, string $apiToken): array — GET https://{prefix}.fakturownia.pl/account.json?api_token=... z cURL + SslCertificateResolver::resolve(). Zwraca ['ok' => bool, 'http_code' => int, 'message' => string].
  • createInvoice() i downloadPdf() — STUB-y rzucające RuntimeException do implementacji w kolejnym planie.

IntegrationsRepository::updateTestResult()

  • Nowa metoda zapisująca last_test_status / last_test_http_code / last_test_message / last_test_at po wywołaniu API test. Używana przez FakturowniaIntegrationController::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 _token CSRF).
  • Wykorzystuje Flash::set('fakturownia.save'|'fakturownia.test'|'fakturownia.error') i RedirectPathResolver.

IntegrationsHubController

  • Nowy parametr konstruktora FakturowniaIntegrationRepository $fakturownia i nowa metoda buildFakturowniaRow() 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=1 i jednym rekordem integrations.type='fakturownia'.
  • getSettings() zasila formularz i hub integracji; saveSettings() zapisuje prefix, token, department/defaults i aktywnosc.
  • getIntegrationId() jest zrodlem prawdy dla delegowanych invoice_configs.integration_id.
  • findAll() zostaje kompatybilnym wrapperem zwracajacym liste z jednym elementem.

FakturowniaIntegrationController + UI

  • /settings/integrations/fakturownia pokazuje jeden formularz i test polaczenia.
  • Legacy /new i /edit przekierowuja do globalnej konfiguracji; delete nie jest oferowany w UI.
  • Hub integracji pokazuje jedna instancje Fakturowni, bez licznika kont.

Invoice Config Delegation

  • InvoiceConfigRepository::save() przy is_delegated=1 ignoruje wieloinstancyjny wybor i ustawia globalny Fakturownia integration id.
  • UI konfiguracji faktury pokazuje status globalnej konfiguracji zamiast selecta kont.
  • invoice_configs.integration_id zostaje dla kompatybilnosci z InvoiceService i istniejaca historia faktur.

Migration 20260512_000109

  • Wybiera aktywna instancje Fakturowni; fallback: uzywana w invoice_configs, potem najnizsze id.
  • Przepina delegowane invoice_configs.integration_id na 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 z company_settings, buyer merged z payload_json+addresses+manual override, items z VAT-aware netto/brutto split), routing do issueLocal() lub issueDelegated() zaleznie od invoice_configs.is_delegated.
  • issueLocal()InvoiceRepository::nextLocalNumber() (atomowy counter z invoice_number_counters) -> insertLocal() -> zwraca {invoice_id, invoice_number, total_gross, mode='local'}.
  • issueDelegated()FakturowniaApiClient::createInvoice() PRZED INSERT lokalnym; on success zapis external_invoice_id/external_pdf_url/invoice_number z odpowiedzi API; on failure rzuca InvoiceIssueException (zaden wiersz w invoices). invoice_number_counters NIE 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-level nip/tax_number. Fallback na order_addresses.company_tax_number.

InvoiceRepository (src/Modules/Accounting/InvoiceRepository.php)

  • findByOrderId/findById — JOIN invoices + invoice_configs + integrations (type='fakturownia') + fakturownia_integration_settings (LEFT JOIN dla account_prefix).
  • insertLocal/insertDelegated — wspolny prywatny insert() z roznymi NULL-amizacjami external_* pol.
  • nextLocalNumber()INSERT ... ON DUPLICATE KEY UPDATE last_number = last_number + 1 na invoice_number_counters, mirror ReceiptRepository::getNextNumber.
  • paginate() — filtry: search (numer/order ref), config_id, mode (local/delegated rozroznia po external_invoice_id IS NULL), date_from/date_to.

FakturowniaApiClient (rozszerzony)

  • createInvoice(array $settings, array $invoice) — POST https://{prefix}.fakturownia.pl/invoices.json z body {api_token, invoice}. cURL z SslCertificateResolver, timeout $timeoutSeconds. On 2xx parsuje JSON na {id, number, view_url, pdf_url, raw}. On non-2xx rzuca RuntimeException("HTTP {code}: {error}").
  • buildPdfUrl(prefix, invoiceId, apiToken) — string-builder dla https://{prefix}.fakturownia.pl/invoices/{id}.pdf?api_token=.... Bez fetcha; uzywany w redirect 302.
  • Dodany httpPostJson() (private) odpowiednik istniejacego httpGet().

InvoiceController (src/Modules/Accounting/InvoiceController.php)

  • create(Request) — GET /orders/{id}/invoice/create. Walidacja orders.invoice_requested=1 (przekierowanie z flash error gdy 0). Active configs (filter is_active=1). NIP auto-prefill via InvoiceService::extractBuyerTaxNumber(). Renderuje accounting/invoice_form.
  • store(Request) — POST /orders/{id}/invoice/store. CSRF, config_id walidacja. Wywoluje InvoiceService::issue() z buyer overrides z formularza. On success: OrdersRepository::recordActivity('invoice_issued'), flash success, redirect na /orders/{id}/invoice/{invoiceId}. On InvoiceIssueException: flash do invoice.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. Gdy external_pdf_url istnieje -> redirect 302; inaczej Dompdf inline z templatu accounting/invoice_pdf.
  • issuedList(Request) — GET /settings/accounting/invoices/issued. Filtry GET, paginacja 50/strona.

orders.invoice_requested toggle

  • OrdersRepository::setInvoiceRequested(int, bool) — UPDATE z updated_at = NOW().
  • OrdersController::toggleInvoiceRequested — POST /orders/{id}/invoice-requested/toggle. CSRF, JSON response {success, invoice_requested}. Loguje order_activity_log z event_type='invoice_requested_changed'.
  • public/assets/js/modules/invoice-requested-toggle.js — vanilla JS, idempotent guard dataset.bound='1'. AJAX POST przy change, optimistic show/hide [data-invoice-button-wrap]. Rollback checkbox przy HTTP/network blad.

Auto-import flagi invoice_requested

  • AllegroOrderImportService::importSingleOrder — przy wasCreated=true jezeli payload.invoice.required truthy -> setInvoiceRequested(true). Tylko pierwszy import (delta-only re-import nie nadpisuje manualnej zmiany).
  • ShopproOrdersSyncService::shouldRequestInvoice($rawOrder) — flexible parser sprawdzajacy wants_invoice, invoice_required, invoice.required, buyer.wants_invoice, buyer.invoice (akceptuje true/1/'1'/'true'/'yes'/'tak'). Wywolany tylko przy wasCreated=true.

View hierarchy

  • accounting/invoice_form.php — formularz wystawiania.
  • accounting/invoice_preview.php — HTML preview po wystawieniu.
  • accounting/invoice_pdf.php — template Dompdf, mirror receipts/print.php z 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()); (po InvoiceConfigRepository).
  • $invoiceService = new InvoiceService($invoiceRepository, $invoiceConfigRepository, $companySettingsRepository, new OrdersRepository(...), $fakturowniaIntegrationRepository, $fakturowniaApiClient);
  • $invoiceController = new InvoiceController($template, $translator, $auth, $invoiceRepository, $invoiceConfigRepository, $companySettingsRepository, new OrdersRepository(...), $invoiceService);
  • $ordersController rozszerzony o 2 trailing params: $invoiceRepository, $invoiceConfigRepository.

BREAKING / migration

  • Zadnych nowych migracji — Phase 113-01 dostarczyla orders.invoice_requested, invoice_configs/invoices/invoice_number_counters i fakturownia_integration_settings.
  • OrdersController ctor 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.created event 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 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, dopisuje default_footer gdy jest skonfigurowana i zapisuje wynik w integrations.last_test_*.

IntegrationsHubController

  • Dodaje wiersz SMSPLANET do /settings/integrations ze 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, dopisuje default_footer gdy jest skonfigurowana, zapisuje finalna tresc w sms_messages i uzywa sender_mode do 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 plain OK, a dopasowanie zamowienia korzysta z order_addresses.phone.
  • Tworzy notifications.type='sms_inbound' z linkiem do /orders/{id}?tab=sms.

Notifications module

  • /notifications pokazuje historie powiadomien i pozwala oznaczac wpisy jako przeczytane.
  • /api/notifications/unread zasila topbar badge oraz public/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_json ma teraz pole vat (procent jako float). Zrodlo: order_items.tax_rate (fallback item.vat, ostatecznie 23.0).
  • Pozycja "Koszt wysylki" (gdy delivery_price > 0) dostaje vat = 23.0.
  • Stary kontrakt (name, quantity, price, total, sku, ean) zachowany — tylko dodatek pola vat. 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 pozycje items_json po vat, oblicza per-grupa net = round(gross / (1 + rate/100), 2) i vat = 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

  • .alert jest 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/--danger nie 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/get dziala bez zmian.

Centralny renderer flash w layoutach

  • layouts/app.php, layouts/auth.php, layouts/public.php na poczatku glownego content area iteruja Flash::all() i wlaczaja komponent alert.php per 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 wpisy Flash::push(...) oraz nieskonsumowane legacy entries.

Migracja widokow

  • Wszystkie inline <div class="alert alert--TYPE">...</div> w widokach (36 plikow razem ze shipments/prepare.php i orders/show.php) zastapione przez <?php $type=...; $message=...; $dismissible=...; include dirname(__DIR__) . '/components/alert.php'; ?>.
  • .flash--error / .flash--success w orders/show.php i shipments/prepare.php zastapione komponentem (klasa .flash--* w SCSS pozostaje bez uzycia, deferred cleanup).
  • Wyjatek: settings/email-mailboxes.php ma 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 dla invoice_configs. InvoiceConfigController.

InvoiceConfigRepository (src/Modules/Settings/InvoiceConfigRepository.php)

  • listAll() JOIN invoice_configs LEFT JOIN integrations (type='fakturownia') — zwraca integration_name gdy is_delegated=1.
  • save(array $data): int — walidacja serwerowa wszystkich pol. Krytyczna regula: gdy is_delegated=1 musi byc integration_id > 0 wskazujacy na integrations.type='fakturownia', inaczej rzuca IntegrationConfigException. Gdy is_delegated=0, ignoruje integration_id (NULL).
  • toggleStatus(int $id) przez ToggleableRepositoryTrait::toggleActive().
  • delete(int $id) — pre-check SELECT 1 FROM invoices WHERE config_id zeby zwrocic czytelny PL komunikat zamiast brzydkiego SQLSTATE z FK RESTRICT.

Seed

  • Migracja 20260511_000107_seed_default_invoice_config.sql — idempotentny insert Domyslny VAT (NOT EXISTS guard, invoice_configs.name nie 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].required zgodnie ze stanem checkboxa; przy unchecked czysci value.

Ujednolicony wyglad list paragonow/faktur

  • Tabela table.table w table-wrap, badge badge--{success,muted} na statusy.
  • Edycja przez <a href=".../edit?id=N">, toggle/delete przez <form> z _token i js-confirm-delete.
  • Wspolny pattern miedzy accounting-receipts.php i accounting-invoices.php (faktury maja dodatkowe kolumny: Tryb, Konto Fakturowni).