Files
orderPRO/.paul/phases/113-fakturownia-integration/113-01-PLAN.md
Jacek Pyziak 2382018739 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>
2026-05-10 22:11:55 +02:00

21 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, delegation
phase plan type wave depends_on files_modified autonomous delegation
113-fakturownia-integration 01 execute 1
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
true off
## 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)
## 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

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

<acceptance_criteria>

AC-1: Schema DB dla faktur i Fakturowni

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

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

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>

Task 1: Migracje DB — invoices, invoice_configs, invoice_number_counters, fakturownia_integration_settings, orders.invoice_requested 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 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).
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. AC-1 satisfied: schema + db_schema.md spójne. Task 2: FakturowniaIntegrationRepository + FakturowniaApiClient (read/save/test) src/Modules/Settings/FakturowniaIntegrationRepository.php, src/Modules/Settings/FakturowniaApiClient.php, bootstrap/app.php, .paul/codebase/architecture.md `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).
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. AC-2 satisfied: szyfrowanie + test API działa, integrations.last_test_* zapisuje przez IntegrationsRepository. Task 3: FakturowniaIntegrationController + view + routes + karta w hubie 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 `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`.
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). AC-3 satisfied: pełen flow integracji Fakturowni działa manualnie.

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

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