---
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 <br/>Deprecated</b>...</br> 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: `<br/><b>Deprecated</b>: curl_close() in...</b>` 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*
