feat(110): statistics summary

Phase 110 complete:
- add Statistics -> Podsumowanie page
- add monthly order count and value charts per integration plus total
- use Chart.js with table fallback and 04-2026 default history start
- update PAUL and DOCS technical documentation
This commit is contained in:
2026-04-28 22:47:14 +02:00
parent 1156ce046c
commit 0b4ffb7146
21 changed files with 2454 additions and 26 deletions

175
DOCS/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,175 @@
# 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** | 51+ | Integration controllers, OAuth clients, API clients, mappers | Allegro/shopPRO/Apaczka/InPost 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 → `OrderImportService``OrdersRepository::insertOrder()``AutomationService::executeForNewOrder()`
2. **Status update**`OrdersController::updateStatus()``OrdersRepository::updateStatus()` → automation check
3. **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

863
DOCS/DB_SCHEMA.md Normal file
View File

@@ -0,0 +1,863 @@
# Database Schema
**Updated:** 2026-04-28 | **Total tables:** 55 | **Engine:** InnoDB | **Charset:** utf8mb4_unicode_ci
---
## Auth / Users
**users** — System user accounts with authentication
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK, AUTO_INCREMENT |
| `name` | VARCHAR(120) | NO | |
| `email` | VARCHAR(190) | NO | UNIQUE |
| `password_hash` | VARCHAR(255) | NO | |
| `remember_token` | VARCHAR(255) | YES | SHA256 of cookie token |
| `created_at` | DATETIME | NO | DEFAULT CURRENT_TIMESTAMP |
---
## Products
**products** — Main product catalog
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `uuid` | CHAR(36) | NO | UNIQUE |
| `type` | ENUM('simple','variant_parent') | NO | DEFAULT 'simple' |
| `sku` | VARCHAR(128) | YES | UNIQUE |
| `ean` | VARCHAR(32) | YES | |
| `status` | TINYINT(1) | NO | DEFAULT 1 |
| `promoted` | TINYINT(1) | NO | DEFAULT 0 |
| `new_to_date` | DATE | YES | |
| `additional_message` | TINYINT(1) | NO | DEFAULT 0 |
| `additional_message_required` | TINYINT(1) | NO | DEFAULT 0 |
| `additional_message_text` | TEXT | YES | |
| `vat` | DECIMAL(5,2) | YES | |
| `weight` | DECIMAL(10,3) | YES | |
| `price_brutto` | DECIMAL(12,2) | NO | DEFAULT 0.00 |
| `price_brutto_promo` | DECIMAL(12,2) | YES | |
| `price_netto` | DECIMAL(12,2) | YES | |
| `price_netto_promo` | DECIMAL(12,2) | YES | |
| `quantity` | DECIMAL(12,3) | NO | DEFAULT 0.000 |
| `producer_id` | INT UNSIGNED | YES | |
| `producer_name` | VARCHAR(255) | YES | |
| `product_unit_id` | INT UNSIGNED | YES | |
| `custom_fields_json` | TEXT | YES | |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | ON UPDATE CURRENT_TIMESTAMP |
| `deleted_at` | DATETIME | YES | Soft delete |
Indexes: `products_status_idx`, `products_type_idx`, `products_updated_at_idx`, `products_ean_idx`
**product_translations** — Localized product names/descriptions
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `product_id` | INT UNSIGNED | NO | FK → products(id) CASCADE |
| `lang` | VARCHAR(8) | NO | |
| `name` | VARCHAR(255) | NO | |
| `short_description` | TEXT | YES | |
| `description` | LONGTEXT | YES | |
| `meta_title` | VARCHAR(255) | YES | |
| `meta_description` | VARCHAR(255) | YES | |
| `meta_keywords` | VARCHAR(255) | YES | |
| `seo_link` | VARCHAR(255) | YES | |
| `security_information` | MEDIUMTEXT | YES | GPSR data |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
UNIQUE: `(product_id, lang)`
**product_images** — Product image storage references
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `product_id` | INT UNSIGNED | NO | FK → products(id) CASCADE |
| `storage_path` | VARCHAR(255) | NO | |
| `alt` | VARCHAR(255) | YES | |
| `sort_order` | INT | NO | DEFAULT 0 |
| `is_main` | TINYINT(1) | NO | DEFAULT 0 |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
**product_categories** — Productcategory associations (m2m)
| Column | Type | Notes |
|--------|------|-------|
| `product_id` | INT UNSIGNED | FK → products(id) CASCADE |
| `category_id` | INT UNSIGNED | |
| `created_at` | DATETIME | |
PK: `(product_id, category_id)`
**product_variants** — Variants for `variant_parent` products
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `product_id` | INT UNSIGNED | NO | FK → products(id) CASCADE |
| `permutation_hash` | VARCHAR(191) | NO | |
| `sku` | VARCHAR(128) | YES | UNIQUE |
| `ean` | VARCHAR(32) | YES | |
| `status` | TINYINT(1) | NO | DEFAULT 1 |
| `stock_0_buy` | TINYINT(1) | NO | DEFAULT 0 |
| `price_brutto` | DECIMAL(12,2) | YES | |
| `price_netto` | DECIMAL(12,2) | YES | |
| `weight` | DECIMAL(10,3) | YES | |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
UNIQUE: `(product_id, permutation_hash)`
**product_variant_attributes** — Variantattribute value mapping
| Column | Type | Notes |
|--------|------|-------|
| `variant_id` | INT UNSIGNED | FK → product_variants(id) CASCADE |
| `attribute_id` | INT UNSIGNED | FK → attributes(id) RESTRICT |
| `value_id` | INT UNSIGNED | FK → attribute_values(id) RESTRICT |
| `created_at` | DATETIME | |
PK: `(variant_id, attribute_id)`
**attributes** — Attribute definitions (e.g., size, color)
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `type` | TINYINT UNSIGNED | NO | DEFAULT 1 |
| `status` | TINYINT(1) | NO | DEFAULT 1 |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
**attribute_translations** — Localized attribute names
| Column | Type | Notes |
|--------|------|-------|
| `attribute_id` | INT UNSIGNED | FK → attributes(id) CASCADE |
| `lang` | VARCHAR(8) | |
| `name` | VARCHAR(255) | |
| `created_at` | DATETIME | |
| `updated_at` | DATETIME | |
PK: `(attribute_id, lang)`
**attribute_values** — Possible values per attribute
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `attribute_id` | INT UNSIGNED | NO | FK → attributes(id) CASCADE |
| `status` | TINYINT(1) | NO | DEFAULT 1 |
| `is_default` | TINYINT(1) | NO | DEFAULT 0 |
| `impact_on_price` | DECIMAL(12,2) | YES | |
| `sort_order` | INT | NO | DEFAULT 0 |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
**attribute_value_translations** — Localized value names
| Column | Type | Notes |
|--------|------|-------|
| `value_id` | INT UNSIGNED | FK → attribute_values(id) CASCADE |
| `lang` | VARCHAR(8) | |
| `name` | VARCHAR(255) | |
| `created_at` | DATETIME | |
| `updated_at` | DATETIME | |
PK: `(value_id, lang)`
**product_change_log** — Audit trail for product modifications
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `product_id` | INT UNSIGNED | NO | FK → products(id) CASCADE |
| `user_id` | INT UNSIGNED | YES | FK → users(id) SET NULL |
| `change_type` | VARCHAR(64) | NO | |
| `before_json` | JSON | YES | |
| `after_json` | JSON | YES | |
| `created_at` | DATETIME | NO | |
**sales_channels** — Selling channel definitions
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `code` | VARCHAR(64) | NO | UNIQUE |
| `name` | VARCHAR(128) | NO | |
| `type` | VARCHAR(64) | NO | |
| `status` | TINYINT(1) | NO | DEFAULT 1 |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
**product_channel_map** — Link products to external channel offers
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `product_id` | INT UNSIGNED | NO | FK → products(id) CASCADE |
| `channel_id` | INT UNSIGNED | NO | FK → sales_channels(id) CASCADE |
| `integration_id` | INT UNSIGNED | YES | FK → integrations(id) SET NULL |
| `external_product_id` | VARCHAR(128) | YES | |
| `external_variant_id` | VARCHAR(128) | YES | |
| `sync_state` | VARCHAR(32) | NO | DEFAULT 'not_linked' |
| `link_type` | VARCHAR(32) | NO | DEFAULT 'manual' |
| `link_status` | VARCHAR(32) | NO | DEFAULT 'active' |
| `confidence` | TINYINT UNSIGNED | YES | |
| `linked_at` | DATETIME | YES | |
| `unlinked_at` | DATETIME | YES | |
| `sync_meta_json` | JSON | YES | |
| `last_sync_at` | DATETIME | YES | |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
UNIQUE: `(product_id, channel_id, external_product_id, external_variant_id)`
**channel_offers** — External channel offer snapshots
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `integration_id` | INT UNSIGNED | NO | FK → integrations(id) CASCADE |
| `channel_id` | INT UNSIGNED | NO | FK → sales_channels(id) CASCADE |
| `external_product_id` | VARCHAR(128) | NO | |
| `external_variant_id` | VARCHAR(128) | YES | |
| `external_offer_id` | VARCHAR(128) | YES | |
| `name` | VARCHAR(255) | NO | |
| `sku` | VARCHAR(128) | YES | |
| `ean` | VARCHAR(32) | YES | |
| `price_brutto` | DECIMAL(12,2) | YES | |
| `quantity` | DECIMAL(12,3) | YES | |
| `currency` | VARCHAR(8) | YES | |
| `offer_status` | VARCHAR(32) | NO | DEFAULT 'active' |
| `source_updated_at` | DATETIME | YES | |
| `last_seen_at` | DATETIME | NO | |
| `payload_json` | JSON | YES | |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
UNIQUE: `(integration_id, external_product_id, external_variant_id)`
**product_link_events** — Audit log for product-channel linking changes
**product_link_alerts** — Alerts for product-channel link issues (ENUM status: `active`, `resolved`)
**product_integration_translations** — Integration-specific product description overrides
---
## Orders
**orders** — Imported orders from sales channels
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `internal_order_number` | VARCHAR(11) | YES | UNIQUE, auto-assigned |
| `integration_id` | INT UNSIGNED | NO | FK → integrations(id) CASCADE |
| `external_order_id` | VARCHAR(64) | NO | |
| `external_order_number` | VARCHAR(128) | YES | |
| `status` | VARCHAR(64) | YES | Internal status code |
| `currency` | CHAR(3) | YES | |
| `total_gross` | DECIMAL(12,2) | YES | |
| `total_net` | DECIMAL(12,2) | YES | |
| `total_with_tax` | DECIMAL(12,2) | YES | |
| `total_paid` | DECIMAL(12,2) | YES | |
| `buyer_email` | VARCHAR(190) | YES | |
| `buyer_name` | VARCHAR(190) | YES | |
| `buyer_phone` | VARCHAR(64) | YES | |
| `payment_method` | VARCHAR(128) | YES | |
| `payment_status` | VARCHAR(64) | YES | |
| `external_payment_type_id` | VARCHAR(128) | YES | |
| `delivery_method` | VARCHAR(128) | YES | |
| `delivery_price` | DECIMAL(12,2) | YES | |
| `delivery_tracking_number` | VARCHAR(128) | YES | |
| `notes` | TEXT | YES | |
| `external_created_at` | DATETIME | YES | |
| `external_updated_at` | DATETIME | YES | |
| `last_status_checked_at` | DATETIME | YES | |
| `payload_json` | JSON | YES | Full raw API payload |
| `fetched_at` | DATETIME | NO | |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
UNIQUE: `(integration_id, external_order_id)`
**order_items** — Line items within orders
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `order_id` | INT UNSIGNED | NO | FK → orders(id) CASCADE |
| `external_item_id` | VARCHAR(64) | YES | |
| `name` | VARCHAR(255) | NO | |
| `sku` | VARCHAR(128) | YES | |
| `ean` | VARCHAR(64) | YES | |
| `quantity` | DECIMAL(12,3) | NO | DEFAULT 0 |
| `price_gross` | DECIMAL(12,2) | YES | |
| `price_net` | DECIMAL(12,2) | YES | |
| `vat` | DECIMAL(6,2) | YES | |
| `personalization` | TEXT | YES | Customer personalization data for design generation |
| `project_generated` | TINYINT(1) | NO | DEFAULT 0 |
| `project_generated_at` | DATETIME | YES | |
| `payload_json` | JSON | YES | |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
**order_activity_log** — Event log for order state changes
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | BIGINT UNSIGNED | NO | PK |
| `order_id` | BIGINT UNSIGNED | NO | FK → orders(id) CASCADE |
| `event_type` | VARCHAR(32) | NO | |
| `summary` | VARCHAR(255) | NO | |
| `details_json` | JSON | YES | |
| `actor_type` | VARCHAR(16) | NO | DEFAULT 'system' |
| `actor_name` | VARCHAR(128) | YES | |
| `created_at` | DATETIME | NO | |
**order_payments** — Payments linked to orders
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `order_id` | INT UNSIGNED | NO | FK → orders(id) CASCADE |
| `source_payment_id` | VARCHAR(64) | YES | |
| `external_payment_id` | VARCHAR(64) | YES | |
| `payment_type_id` | VARCHAR(64) | NO | |
| `payment_date` | DATETIME | YES | |
| `amount` | DECIMAL(12,2) | YES | |
| `currency` | CHAR(3) | YES | |
| `comment` | VARCHAR(255) | YES | |
| `payload_json` | JSON | YES | |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
UNIQUE: `(order_id, source_payment_id)`
---
## Order Statuses
**order_status_groups** — Status group categories (e.g., "Nowe", "W realizacji")
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `name` | VARCHAR(120) | NO | |
| `code` | VARCHAR(64) | NO | UNIQUE |
| `color_hex` | CHAR(7) | NO | DEFAULT '#64748b' |
| `sort_order` | INT | NO | DEFAULT 0 |
| `is_active` | TINYINT(1) | NO | DEFAULT 1 |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
**order_statuses** — Individual statuses within groups
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `group_id` | INT UNSIGNED | NO | FK → order_status_groups(id) CASCADE |
| `name` | VARCHAR(120) | NO | |
| `code` | VARCHAR(64) | NO | UNIQUE |
| `sort_order` | INT | NO | DEFAULT 0 |
| `is_active` | TINYINT(1) | NO | DEFAULT 1 |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
**order_status_mappings** — Map external (e.g., shopPRO) statuses to internal ones
| Column | Type | Notes |
|--------|------|-------|
| `id` | INT UNSIGNED | PK |
| `integration_id` | INT UNSIGNED | FK → integrations(id) CASCADE |
| `shoppro_status_code` | VARCHAR(64) | |
| `shoppro_status_name` | VARCHAR(128) | |
| `orderpro_status_code` | VARCHAR(64) | |
| `created_at` | DATETIME | |
| `updated_at` | DATETIME | |
UNIQUE: `(integration_id, shoppro_status_code)`
**integration_order_sync_state** — Track order fetch progress per integration
**integration_order_status_sync_state** — Track status sync progress per integration and direction
---
## Shipments & Delivery
**shipment_packages** — Prepared shipments with tracking
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | BIGINT UNSIGNED | NO | PK |
| `order_id` | BIGINT UNSIGNED | NO | FK → orders(id) CASCADE |
| `provider` | VARCHAR(32) | NO | DEFAULT 'allegro_wza' |
| `delivery_method_id` | VARCHAR(128) | YES | |
| `credentials_id` | VARCHAR(128) | YES | |
| `command_id` | VARCHAR(64) | YES | |
| `shipment_id` | VARCHAR(64) | YES | |
| `tracking_number` | VARCHAR(128) | YES | |
| `status` | VARCHAR(32) | NO | DEFAULT 'draft' |
| `delivery_status` | VARCHAR(32) | NO | DEFAULT 'unknown' |
| `delivery_status_raw` | VARCHAR(128) | YES | Provider's original status string |
| `delivery_status_updated_at` | DATETIME | YES | |
| `carrier_id` | VARCHAR(64) | YES | |
| `package_type` | VARCHAR(16) | NO | DEFAULT 'PACKAGE' |
| `weight_kg` | DECIMAL(8,3) | YES | |
| `length_cm` | DECIMAL(8,1) | YES | |
| `width_cm` | DECIMAL(8,1) | YES | |
| `height_cm` | DECIMAL(8,1) | YES | |
| `insurance_amount` | DECIMAL(12,2) | YES | |
| `cod_amount` | DECIMAL(12,2) | YES | |
| `label_format` | VARCHAR(8) | NO | DEFAULT 'PDF' |
| `label_path` | VARCHAR(512) | YES | |
| `receiver_point_id` | VARCHAR(64) | YES | Parcel locker ID |
| `sender_point_id` | VARCHAR(64) | YES | |
| `reference_number` | VARCHAR(128) | YES | |
| `error_message` | TEXT | YES | |
| `payload_json` | JSON | YES | |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
Indexes: `shipment_packages_order_idx`, `shipment_packages_status_idx`, `shipment_packages_tracking_idx`, `idx_delivery_status`
**shipment_presets** — Predefined shipment configurations (saved templates)
| Column | Type | Notes |
|--------|------|-------|
| `id` | BIGINT UNSIGNED | PK |
| `name` | VARCHAR(100) | |
| `color` | VARCHAR(7) | DEFAULT '#3b82f6' |
| `carrier` | VARCHAR(32) | |
| `provider_code` | VARCHAR(32) | |
| `delivery_method_id` | VARCHAR(128) | |
| `credentials_id` | VARCHAR(128) | DEFAULT '' |
| `carrier_id` | VARCHAR(64) | DEFAULT '' |
| `package_type` | VARCHAR(16) | DEFAULT 'PACKAGE' |
| `length_cm` | DECIMAL(8,1) | DEFAULT 25.0 |
| `width_cm` | DECIMAL(8,1) | DEFAULT 20.0 |
| `height_cm` | DECIMAL(8,1) | DEFAULT 8.0 |
| `weight_kg` | DECIMAL(8,3) | DEFAULT 1.000 |
| `sender_point_id` | VARCHAR(64) | DEFAULT '' |
| `label_format` | VARCHAR(8) | DEFAULT 'PDF' |
| `sort_order` | INT UNSIGNED | DEFAULT 0 |
| `created_at` | TIMESTAMP | |
| `updated_at` | TIMESTAMP | |
**delivery_statuses** — Normalized delivery status definitions (Phase 108)
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `key` | VARCHAR(50) | NO | UNIQUE |
| `label_pl` | VARCHAR(100) | NO | Polish label |
| `color` | VARCHAR(7) | NO | DEFAULT '#6c757d' |
| `sort_order` | TINYINT UNSIGNED | NO | DEFAULT 0 |
| `is_terminal` | TINYINT(1) | NO | DEFAULT 0 — marks final states |
| `is_system` | TINYINT(1) | NO | DEFAULT 0 — system-managed |
| `created_at` | DATETIME | NO | |
**delivery_status_mappings** — Map provider-specific raw statuses to normalized keys (Phase 108)
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `provider` | VARCHAR(32) | NO | |
| `raw_status` | VARCHAR(64) | NO | |
| `normalized_status` | VARCHAR(32) | NO | FK ref → delivery_statuses.key |
| `description` | VARCHAR(255) | NO | DEFAULT '' |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
UNIQUE: `(provider, raw_status)`
---
## Integrations
**integrations** — Integration configurations for external services
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `type` | VARCHAR(32) | NO | e.g., 'allegro', 'shoppro', 'apaczka', 'inpost' |
| `name` | VARCHAR(128) | NO | |
| `base_url` | VARCHAR(255) | NO | |
| `api_key_encrypted` | TEXT | YES | AES-encrypted |
| `timeout_seconds` | SMALLINT UNSIGNED | NO | DEFAULT 10 |
| `is_active` | TINYINT(1) | NO | DEFAULT 1 |
| `orders_fetch_enabled` | TINYINT(1) | NO | DEFAULT 0 |
| `orders_fetch_start_date` | DATE | YES | |
| `order_status_sync_direction` | VARCHAR(32) | NO | DEFAULT 'shoppro_to_orderpro' |
| `last_test_status` | VARCHAR(16) | YES | |
| `last_test_http_code` | SMALLINT UNSIGNED | YES | |
| `last_test_message` | VARCHAR(255) | YES | |
| `last_test_at` | DATETIME | YES | |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
UNIQUE: `(type, name)`
**integration_test_logs** — API test results log
**allegro_integration_settings** — Allegro OAuth tokens and API config
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `integration_id` | INT UNSIGNED | YES | UNIQUE, FK → integrations(id) CASCADE |
| `environment` | VARCHAR(16) | NO | DEFAULT 'sandbox' |
| `client_id` | VARCHAR(128) | YES | |
| `client_secret_encrypted` | TEXT | YES | |
| `redirect_uri` | VARCHAR(255) | YES | |
| `orders_fetch_enabled` | TINYINT(1) | NO | DEFAULT 0 |
| `orders_fetch_start_date` | DATE | YES | |
| `access_token_encrypted` | MEDIUMTEXT | YES | AES-encrypted |
| `refresh_token_encrypted` | MEDIUMTEXT | YES | AES-encrypted |
| `token_type` | VARCHAR(32) | YES | |
| `token_scope` | VARCHAR(255) | YES | |
| `token_expires_at` | DATETIME | YES | |
| `connected_at` | DATETIME | YES | |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
**allegro_order_status_mappings** — Allegro status → internal status
| Column | Type | Notes |
|--------|------|-------|
| `id` | INT UNSIGNED | PK |
| `allegro_status_code` | VARCHAR(64) | UNIQUE |
| `allegro_status_name` | VARCHAR(120) | |
| `orderpro_status_code` | VARCHAR(64) | |
| `created_at` | DATETIME | |
| `updated_at` | DATETIME | |
**allegro_delivery_method_mappings** — Map order delivery method strings to Allegro services
| Column | Type | Notes |
|--------|------|-------|
| `id` | INT UNSIGNED | PK |
| `order_delivery_method` | VARCHAR(200) | UNIQUE |
| `allegro_delivery_method_id` | VARCHAR(128) | |
| `allegro_credentials_id` | VARCHAR(128) | |
| `allegro_carrier_id` | VARCHAR(128) | |
| `allegro_service_name` | VARCHAR(255) | |
| `created_at` | DATETIME | |
| `updated_at` | DATETIME | |
**apaczka_integration_settings** — Apaczka API credentials
| Column | Type | Notes |
|--------|------|-------|
| `id` | TINYINT UNSIGNED | PK (fixed 1 row) |
| `integration_id` | INT UNSIGNED | UNIQUE, FK → integrations(id) |
| `api_key_encrypted` | TEXT | |
| `created_at` | DATETIME | |
| `updated_at` | DATETIME | |
**inpost_integration_settings** — InPost ShipX settings
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | TINYINT UNSIGNED | NO | PK (fixed 1 row) |
| `integration_id` | INT UNSIGNED | YES | UNIQUE, FK → integrations(id) |
| `api_token_encrypted` | TEXT | YES | |
| `organization_id` | VARCHAR(50) | YES | |
| `environment` | ENUM('sandbox','production') | NO | DEFAULT 'sandbox' |
| `default_dispatch_method` | ENUM('pop','parcel_locker','courier') | NO | DEFAULT 'pop' |
| `default_dispatch_point` | VARCHAR(50) | YES | |
| `default_insurance` | DECIMAL(10,2) | YES | |
| `default_locker_size` | ENUM('small','medium','large') | NO | DEFAULT 'small' |
| `default_courier_length` | SMALLINT UNSIGNED | YES | DEFAULT 20 |
| `default_courier_width` | SMALLINT UNSIGNED | YES | DEFAULT 15 |
| `default_courier_height` | SMALLINT UNSIGNED | YES | DEFAULT 8 |
| `label_format` | ENUM('Pdf','Zpl','Epl') | NO | DEFAULT 'Pdf' |
| `weekend_delivery` | TINYINT(1) | NO | DEFAULT 0 |
| `auto_insurance_value` | TINYINT(1) | NO | DEFAULT 0 |
| `multi_parcel` | TINYINT(1) | NO | DEFAULT 0 |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
---
## Accounting / Receipts
**receipt_configs** — Receipt generation configurations
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `name` | VARCHAR(128) | NO | |
| `is_active` | TINYINT(1) | NO | DEFAULT 1 |
| `number_format` | VARCHAR(64) | NO | DEFAULT 'PAR/%N/%M/%Y' |
| `numbering_type` | ENUM('monthly','yearly') | NO | DEFAULT 'monthly' |
| `is_named` | TINYINT(1) | NO | DEFAULT 0 |
| `sale_date_source` | ENUM('order_date','payment_date','issue_date') | NO | DEFAULT 'issue_date' |
| `order_reference` | ENUM('none','orderpro','integration') | NO | DEFAULT 'none' |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
**receipts** — Generated receipts / invoices
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `order_id` | BIGINT UNSIGNED | NO | FK → orders(id) CASCADE |
| `config_id` | INT UNSIGNED | NO | FK → receipt_configs(id) RESTRICT |
| `receipt_number` | VARCHAR(64) | NO | UNIQUE |
| `issue_date` | DATETIME | NO | |
| `sale_date` | DATETIME | NO | |
| `seller_data_json` | JSON | NO | Snapshot of company data at issue time |
| `buyer_data_json` | JSON | YES | |
| `items_json` | JSON | NO | |
| `total_net` | DECIMAL(12,2) | NO | DEFAULT 0.00 |
| `total_gross` | DECIMAL(12,2) | NO | DEFAULT 0.00 |
| `order_reference_value` | VARCHAR(128) | YES | |
| `created_by` | INT UNSIGNED | YES | |
| `created_at` | DATETIME | NO | |
**receipt_number_counters** — Sequential numbering per config/period
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `config_id` | INT UNSIGNED | NO | FK → receipt_configs(id) CASCADE |
| `year` | SMALLINT UNSIGNED | NO | |
| `month` | TINYINT UNSIGNED | YES | NULL for yearly numbering |
| `last_number` | INT UNSIGNED | NO | DEFAULT 0 |
UNIQUE: `(config_id, year, month)`
---
## Email
**email_mailboxes** — SMTP mailbox configurations
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `name` | VARCHAR(100) | NO | |
| `smtp_host` | VARCHAR(255) | NO | |
| `smtp_port` | SMALLINT UNSIGNED | NO | DEFAULT 587 |
| `smtp_encryption` | ENUM('tls','ssl','none') | NO | DEFAULT 'tls' |
| `smtp_username` | VARCHAR(255) | NO | |
| `smtp_password_encrypted` | TEXT | NO | AES-encrypted |
| `sender_email` | VARCHAR(255) | NO | |
| `sender_name` | VARCHAR(200) | YES | |
| `html_layout` | LONGTEXT | YES | Wrapper HTML for all emails from this mailbox |
| `is_default` | TINYINT(1) | NO | DEFAULT 0 |
| `is_active` | TINYINT(1) | NO | DEFAULT 1 |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
**email_templates** — Email message templates with variable placeholders
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `name` | VARCHAR(200) | NO | |
| `subject` | VARCHAR(500) | NO | |
| `body_html` | TEXT | NO | Supports `{{variable}}` placeholders |
| `mailbox_id` | INT UNSIGNED | YES | FK → email_mailboxes(id) SET NULL |
| `attachment1` | LONGTEXT | YES | |
| `is_active` | TINYINT(1) | NO | DEFAULT 1 |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
**email_logs** — Sent email history
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | BIGINT UNSIGNED | NO | PK |
| `template_id` | INT UNSIGNED | YES | FK → email_templates(id) SET NULL |
| `mailbox_id` | INT UNSIGNED | YES | FK → email_mailboxes(id) SET NULL |
| `order_id` | INT UNSIGNED | YES | |
| `recipient_email` | VARCHAR(255) | NO | |
| `recipient_name` | VARCHAR(200) | YES | |
| `subject` | VARCHAR(500) | NO | |
| `body_html` | TEXT | NO | |
| `attachments_json` | JSON | YES | |
| `status` | ENUM('sent','failed','pending') | NO | DEFAULT 'pending' |
| `error_message` | TEXT | YES | |
| `sent_at` | DATETIME | YES | |
| `created_at` | DATETIME | NO | |
---
## Automation
**automation_rules** — Business rules for order event automation
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `name` | VARCHAR(128) | NO | |
| `event_type` | VARCHAR(64) | NO | e.g., 'order_status_changed', 'order_status_aged' |
| `is_active` | TINYINT(1) | NO | DEFAULT 1 |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
Index: `(event_type, is_active)`
**automation_conditions** — Conditions for automation rules
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `rule_id` | INT UNSIGNED | NO | FK → automation_rules(id) CASCADE |
| `condition_type` | VARCHAR(64) | NO | |
| `condition_value` | JSON | NO | |
| `sort_order` | SMALLINT UNSIGNED | NO | DEFAULT 0 |
**automation_actions** — Actions executed when rules trigger
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `rule_id` | INT UNSIGNED | NO | FK → automation_rules(id) CASCADE |
| `action_type` | VARCHAR(64) | NO | e.g., 'send_email', 'update_status', 'create_receipt' |
| `action_config` | JSON | NO | |
| `sort_order` | SMALLINT UNSIGNED | NO | DEFAULT 0 |
**automation_execution_logs** — Audit log for rule executions
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | BIGINT UNSIGNED | NO | PK |
| `event_type` | VARCHAR(64) | NO | |
| `rule_id` | INT UNSIGNED | YES | FK → automation_rules(id) SET NULL |
| `rule_name` | VARCHAR(128) | NO | Snapshot at execution time |
| `order_id` | INT UNSIGNED | NO | FK → orders(id) CASCADE |
| `execution_status` | VARCHAR(16) | NO | |
| `result_message` | VARCHAR(500) | YES | |
| `context_json` | JSON | YES | |
| `executed_at` | DATETIME | NO | |
| `created_at` | DATETIME | NO | |
**automation_email_once_deliveries** — Idempotency guard: email sent-once per rule+action+order (Phase 107)
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | BIGINT UNSIGNED | NO | PK |
| `rule_id` | INT UNSIGNED | NO | FK → automation_rules(id) CASCADE |
| `action_id` | INT UNSIGNED | NO | FK → automation_actions(id) CASCADE |
| `order_id` | BIGINT UNSIGNED | NO | FK → orders(id) CASCADE |
| `created_at` | DATETIME | NO | |
UNIQUE: `(rule_id, action_id, order_id)`
---
## Print Queue
**print_api_keys** — API keys for remote print client authentication
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT | NO | PK |
| `name` | VARCHAR(128) | NO | |
| `key_hash` | VARCHAR(128) | NO | UNIQUE, SHA256 |
| `key_prefix` | VARCHAR(8) | NO | Shown in UI for identification |
| `is_active` | TINYINT(1) | NO | DEFAULT 1 |
| `last_used_at` | DATETIME | YES | |
| `created_at` | DATETIME | NO | |
**print_jobs** — Print queue for remote label printing
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `order_id` | BIGINT UNSIGNED | NO | |
| `package_id` | BIGINT UNSIGNED | NO | |
| `label_path` | VARCHAR(255) | NO | |
| `status` | ENUM('pending','printing','completed','failed') | NO | DEFAULT 'pending' |
| `created_by` | INT UNSIGNED | NO | |
| `created_at` | DATETIME | NO | |
| `completed_at` | DATETIME | YES | |
---
## Cron & Scheduling
**cron_jobs** — Individual cron job queue entries
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `job_type` | VARCHAR(80) | NO | |
| `status` | ENUM('pending','processing','completed','failed','cancelled') | NO | DEFAULT 'pending' |
| `priority` | TINYINT UNSIGNED | NO | DEFAULT 100 |
| `payload` | JSON | YES | |
| `result` | JSON | YES | |
| `attempts` | SMALLINT UNSIGNED | NO | DEFAULT 0 |
| `max_attempts` | SMALLINT UNSIGNED | NO | DEFAULT 3 |
| `last_error` | VARCHAR(500) | YES | |
| `scheduled_at` | DATETIME | NO | |
| `started_at` | DATETIME | YES | |
| `completed_at` | DATETIME | YES | |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
Index: `(status, priority, scheduled_at)`
**cron_schedules** — Recurring job definitions
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `job_type` | VARCHAR(80) | NO | UNIQUE |
| `interval_seconds` | INT UNSIGNED | NO | |
| `priority` | TINYINT UNSIGNED | NO | DEFAULT 100 |
| `max_attempts` | SMALLINT UNSIGNED | NO | DEFAULT 3 |
| `payload` | JSON | YES | |
| `enabled` | TINYINT(1) | NO | DEFAULT 1 |
| `last_run_at` | DATETIME | YES | |
| `next_run_at` | DATETIME | YES | |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
---
## Settings & Configuration
**app_settings** — Global key-value configuration store
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `setting_key` | VARCHAR(120) | NO | UNIQUE |
| `setting_value` | TEXT | YES | |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
Default keys: `cron_run_on_web`, `cron_web_limit`, `gs1_api_login`, `gs1_prefix`, `products_sku_format`
**company_settings** — Single-record seller/company configuration (always id=1)
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | TINYINT UNSIGNED | NO | PK, always 1 |
| `company_name` | VARCHAR(200) | YES | |
| `person_name` | VARCHAR(200) | YES | |
| `street` | VARCHAR(200) | YES | |
| `city` | VARCHAR(128) | YES | |
| `postal_code` | VARCHAR(16) | YES | |
| `country_code` | CHAR(2) | NO | DEFAULT 'PL' |
| `phone` | VARCHAR(64) | YES | |
| `email` | VARCHAR(128) | YES | |
| `tax_number` | VARCHAR(64) | YES | NIP |
| `bank_account` | VARCHAR(64) | YES | |
| `bank_owner_name` | VARCHAR(200) | YES | |
| `contact_person_first_name` | VARCHAR(100) | YES | |
| `contact_person_last_name` | VARCHAR(100) | YES | |
| `contact_person_phone` | VARCHAR(64) | YES | |
| `contact_person_email` | VARCHAR(128) | YES | |
| `default_package_length_cm` | DECIMAL(8,1) | NO | DEFAULT 25.0 |
| `default_package_width_cm` | DECIMAL(8,1) | NO | DEFAULT 20.0 |
| `default_package_height_cm` | DECIMAL(8,1) | NO | DEFAULT 8.0 |
| `default_package_weight_kg` | DECIMAL(8,3) | NO | DEFAULT 1.000 |
| `default_label_format` | VARCHAR(8) | NO | DEFAULT 'PDF' |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
---
## Design Generation
**project_mappings** — Map product name patterns to graphic generation scripts
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `product_name_pattern` | VARCHAR(255) | NO | Pattern matched against order_items.name |
| `script_name` | VARCHAR(255) | NO | Script filename in tools/generowanie/ |
| `output_dir` | VARCHAR(500) | YES | |
| `is_active` | TINYINT(1) | NO | DEFAULT 1 |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
---
## Schema Characteristics
| Property | Value |
|----------|-------|
| Engine | InnoDB (all tables) |
| Charset | utf8mb4_unicode_ci |
| Encrypted columns | `*_encrypted` suffix — AES via `IntegrationSecretCipher` |
| Soft deletes | `products.deleted_at` only |
| Audit via JSON | `payload_json` snapshots in orders, shipments, receipts |
| Migrations | `database/migrations/YYYYMMDD_NNNNNN_description.sql` |
| Deferred indexes | `idx_order_addresses_order_type`, `idx_shipment_packages_order_delivery` — apply at >50k orders |
## Reporting Usage
**Statistics Summary (`/statistics/summary`)** — no dedicated reporting tables.
- Reads existing `orders` rows and groups by month using the same effective order date used by `/statistics/orders`.
- Default summary history starts at April 2026 (`2026-04-01`), even if older rows exist.
- Splits series by channel key: Allegro as one series and each shopPRO integration by `orders.integration_id`.
- Uses `integrations.name` only for display labels when available.
- Filters by selected status groups through `order_status_groups` and `order_statuses`.
- Uses existing gross amount columns via `OrdersStatisticsRepository::grossAmountSql()`.
- No schema migration was introduced for Phase 110.

59
DOCS/TECH_CHANGELOG.md Normal file
View File

@@ -0,0 +1,59 @@
# Technical Changelog
## 2026-04-28 - Phase 110 Plan 01: Statistics Summary
**Co zrobiono:**
- `/statistics/summary` - nowy widok podsumowania w menu `Statystyki -> Podsumowanie`.
- `OrdersStatisticsController::summary()` - buduje miesieczny view-model dla wykresow liczby i wartosci zamowien.
- `OrdersStatisticsRepository::aggregateByMonth()` - agreguje istniejace zamowienia po miesiacu i kanale/integracji.
- `public/assets/js/modules/statistics-summary-charts.js` - renderer dwoch interaktywnych wykresow liniowych oparty o Chart.js 4.4.8 CDN.
- `resources/views/statistics/summary.php` - filtry zgodne z raportem dziennym, dwa wykresy obok siebie na desktopie oraz dwie tabele fallback pod nimi.
- Domyslny poczatek historii ustawiony na `2026-04-01` (`04-2026`) mimo starszych danych.
**Dlaczego:**
- Operator potrzebuje szybkiego trendu miesiecznego przed przejsciem do szczegolowych dziennych statystyk.
- Wykresy uzywaja obecnych tabel `orders`, `integrations`, `order_status_groups` i `order_statuses`, wiec migracja DB nie jest potrzebna.
- Seria `Razem` jest liczona z tych samych danych co serie integracji, co ulatwia sprawdzenie sum miesiecznych.
## 2026-04-28 - Phase 109 Plan 01: Checkbox Multiselect Filters
**Co zrobiono:**
- `public/assets/js/modules/checkbox-multiselect.js` - nowy vanilla JS enhancer dla natywnych `<select multiple data-checkbox-multiselect>`.
- `resources/views/layouts/app.php` - globalne podpiecie modulu z cache busting przez `filemtime()`.
- `resources/views/statistics/orders.php` - filtry `channels[]` i `status_groups[]` oznaczone do progresywnego ulepszenia bez zmiany nazw pol formularza.
- `resources/scss/app.scss` - kompaktowe style dropdownu z checkboxami i opcja "Wszystkie".
**Dlaczego:**
- Natywne selecty multiple byly malo czytelne i zajmowaly za duzo miejsca w filtrach statystyk.
- Zachowanie oryginalnego selecta w DOM utrzymuje obecny kontrakt GET i fallback bez JavaScript.
- Brak zmian w schemacie DB i logice agregacji statystyk.
> Chronologiczny log zmian technicznych — co i dlaczego.
## 2026-04-27 — Phase 108 Plan 02: Automation Dropdowns z DB
**Co zrobiono:**
- `AutomationController` — usunięto stałą `SHIPMENT_STATUS_OPTIONS` (8 grupowych kluczy)
- Dropdown statusów w warunku `shipment_status` i akcji `update_shipment_status` ładuje statusy z DB przez `DeliveryStatus::getAllOptions()`
- Walidacja w `parseConditionValue()` i `parseActionConfig()` używa `DeliveryStatus::getAllStatuses()`
- `AutomationService` — usunięto stałą `SHIPMENT_STATUS_OPTION_MAP`; ewaluacja `evaluateShipmentStatusCondition()` porównuje klucze bezpośrednio
- `resolveStatusFromActionKey()` — bezpośredni klucz statusu z DB jako target (zamiast pierwszego z grupy)
**Dlaczego:**
- Zamknięcie integracji z Plan 01 — operator dodaje status w `/settings/delivery-statuses` i jest on od razu dostępny w dropdownach automatyzacji bez deploymentu
- Eliminacja kolizji semantycznej: stary klucz grupowy `picked_up` mapował na `delivered` (paczka odebrana przez klienta), nowy klucz DB `picked_up` to "Odebrana przez kuriera" (od nadawcy)
- BREAKING: stare reguły z grupowymi kluczami (`registered`, `courier_pickup`, `dropped_at_point`, `unclaimed`, `picked_up_return`, oraz `picked_up`/`ready_for_pickup`/`cancelled` w starym znaczeniu) nie matchują — wymagają ręcznego odtworzenia z nowymi kluczami DB
## 2026-04-27 — Phase 108 Plan 01: Delivery Status Management
**Co zrobiono:**
- Tabela `delivery_statuses` z seedem 11 statusów (migracja `20260427_000103`)
- `DeliveryStatusRepository` — CRUD + per-request cache
- `DeliveryStatus.php` — dynamiczne ładowanie statusów z DB (`setRepository()`)
- Panel `/settings/delivery-statuses` z CRUD (zakładka "Statusy") i mapowaniem (zakładka "Mapowanie dostawy")
- Sidebar: "Statusy" → "Statusy zamówień", nowe "Statusy przesyłek" z badge niezmapowanych
- Badge przesyłek: inline CSS custom property `--status-color` dla niestandardowych statusów
**Dlaczego:**
- Dodanie nowego statusu wymagało zmiany kodu + deploymentu; teraz z UI
- Operator może definiować własne statusy znormalizowane bez ingerencji w kod