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>
403 lines
32 KiB
Markdown
403 lines
32 KiB
Markdown
# 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 → `OrderImportService` → `OrdersRepository::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/replaceStatusHistory` — `order_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 update** — `OrdersController::updateStatus()` → `OrdersRepository::updateStatus()` → automation check
|
|
4. **Status sync** — Cron → `AllegroStatusSyncService` / `ShopproStatusSyncService` → carrier API
|
|
|
|
### Statistics Summary
|
|
1. **Request** — `/statistics/summary` → `OrdersStatisticsController::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. **Aggregation** — `OrdersStatisticsRepository::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. **Render** — `resources/views/statistics/summary.php` renders filters, chart JSON, two canvas targets, and table fallbacks.
|
|
|
|
### Shipment Flow
|
|
1. **Create** — `ShipmentController::create()` → `ShipmentProviderRegistry` → carrier `ShipmentService::createShipment()` → `ShipmentPackageRepository::insert()`
|
|
2. **Track** — Cron `ShipmentTrackingHandler` → `ShipmentTrackingRegistry` → carrier tracking API → `ShipmentPackageRepository::updateDeliveryStatus()`
|
|
|
|
### Receipt / Invoice
|
|
1. **Generate** — `ReceiptController::store()` → `ReceiptService::generateReceipt()` → `ReceiptRepository::insert()` + Dompdf PDF
|
|
2. **Email** — `EmailSendingService::send()` → `VariableResolver::resolve()` → `AttachmentGenerator::generatePdf()` → PHPMailer SMTP
|
|
|
|
### Automation Rules
|
|
1. **Setup** — `AutomationController` → `AutomationRepository::insertRule()`
|
|
2. **Trigger** — `AutomationService::executeForOrder()` → evaluates trigger (`order_status_changed`, `order_status_aged`) → runs action (send email, update status)
|
|
3. **Log** — `AutomationExecutionLogRepository` 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:
|
|
|
|
```php
|
|
$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 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).
|
|
|