feat(113): fakturownia integration foundation

Phase 113 complete (v3.7 Invoices):
- DB: invoices, invoice_configs, invoice_number_counters, fakturownia_integration_settings + orders.invoice_requested
- FakturowniaIntegrationRepository (multi-account via integrations.type='fakturownia')
- FakturowniaApiClient (testConnection; createInvoice/downloadPdf STUBs)
- IntegrationsRepository::updateTestResult() (reusable test-result writer)
- /settings/integrations/fakturownia (list + edit + test + delete)
- Karta Fakturownia w hubie /settings/integrations

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 22:11:55 +02:00
parent 322b23b7be
commit 2382018739
20 changed files with 1766 additions and 32 deletions

View File

@@ -39,7 +39,7 @@ HTTP Request
| **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 |
| **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 |
@@ -174,3 +174,32 @@ tests/
- `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).

View File

@@ -1,6 +1,6 @@
# Database Schema
**Updated:** 2026-04-28 | **Total tables:** 55 | **Engine:** InnoDB | **Charset:** utf8mb4_unicode_ci
**Updated:** 2026-05-10 | **Total tables:** 59 | **Engine:** InnoDB | **Charset:** utf8mb4_unicode_ci
---
@@ -263,7 +263,7 @@ UNIQUE: `(integration_id, external_product_id, external_variant_id)`
| `delivery_method` | VARCHAR(128) | YES | |
| `delivery_price` | DECIMAL(12,2) | YES | |
| `delivery_tracking_number` | VARCHAR(128) | YES | |
| `notes` | TEXT | YES | |
| `invoice_requested` | TINYINT(1) | NO | DEFAULT 0 (Phase 113-01) |
| `external_created_at` | DATETIME | YES | |
| `external_updated_at` | DATETIME | YES | |
| `last_status_checked_at` | DATETIME | YES | |
@@ -274,6 +274,8 @@ UNIQUE: `(integration_id, external_product_id, external_variant_id)`
UNIQUE: `(integration_id, external_order_id)`
> Note: Order notes are stored in the separate `order_notes` table (no `notes` column on `orders`).
**order_items** — Line items within orders
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
@@ -556,6 +558,21 @@ UNIQUE: `(type, name)`
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
**fakturownia_integration_settings** — Fakturownia account credentials (Phase 113-01; multi-account via integration_id)
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `integration_id` | INT UNSIGNED | NO | UNIQUE, FK → integrations(id) CASCADE |
| `account_prefix` | VARCHAR(64) | NO | Subdomain: {prefix}.fakturownia.pl |
| `api_token_encrypted` | TEXT | YES | AES-encrypted via `IntegrationSecretCipher` |
| `department_id` | VARCHAR(64) | YES | Optional Fakturownia department |
| `default_kind` | VARCHAR(32) | NO | DEFAULT 'vat' |
| `default_payment_to_days` | TINYINT UNSIGNED | NO | DEFAULT 7 |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
UNIQUE: `(integration_id)` — one settings row per Fakturownia integration. Multiple integrations of `type='fakturownia'` allowed.
---
## Accounting / Receipts
@@ -605,6 +622,62 @@ UNIQUE: `(config_id, year, month)`
---
## Invoices (Phase 113-01)
**invoice_configs** — Invoice generation configurations (analogous to `receipt_configs` plus delegation flag)
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `name` | VARCHAR(128) | NO | |
| `integration_id` | INT UNSIGNED | YES | FK → integrations(id) SET NULL — points to Fakturownia account when delegated |
| `is_delegated` | TINYINT(1) | NO | DEFAULT 0 — when 1, invoice creation calls Fakturownia API |
| `is_active` | TINYINT(1) | NO | DEFAULT 1 |
| `number_format` | VARCHAR(64) | NO | DEFAULT 'FV/%N/%M/%Y' |
| `numbering_type` | ENUM('monthly','yearly') | NO | DEFAULT 'monthly' |
| `sale_date_source` | ENUM('order_date','payment_date','issue_date') | NO | DEFAULT 'issue_date' |
| `order_reference` | ENUM('none','orderpro','integration') | NO | DEFAULT 'none' |
| `payment_to_days` | TINYINT UNSIGNED | NO | DEFAULT 7 |
| `default_kind` | VARCHAR(32) | NO | DEFAULT 'vat' (vat/proforma/etc) |
| `created_at` | DATETIME | NO | |
| `updated_at` | DATETIME | NO | |
**invoices** — Generated invoices (snapshot pattern like `receipts`, plus external linkage when delegated)
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `order_id` | BIGINT UNSIGNED | NO | FK → orders(id) CASCADE |
| `config_id` | INT UNSIGNED | NO | FK → invoice_configs(id) RESTRICT |
| `invoice_number` | VARCHAR(64) | NO | UNIQUE |
| `issue_date` | DATETIME | NO | |
| `sale_date` | DATETIME | NO | |
| `payment_due_date` | DATETIME | YES | |
| `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 | |
| `external_invoice_id` | VARCHAR(128) | YES | Fakturownia invoice id when delegated |
| `external_pdf_url` | VARCHAR(500) | YES | URL returned by Fakturownia |
| `kind` | VARCHAR(32) | NO | DEFAULT 'vat' |
| `created_by` | INT UNSIGNED | YES | |
| `created_at` | DATETIME | NO | |
Indexes: `invoices_number_unique`, `invoices_order_idx`, `invoices_config_date_idx (config_id, issue_date)`, `invoices_external_idx`
**invoice_number_counters** — Sequential numbering per config/period (mirrors `receipt_number_counters`)
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK |
| `config_id` | INT UNSIGNED | NO | FK → invoice_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

View File

@@ -1,5 +1,33 @@
# Technical Changelog
## 2026-05-10 - Phase 113 Plan 01: Fakturownia Integration Foundation
**Co zrobiono:**
- Migracje SQL:
- `20260510_000104_create_invoices_tables.sql` - cztery nowe tabele: `invoice_configs`, `invoices`, `invoice_number_counters`, `fakturownia_integration_settings` (multi-account, `integration_id UNIQUE FK` -> `integrations`).
- `20260510_000105_add_invoice_requested_to_orders.sql` - `orders.invoice_requested TINYINT(1) NOT NULL DEFAULT 0` + index `idx_orders_invoice_requested`.
- `20260510_000106_seed_fakturownia_integration_type.sql` - no-op placeholder dokumentujacy uznanie `integrations.type='fakturownia'` jako oficjalnie wspieranego.
- `FakturowniaIntegrationRepository` - CRUD kont Fakturowni z resolved encryption (`integrations.api_key_encrypted` jako zrodlo prawdy, `settings.api_token_encrypted` jako cache). `findAll/findByIntegrationId/save/delete/getDecryptedToken`.
- `FakturowniaApiClient::testConnection()` - GET `https://{prefix}.fakturownia.pl/account.json?api_token=...` z cURL + `SslCertificateResolver`. `createInvoice`/`downloadPdf` jako STUB-y rzucajace `RuntimeException` (do implementacji w kolejnym planie).
- `IntegrationsRepository::updateTestResult()` - nowa publiczna metoda do zapisu `last_test_status / last_test_http_code / last_test_message / last_test_at`. Wykorzystywana przez `FakturowniaIntegrationController::test()`.
- `FakturowniaIntegrationController` - lista (`/settings/integrations/fakturownia`), edycja (`/edit`, `/new`), save, test, delete. CSRF via `_token`, flash `fakturownia.save/.test/.error`.
- Widoki `resources/views/settings/fakturownia.php` (lista z badge'ami) i `resources/views/settings/fakturownia-edit.php` (form: name, account_prefix, api_token, department_id, default_kind, default_payment_to_days, is_active).
- `IntegrationsHubController::buildFakturowniaRow()` - karta Fakturowni w hubie `/settings/integrations` z agregowanym statusem wszystkich kont.
- Routy w `routes/web.php` (`get /index`, `get /new`, `get /edit`, `post /save`, `post /test`, `post /delete`) + DI wiring (`$fakturowniaIntegrationRepository`, `$fakturowniaApiClient`, `$fakturowniaIntegrationController`).
- Dokumentacja: `db_schema.md` (sekcja "Invoices" + `fakturownia_integration_settings` + kolumna `orders.invoice_requested`, total tables 55 -> 59), `architecture.md` (sekcja "Phase 113").
**Dlaczego:**
- v3.7 Invoices wprowadza wystawianie faktur dla klientow wymagajacych dokumentu z NIP (clarifications: `orders.invoice_requested` z importera + manual override). Bez fundamentu DB i konfiguracji konta Fakturowni zaden kolejny plan v3.7 (CRUD configs, wystawianie, lista) nie ma sensu.
- Multi-account przez `integrations.type='fakturownia'` zachowuje spojnosc z Allegro/shopPRO (rozne instancje) i pozwala na rozne konta Fakturowni dla roznych marek/oddzialow.
- `is_delegated` flag w `invoice_configs` umozliwia w przyszlym planie dwa tryby: lokalna numeracja+PDF dompdf (default) lub delegacja do Fakturowni (numer+PDF z API).
- STUB-y `createInvoice/downloadPdf` celowo rzucaja exception zamiast "TODO" - kazda przedwczesna probaba uzycia rzuci jasny blad zamiast cichego no-op.
- `IntegrationsRepository::updateTestResult()` jest reusable - przyszle integracje (np. kolejne API) beda mogly korzystac z tej samej metody zamiast inline UPDATE.
**BREAKING:**
- Brak zmian breaking. `IntegrationsHubController` ma nowy parametr konstruktora (`FakturowniaIntegrationRepository`) - wszystkie miejsca wywolania zaktualizowane.
---
## 2026-05-07 - Phase 112 Plan 01: Re-import Data Protection
**Co zrobiono:**