--- phase: 115-invoice-from-order plan: 01 subsystem: accounting tags: [fakturownia, invoice, dompdf, nip-lookup, mf-whitelist, vat] requires: - phase: 113-fakturownia-integration provides: [tabele invoices/invoice_configs/invoice_number_counters/fakturownia_integration_settings, FakturowniaApiClient stub, IntegrationsRepository::updateTestResult] - phase: 114-accounting-configs-refactor provides: [InvoiceConfigRepository CRUD, hub `/settings/accounting`, seed Domyslny VAT config] provides: - Wystawianie faktury z zamowienia (lokalne + delegowane przez Fakturownie) - Toggle orders.invoice_requested w UI + auto-import flagi z Allegro/shopPRO - Auto-fill nabywcy z NIP przez MF Biala Lista (publiczny endpoint) - Lista wystawionych faktur z filtrami + podglad/PDF dual-path (Dompdf lokalny / redirect 302 Fakturownia) affects: [v3.7 next phases, statistics_orders (jezeli kiedys liczy faktury), automation (jezeli kiedys invoice.created event)] tech-stack: added: [MfWhitelistApiClient (Core/Http)] patterns: - "Dual-mode invoice issuance: lokalna numeracja (atomowy counter) lub delegacja do API z importem rezultatu" - "Snapshot pattern: seller/buyer/items zamrozone jako JSON niezaleznie od trybu wystawienia" - "Public NIP lookup przez MF Biala Lista — bez rejestracji/klucza" key-files: created: - src/Modules/Accounting/InvoiceRepository.php - src/Modules/Accounting/InvoiceService.php - src/Modules/Accounting/InvoiceController.php - src/Modules/Accounting/InvoiceIssueException.php - src/Core/Http/MfWhitelistApiClient.php - resources/views/accounting/invoice_form.php - resources/views/accounting/invoice_preview.php - resources/views/accounting/invoice_pdf.php - resources/views/accounting/invoices_issued_list.php - public/assets/js/modules/invoice-requested-toggle.js modified: - src/Modules/Settings/FakturowniaApiClient.php - src/Modules/Orders/OrdersController.php - src/Modules/Orders/OrdersRepository.php - src/Modules/Settings/AllegroOrderImportService.php - src/Modules/Settings/ShopproOrdersSyncService.php - resources/views/orders/show.php - resources/views/layouts/app.php - resources/views/settings/accounting.php - routes/web.php key-decisions: - "GUS lookup: switch z (nieistniejacego) Fakturownia endpoint na MF Biala Lista — public API, bez rejestracji" - "Fakturownia payload: NIE wysylamy seller_* — security konta interpretuje jako 'utworz nowy dzial' i odrzuca" - "Delegated invoice: POST do Fakturowni PRZED INSERT lokalnym; on success zapis external_invoice_id+pdf_url+numer Fakturowni" - "PHP 8.5: usuniete wszystkie curl_close() (deprecated, wycieka HTML do JSON response)" - "invoice_requested auto-set tylko przy created=true; manualny toggle nie nadpisywany przez delta-only re-import" - "PDF dual-path: lokalna -> Dompdf inline; delegowana -> redirect 302 do external_pdf_url (brak cache)" patterns-established: - "Confirm dialogs: OrderProAlerts.confirm options-object API ({title, message, onConfirm, danger}) — kontynuacja Phase 114" - "AJAX endpoint zwracajacy JSON: zawsze try/catch Throwable na koncu controller method, mapowanie do {success, error}" - "Vanilla JS modul z idempotent data-bound guard (jak invoice-requested-toggle.js)" duration: ~4h started: 2026-05-10T~14:00:00Z completed: 2026-05-10T~18:00:00Z --- # Phase 115 Plan 01: Wystawianie faktury z zamowienia — Summary **Vertical slice "zamowienie z NIP → faktura PDF" dziala end-to-end w trybach lokalnym (Dompdf + lokalna numeracja z `invoice_number_counters`) i delegowanym (POST do Fakturowni + redirect 302 do natywnego PDF). NIP rozwijany przez MF Biala Liste. Phase 115 zamyka v3.7 milestone w wymiarze "wystawianie".** ## Performance | Metric | Value | |--------|-------| | Duration | ~4h (planowanie+implementacja+smoke+5 iteracji bugfix) | | Tasks | 4 zaplanowane + 1 dodany w trakcie (Task 3b: NIP lookup) | | Files created | 10 | | Files modified | 9 | | New DB migrations | 0 (Phase 113 wystarczyl) | ## Acceptance Criteria Results | Criterion | Status | Notes | |-----------|--------|-------| | AC-1: Flaga invoice_requested ustawia widocznosc przycisku | Pass | Toggle dziala AJAX-em, log w order_activity_log; auto-set z Allegro `invoice.required`; shopPRO heuristic dla 5 znanych kluczy payloadu | | AC-2: Wystawienie faktury — tryb lokalny | Pass | Numer z `invoice_number_counters` (atomowy), snapshoty zamrozone, Dompdf renderuje fakture VAT | | AC-3: Wystawienie faktury — tryb delegowany (Fakturownia) | Pass | Smoke test approved przez usera: POST przed INSERT, sukces -> external_invoice_id zapisane, blad -> brak wiersza w invoices + czytelny komunikat | | AC-4: Lista, podglad i PDF | Pass | `/settings/accounting/invoices/issued` z filtrami; PDF lokalny przez Dompdf, delegowany przez redirect 302 do external_pdf_url | ## Accomplishments - Operator widzi przycisk "Wystaw fakture" tylko gdy `invoice_requested=1` (auto z importu lub manualnie z zakladki Platnosci) - Formularz prefilled NIP-em z payload_json zamowienia + "Pobierz z GUS" wypelnia 4 pola firmy z MF Biala Lista jednym klikiem - Dual flow działa: lokalne faktury z Dompdf PDF, delegowane z natywnym PDF Fakturowni — operator wybiera config, reszta automatyczna - Lista `/settings/accounting/invoices/issued` z filtrami (search/config/mode/date) i paginacja - Snapshot pattern: lokalna kopia `seller/buyer/items_json` w `invoices` niezalezna od trybu i przyszlych zmian zrodla - 5 bug-fixow iteracyjnych w trakcie smoke testu — caly pipeline error reporting/parser uodporniony na PHP 8.5 i edge cases Fakturowni ## Files Created/Modified | File | Change | Purpose | |------|--------|---------| | src/Modules/Accounting/InvoiceRepository.php | Created | CRUD + atomowy nextLocalNumber + paginate z filtrami | | src/Modules/Accounting/InvoiceService.php | Created | Orchestrator dual-flow (lokalny/delegowany) + buildFakturowniaPayload + extractBuyerTaxNumber | | src/Modules/Accounting/InvoiceController.php | Created | Endpointy create/store/show/pdf/issuedList/nipLookup | | src/Modules/Accounting/InvoiceIssueException.php | Created | Typed exception dla bledow biznesowych | | src/Core/Http/MfWhitelistApiClient.php | Created | Klient MF Biala Lista — public API, parser adresu | | src/Modules/Settings/FakturowniaApiClient.php | Modified | createInvoice() (POST /invoices.json), buildPdfUrl(); usuniete curl_close (PHP 8.5); flatten errors parser | | src/Modules/Orders/OrdersController.php | Modified | toggleInvoiceRequested AJAX + invoices/invoiceConfigs do view + 2 optional ctor params | | src/Modules/Orders/OrdersRepository.php | Modified | setInvoiceRequested() | | src/Modules/Settings/AllegroOrderImportService.php | Modified | Auto-set invoice_requested z payload.invoice.required przy created=true | | src/Modules/Settings/ShopproOrdersSyncService.php | Modified | shouldRequestInvoice() flexible parser dla 5 znanych kluczy payloadu | | resources/views/accounting/invoice_form.php | Created | Form wystawiania + "Pobierz z GUS" + confirm dialog dla duplikatu | | resources/views/accounting/invoice_preview.php | Created | HTML preview faktury z badgem Tryb | | resources/views/accounting/invoice_pdf.php | Created | Dompdf template "FAKTURA VAT" | | resources/views/accounting/invoices_issued_list.php | Created | Lista wystawionych z filtrami + paginacja | | resources/views/orders/show.php | Modified | Toggle w zakladce Platnosci + warunkowy przycisk + sekcja Faktury w tabie Documents | | resources/views/layouts/app.php | Modified | Rejestracja invoice-requested-toggle.js | | resources/views/settings/accounting.php | Modified | Link "Faktury wystawione" w karcie Faktury | | public/assets/js/modules/invoice-requested-toggle.js | Created | Vanilla JS AJAX toggle z idempotent bound guard | | routes/web.php | Modified | 6 nowych routes + DI wiring InvoiceService/InvoiceController/InvoiceRepository | ## Decisions Made | Decision | Rationale | Impact | |----------|-----------|--------| | Switch GUS lookup na MF Biala Liste | Fakturownia API NIE MA endpointu GUS — bylo wrong assumption. MF Biala Lista publiczna, bez rejestracji, zwraca te same dane (nazwa+adres) | `/api/nip/lookup` zamiast `/api/fakturownia/gus-lookup`; usuniete deps na Fakturownia client w lookupie | | Nie wysylamy seller_* do Fakturowni | Konta Fakturowni z podwyzszonym security interpretuja roznice w seller_bank_account jako proba "utworz nowy dzial" -> HTTP 422 | Fakturownia uzywa danych konta jako sprzedawca; lokalny snapshot w invoices.seller_data_json zachowany dla audytu | | Delegated: POST przed INSERT lokalnym | Gwarancja braku orphan rows w `invoices` gdy API padnie | Trade-off: brak idempotencji przy double-POST -> INVOICE-IDEMP-115 w todo.md | | PHP 8.5: usuniete wszystkie curl_close() | Deprecated od 8.5 (no-op od 8.0), wycieka HTML
Deprecated...
przed JSON -> "json is not valid" w fetch().json() | Dwa pliki: MfWhitelistApiClient + FakturowniaApiClient (2 miejsca). ShopproIntegrationsRepository ma to samo (poza zakresem 115) | | Pominiete department_id w payloadzie | Konta Fakturowni z security level blokuja samo wystepowanie pola jako "create new department" attempt | Fakturownia uzywa domyslnego dzialu konta | | Auto-set invoice_requested tylko przy created=true | Delta-only re-import (Phase 112) zachowuje stabilnosc manualnej flagi operatora | Manualny toggle nie zostaje nadpisany przez re-import | ## Deviations from Plan ### Summary | Type | Count | Impact | |------|-------|--------| | Auto-fixed | 5 | Wszystkie podczas smoke testu - essential, no scope creep | | Scope additions | 1 | Task 3b (GUS lookup) dodany po user request, potem refactor na MF | | Deferred | 2 | INVOICE-IDEMP-115 (idempotencja Fakturowni), curl_close w ShopproIntegrationsRepository | **Total impact:** Essential bugfixes podczas smoke testu (rzeczywiste zachowanie Fakturowni rozne od dokumentacji), plus 1 zaakceptowane scope-extension (NIP lookup z prefillem zamiast manualnego). Bez scope creep. ### Auto-fixed Issues **1. [GUS] FakturowniaApiClient::lookupClientByNip wolal nieistniejacy endpoint** - Found during: Task 3b smoke (klik "Pobierz z GUS") - Issue: URL `/clients/gus.json` zwracal HTML 404, json_decode null - Fix: Switch na publiczne MF Biala Lista API (`https://wl-api.mf.gov.pl/api/search/nip/{nip}`) - Files: Created `src/Core/Http/MfWhitelistApiClient.php`, removed lookupClientByNip from FakturowniaApiClient - Verification: Live test z NIP CD Projekt (7342867148) zwraca poprawne dane firmy **2. [PHP 8.5] curl_close() wycieka HTML deprecation przed JSON response** - Found during: Pierwszy smoke test "Pobierz z GUS" - Issue: `
Deprecated: curl_close() in...` pojawialo sie przed JSON -> fetch().json() throws - Fix: Usuniete `curl_close($ch)` z `httpGet()` w MfWhitelistApiClient i z `httpGet()` + `httpPostJson()` w FakturowniaApiClient - Files: src/Core/Http/MfWhitelistApiClient.php, src/Modules/Settings/FakturowniaApiClient.php - Verification: Po fixie response zaczyna sie czystym JSON, fetch parsuje OK **3. [Fakturownia] payload `payment_to_kind_days` nie istnieje w API** - Found during: Smoke wystawienia delegowanej faktury - Issue: HTTP 422 "Nieprawidlowy atrybut: 'payment_to_kind_days'" - Fix: Usuniete `payment_to_kind` i `payment_to_kind_days` (sama data `payment_to` wystarcza) - Files: src/Modules/Accounting/InvoiceService.php (buildFakturowniaPayload) - Verification: Drugi POST nie wraca z tym bledem **4. [Fakturownia] generyczny `error` bez kontekstu w parserze bledow** - Found during: Smoke iteracja 2 wystawienia delegowanej - Issue: `HTTP 422: error` — bez konkretow co odrzucone - Fix: Parser plaskuje `errors: {pole: [...]}` na `pole: msg1, msg2; pole2: msg3`, fallback do raw body, `error_log()` z 1000 znakow body - Files: src/Modules/Settings/FakturowniaApiClient.php (resolveErrorMessage + flattenFieldErrors) - Verification: Kolejny blad pokazal: `{"department":["Poziom zabezpieczenia ... nie pozwala na utworzenie dzialu"]}` **5. [Fakturownia] seller_* fields triggeruja "create new department" przy security level** - Found during: Smoke iteracja 3 wystawienia delegowanej - Issue: HTTP 422 "Poziom zabezpieczenia przed zmiana konta bankowego nie pozwala na utworzenie dzialu" — Fakturownia probowala auto-utworzyc dzial gdy seller_* roznia sie od konta - Fix: Usuniete VSZYSTKIE pola `seller_*` + `department_id` z payloadu. Fakturownia uzywa danych konta jako sprzedawca. Lokalny snapshot w `invoices.seller_data_json` zachowany dla audytu - Files: src/Modules/Accounting/InvoiceService.php (buildFakturowniaPayload) - Verification: Iteracja 4 — wystawienie sie powiodlo, user potwierdzil "Działa." ### Scope Additions **1. Task 3b: GUS/NIP lookup (post-Task 3 implementacja)** - Reason: User po smoke teście Task 1-3 zadal pytanie czy Fakturownia API wspiera pobieranie danych z NIP. Odpowiedz: nie bezposrednio. User zatwierdzil dodanie feature poprzez Fakturownia GUS endpoint (okazalo sie nie istnieje), nastepnie zaakceptowal switch na MF Biala Liste - Scope: 1 nowy client API (`MfWhitelistApiClient`), 1 endpoint controller method (`nipLookup`), 1 route, 1 przycisk + inline JS w invoice_form.php - Impact: Operator nie musi recznie wpisywac danych firmy po wpisaniu NIP ### Deferred Items - **INVOICE-IDEMP-115:** Idempotencja podwojnego POST do Fakturowni (gdy response sie nie zwroci, ale faktura w Fakturowni juz powstala) — `.paul/codebase/todo.md` - **curl_close() w ShopproIntegrationsRepository.php:** Ten sam bug klasy co fix #2, ale poza zakresem Phase 115. Sugerowane jako mini-fix lub Phase 116 ## Issues Encountered | Issue | Resolution | |-------|------------| | Bootstrap loads but DI wiring fails offline (DB connection refused) | Akceptowalne - XAMPP offline; weryfikacja przez `php -l` + autoload + reflection, smoke test przez usera na zywej bazie | | Receipt-create.php uzywa positional `OrderProAlerts.confirm(string, callback)` (legacy bug Phase 114) | Invoice form uzywa options-object API zgodnie z memory; receipt-create do osobnej fazy | | 5 iteracji bug-fix podczas smoke testu | Wszystkie potraktowane jako auto-fix, dokumentowane w deviations, pipeline zostal odporniejszy | ## Skill Audit | Expected | Invoked | Notes | |----------|---------|-------| | (brak required skills w plan frontmatter) | n/a | Plan nie zglaszal SPECIAL-FLOWS requirements | ## Next Phase Readiness **Ready:** - Phase 115 zamknieta — wystawianie faktur dziala end-to-end (lokalne + delegowane) - `MfWhitelistApiClient` w `src/Core/Http/` dostepny dla innych modulow (np. weryfikacja NIP w order_addresses, statusVat check przed wystawieniem) - Pattern dual-flow (lokalny/delegowany) jako wzorzec dla przyszlych integracji ksiegowych - `invoice_requested` flaga w `orders` table — moze byc wykorzystana w przyszlych filtrach/raportach - `invoice_pdf.php` template gotowy do dalszej stylizacji (logo, podpisy itp.) bez zmian backendu **Concerns:** - INVOICE-IDEMP-115 — przy braku idempotencji, operator musi recznie weryfikowac w panelu Fakturowni przy bledach sieci - `curl_close()` w ShopproIntegrationsRepository pozostal — ten sam bug klasy uderzy w shopPRO API calls na PHP 8.5 produkcji - Brak `invoice.created` automation event (per Phase 113 decision) — wysylka faktury mailem wymaga osobnego mechanizmu - `seller_data_json` snapshot w lokalnej DB moze odbiegac od danych faktury w Fakturowni (Fakturownia uzywa wlasnego konta) — niespojnosc tylko dla audytu, klient widzi PDF z Fakturowni **Blockers:** - None — Phase 115 closed, ready for Phase 116 lub kandydatow z roadmapu (lista faktur juz dolaczona, automation event invoice.created jako kandydat, INVOICE-IDEMP-115 jako tech-debt phase) --- *Phase: 115-invoice-from-order, Plan: 01* *Completed: 2026-05-10*