Files
crmPRO/.paul/phases/05-finances-fakturownia-import/05-04-PLAN.md
2026-04-15 01:24:42 +02:00

197 lines
9.4 KiB
Markdown

---
phase: 05-finances-fakturownia-import
plan: 04
type: execute
wave: 1
depends_on: []
files_modified:
- autoload/Domain/Finances/FakturowniaApiClient.php
- autoload/Domain/Finances/FakturowniaInvoiceImporter.php
- tests/Domain/Finances/FakturowniaInvoiceImporterTest.php
autonomous: false
delegation: off
---
<objective>
## Goal
Naprawic import faktur kosztowych z Fakturowni tak, by dokumenty typu "invoice" z `income=false` (np. faktura 486639934 pod URL /invoices/486639934) byly niezawodnie pobierane, normalizowane i importowane do finansow.
## Purpose
Zgloszony bug: faktura kosztowa 486639934 nie pojawila sie w finansach po imporcie. Phase 5 dostarczyla mechanizm importu, ale obecna logika `FakturowniaApiClient::fetchCostDocuments()` i `FakturowniaInvoiceImporter::processDocumentType()` ma trzy potencjalne dziury, przez ktore dokumenty kosztowe typu `invoices.json?income=no` moga zostac calkowicie pominiete.
## Output
- Poprawiony `FakturowniaApiClient` zawsze odpytujacy `/invoices.json?income=no` dla kosztow (z `/costs.json` i `/expenses.json` jako uzupelnienie, a nie jako early-exit).
- Poprawiony `FakturowniaInvoiceImporter`: usuniete pressie "wczesnego przerwania" na podstawie daty z listy oraz warunkowe `period=this_month` dla kosztow.
- Test jednostkowy odtwarzajacy brak faktury 486639934 i potwierdzajacy fix.
- UAT: ponowny import zwraca faktura 486639934 do panelu mapowan lub do finansow.
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Prior Work
@.paul/phases/05-finances-fakturownia-import/05-01-SUMMARY.md
@.paul/phases/05-finances-fakturownia-import/05-03-SUMMARY.md
## Source Files
@autoload/Domain/Finances/FakturowniaApiClient.php
@autoload/Domain/Finances/FakturowniaInvoiceImporter.php
@autoload/Domain/Finances/FakturowniaImportRepository.php
@tests/Domain/Finances/FakturowniaInvoiceImporterTest.php
</context>
<acceptance_criteria>
## AC-1: Koszty pobierane zawsze z /invoices.json?income=no
```gherkin
Given token Fakturowni jest skonfigurowany
And faktura kosztowa istnieje pod /invoices/486639934 (income=false)
When importer wywoluje fetchCostDocuments
Then zapytanie do /invoices.json?income=no jest wykonane niezaleznie od odpowiedzi /costs.json i /expenses.json
And lista zwrocona importerowi zawiera rekord 486639934
```
## AC-2: Brak filtra period=this_month dla kosztow
```gherkin
Given FAKTUROWNIA_START_DATE jest pierwszym dniem biezacego miesiaca
When fetchCostDocuments buduje query
Then parametr period nie jest wysylany
And API zwraca takze faktury z wczesniejszych dni miesiaca wg issue_date
```
## AC-3: Importer nie przerywa paginacji na podstawie dat listy
```gherkin
Given pierwsza strona z API zawiera dokumenty posortowane malejaco po updated_at
And wszystkie dokumenty na stronie maja issue_date starsze niz startDate
When processDocumentType przetwarza strone
Then petla kontynuuje do kolejnej strony dopoki API zwraca pelna strone (per_page)
And faktura 486639934 nie jest pomijana wylacznie z powodu sortowania
```
## AC-4: Test regresyjny
```gherkin
Given test z fake'owym api clientem zwracajacym liste dokumentow z jednym kosztem income=false
When FakturowniaInvoiceImporter::import zostaje wywolany
Then dokument trafia do kolejki unmapped lub do finance_operations (nie jest skipped)
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Napraw fetchCostDocuments w FakturowniaApiClient</name>
<files>autoload/Domain/Finances/FakturowniaApiClient.php</files>
<action>
Przebuduj fetchCostDocuments tak, by:
- Zawsze probowal /invoices.json?income=no (to jest primary source — URL faktury to /invoices/ID).
- Uzupelniajaco probowal /costs.json i /expenses.json i laczyl wyniki (dedup po id), aby nie gubic dokumentow obecnych pod jednym endpointem a nieobecnych pod innym.
- NIE stosowal period=this_month dla kosztow (usun canUseCurrentMonthPeriod z tej sciezki; sales moze zostac bez zmian albo tez ma byc usuniete — zdecyduj w ramach Task 1 spojnie i udokumentuj w komentarzu).
- Zachowal softFail dla endpointow pobocznych (/costs.json, /expenses.json zwracajacych 404), ale traktowal 2xx z pusta lista jako "ta sciezka pusta" i szedl dalej zamiast zwracac wynik.
- W razie gdy wszystkie trzy sciezki zwroca puste listy BEZ bledu HTTP, zwraca [] (nie rzuca wyjatku). Wyjatek tylko gdy wszystkie trzy rzucily bledy.
Unikaj: dodawania nowych zaleznosci, zmiany sygnatur publicznych metod, ukrywania bledow HTTP.
</action>
<verify>
php -l autoload/Domain/Finances/FakturowniaApiClient.php
Manualne wywolanie z tokenem testowym: var_dump($client->fetchCostDocuments('2026-04-01', 1)) zawiera faktura 486639934.
</verify>
<done>AC-1, AC-2 spelnione</done>
</task>
<task type="auto">
<name>Task 2: Popraw paginacje i filtr dat w FakturowniaInvoiceImporter</name>
<files>autoload/Domain/Finances/FakturowniaInvoiceImporter.php</files>
<action>
W processDocumentType:
- Usun wczesne przerywanie petli na podstawie !hasRelevantDateInPage. API moze sortowac po updated_at lub created_at, nie po issue_date — wczesne przerwanie gubi faktury z odroznionym issue_date.
- Zachowaj twardy limit paginacji ($page > 100) jako safety-valve.
- Zostaw filtr "za stare" na poziomie processSingleDocument (tam gdzie porownuje $document['date'] z startDate). Ten filtr dziala per-dokument i jest bezpieczny.
Dodatkowo w normalizeDocument: dla dokumentow bez positions w liscie (gdy API zwraca skrocona forme), upewnij sie ze fallback do apiClient->fetchInvoiceDetails dziala i nie konsumuje zbyt wielu requestow (logika juz jest w resolvePositions — zweryfikuj ze nie regresuje przy naprawie).
Unikaj: rekurencyjnego pobierania wszystkich stron w nieskonczonosc, cichego polykania wyjatkow API.
</action>
<verify>
php -l autoload/Domain/Finances/FakturowniaInvoiceImporter.php
php tests/run.php (istniejace testy przechodza).
</verify>
<done>AC-3 spelnione</done>
</task>
<task type="auto">
<name>Task 3: Test regresyjny dla faktury kosztowej typu invoice</name>
<files>tests/Domain/Finances/FakturowniaInvoiceImporterTest.php</files>
<action>
Dodaj test case "cost_invoice_from_invoices_endpoint_is_imported":
- Zamockuj FakturowniaApiClient (interface lub testowa podklasa) tak, by fetchCostDocuments zwracalo [[
'id' => 486639934,
'number' => 'FV 100/04/2026',
'kind' => 'vat',
'income' => false,
'issue_date' => '2026-04-05',
'seller_name' => 'Dostawca Sp. z o.o.',
'seller_tax_no' => '5252344567',
'positions' => [[ 'name' => 'Uslugi IT', 'total_price_net' => 1230.00, 'quantity' => 1 ]]
]].
- Uruchom importer z czystymi tabelami mapowan.
- Oczekuj: summary['unmapped'] === 1 (bo brak mapowania klienta) oraz wpis w kolejce unmapped dla client_key='tax:5252344567'.
- Dodatkowy test: z pre-zalozonym mapowaniem klienta i pozycji — oczekuj summary['imported'] === 1 i rekordu w finance_operations z amount = -1230.00.
Unikaj: zaleznosci od prawdziwego API, pomijania czyszczenia bazy testowej miedzy testami.
</action>
<verify>php tests/run.php — oba nowe testy zielone.</verify>
<done>AC-4 spelnione</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>Poprawiony import kosztow z Fakturowni + test regresyjny.</what-built>
<how-to-verify>
1. Uruchom import recznie: otworz /finances/main_view/ i kliknij "Importuj z Fakturowni" (lub wywolaj cron.php).
2. Sprawdz komunikat podsumowania — powinno byc co najmniej 1 nowy rekord w kategoriach imported/unmapped.
3. W bazie: SELECT * FROM fakturownia_import_documents WHERE external_id = '486639934' — rekord istnieje.
4. Jesli klient niezmapowany: panel mapowan w /finances/main_view/ pokazuje faktura 486639934 w kolejce unmapped.
5. Po zmapowaniu klienta i pozycji: ponowny import tworzy rekord w finance_operations z ujemna kwota (koszt).
</how-to-verify>
<resume-signal>Napisz "approved" lub opisz problem do naprawy.</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- FakturowniaImportRepository.php (schemat tabel zablokowany — zmiany tylko jesli niezbedne do poprawki bugu).
- Logika mapowania klientow po NIP (05-02) — nie modyfikuj buildClientKey / normalizeTaxNo.
- Logika filtra proforma (05-03) — isProformaDocument zostaje bez zmian.
- Publiczny kontrakt FakturowniaInvoiceImporter::import() (zwracany shape summary).
## SCOPE LIMITS
- Nie dodajemy nowych endpointow API Fakturowni (tylko naprawiamy uzycie istniejacych).
- Nie zmieniamy UI panelu mapowan w /finances/main_view/.
- Nie wprowadzamy nowego cachingu ani retry-logic — tylko poprawa logiki pobierania/paginacji.
- Nie refaktoryzujemy importera pod testy E2E z prawdziwym API.
</boundaries>
<verification>
- [ ] php -l dla wszystkich zmienionych plikow
- [ ] php tests/run.php — wszystkie testy zielone
- [ ] Manualny import pobieral faktura 486639934 (checkpoint human-verify)
- [ ] Wszystkie AC zaspokojone
</verification>
<success_criteria>
- Faktura 486639934 pojawia sie w wynikach importu (imported lub unmapped, zaleznie od mapowan).
- Nowy test regresyjny przechodzi i zostaje w suite.
- Zadne istniejace testy nie regresuja.
- Brak nowych warningow php -l.
</success_criteria>
<output>
Po zakonczeniu utworz `.paul/phases/05-finances-fakturownia-import/05-04-SUMMARY.md` z:
- opisem zmian w API client i importerze,
- root cause bugu,
- przyklad payloadu z API Fakturowni dla faktury 486639934,
- wynikami UAT.
</output>