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

@@ -0,0 +1,335 @@
---
phase: 113-fakturownia-integration
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- database/migrations/20260510_000104_create_invoices_tables.sql
- database/migrations/20260510_000105_add_invoice_requested_to_orders.sql
- database/migrations/20260510_000106_seed_fakturownia_integration_type.sql
- src/Modules/Settings/FakturowniaIntegrationRepository.php
- src/Modules/Settings/FakturowniaApiClient.php
- src/Modules/Settings/FakturowniaIntegrationController.php
- src/Modules/Settings/IntegrationsHubController.php
- routes/web.php
- bootstrap/app.php
- resources/views/settings/integrations/fakturownia.php
- resources/views/settings/integrations/index.php
- .paul/codebase/db_schema.md
- .paul/codebase/architecture.md
- .paul/codebase/tech_changelog.md
autonomous: true
delegation: off
---
<objective>
## Goal
Położyć fundament pod moduł faktur: zbudować schema DB (tabele `invoices`, `invoice_configs`, `invoice_number_counters`, `fakturownia_integration_settings`, kolumna `orders.invoice_requested`) oraz dostarczyć działający CRUD ustawień integracji Fakturownia (`/settings/integrations/fakturownia`) wraz z testem połączenia API.
## Purpose
v3.7 Invoices wprowadza wystawianie faktur dla klientów wymagających dokumentu z NIP. Faktury będą numerowane lokalnie (analogicznie do paragonów z Phase 812), z opcją delegacji do Fakturowni w `invoice_configs.integration_id`. Pierwszy plan przygotowuje grunt: bez DB i konfiguracji integracji żaden kolejny plan (CRUD configs, wystawianie, lista) nie ma sensu.
## Output
- 3 migracje SQL + zaktualizowane `db_schema.md`
- `FakturowniaIntegrationRepository`, `FakturowniaApiClient`, `FakturowniaIntegrationController` (wzorzec InPost/Apaczka)
- Routy `/settings/integrations/fakturownia` (GET form, POST save, POST test)
- Karta "Fakturownia" w hubie `/settings/integrations`
- Szyfrowanie `api_token` przez `IntegrationSecretCipher` (spójne z InPost/Apaczka)
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
@.paul/codebase/architecture.md
@.paul/codebase/db_schema.md
## Reference patterns (do naśladowania, nie modyfikowania)
@src/Modules/Settings/InpostIntegrationController.php
@src/Modules/Settings/InpostIntegrationRepository.php
@src/Modules/Settings/ApaczkaApiClient.php
@src/Modules/Settings/IntegrationSecretCipher.php
@src/Modules/Settings/IntegrationsHubController.php
@database/migrations/20260315_000043_create_receipts_tables.sql
## Routy/bootstrap
@routes/web.php
@bootstrap/app.php
## Hub integracji
@resources/views/settings/integrations/index.php
<clarifications>
- **Generowanie** — Kto generuje PDF i numer faktury?
→ Odpowiedź: Domyślnie lokalnie, ale `invoice_configs` musi mieć opcję delegacji do wybranej integracji Fakturowni — wtedy numeracja po stronie Fakturowni, orderPRO pobiera PDF.
- **Schema** — Gdzie zapisujemy faktury w DB?
→ Odpowiedź: Nowa tabela `invoices` (rozdzielona od `receipts`).
- **Automation** — Czy `receipt.created` ma odpalać się dla faktur?
→ Odpowiedź: Brak eventu dla faktury (na razie). `receipt.created` pozostaje wyłącznie dla paragonów — wystawienie faktury nie emituje tego eventu.
- **Wybór dokumentu** — Co determinuje czy klient chce fakturę?
→ Odpowiedź: Kolumna `orders.invoice_requested` (TINYINT(1)) ustawiana przez importer Allegro/shopPRO oraz ręcznie przełączalna w UI zamówienia (checkbox).
- **Integracja** — Jak modelujemy konto Fakturowni?
→ Odpowiedź: Wpis w `integrations` (type='fakturownia') + `fakturownia_integration_settings(integration_id)`. Spójne z Allegro/InPost/Apaczka, wspiera wiele kont.
- **Override** — Czy `invoice_requested` przetłączalne w UI?
→ Odpowiedź: Tak — checkbox w szczegółach zamówienia (implementacja w kolejnym planie, ale kolumna DB tworzona teraz).
- **UI Księg.** — Strona edycji konfiguracji?
→ Odpowiedź: Osobne podstrony dla `receipt-configs` i `invoice-configs` (poza zakresem tego planu — zostanie w 113-02 / 114-01).
</clarifications>
</context>
<acceptance_criteria>
## AC-1: Schema DB dla faktur i Fakturowni
```gherkin
Given puste schema bazy bez tabel faktur
When uruchomię `php bin/migrate.php`
Then powstają tabele `invoices`, `invoice_configs`, `invoice_number_counters`, `fakturownia_integration_settings`
And `orders` zyskuje kolumnę `invoice_requested TINYINT(1) NOT NULL DEFAULT 0`
And `integrations.type` akceptuje wartość `'fakturownia'`
And wszystkie tabele mają `InnoDB + utf8mb4_unicode_ci`
And `invoices.invoice_number` jest UNIQUE
And `invoice_number_counters` ma UNIQUE `(config_id, year, month)`
And `fakturownia_integration_settings.integration_id` jest UNIQUE FK `integrations(id) ON DELETE CASCADE`
```
## AC-2: Repozytorium i klient API Fakturowni
```gherkin
Given operator skonfigurował token API Fakturowni w UI
When kontroler ustawień zapisuje token przez `FakturowniaIntegrationRepository::save()`
Then token jest szyfrowany przez `IntegrationSecretCipher::encrypt()` zanim trafi do `api_token_encrypted`
And `FakturowniaApiClient::testConnection()` wykonuje GET `https://{prefix}.fakturownia.pl/account.json?api_token=...` z `cURL` i `SslCertificateResolver` (spójne z `ApaczkaApiClient`)
And przy HTTP 200 zwraca `['ok' => true, 'http_code' => 200, 'message' => 'OK']`
And przy 401/404 zwraca `['ok' => false, ...]` z czytelną wiadomością
And po teście `integrations.last_test_status/last_test_http_code/last_test_at` są zaktualizowane (przez `IntegrationsRepository::updateTestResult()`)
```
## AC-3: UI ustawień integracji Fakturownia
```gherkin
Given zalogowany operator wchodzi na `/settings/integrations/fakturownia`
When otwiera formularz integracji
Then widzi pola: `name`, `account_prefix` (subdomena), `api_token`, `department_id` (opcjonalne), `kind` (vat/invoice domyślnie `vat`), `is_active`
And może kliknąć "Testuj połączenie" wynik wyświetla się inline jako flash message (`Flash::set('fakturownia.test')`)
And po zapisaniu wraca na `/settings/integrations` z flashem `fakturownia.save = "Zapisano"`
And karta "Fakturownia" pojawia się w `/settings/integrations` (hub) z aktualnym statusem ostatniego testu
And CSRF chroniony przez `_token` (zgodnie z konwencją projektu)
And istniejący `api_token_encrypted` NIE jest wyświetlany w polu (placeholder "********")
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Migracje DB — invoices, invoice_configs, invoice_number_counters, fakturownia_integration_settings, orders.invoice_requested</name>
<files>database/migrations/20260510_000104_create_invoices_tables.sql, database/migrations/20260510_000105_add_invoice_requested_to_orders.sql, database/migrations/20260510_000106_seed_fakturownia_integration_type.sql, .paul/codebase/db_schema.md</files>
<action>
Migracja `20260510_000104_create_invoices_tables.sql`:
- `invoice_configs` (struktura jak `receipt_configs`, ale dodatkowo:
`integration_id INT UNSIGNED NULL`, FK → `integrations(id) ON DELETE SET NULL`,
`is_delegated TINYINT(1) NOT NULL DEFAULT 0`,
`number_format VARCHAR(64) NOT NULL DEFAULT 'FV/%N/%M/%Y'`,
`numbering_type ENUM('monthly','yearly') DEFAULT 'monthly'`,
`order_reference ENUM('none','orderpro','integration') DEFAULT 'none'`,
`is_active TINYINT(1) NOT NULL DEFAULT 1`,
`name VARCHAR(128) NOT NULL`,
`created_at/updated_at`).
- `invoices` (analogicznie do `receipts`):
`id`, `order_id BIGINT UNSIGNED NOT NULL` FK → `orders(id) ON DELETE CASCADE`,
`config_id INT UNSIGNED NOT NULL` FK → `invoice_configs(id) ON DELETE RESTRICT`,
`invoice_number VARCHAR(64) NOT NULL UNIQUE`,
`issue_date DATETIME NOT NULL`,
`sale_date DATETIME NOT NULL`,
`seller_data_json JSON NOT NULL`,
`buyer_data_json JSON NULL`,
`items_json JSON NOT NULL`,
`total_net DECIMAL(12,2) NOT NULL DEFAULT 0.00`,
`total_gross DECIMAL(12,2) NOT NULL DEFAULT 0.00`,
`order_reference_value VARCHAR(128) NULL`,
`external_invoice_id VARCHAR(128) NULL` (id z Fakturowni gdy delegowane),
`external_pdf_url VARCHAR(500) NULL` (URL do PDF z Fakturowni),
`created_by INT UNSIGNED NULL`,
`created_at DATETIME NOT NULL`.
Index: `(order_id)`, `(config_id, issue_date)`.
- `invoice_number_counters` (lustrzane do `receipt_number_counters`):
`id`, `config_id INT UNSIGNED NOT NULL` FK → `invoice_configs(id) ON DELETE CASCADE`,
`year SMALLINT UNSIGNED NOT NULL`, `month TINYINT UNSIGNED NULL`,
`last_number INT UNSIGNED NOT NULL DEFAULT 0`,
UNIQUE `(config_id, year, month)`.
- `fakturownia_integration_settings` (analogicznie do `inpost_integration_settings`):
`id TINYINT UNSIGNED` PK,
`integration_id INT UNSIGNED NULL` UNIQUE, FK → `integrations(id) ON DELETE CASCADE`,
`account_prefix VARCHAR(64) NOT NULL` (subdomena: {prefix}.fakturownia.pl),
`api_token_encrypted TEXT NULL`,
`department_id VARCHAR(64) NULL`,
`default_kind VARCHAR(32) NOT NULL DEFAULT 'vat'` (vat | proforma | receipt),
`default_payment_to_days TINYINT UNSIGNED NOT NULL DEFAULT 7`,
`created_at`, `updated_at`.
Migracja `20260510_000105_add_invoice_requested_to_orders.sql`:
- `ALTER TABLE orders ADD COLUMN invoice_requested TINYINT(1) NOT NULL DEFAULT 0 AFTER notes;`
- `ALTER TABLE orders ADD INDEX `idx_orders_invoice_requested` (`invoice_requested`);` (filtry w UI).
Migracja `20260510_000106_seed_fakturownia_integration_type.sql`:
- Brak schema change — to placeholder/comment SQL dokumentujący, że `integrations.type` akceptuje `'fakturownia'` (kolumna jest VARCHAR(32), bez ENUM). Migracja musi być idempotentna: `SELECT 1;` + komentarz blok HEAD.
- (Faktyczne wpisy `integrations` tworzone są przez UI — IntegrationsRepository::insert podczas pierwszego zapisu Fakturowni).
Aktualizacja `.paul/codebase/db_schema.md`:
- Dodaj sekcję "Invoices" (analogicznie do "Accounting / Receipts") z opisem `invoices`, `invoice_configs`, `invoice_number_counters`.
- W sekcji "Integrations" dodaj `fakturownia_integration_settings`.
- W sekcji "Orders" dodać wiersz `invoice_requested` w opisie tabeli `orders`.
- Zaktualizuj total tables count (55 → 59).
- Zaktualizuj `Updated:` na 2026-05-10.
Avoid:
- Brak FK na `invoices.config_id` bez `ON DELETE RESTRICT` (chcemy zablokować usunięcie configa, gdy istnieją faktury).
- Używania `ENUM` dla `default_kind` — trzymaj VARCHAR (rozszerzalne, spójne z `provider` w `shipment_packages`).
- Pisania `SET NAMES utf8mb4` w migracjach (klient migracji sam to ustawia).
</action>
<verify>
php bin/migrate.php (XAMPP online); następnie:
`SHOW TABLES LIKE 'invoice%'` → 3 wiersze;
`SHOW TABLES LIKE 'fakturownia%'` → 1 wiersz;
`SHOW COLUMNS FROM orders LIKE 'invoice_requested'` → 1 wiersz, TINYINT(1) NOT NULL DEFAULT 0.
</verify>
<done>AC-1 satisfied: schema + db_schema.md spójne.</done>
</task>
<task type="auto">
<name>Task 2: FakturowniaIntegrationRepository + FakturowniaApiClient (read/save/test)</name>
<files>src/Modules/Settings/FakturowniaIntegrationRepository.php, src/Modules/Settings/FakturowniaApiClient.php, bootstrap/app.php, .paul/codebase/architecture.md</files>
<action>
`FakturowniaIntegrationRepository` (final class, wzorzec `InpostIntegrationRepository`):
- Konstruktor przyjmuje `\Medoo\Medoo $db, IntegrationSecretCipher $cipher, IntegrationsRepository $integrations`.
- `findByIntegrationId(int $integrationId): ?array` — zwraca wiersz `fakturownia_integration_settings` JOIN `integrations`, deszyfruje `api_token_encrypted` do klucza `api_token` w returned array.
- `findAll(): array` — lista wszystkich integracji Fakturowni (JOIN po `integrations.type='fakturownia'`).
- `save(int $integrationId, array $data): void` — idempotentny upsert (`SELECT ... LIMIT 1` + insert/update). Szyfruje `api_token` przez `cipher->encrypt()`. Jeśli `api_token` jest pusty string — NIE nadpisuj istniejącego tokenu (zachowanie spójne z `InpostIntegrationRepository`).
- `delete(int $integrationId): void` — usuwa wiersz (CASCADE z integrations).
`FakturowniaApiClient` (final class, wzorzec `ApaczkaApiClient`):
- Konstruktor przyjmuje `SslCertificateResolver $ssl, int $timeoutSeconds = 10`.
- `testConnection(string $prefix, string $apiToken): array` — GET `https://{prefix}.fakturownia.pl/account.json?api_token={apiToken}` z cURL, weryfikacja SSL przez `$ssl->resolveCaBundle()`. Zwraca `['ok' => bool, 'http_code' => int, 'message' => string]`.
- `createInvoice(array $settings, array $payload): array` — STUB (rzuć `\RuntimeException('Not implemented in Phase 113-01')`) — implementacja w kolejnym planie.
- `downloadPdf(array $settings, string $invoiceId): string` — STUB.
- Sprawdzaj puste odpowiedzi, decoduj JSON z `JSON_THROW_ON_ERROR`.
Bootstrap (`bootstrap/app.php`):
- Dodaj rejestrację `FakturowniaIntegrationRepository` oraz `FakturowniaApiClient` w containerze (wzorzec InPost).
Architecture doc (`.paul/codebase/architecture.md`):
- W tabeli "Module Inventory" zaktualizuj Settings (51+ → 54+ files; dodać wzmianka o Fakturownia w "OAuth clients, API clients").
- Dodać sekcję "Phase 113 — Fakturownia Integration Foundation" z krótkim opisem repo/clienta + flow testConnection.
Avoid:
- Używania `file_get_contents()` zamiast cURL (potrzebujemy SslCertificateResolver, spójność z resztą).
- `final` na `IntegrationSecretCipher` jest już — nie modyfikuj.
- Logowania niezaszyfrowanego tokenu nigdzie (w błędach API, w activity log).
</action>
<verify>
Manualne: w `php -a`:
`$client = new App\Modules\Settings\FakturowniaApiClient(new App\Modules\Core\SslCertificateResolver());`
`var_dump($client->testConnection('demo', 'invalid-token'));``['ok' => false, 'http_code' => 401, ...]`.
PHPUnit (jeśli vendor obecny): `vendor/bin/phpunit tests/Unit/Settings/FakturowniaIntegrationRepositoryTest.php` (test podstawowy upsertu — mockowany Medoo). Jeśli vendor brak — testy manualnie po wdrożeniu.
</verify>
<done>AC-2 satisfied: szyfrowanie + test API działa, integrations.last_test_* zapisuje przez IntegrationsRepository.</done>
</task>
<task type="auto">
<name>Task 3: FakturowniaIntegrationController + view + routes + karta w hubie</name>
<files>src/Modules/Settings/FakturowniaIntegrationController.php, src/Modules/Settings/IntegrationsHubController.php, routes/web.php, resources/views/settings/integrations/fakturownia.php, resources/views/settings/integrations/index.php, .paul/codebase/tech_changelog.md</files>
<action>
`FakturowniaIntegrationController` (final, wzorzec `InpostIntegrationController`):
- Konstruktor: `Template, Translator, AuthService, FakturowniaIntegrationRepository, FakturowniaApiClient, IntegrationsRepository`.
- `index(Request $request): Response` — list view (lista wszystkich integracji Fakturowni, link "Dodaj" + "Edytuj" per wpis).
- `edit(Request $request): Response` — form (GET); param `id` z `$request->input('id')` zgodnie z konwencją routera. Jeśli brak `id` — form nowej integracji.
- `save(Request $request): Response` — CSRF check (`_token`), walidacja (`name` required, `account_prefix` required + regex `^[a-z0-9-]+$`, `api_token` required przy tworzeniu nowej), upsert do `integrations` (`type='fakturownia'`) + `fakturownia_integration_settings`. Flash `fakturownia.save` po sukcesie, redirect na `/settings/integrations`.
- `test(Request $request): Response` — wywołuje `FakturowniaApiClient::testConnection()` + `IntegrationsRepository::updateTestResult()`. Flash `fakturownia.test` z wynikiem.
- `delete(Request $request): Response` — CSRF check, usuwa `integrations` (CASCADE usuwa settings). Blokuj usuniecie gdy istnieje `invoice_configs.integration_id = X`.
View `resources/views/settings/integrations/fakturownia.php`:
- Layout `settings/layout` jak Apaczka/InPost.
- Lista (gdy brak `id`): tabela `name | account_prefix | last_test | actions`.
- Form (gdy `id` lub `new`): pola `name`, `account_prefix`, `api_token` (type=password, placeholder=`********` gdy edycja istniejącej), `department_id`, `default_kind` (select: vat/proforma), `default_payment_to_days` (number), `is_active` (checkbox).
- Przycisk "Testuj połączenie" (POST do `/settings/integrations/fakturownia/test`).
- Flash messages: `Flash::get('fakturownia.save', '')`, `Flash::get('fakturownia.test', '')`.
- Używaj `$e()` na każdym output i `_token` w form.
Hub (`IntegrationsHubController` + `resources/views/settings/integrations/index.php`):
- Dodać sekcję/kartę "Fakturownia" w gridzie integracji (link do `/settings/integrations/fakturownia`).
- Pobieraj listę integracji Fakturowni przez `FakturowniaIntegrationRepository::findAll()` i wyświetl status (`last_test_status` z `integrations`).
Routy (`routes/web.php`):
- `GET /settings/integrations/fakturownia``FakturowniaIntegrationController::index`
- `GET /settings/integrations/fakturownia/edit``edit` (param `id` jako query/path)
- `GET /settings/integrations/fakturownia/new``edit` (bez id)
- `POST /settings/integrations/fakturownia/save``save`
- `POST /settings/integrations/fakturownia/test``test`
- `POST /settings/integrations/fakturownia/delete``delete`
- Wszystkie pod `AuthMiddleware`.
- Manualna DI w stylu existing controllers.
Tech changelog (`.paul/codebase/tech_changelog.md`):
- Dodać wpis "2026-05-10 — Phase 113-01: Invoices foundation + Fakturownia integration settings" z listą zmian (migracje, repo, client, controller, view, routes).
Avoid:
- Nowych natywnych `alert()`/`confirm()` w widoku — używaj `window.OrderProAlerts.confirm()` dla delete.
- Inline CSS w widoku — dodaj klasy `.settings-form` (już istniejące w SCSS) lub rozbuduj `resources/scss/modules/_settings.scss`.
- Bezpośrednich zapisów `$_SESSION` zamiast `Flash::set()`.
- Pola CSRF `_csrf_token` — zawsze `_token`.
</action>
<verify>
Otwórz `/settings/integrations/fakturownia/new` → widzisz pusty form.
Wypełnij prefix+token, kliknij "Testuj" → inline flash z wynikiem HTTP.
Zapisz → redirect na `/settings/integrations`, karta Fakturownia widoczna ze statusem `OK` (albo `FAIL` zależnie od testu).
`SELECT * FROM integrations WHERE type='fakturownia'` → 1 wiersz; `SELECT api_token_encrypted FROM fakturownia_integration_settings` → zaszyfrowane (nie plaintext).
</verify>
<done>AC-3 satisfied: pełen flow integracji Fakturowni działa manualnie.</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- `src/Modules/Accounting/ReceiptService.php` — logika paragonów stabilna; faktura ma własną ścieżkę (InvoiceService powstanie w kolejnym planie).
- `src/Modules/Accounting/ReceiptRepository.php` — nie mieszać z fakturami.
- `src/Modules/Automation/AutomationService.php` — brak nowych eventów w tym planie (`invoice.created` poza zakresem; decyzja: brak eventu na razie).
- `database/migrations/20260315_*_create_receipts_tables.sql` — archiwalne.
- `routes/web.php` istniejące routy paragonów — nie modyfikuj.
## SCOPE LIMITS
- Brak UI wystawiania faktury z zamówienia (oddzielny plan, np. 114-01 lub 115-01).
- Brak CRUD `invoice_configs` w UI Księgowości (oddzielny plan).
- Brak refaktoru edycji `receipt_configs` na osobną podstronę (oddzielny plan).
- Brak listy faktur w sekcji Księgowość (oddzielny plan).
- Brak togglle `invoice_requested` w UI szczegółów zamówienia (oddzielny plan; kolumna jest tworzona tu).
- Brak importerów czytających `invoice_requested` z Allegro/shopPRO (oddzielny plan).
- `FakturowniaApiClient::createInvoice/downloadPdf` to STUBy — implementacja w kolejnym planie.
</boundaries>
<verification>
Przed deklaracją zakończenia planu:
- [ ] `php bin/migrate.php` przechodzi bez błędów (XAMPP online)
- [ ] `SHOW TABLES LIKE 'invoice%'` zwraca 3 wiersze, `LIKE 'fakturownia%'` zwraca 1
- [ ] `SHOW COLUMNS FROM orders LIKE 'invoice_requested'` zwraca poprawną definicję
- [ ] PHP-CS-Fixer / lint na nowych plikach (jeśli skonfigurowane)
- [ ] Manualny smoke test: dodanie integracji Fakturownia + test API + zapis + redirect
- [ ] Token API NIE jest widoczny w plaintext w DB (`SELECT api_token_encrypted` wyglada na base64/hex)
- [ ] `.paul/codebase/db_schema.md`, `architecture.md`, `tech_changelog.md` zaktualizowane
- [ ] Wszystkie AC spełnione
</verification>
<success_criteria>
- Migracje zastosowane, schema spójne z `db_schema.md`
- Operator może skonfigurować konto Fakturowni przez UI i przetestować połączenie
- Token API szyfrowany przez `IntegrationSecretCipher`
- Karta Fakturownia widoczna w hubie integracji `/settings/integrations`
- Brak regresji w istniejących ustawieniach (Allegro/InPost/Apaczka/shopPRO)
- Plan stanowi fundament dla kolejnych planów v3.7 (CRUD configs, wystawianie, lista)
</success_criteria>
<output>
Po zakończeniu utwórz `.paul/phases/113-fakturownia-integration/113-01-SUMMARY.md` z listą dostarczonych artefaktów, decyzjami architektonicznymi (np. `default_kind='vat'`, format numeracji `FV/%N/%M/%Y`) oraz następnymi krokami (Phase 113-02 / 114).
</output>

View File

@@ -0,0 +1,207 @@
---
phase: 113-fakturownia-integration
plan: 01
subsystem: integrations
tags: [fakturownia, invoices, settings, integration, api, encryption]
requires:
- phase: 08-receipts-foundation
provides: receipt_configs/receipts/receipt_number_counters schema patterns mirrored for invoices
- phase: 35-inpost-settings
provides: IntegrationsRepository + IntegrationSecretCipher patterns
provides:
- DB: invoices, invoice_configs, invoice_number_counters, fakturownia_integration_settings, orders.invoice_requested
- FakturowniaIntegrationRepository (multi-account CRUD)
- FakturowniaApiClient (testConnection + STUBs for createInvoice/downloadPdf)
- IntegrationsRepository::updateTestResult() (reusable test-result writer)
- /settings/integrations/fakturownia (list + edit + test + delete UI)
- Fakturownia card in integrations hub
affects: [114-invoice-configs, 115-invoice-issuance, 116-invoice-list, importers Allegro/shopPRO (invoice_requested flag)]
tech-stack:
added: [Fakturownia REST API (app.fakturownia.pl)]
patterns:
- "Multi-account integration via integrations.type='fakturownia' + dedicated settings table with UNIQUE integration_id"
- "Local snapshot pattern reused: seller/buyer/items as JSON in invoices (mirroring receipts)"
- "is_delegated flag in invoice_configs - local-vs-API generation toggle"
key-files:
created:
- database/migrations/20260510_000104_create_invoices_tables.sql
- database/migrations/20260510_000105_add_invoice_requested_to_orders.sql
- database/migrations/20260510_000106_seed_fakturownia_integration_type.sql
- src/Modules/Settings/FakturowniaIntegrationRepository.php
- src/Modules/Settings/FakturowniaApiClient.php
- src/Modules/Settings/FakturowniaIntegrationController.php
- resources/views/settings/fakturownia.php
- resources/views/settings/fakturownia-edit.php
modified:
- src/Modules/Settings/IntegrationsHubController.php
- src/Modules/Settings/IntegrationsRepository.php
- routes/web.php
- .paul/codebase/db_schema.md
- .paul/codebase/architecture.md
- .paul/codebase/tech_changelog.md
key-decisions:
- "Multi-account przez integrations.type='fakturownia' (zamiast singletona jak InPost/Apaczka) - spojnosc z shopPRO + przyszla obsluga wielu marek/oddzialow"
- "is_delegated flag w invoice_configs - default lokalna numeracja+PDF, opcjonalnie delegacja do Fakturowni"
- "Brak osobnego eventu invoice.created - receipt.created pozostaje wylacznie dla paragonow (zero ryzyka cross-matching regul automatyzacji)"
- "orders.invoice_requested z importera + manual override (UI checkbox w kolejnym planie)"
- "Migracja 106 jako DDL no-op (ALTER TABLE COMMENT) zamiast SELECT 1 - SELECT zwraca result set i blokuje kolejne zapytania pod PDO unbuffered"
patterns-established:
- "Token API szyfrowany przez IntegrationSecretCipher; integrations.api_key_encrypted = zrodlo prawdy, settings.api_token_encrypted = cache"
- "ApiClient::testConnection() zwraca array ['ok', 'http_code', 'message']; controller mapuje na IntegrationsRepository::updateTestResult()"
- "Widoki list integracji: section.card + table-wrap + table.table + badge.badge--{success,muted,info}; usun przez js-confirm-delete + js-delete-btn (window.OrderProAlerts)"
- "STUB-y metod API rzucaja RuntimeException z czytelnym komunikatem 'Not implemented in Phase X' zamiast cichego no-op"
duration: 90min
started: 2026-05-10T19:00:00Z
completed: 2026-05-10T20:30:00Z
---
# Phase 113 Plan 01: Fakturownia Integration Foundation Summary
**Fundament v3.7 Invoices gotowy: schema DB (4 tabele + kolumna `orders.invoice_requested`), CRUD kont Fakturowni z testem polaczenia API i kartą w hubie integracji. Numeracja lokalna z opcja delegacji do Fakturowni (flag `is_delegated` w `invoice_configs`).**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~90min |
| Started | 2026-05-10T19:00:00Z |
| Completed | 2026-05-10T20:30:00Z |
| Tasks | 3 completed (auto, inline) |
| Files created | 8 |
| Files modified | 6 |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Schema DB dla faktur i Fakturowni | Pass | 4 nowe tabele + `orders.invoice_requested`, FK + UNIQUE wlasciwe. Migracje przeszly po naprawie `AFTER notes` (kolumna nie istnieje w `orders`) i zamianie `SELECT 1;` na `ALTER TABLE ... COMMENT` w migracji 106. |
| AC-2: Repozytorium i klient API Fakturowni | Pass | `FakturowniaIntegrationRepository::save()` szyfruje token przez `IntegrationSecretCipher` przed zapisem. `FakturowniaApiClient::testConnection()` wykonuje GET `account.json` z cURL + SslCertificateResolver. `IntegrationsRepository::updateTestResult()` (nowa metoda) zapisuje wynik testu. |
| AC-3: UI ustawien integracji Fakturownia | Pass | Lista `/settings/integrations/fakturownia` + edycja `/edit` + nowy `/new` + test + delete; CSRF `_token`; flash `fakturownia.save/.test/.error`; karta w hubie `/settings/integrations` z agregowanym statusem. Po pierwszej iteracji widok przepisany w stylu istniejacych list (`table.table` + `table-wrap` + `badge`). |
## Accomplishments
- Schema DB v3.7 Invoices gotowe: `invoices/invoice_configs/invoice_number_counters/fakturownia_integration_settings` plus `orders.invoice_requested` (multi-account przez `integrations.type='fakturownia'`).
- Pelen flow CRUD kont Fakturowni z szyfrowanym tokenem API i testem polaczenia (GET `account.json`) - operator moze dodawac/edytowac/usuwac konta i weryfikowac credentialsy bez restartu aplikacji.
- `IntegrationsRepository::updateTestResult()` jako reusable wzorzec dla przyszlych integracji (eliminuje inline UPDATE w kazdym controllerze).
- Dokumentacja techniczna zaktualizowana: `db_schema.md` (total tables 55 -> 59), `architecture.md` (sekcja Phase 113), `tech_changelog.md` (wpis 2026-05-10).
## Task Commits
Inline execution (delegation: off) - bez atomowych commits per task. Calosc do jednego git commit:
| Task | Status | Type | Description |
|------|--------|------|-------------|
| Task 1: Migracje DB | Done | feat | 3 migracje SQL + db_schema.md (4 nowe tabele, kolumna `orders.invoice_requested`) |
| Task 2: Repository + ApiClient + updateTestResult | Done | feat | FakturowniaIntegrationRepository, FakturowniaApiClient, IntegrationsRepository::updateTestResult, architecture.md |
| Task 3: Controller + views + routes + hub | Done | feat | FakturowniaIntegrationController, 2 widoki, 6 routes, hub card, tech_changelog.md |
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `database/migrations/20260510_000104_create_invoices_tables.sql` | Created | 4 tabele v3.7 Invoices |
| `database/migrations/20260510_000105_add_invoice_requested_to_orders.sql` | Created | Kolumna `orders.invoice_requested` + index |
| `database/migrations/20260510_000106_seed_fakturownia_integration_type.sql` | Created | DDL no-op stamp dla type='fakturownia' |
| `src/Modules/Settings/FakturowniaIntegrationRepository.php` | Created | CRUD kont Fakturowni (multi-account) |
| `src/Modules/Settings/FakturowniaApiClient.php` | Created | testConnection + STUB createInvoice/downloadPdf |
| `src/Modules/Settings/FakturowniaIntegrationController.php` | Created | Index/edit/save/test/delete |
| `resources/views/settings/fakturownia.php` | Created | Lista integracji (table.table + badge) |
| `resources/views/settings/fakturownia-edit.php` | Created | Form edycji integracji |
| `src/Modules/Settings/IntegrationsHubController.php` | Modified | Nowy param konstruktora + buildFakturowniaRow() |
| `src/Modules/Settings/IntegrationsRepository.php` | Modified | Nowa metoda updateTestResult() |
| `routes/web.php` | Modified | 3 use'y + DI wiring + 6 routes |
| `.paul/codebase/db_schema.md` | Modified | Sekcja Invoices + fakturownia_integration_settings + orders.invoice_requested |
| `.paul/codebase/architecture.md` | Modified | Sekcja Phase 113 + reorg Phase 108 + Module Inventory bump |
| `.paul/codebase/tech_changelog.md` | Modified | Wpis 2026-05-10 |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Multi-account przez `integrations.type='fakturownia'` (nie singleton) | Klient moze miec wiele kont Fakturowni (rozne marki/oddzialy); spojne z shopPRO | `FakturowniaIntegrationRepository::findAll()` zwraca liste; hub agreguje status; `invoice_configs.integration_id` wskazuje konkretne konto |
| `is_delegated` flag w `invoice_configs` (zamiast osobnej tabeli `invoice_delegations`) | Jeden config = jeden tryb (lokalny vs delegowany); operator wybiera per config | Kolejny plan: w `InvoiceService::generate()` rozgalez logike po `config.is_delegated` |
| Brak eventu `invoice.created` (decyzja z clarifications) | `receipt.created` pozostaje czysty - regula wysylki paragonu mailem nie zostanie odpalona dla faktury | Bezpieczenstwo automatyzacji; mozliwe rozszerzenie w przyszlosci jako osobny plan |
| Migracja 106 jako `ALTER TABLE ... COMMENT` (nie SELECT 1) | `SELECT 1;` zwraca result set i blokuje kolejne zapytania pod PDO unbuffered (`SQLSTATE[HY000] 2014`) | Wzorzec: migracje no-op zawsze jako DDL (ALTER COMMENT) - nie SELECT |
| `AFTER notes` usuniete z migracji 105 | Kolumna `notes` w `orders` nie istnieje (notatki w osobnej tabeli `order_notes`); db_schema.md byl stale | Naprawione + dodana adnotacja w db_schema.md |
| Widoki list - `table.table` + `table-wrap` + `badge.badge--*` (po feedbacku) | Spojnosc z `email-mailboxes.php`/`integrations.php`; brak inline styles strukturalnych | Wzorzec dla przyszlych list integracji |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 3 | Drobne, naprawione w czasie APPLY |
| Scope additions | 1 | Reusable helper (updateTestResult) - korzysc dla przyszlych integracji |
| Deferred | 0 | - |
**Total impact:** Bez scope creep. Trzy drobne fixy w czasie APPLY (kolumna `notes`, `SELECT 1`, styl widoku), wszystkie naprawione w jednym przebiegu.
### Auto-fixed Issues
**1. [Migration] Brak kolumny `notes` w `orders`**
- **Found during:** Task 1 verify (pierwsze uruchomienie `php bin/migrate.php`)
- **Issue:** Migracja 105 uzywala `ADD COLUMN ... AFTER notes`. Kolumna `notes` byla zadeklarowana w pierwotnej migracji 18, ale w nieznanym momencie zostala usunieta (notatki migrowane do `order_notes`). `db_schema.md` byl stale i pokazywal `notes` jako kolumne `orders`.
- **Fix:** Usunieto `AFTER notes` z migracji 105 (kolumna trafia na koniec tabeli). Zaktualizowano `db_schema.md` (usunieto wiersz `notes`, dodano notke "Order notes are stored in the separate `order_notes` table").
- **Files:** `database/migrations/20260510_000105_add_invoice_requested_to_orders.sql`, `.paul/codebase/db_schema.md`
- **Verification:** `php bin/migrate.php` po naprawie - migracja przeszla.
**2. [Migration] SELECT 1 w migracji 106 blokuje runner**
- **Found during:** Task 1 verify (drugie uruchomienie `php bin/migrate.php`)
- **Issue:** `SELECT 1;` zwraca result set ktory pozostawal otwarty przy PDO unbuffered queries -> `SQLSTATE[HY000] 2014: Cannot execute queries while other unbuffered queries are active`.
- **Fix:** Zastapiono `SELECT 1;` przez `ALTER TABLE integrations COMMENT = '...';` - bezpieczny DDL no-op ktory nie zwraca result setu i jest idempotentny.
- **Files:** `database/migrations/20260510_000106_seed_fakturownia_integration_type.sql`
- **Verification:** Migracja przeszla.
**3. [UI] Lista integracji Fakturownia z zlym stylem**
- **Found during:** User smoke test po wdrozeniu
- **Issue:** Pierwsza wersja widoku `fakturownia.php` uzywala `class="data-table"` (nie istniejace) i inline'owych stylow strukturalnych - tabela wygladala obco wzgledem reszty UI.
- **Fix:** Przepisano widok w stylu `email-mailboxes.php`: `section.card` + `table-wrap` + `table.table` + `badge.badge--{success,muted,info}` + przyciski usun przez `js-confirm-delete`/`js-delete-btn` (`window.OrderProAlerts`).
- **Files:** `resources/views/settings/fakturownia.php`
- **Verification:** User potwierdzil "Teraz jest ok".
### Scope Addition
**1. [Helper] `IntegrationsRepository::updateTestResult()`**
- **Context:** Plan zakladal "after teście `integrations.last_test_status/last_test_http_code/last_test_at` są zaktualizowane (przez `IntegrationsRepository::updateTestResult()`)" - ale metoda nie istniala w `IntegrationsRepository`.
- **Decision:** Dodano publiczna metode `updateTestResult(int $integrationId, string $status, ?int $httpCode, string $message)` zamiast inline'owego UPDATE w `FakturowniaIntegrationController::test()`.
- **Korzysc:** Reusable wzorzec dla przyszlych integracji (np. STAT-NET czy nowe API) - jeden punkt zapisu wynikow testow.
### Deferred Items
Brak. Plan wykonany w pelnym zakresie (z auto-fixami w czasie APPLY).
## Issues Encountered
| Issue | Resolution |
|-------|------------|
| MySQL/XAMPP offline przy pierwszym `php -l + bin/migrate.php` | Uruchomione po stronie usera; po naprawach 105/106 migracje przeszly. |
| Stary `notes` w `db_schema.md` | Usuniety; dodana adnotacja o `order_notes`. |
| SonarQube scan nie uruchomiony | XAMPP env; gap odnotowany do nastepnego planu (kontynuacja z Phase 108/110 gap). |
## Next Phase Readiness
**Ready:**
- Schema DB v3.7 Invoices kompletna - kolejne plany moga budowac `InvoiceService`, `InvoiceRepository`, `InvoiceConfigController` bez migracji DB.
- `FakturowniaIntegrationRepository::getDecryptedToken(int)` gotowy do uzycia przez `InvoiceService` w trybie `is_delegated=1`.
- `FakturowniaApiClient` ma STUB-y `createInvoice/downloadPdf` z jasnym RuntimeException - kolejny plan tylko implementuje cialo.
- `orders.invoice_requested` gotowe do (a) importera Allegro/shopPRO, (b) toggle w UI szczegolow zamowienia.
**Concerns:**
- Operator musi recznie utworzyc pierwszy `invoice_config` przed kolejnym planem (CRUD `invoice_configs`). Alternatywnie: kolejny plan moze dodac seed migracji z default config (`name='Domyslny VAT'`, `numbering_type='monthly'`, `is_delegated=0`).
- Refaktor edycji `receipt_configs` na osobna podstrone (z planu pierwotnego, poza zakresem 113-01) musi zostac zaplanowany - obecnie zarzadzanie `receipt_configs` jest pod tabela na `/settings/accounting`. User zaznaczyl ze chce ten sam refaktor zrobic dla paragonow przy okazji wprowadzania faktur.
**Blockers:**
- Brak.
---
*Phase: 113-fakturownia-integration, Plan: 01*
*Completed: 2026-05-10*