Files
orderPRO/.paul/codebase/architecture.md
Jacek Pyziak 6129042ff6 feat(114): accounting configs refactor + invoice configs CRUD
Phase 114 complete (v3.7 Invoices):
- /settings/accounting jako hub-rozdroze (Paragony / Faktury)
- /settings/accounting/receipts + /invoices osobne podstrony list i edycji
- InvoiceConfigRepository + Controller (CRUD z walidacja delegacji)
- Seed Domyslny VAT (NOT EXISTS idempotent)
- invoice-config-form.js (toggle is_delegated -> integration_id)
- confirm-delete.js (globalny modul OrderProAlerts.confirm)
- Legacy aliasy starych endpointow /settings/accounting/save|toggle|delete

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:32:29 +02:00

16 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
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)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.
  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 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).