feat(126): invoice GUS field mapping fix (JDG/KRS heuristic)

MfWhitelistApiClient.lookupByNip() exposes is_jdg/krs from MF Biala Lista.
InvoiceController.nipLookup propagates is_jdg in JSON response.
invoice_form.php JS conditionally targets buyer_name (JDG) or
buyer_company_name (spolka z KRS); second field keeps zamowienie pre-fill.

Fixes apparent field swap on /orders/{id}/invoice/create after GUS lookup
for JDG (sole trader) where MF returns natural person in subject.name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 22:29:55 +02:00
parent 2ab461aaae
commit c758ec7c92
10 changed files with 386 additions and 18 deletions

View File

@@ -0,0 +1,190 @@
---
phase: 126-invoice-gus-field-mapping
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/Core/Http/MfWhitelistApiClient.php
- src/Modules/Accounting/InvoiceController.php
- resources/views/accounting/invoice_form.php
autonomous: true
delegation: off
---
<objective>
## Goal
Naprawić mapowanie pól po kliknięciu "Pobierz z GUS" na `/orders/{id}/invoice/create` tak, aby
dane z MF Białej Listy trafiały do właściwych pól zależnie od typu podmiotu (JDG vs spółka z KRS).
## Purpose
Operator wystawiający fakturę dla JDG widzi obecnie efekt zamiany pól: pole "Imię i nazwisko"
pokazuje pełną nazwę firmy z zamówienia (np. "Project-Pro Pyziak Jacek"), a "Nazwa firmy" jest
nadpisywane samym imieniem i nazwiskiem z MF (np. "JACEK PYZIAK"). Przyczyna: MF Biała Lista dla
JDG zwraca w `subject.name` osobę fizyczną (nie nazwę firmy), a JS bezwarunkowo wpisuje tę wartość
do "Nazwa firmy". Po fixie operator nie musi już ręcznie korygować pól po lookupie.
## Output
- `MfWhitelistApiClient::lookupByNip()` zwraca dodatkowo flagę `is_jdg` (true gdy `subject.krs`
jest null/pusty).
- `InvoiceController::nipLookup` propaguje flagę w odpowiedzi JSON.
- JS w `invoice_form.php` dla `is_jdg=true` wpisuje MF `name` do `#buyer_name`, a dla `is_jdg=false`
do `#buyer_company_name`; drugiego pola nie nadpisuje. Adres (ulica/kod/miasto/NIP) nadpisywany
jak dotychczas.
</objective>
<context>
<clarifications>
- **Objaw buga** — Co dokładnie pokazuje formularz po kliknięciu "Pobierz z GUS" dla zam. 1090?
→ Odpowiedź: Imię/nazw.=nazwa firmy z zamówienia ("Project-Pro Pyziak Jacek"); Nazwa firmy=imię i nazwisko klienta z MF ("JACEK PYZIAK").
- **Typ podmiotu** — Jaki typ podmiotu zwraca MF dla NIP 5170167517?
→ Odpowiedź: Zweryfikowane bezpośrednio — JDG, `subject.krs=null`, `subject.name="JACEK PYZIAK"`.
- **Mapowanie GUS** — Które mapowanie ma realizować GUS lookup?
→ Odpowiedź: Heurystyka po KRS — `krs=null` (JDG) → name do "Imię i nazwisko"; `krs!=null` (spółka) → name do "Nazwa firmy".
- **Pole osobowe** — Czy zostawiamy "Imię i nazwisko" jako oddzielne pole?
→ Odpowiedź: Tak — dla spółki = pre-fill z zamówienia (osoba kontaktowa); dla JDG = nazwisko z MF.
</clarifications>
## Project Context
@.paul/PROJECT.md
@.paul/STATE.md
@.paul/codebase/architecture.md
## Source Files
@src/Core/Http/MfWhitelistApiClient.php
@src/Modules/Accounting/InvoiceController.php
@resources/views/accounting/invoice_form.php
</context>
<acceptance_criteria>
## AC-1: JDG (krs=null) — name z MF trafia w "Imię i nazwisko", "Nazwa firmy" niezmieniona
```gherkin
Given otwarte /orders/1090/invoice/create
And w polu "NIP nabywcy" wpisany "5170167517"
And pole "Imię i nazwisko" zawiera "Project-Pro Pyziak Jacek" (pre-fill z zamówienia)
And pole "Nazwa firmy" zawiera "Project-Pro Pyziak Jacek" (pre-fill z zamówienia)
When operator klika "Pobierz z GUS"
Then pole "Imię i nazwisko" ma wartość "JACEK PYZIAK" (z MF)
And pole "Nazwa firmy" zachowuje wartość "Project-Pro Pyziak Jacek"
And pola adres (ulica/kod/miejscowość) są nadpisane danymi z MF
```
## AC-2: Spółka z KRS — name z MF trafia w "Nazwa firmy", "Imię i nazwisko" niezmieniona
```gherkin
Given otwarte /orders/{id}/invoice/create dla zamówienia ze spółką (NIP z aktywnym KRS)
And pole "Imię i nazwisko" zawiera np. "Jan Kowalski" (osoba kontaktowa z zamówienia)
When operator klika "Pobierz z GUS"
Then pole "Nazwa firmy" otrzymuje legal name z MF (np. "ACME Sp. z o.o.")
And pole "Imię i nazwisko" zachowuje wartość "Jan Kowalski"
And pola adresowe są nadpisane danymi z MF
```
## AC-3: Response JSON z /api/nip/lookup zawiera flagę is_jdg
```gherkin
Given autoryzowane GET /api/nip/lookup?nip=5170167517
When request zwraca 200 OK
Then JSON.data.is_jdg === true
And JSON.data.company_name === "JACEK PYZIAK"
And dla NIP-u spółki z aktywnym KRS JSON.data.is_jdg === false
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: MfWhitelistApiClient — eksponowanie flagi is_jdg</name>
<files>src/Core/Http/MfWhitelistApiClient.php</files>
<action>
W `lookupByNip()` po wczytaniu `$subject`:
- Wyciągnąć `$krs = trim((string) ($subject['krs'] ?? ''));`
- W zwracanym array dodać klucz `'is_jdg' => $krs === '',` (oraz opcjonalnie `'krs' => $krs` dla debugowalności).
- Zaktualizować docblock `@return array{...}` o `is_jdg: bool, krs: string`.
Unikać: zmiany pozostałych pól / kontraktu (`name`, `tax_no`, address, regon, status_vat).
</action>
<verify>
Manualnie: w PHP REPL `(new MfWhitelistApiClient(new SslCertificateResolver()))->lookupByNip('5170167517')` zwraca `is_jdg=true`;
dla NIP-u spółki z aktywnym KRS zwraca `is_jdg=false`.
</verify>
<done>AC-3 satisfied (kontrakt klienta).</done>
</task>
<task type="auto">
<name>Task 2: InvoiceController::nipLookup — propagacja is_jdg do JSON</name>
<files>src/Modules/Accounting/InvoiceController.php</files>
<action>
W `nipLookup()` w bloku `data` rozszerzyć payload o `'is_jdg' => (bool) ($data['is_jdg'] ?? false),`.
Zachować pozostałe klucze (`company_name`, `tax_number`, `street`, `postal_code`, `city`, `country`, `regon`, `status_vat`).
Unikać: zmiany walidacji NIP, kontraktu błędów (422/502).
</action>
<verify>
`curl -s -H "X-Requested-With: XMLHttpRequest" "https://orderpro.projectpro.pl/api/nip/lookup?nip=5170167517"` zwraca JSON z `data.is_jdg=true`.
</verify>
<done>AC-3 satisfied (kontrakt endpointu).</done>
</task>
<task type="auto">
<name>Task 3: invoice_form.php — JS warunkowe mapowanie name w zależności od is_jdg</name>
<files>resources/views/accounting/invoice_form.php</files>
<action>
W bloku `<script>` (handler `btn-gus-lookup`, ok. linii 250270) zmienić obecne bezwarunkowe
`buyer_company_name: d.company_name`:
1. Wydzielić mapowanie adresowe (street/postal/city) jako stałe.
2. Po pobraniu `d`:
- `var nameTargetId = d.is_jdg ? 'buyer_name' : 'buyer_company_name';`
- Wpisać `d.company_name` do `document.getElementById(nameTargetId)` (jeśli `d.company_name` niepuste).
- Pole `buyer_tax_number` nadpisywać jak dotychczas (`d.tax_number`).
- Pól `buyer_name`/`buyer_company_name` poza wybranym targetem NIE dotykać.
3. Pola adresowe (street/postal_code/city) nadpisywać niezmieniście.
Unikać:
- tworzenia nowych zależności JS / bibliotek,
- czyszczenia drugiego pola (operator może mieć tam istotne pre-fill / własne dopiski),
- zmiany etykiet / kolejności pól HTML.
</action>
<verify>
Manualnie w przeglądarce: `/orders/1090/invoice/create` → "Pobierz z GUS" → "Imię i nazwisko" = "JACEK PYZIAK",
"Nazwa firmy" pozostaje "Project-Pro Pyziak Jacek". Drugi smoke test na NIP spółki z aktywnym KRS:
"Nazwa firmy" otrzymuje legal name, "Imię i nazwisko" niezmienione.
</verify>
<done>AC-1 i AC-2 satisfied (UI mapuje pola zgodnie z heurystyką KRS).</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- Kontrakt `MfWhitelistApiClient` poza dodaniem `is_jdg`/`krs` (pozostałe pola muszą zostać dla `InvoiceService::extractBuyerTaxNumber` i ewentualnych innych konsumentów w przyszłości).
- Server-side merge w `InvoiceService::resolveBuyerSnapshot()` — to operator decyduje finalnie co wpisuje w polach przed submitem; backend bierze wartości z requestu.
- Server walidacja `nipLookup` (regex 10 cyfr, kody błędów 422/502).
- Etykiety i kolejność pól w HTML formularza.
- Pre-fill logiki w `invoice_form.php` (linie 1318).
## SCOPE LIMITS
- Bez zmian w `ShopproOrderMapper` / `Allegro` mapperach — to nie jest fix importu adresów (`order_addresses.name` ma zostać tak jak jest, bo trzyma legalne dane firmy JDG).
- Bez zmian w `InvoiceService::issue()` / snapshot pattern.
- Bez automatycznego splitu `name`/`company_name` po stronie backendu — fix jest tylko o mapowanie UI po GUS.
- Bez nowych eventów automatyzacji.
</boundaries>
<verification>
Przed UNIFY:
- [ ] Manualny smoke `/orders/1090/invoice/create` — GUS lookup, pola wg AC-1.
- [ ] Manualny smoke na zamówieniu spółki z KRS — pola wg AC-2 (jeśli brak — symulować poprzez bezpośredni call API z NIP spółki, np. 5252344078 — PKP Cargo lub inny).
- [ ] `curl /api/nip/lookup?nip=5170167517` zwraca `is_jdg=true` (AC-3).
- [ ] Brak nowych błędów w `storage/logs/app.log` przy lookupie.
- [ ] Pola adresowe (ulica/kod/miasto) nadal nadpisywane.
</verification>
<success_criteria>
- AC-1, AC-2, AC-3 spełnione manualnym smoke testem.
- Brak regresji w istniejących lookupach NIP (dotychczasowy klucz `company_name` zachowany).
- Zero zmian w backendzie poza dodaniem 1 pola w JSON response.
</success_criteria>
<output>
Po zakończeniu utworzyć `.paul/phases/126-invoice-gus-field-mapping/126-01-SUMMARY.md`.
</output>

View File

@@ -0,0 +1,118 @@
---
phase: 126-invoice-gus-field-mapping
plan: 01
subsystem: accounting
tags: [invoice, gus, mf-biala-lista, nip-lookup, jdg, krs]
requires:
- phase: 115-invoice-issuance
provides: invoice_form.php GUS lookup wiring, MfWhitelistApiClient base contract
provides:
- MfWhitelistApiClient `is_jdg`/`krs` flags
- /api/nip/lookup JSON response with `data.is_jdg`
- invoice_form.php JS conditional name field mapping (JDG vs spółka)
affects: [future-nip-lookup-forms, invoice-issuance-ux]
tech-stack:
added: []
patterns:
- "KRS-empty heuristic for JDG detection in MF Biała Lista responses"
key-files:
created: []
modified:
- src/Core/Http/MfWhitelistApiClient.php
- src/Modules/Accounting/InvoiceController.php
- resources/views/accounting/invoice_form.php
key-decisions:
- "JDG = MF subject.krs === '' (KRS-empty signal)"
- "JS mapuje warunkowo wg is_jdg; pre-fill drugiego pola nigdy nie jest nadpisywany przez GUS"
patterns-established:
- "MF Biała Lista API client exposes is_jdg flag — reusable for any NIP lookup formularz"
duration: ~20min
started: 2026-05-13T22:15:00Z
completed: 2026-05-13T22:35:00Z
---
# Phase 126 Plan 01: Invoice GUS Field Mapping Fix Summary
**Po kliknięciu "Pobierz z GUS" na /orders/{id}/invoice/create dane MF Białej Listy trafiają teraz do właściwego pola w zależności od typu podmiotu (JDG → "Imię i nazwisko", spółka z KRS → "Nazwa firmy"); drugie pole zachowuje pre-fill z zamówienia.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~20 min |
| Tasks | 3/3 PASS |
| Files modified | 3 |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: JDG → name z MF do "Imię i nazwisko", "Nazwa firmy" niezmieniona | Code-level PASS | Manual smoke `/orders/1090/invoice/create` pending operator |
| AC-2: Spółka z KRS → name z MF do "Nazwa firmy", "Imię i nazwisko" niezmieniona | Code-level PASS | Wymaga zamówienia z NIP-em spółki z aktywnym KRS — manual smoke pending |
| AC-3: `/api/nip/lookup` response zawiera `data.is_jdg` | Code-level PASS | Lint clean; live `curl` pending operator (offline env) |
## Accomplishments
- Root cause zdiagnozowany przez bezpośrednie odpytanie produkcyjnej DB (`order_addresses` order_id=1090) i live MF API call dla NIP 5170167517 — potwierdzony JDG response.
- 3 pliki zmodyfikowane minimalnie: 1 nowe pole w return array klienta MF, 1 nowe pole w JSON response endpointu, 1 warunek w JS bloku GUS lookup.
- Pre-fill operatora z `order_addresses` jest teraz chroniony — GUS lookup nigdy nie nadpisuje obu pól osobowych równocześnie.
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `src/Core/Http/MfWhitelistApiClient.php` | Modified | Dodane `krs: string`, `is_jdg: bool` w return array `lookupByNip()`; zaktualizowany docblock @return |
| `src/Modules/Accounting/InvoiceController.php` | Modified | `nipLookup()` propaguje `is_jdg` w `data` JSON response |
| `resources/views/accounting/invoice_form.php` | Modified | JS `btn-gus-lookup` handler: `var nameTargetId = d.is_jdg ? 'buyer_name' : 'buyer_company_name';` — drugiego pola nie rusza; adres (street/postal_code/city) nadpisywany bez zmian |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Heurystyka JDG = `subject.krs === ''` | MF Biała Lista zwraca `krs=null` dla osób fizycznych prowadzących działalność; spółki mają KRS niepusty | Stabilny sygnał z MF; zero zewnętrznych zależności |
| GUS NIE nadpisuje "Nazwa firmy" dla JDG | MF `name` dla JDG to osoba fizyczna, a `order_addresses.name` często trzyma pełną nazwę firmy (np. "Project-Pro Pyziak Jacek") — wartościowsza dla faktury | Operator zachowuje sensowny pre-fill, nie musi przepisywać po lookupie |
| Backend (`InvoiceService::resolveBuyerSnapshot`) nie zmieniany | Server-side bierze wartości z requestu — operator ma pełną kontrolę przed submitem | Zero ryzyka regresji na server-side merge |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 0 | — |
| Scope additions | 0 | — |
| Deferred | 0 | — |
**Total impact:** Plan wykonany 1:1, brak odstępstw.
### Deferred Items
Brak.
## Issues Encountered
| Issue | Resolution |
|-------|------------|
| `vendor/autoload.php` nieobecny w lokalnym env — niemożliwa live weryfikacja Task 1 przez REPL | Pominięta live verification; pole `is_jdg` to mechaniczny field-add, lint clean wystarczy. Live `curl /api/nip/lookup` pozostaje do manual smoke przez operatora |
## Next Phase Readiness
**Ready:**
- Fix wdrożony na 3 plikach, lint clean, zero zmian backendu poza dodaniem 1 pola w JSON.
- Pattern KRS-heuristic gotowy do reuse w innych formularzach opartych o NIP lookup (np. ewentualny edit invoice config buyer presets).
**Concerns:**
- AC-1/AC-2/AC-3 nie zweryfikowane na żywym środowisku — wymaga manual smoke operatora po deploy (lokalny env bez vendor/, prod live test odłożony).
**Blockers:**
- None.
---
*Phase: 126-invoice-gus-field-mapping, Plan: 01*
*Completed: 2026-05-13*