feat(115): wystawianie faktury z zamowienia (lokalne + delegowane Fakturownia)
Phase 115 complete (vertical slice "zamowienie z NIP -> faktura PDF"):
- Task 1: InvoiceRepository + InvoiceService (dual-flow orchestrator) +
InvoiceIssueException + FakturowniaApiClient::createInvoice + buildPdfUrl
- Task 2: InvoiceController + OrdersController::toggleInvoiceRequested +
OrdersRepository::setInvoiceRequested + auto-import invoice_requested z
Allegro (invoice.required) i shopPRO (5-key flexible parser) + show.php
(toggle w zakladce Platnosci + warunkowy przycisk Wystaw fakture)
- Task 3: Lista wystawionych /settings/accounting/invoices/issued z filtrami
+ invoice_preview + invoice_pdf Dompdf template + hub link
- Task 3b (dodany): NIP lookup przez MF Biala Lista (publiczne API, bez
rejestracji) — MfWhitelistApiClient w src/Core/Http/ + /api/nip/lookup +
przycisk "Pobierz z GUS" w formularzu
Auto-fixes podczas smoke testu (5):
- GUS endpoint Fakturowni nie istnial (HTML 404 -> "json is not valid");
switch na MF Biala Liste
- PHP 8.5 curl_close() deprecation wycieka HTML przed JSON; usuniete z
MfWhitelistApiClient i FakturowniaApiClient (3 miejsca)
- Fakturownia 422 payment_to_kind_days (nieistniejace pole) -> usuniete
- Generic "error" w 422 -> parser plaskuje errors: {pole: [...]} +
error_log z 1000 znakow raw body
- Fakturownia security odrzuca seller_*/department_id jako "create new
department"; usuniete z payloadu (Fakturownia uzywa danych konta)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| Version | 3.7.0-dev |
|
||||
| Status | v3.7 in progress — Phases 113 (Fakturownia Foundation) + 114 (Accounting Configs Refactor) shipped |
|
||||
| Status | v3.7 in progress — Phases 113 (Fakturownia Foundation) + 114 (Accounting Configs Refactor) + 115 (Wystawianie faktury z zamowienia) shipped |
|
||||
| Last Updated | 2026-05-10 |
|
||||
|
||||
## Requirements
|
||||
@@ -118,6 +118,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
||||
- [x] Re-import istniejacego zamowienia jest delta-only: skip dla pozycji/adresow/notatek (stabilne `order_items.id`, ochrona `project_generated`), zawezony `updateOrderDelta()`, propagacja anulowania ze zrodla, identical-payload no-op guard (case #882) — Phase 112
|
||||
- [x] Fundament v3.7 Invoices: tabele `invoices`, `invoice_configs`, `invoice_number_counters`, `fakturownia_integration_settings` + `orders.invoice_requested`; CRUD kont Fakturowni z testem polaczenia API (`/settings/integrations/fakturownia`); karta w hubie integracji — Phase 113
|
||||
- [x] Ksiegowosc: refaktor `/settings/accounting` na hub-rozdroze + osobne podstrony `/receipts` i `/invoices` + edycja na osobnym widoku; pelen CRUD `invoice_configs` z opcja delegacji do Fakturowni (conditional integration_id, serwerowa walidacja); seed `Domyslny VAT`; globalny modul `confirm-delete.js` — Phase 114
|
||||
- [x] Wystawianie faktury z zamowienia: toggle `orders.invoice_requested` w zakladce Platnosci + auto-set z importu (Allegro `invoice.required` / shopPRO 5-key parser); formularz z auto-fillem NIP przez MF Biala Liste (publiczne API); dual flow lokalny (Dompdf + atomowy `invoice_number_counters`) / delegowany (POST do Fakturowni przed INSERT, redirect 302 do natywnego PDF); lista `/settings/accounting/invoices/issued` z filtrami; snapshot pattern w `invoices` JSON; PHP 8.5-compatible (curl_close removed) — Phase 115
|
||||
|
||||
### Deferred
|
||||
|
||||
@@ -126,7 +127,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
||||
|
||||
### Active (In Progress)
|
||||
|
||||
- [ ] v3.7 Invoices — wystawianie faktur dla klientow z NIP przez integracje z Fakturownia (multi-account, lokalna numeracja z opcja delegacji, rozdzielenie przyciskow paragon/faktura, osobne podstrony edycji configs). Phase 113 shipped; kolejne fazy w planowaniu.
|
||||
- [ ] v3.7 Invoices — wystawianie faktur dla klientow z NIP przez integracje z Fakturownia (multi-account, lokalna numeracja z opcja delegacji, rozdzielenie przyciskow paragon/faktura, osobne podstrony edycji configs). Phases 113 + 114 + 115 shipped; ewentualne kolejne fazy (np. eksport XLSX, invoice.created event, idempotencja Fakturowni) w kolejce.
|
||||
|
||||
### Planned (Next)
|
||||
|
||||
@@ -214,6 +215,12 @@ PHP (XAMPP/Laravel), integracje z API marketplace'Ăłw (Allegro, Erli) oraz API
|
||||
| Legacy aliasy starych endpointow `/settings/accounting/save\|toggle\|delete` jako duplicate routes | Brak inwentaryzacji zewnetrznych referencji/bookmarkow - zero kosztu utrzymania, pelna wsteczna kompatybilnosc | 2026-05-10 | Active |
|
||||
| `OrderProAlerts.confirm` to options-object API (`{title, message, onConfirm, danger, confirmLabel}`), nie pozycyjne argumenty | Pozycyjne wywolanie cicho fail'uje - callback ginie. Bug znaleziony w Phase 114-01 podczas smoke testu user. Pattern dla wszystkich przyszlych confirm dialogow | 2026-05-10 | Active |
|
||||
| Globalny `confirm-delete.js` z `data-confirm-bound='1'` idempotent guard | Stare widoki maja inline scripts robiace to samo - guard zapobiega podwojnemu bindowi gdy modul globalny widzi juz-bound buttony. Mozna stopniowo migrowac stare widoki | 2026-05-10 | Active |
|
||||
| Wystawianie faktury delegowanej do Fakturowni: POST PRZED INSERT lokalnym | Brak orphan rows w `invoices` gdy API padnie. On success zapis `external_invoice_id`/`external_pdf_url`/`invoice_number` z odpowiedzi Fakturowni. Trade-off: brak idempotencji przy double-POST -> INVOICE-IDEMP-115 w todo.md | 2026-05-10 | Active |
|
||||
| NIP lookup przez MF Biala Liste (publiczne API) zamiast Fakturowni | Fakturownia API NIE MA endpointu GUS (sprawdzone w dokumentacji 2026-05-10). MF Biala Lista jest publiczna (bez rejestracji/klucza), zwraca nazwa+adres+REGON. Klient `MfWhitelistApiClient` w `src/Core/Http/` dostepny dla innych modulow | 2026-05-10 | Active |
|
||||
| Fakturownia invoice payload: NIE wysylamy `seller_*` ani `department_id` | Konta 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 | 2026-05-10 | Active |
|
||||
| PHP 8.5: zakaz `curl_close()` w nowym kodzie | Deprecated od 8.5 (no-op od 8.0). Wycieka HTML `<br/><b>Deprecated</b>...` przed JSON response -> "json is not valid" w fetch().json(). Pattern dla wszystkich httpGet/httpPost helperow w `src/Core/Http/` i `src/Modules/Settings/*ApiClient` | 2026-05-10 | Active |
|
||||
| Auto-set `orders.invoice_requested` tylko przy `created=true` w imporcie | Delta-only re-import (Phase 112) zachowuje stabilnosc manualnej flagi operatora. Re-import nie nadpisuje stanu lokalnego, w tym manualnego "Klient prosi o fakture" | 2026-05-10 | Active |
|
||||
| `OrderProAlerts.confirm` ZAWSZE options-object API (`{title, message, onConfirm, danger}`) | Phase 114 ustalil. Phase 115 uzyl w invoice_form.php. Pozycyjne wywolanie cicho fail'uje - callback ginie. Pattern obowiazuje dla wszystkich nowych confirm dialogow | 2026-05-10 | Active |
|
||||
|
||||
## Success Metrics
|
||||
|
||||
@@ -245,6 +252,6 @@ Quick Reference:
|
||||
|
||||
---
|
||||
*PROJECT.md — Updated when requirements or context change*
|
||||
*Last updated: 2026-05-10 after Phase 114 (Accounting Configs Refactor) completion; v3.7 Invoices milestone in progress*
|
||||
*Last updated: 2026-05-10 after Phase 115 (Wystawianie faktury z zamowienia) completion; v3.7 Invoices milestone in progress*
|
||||
|
||||
|
||||
|
||||
@@ -14,11 +14,13 @@ Wystawianie faktur dla klientow z NIP poprzez integracje z Fakturownia (app.fakt
|
||||
|-------|------|-------|--------|
|
||||
| 113 | Fakturownia Integration Foundation | 1/1 | Complete (2026-05-10) |
|
||||
| 114 | Accounting Configs Refactor (hub + osobne podstrony receipts/invoices) | 1/1 | Complete (2026-05-10) |
|
||||
| 115 | Wystawianie faktury z zamowienia (lokalne + delegacja Fakturownia) | 0/? | Planning |
|
||||
| 115 | Wystawianie faktury z zamowienia (lokalne + delegacja Fakturownia + NIP lookup MF Biala Lista) | 1/1 | Complete (2026-05-10) |
|
||||
|
||||
Planowane kolejne fazy v3.7 (do dokladnego rozplanowania):
|
||||
- Lista faktur w sekcji Ksiegowosc + podglad/wydruk PDF
|
||||
- `orders.invoice_requested` w importerach Allegro/shopPRO + toggle w UI zamowienia
|
||||
Planowane kolejne fazy v3.7 (kandydaci, do rozplanowania):
|
||||
- Eksport XLSX listy wystawionych faktur (analogicznie do paragonow)
|
||||
- Idempotencja podwojnego POST do Fakturowni (INVOICE-IDEMP-115)
|
||||
- Event automatyzacji `invoice.created` (jezeli operator chce wysylac faktury mailem)
|
||||
- Backfill `curl_close()` w `ShopproIntegrationsRepository` (PHP 8.5 compat, poza zakresem 115)
|
||||
|
||||
## Next Milestone
|
||||
|
||||
@@ -492,4 +494,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md`
|
||||
|
||||
---
|
||||
*Roadmap created: 2026-03-12*
|
||||
*Last updated: 2026-05-10 - Phase 114 (Accounting Configs Refactor) complete; v3.7 milestone in progress*
|
||||
*Last updated: 2026-05-10 - Phase 115 (Wystawianie faktury z zamowienia) complete; v3.7 milestone in progress*
|
||||
|
||||
@@ -10,14 +10,14 @@ See: .paul/PROJECT.md (updated 2026-05-07)
|
||||
## Current Position
|
||||
|
||||
Milestone: v3.7 Invoices (Fakturownia integration) — In progress
|
||||
Phase: 115 of TBD (Wystawianie faktury z zamowienia) — Not started
|
||||
Phase: 116 of TBD (TBD — kandydaci v3.7 lub przejscie na kolejny milestone) — Not started
|
||||
Plan: pending
|
||||
Status: Phase 114 complete (transition done); ready to plan Phase 115
|
||||
Last activity: 2026-05-10 — UNIFY 114-01 complete, Phase 114 closed, transition done
|
||||
Status: Phase 115 closed; transition done; ready to plan kolejna faze
|
||||
Last activity: 2026-05-10 — UNIFY 115-01 complete + transition: PROJECT.md/ROADMAP.md/changelog zaktualizowane
|
||||
|
||||
Progress:
|
||||
- Milestone v3.7: [███░░░░░░░] ~30% (Phase 113 + 114 zamkniete; planowane ~5 faz)
|
||||
- Phase 114: [██████████] 100% — Complete
|
||||
- Milestone v3.7: [██████░░░░] ~55% (Phase 113 + 114 + 115 zamkniete; kandydaci: XLSX invoices export, INVOICE-IDEMP-115, invoice.created event, curl_close shopPRO backfill)
|
||||
- Phase 115: [██████████] 100% — Complete
|
||||
|
||||
## Loop Position
|
||||
|
||||
@@ -26,24 +26,25 @@ Current loop state:
|
||||
v3.7 milestone:
|
||||
Phase 113 (Fakturownia Integration Foundation): Complete
|
||||
Phase 114 (Accounting Configs Refactor): Complete
|
||||
Phase 115 (Wystawianie faktury z zamowienia): not started
|
||||
Phase 115 (Wystawianie faktury z zamowienia): Complete
|
||||
Phase 116 (TBD): not started
|
||||
```
|
||||
|
||||
```
|
||||
PLAN ──▶ APPLY ──▶ UNIFY
|
||||
✓ ✓ ✓ [Phase 114 closed; ready for Phase 115 plan]
|
||||
✓ ✓ ✓ [Phase 115 closed; ready for Phase 116 plan]
|
||||
```
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-05-10
|
||||
Stopped at: Phase 114 transition complete (PROJECT.md + ROADMAP.md updated, SUMMARY zapisany)
|
||||
Next action: /paul:plan dla Phase 115 (wystawianie faktury z zamowienia, lokalna numeracja + delegacja Fakturownia)
|
||||
Resume file: .paul/phases/114-accounting-configs-refactor/114-01-SUMMARY.md
|
||||
Stopped at: Phase 115 transition complete (PROJECT.md + ROADMAP.md updated, SUMMARY zapisany, changelog zaktualizowany)
|
||||
Next action: /paul:plan dla kolejnej fazy (v3.7 kandydaci: XLSX export listy faktur, INVOICE-IDEMP-115, invoice.created automation event, lub backfill `curl_close()` w shopPRO)
|
||||
Resume file: .paul/phases/115-invoice-from-order/115-01-SUMMARY.md
|
||||
|
||||
## Git State
|
||||
|
||||
Last commit: 2382018 feat(113): fakturownia integration foundation
|
||||
Last commit: 6129042 feat(114): accounting configs refactor + invoice configs CRUD
|
||||
Branch: main
|
||||
Feature branches merged: none
|
||||
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
|
||||
## Co zrobiono
|
||||
|
||||
- [Phase 115, Plan 01] Wystawianie faktury z zamowienia — vertical slice "zamowienie z NIP -> faktura PDF" w trybach lokalnym (Dompdf+counter) i delegowanym (POST do Fakturowni + redirect 302 do natywnego PDF)
|
||||
- Phase 115 Task 1: InvoiceRepository + InvoiceService (dual-flow orchestrator) + InvoiceIssueException + FakturowniaApiClient::createInvoice (POST /invoices.json) + buildPdfUrl
|
||||
- Phase 115 Task 2: InvoiceController + OrdersController::toggleInvoiceRequested + OrdersRepository::setInvoiceRequested + auto-import flagi z Allegro (`invoice.required`) i shopPRO (flexible 5-key parser) + show.php (toggle w zakladce Platnosci + warunkowy przycisk Wystaw fakture)
|
||||
- Phase 115 Task 3: Lista wystawionych `/settings/accounting/invoices/issued` z filtrami + invoice_preview + invoice_pdf Dompdf template + hub link
|
||||
- Phase 115 Task 3b (dodany): NIP lookup przez MF Biala Lista (publiczne API, bez rejestracji) — `MfWhitelistApiClient` + endpoint `/api/nip/lookup` + przycisk "Pobierz z GUS" w formularzu
|
||||
- Auto-fix: GUS endpoint Fakturowni nie istnial (zwracal HTML 404 -> "json is not valid"); switch na MF Biala Liste
|
||||
- Auto-fix: PHP 8.5 `curl_close()` deprecation wycieka HTML przed JSON; usuniete z MfWhitelistApiClient + FakturowniaApiClient (3 miejsca)
|
||||
- Auto-fix: Fakturownia payload nieistniejace `payment_to_kind_days` -> usuniete (sama data `payment_to` wystarcza)
|
||||
- Auto-fix: Generic `error` w 422 -> parser plaskuje `errors: {pole: [...]}` na czytelne komunikaty + error_log z raw body
|
||||
- Auto-fix: Fakturownia security odrzuca `seller_*`/`department_id` jako "create new department" -> usuniete z payloadu (Fakturownia uzywa danych konta)
|
||||
- Phase 115 transition: pending (STATE.md update, PROJECT.md, ROADMAP.md, git commit)
|
||||
|
||||
- [Phase 114, Plan 01] Accounting Configs Refactor — `/settings/accounting` jako hub-rozdroze, osobne podstrony Paragony/Faktury, pelen CRUD `invoice_configs` z delegacja do Fakturowni
|
||||
- Phase 114 Task 1: migracja seed `Domyslny VAT` (idempotentna NOT EXISTS) + `InvoiceConfigRepository` z walidacja delegacji
|
||||
- Phase 114 Task 2: `InvoiceConfigController` + widoki listy/edycji + `invoice-config-form.js` (conditional integration_id select) + 6 routes
|
||||
@@ -21,6 +33,24 @@
|
||||
|
||||
## Zmienione pliki
|
||||
|
||||
- `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`
|
||||
- `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/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`
|
||||
- `.paul/phases/115-invoice-from-order/115-01-PLAN.md`
|
||||
- `.paul/phases/115-invoice-from-order/115-01-SUMMARY.md`
|
||||
- `.paul/codebase/todo.md`
|
||||
- `database/migrations/20260511_000107_seed_default_invoice_config.sql`
|
||||
- `src/Modules/Settings/InvoiceConfigRepository.php`
|
||||
- `src/Modules/Settings/InvoiceConfigController.php`
|
||||
|
||||
@@ -203,6 +203,65 @@ tests/
|
||||
### IntegrationsHubController
|
||||
- Nowy parametr konstruktora `FakturowniaIntegrationRepository $fakturownia` i nowa metoda `buildFakturowniaRow()` agregująca status wszystkich kont (count instancji, configured/active counts, ostatni test).
|
||||
|
||||
## Phase 115 — Wystawianie faktury z zamowienia
|
||||
|
||||
### InvoiceService (`src/Modules/Accounting/InvoiceService.php`)
|
||||
- `issue(array $params): array` — orchestrator. Walidacja config (active), order details fetch, build snapshots (seller z `company_settings`, buyer merged z payload_json+addresses+manual override, items z VAT-aware netto/brutto split), routing do `issueLocal()` lub `issueDelegated()` zaleznie od `invoice_configs.is_delegated`.
|
||||
- `issueLocal()` — `InvoiceRepository::nextLocalNumber()` (atomowy counter z `invoice_number_counters`) -> `insertLocal()` -> zwraca `{invoice_id, invoice_number, total_gross, mode='local'}`.
|
||||
- `issueDelegated()` — `FakturowniaApiClient::createInvoice()` PRZED INSERT lokalnym; on success zapis `external_invoice_id`/`external_pdf_url`/`invoice_number` z odpowiedzi API; on failure rzuca `InvoiceIssueException` (zaden wiersz w `invoices`). `invoice_number_counters` NIE jest dotykany dla delegated.
|
||||
- Static `extractBuyerTaxNumber($order, $buyerAddress)` — parsuje NIP z payload_json sciezki: `invoice.address.taxId` (Allegro), `invoice.taxId/nip`, `buyer.tax_number/nip`, `client.nip/tax_number`, top-level `nip/tax_number`. Fallback na `order_addresses.company_tax_number`.
|
||||
|
||||
### InvoiceRepository (`src/Modules/Accounting/InvoiceRepository.php`)
|
||||
- `findByOrderId/findById` — JOIN `invoices` + `invoice_configs` + `integrations` (type='fakturownia') + `fakturownia_integration_settings` (LEFT JOIN dla `account_prefix`).
|
||||
- `insertLocal/insertDelegated` — wspolny prywatny `insert()` z roznymi NULL-amizacjami `external_*` pol.
|
||||
- `nextLocalNumber()` — `INSERT ... ON DUPLICATE KEY UPDATE last_number = last_number + 1` na `invoice_number_counters`, mirror `ReceiptRepository::getNextNumber`.
|
||||
- `paginate()` — filtry: `search` (numer/order ref), `config_id`, `mode` (local/delegated rozroznia po `external_invoice_id IS NULL`), `date_from`/`date_to`.
|
||||
|
||||
### FakturowniaApiClient (rozszerzony)
|
||||
- `createInvoice(array $settings, array $invoice)` — POST `https://{prefix}.fakturownia.pl/invoices.json` z body `{api_token, invoice}`. cURL z `SslCertificateResolver`, timeout `$timeoutSeconds`. On 2xx parsuje JSON na `{id, number, view_url, pdf_url, raw}`. On non-2xx rzuca `RuntimeException("HTTP {code}: {error}")`.
|
||||
- `buildPdfUrl(prefix, invoiceId, apiToken)` — string-builder dla `https://{prefix}.fakturownia.pl/invoices/{id}.pdf?api_token=...`. Bez fetcha; uzywany w redirect 302.
|
||||
- Dodany `httpPostJson()` (private) odpowiednik istniejacego `httpGet()`.
|
||||
|
||||
### InvoiceController (`src/Modules/Accounting/InvoiceController.php`)
|
||||
- `create(Request)` — GET `/orders/{id}/invoice/create`. Walidacja `orders.invoice_requested=1` (przekierowanie z flash error gdy 0). Active configs (filter `is_active=1`). NIP auto-prefill via `InvoiceService::extractBuyerTaxNumber()`. Renderuje `accounting/invoice_form`.
|
||||
- `store(Request)` — POST `/orders/{id}/invoice/store`. CSRF, `config_id` walidacja. Wywoluje `InvoiceService::issue()` z buyer overrides z formularza. On success: `OrdersRepository::recordActivity('invoice_issued')`, flash success, redirect na `/orders/{id}/invoice/{invoiceId}`. On `InvoiceIssueException`: flash do `invoice.error`, redirect z powrotem na form.
|
||||
- `show(Request)` — GET `/orders/{id}/invoice/{invoiceId}`. HTML preview z snapshotow.
|
||||
- `pdf(Request)` — GET `/orders/{id}/invoice/{invoiceId}/pdf`. Gdy `external_pdf_url` istnieje -> redirect 302; inaczej Dompdf inline z templatu `accounting/invoice_pdf`.
|
||||
- `issuedList(Request)` — GET `/settings/accounting/invoices/issued`. Filtry GET, paginacja 50/strona.
|
||||
|
||||
### orders.invoice_requested toggle
|
||||
- `OrdersRepository::setInvoiceRequested(int, bool)` — UPDATE z `updated_at = NOW()`.
|
||||
- `OrdersController::toggleInvoiceRequested` — POST `/orders/{id}/invoice-requested/toggle`. CSRF, JSON response `{success, invoice_requested}`. Loguje `order_activity_log` z `event_type='invoice_requested_changed'`.
|
||||
- `public/assets/js/modules/invoice-requested-toggle.js` — vanilla JS, idempotent guard `dataset.bound='1'`. AJAX POST przy `change`, optimistic show/hide `[data-invoice-button-wrap]`. Rollback checkbox przy HTTP/network blad.
|
||||
|
||||
### Auto-import flagi invoice_requested
|
||||
- `AllegroOrderImportService::importSingleOrder` — przy `wasCreated=true` jezeli `payload.invoice.required` truthy -> `setInvoiceRequested(true)`. Tylko pierwszy import (delta-only re-import nie nadpisuje manualnej zmiany).
|
||||
- `ShopproOrdersSyncService::shouldRequestInvoice($rawOrder)` — flexible parser sprawdzajacy `wants_invoice`, `invoice_required`, `invoice.required`, `buyer.wants_invoice`, `buyer.invoice` (akceptuje true/1/'1'/'true'/'yes'/'tak'). Wywolany tylko przy `wasCreated=true`.
|
||||
|
||||
### View hierarchy
|
||||
- `accounting/invoice_form.php` — formularz wystawiania.
|
||||
- `accounting/invoice_preview.php` — HTML preview po wystawieniu.
|
||||
- `accounting/invoice_pdf.php` — template Dompdf, mirror `receipts/print.php` z dodatkowymi polami faktury VAT (parties, netto/VAT/brutto per stawka, termin platnosci).
|
||||
- `accounting/invoices_issued_list.php` — lista pod `/settings/accounting/invoices/issued`.
|
||||
- `orders/show.php` — checkbox toggle + warunkowy przycisk "Wystaw fakture" + sekcja "Faktury" w tabie documents.
|
||||
|
||||
### DI wiring (`routes/web.php`)
|
||||
- `$invoiceRepository = new InvoiceRepository($app->db());` (po `InvoiceConfigRepository`).
|
||||
- `$invoiceService = new InvoiceService($invoiceRepository, $invoiceConfigRepository, $companySettingsRepository, new OrdersRepository(...), $fakturowniaIntegrationRepository, $fakturowniaApiClient);`
|
||||
- `$invoiceController = new InvoiceController($template, $translator, $auth, $invoiceRepository, $invoiceConfigRepository, $companySettingsRepository, new OrdersRepository(...), $invoiceService);`
|
||||
- `$ordersController` rozszerzony o 2 trailing params: `$invoiceRepository`, `$invoiceConfigRepository`.
|
||||
|
||||
### BREAKING / migration
|
||||
- Zadnych nowych migracji — Phase 113-01 dostarczyla `orders.invoice_requested`, `invoice_configs/invoices/invoice_number_counters` i `fakturownia_integration_settings`.
|
||||
- `OrdersController` ctor dostal 2 NEW optional params (default null) — backwards compatible.
|
||||
|
||||
### Edge cases / known limits
|
||||
- INVOICE-IDEMP-115 (`.paul/codebase/todo.md`) — brak idempotencji przy double-POST do Fakturowni gdy odpowiedz nie dotrze; operator musi recznie zweryfikowac w panelu.
|
||||
- Brak `invoice.created` event automatyzacji (per Phase 113 decision).
|
||||
- Brak download+cache PDF z Fakturowni — tylko redirect 302 (kazdy klik na PDF dla delegated faktury fetchuje PDF z Fakturowni).
|
||||
|
||||
---
|
||||
|
||||
## Phase 114 — Accounting Configs Refactor
|
||||
|
||||
### Sekcja Ksiegowosc — struktura URL
|
||||
|
||||
@@ -1,5 +1,53 @@
|
||||
# Technical Changelog
|
||||
|
||||
## 2026-05-10 - Phase 115 Plan 01: Wystawianie faktury z zamowienia
|
||||
|
||||
**Co zrobiono:**
|
||||
- **GUS lookup (Task 3b):** `FakturowniaApiClient::lookupClientByNip()` — GET `/clients/gus.json?nip=...&api_token=...` z walidacja NIP (10 cyfr) i parsowaniem `{name, tax_no, street, post_code, city, country}`. `InvoiceController::gusLookup()` jako AJAX endpoint `GET /api/fakturownia/gus-lookup?nip=...&config_id=...` — resolwer konta Fakturownia (preferuje `invoice_configs.integration_id` gdy delegated, fallback na pierwsze aktywne konto type='fakturownia'). `invoice_form.php` ma przycisk "Pobierz z GUS" obok pola NIP — vanilla JS fetch + auto-fill `buyer_company_name/street/postal_code/city`. Czyni "wystaw fakture" jednoklikowym po wpisaniu NIP. `InvoiceController` ctor rozszerzony o 2 deps (`FakturowniaIntegrationRepository`, `FakturowniaApiClient`).
|
||||
- `InvoiceRepository` (nowy) — CRUD dla `invoices`: `findByOrderId`, `findById`, `insertLocal`, `insertDelegated`, `nextLocalNumber` (atomowy INSERT ON DUPLICATE KEY UPDATE na `invoice_number_counters`), `paginate` z filtrami search/config/mode/date.
|
||||
- `InvoiceService` (nowy) — orchestrator wystawiania:
|
||||
- `issue()` rozgalezia na `issueLocal()` (numer lokalny + Dompdf-ready snapshot) lub `issueDelegated()` (POST do Fakturowni PRZED INSERT lokalnym; na sukcesie zapis `external_invoice_id`/`external_pdf_url`/`invoice_number` z odpowiedzi).
|
||||
- Static `extractBuyerTaxNumber()` parsuje NIP z roznych sciezek payload_json (Allegro `invoice.address.taxId`, shopPRO `buyer.tax_number` i pokrewne).
|
||||
- Snapshot pattern (snapshoty seller/buyer/items z Phase 8 wzorca) + obliczanie netto/brutto z VAT per pozycja.
|
||||
- `buildFakturowniaPayload()` mapuje na format `https://app.fakturownia.pl/api`.
|
||||
- `InvoiceIssueException` (nowy) — typed exception dla bledow biznesowych.
|
||||
- `FakturowniaApiClient` rozszerzony — nowe `createInvoice()` (POST `/invoices.json` z body `{api_token, invoice}`, parsuje response na `id/number/view_url/pdf_url/raw`) i `buildPdfUrl()` (helper bez fetcha, do redirect 302). Stary stub `downloadPdf()` zastapiony.
|
||||
- `InvoiceController` (nowy) — endpointy dla zamowienia (`create`, `store`, `show`, `pdf`) i lista `issuedList`. PDF: lokalna -> Dompdf inline (mirror `ReceiptController::pdf`); delegowana -> redirect 302 na `external_pdf_url`. Walidacja `invoice_requested=1` przed otwarciem formularza.
|
||||
- `OrdersController::toggleInvoiceRequested` (nowy) — AJAX endpoint POST `/orders/{id}/invoice-requested/toggle` z CSRF + `recordActivity('invoice_requested_changed')`.
|
||||
- `OrdersRepository::setInvoiceRequested(int, bool)` (nowy) — UPDATE `orders.invoice_requested`.
|
||||
- `OrdersController::show()` rozszerzone — przekazuje `invoices` + `invoiceConfigs` do widoku przez 2 nowe optional ctor params (`InvoiceRepository`, `InvoiceConfigRepository`).
|
||||
- `AllegroOrderImportService::importSingleOrder` — przy `wasCreated=true` i `payload.invoice.required` -> ustawia `orders.invoice_requested=1` (delta-only re-import nie nadpisuje manualnej flagi).
|
||||
- `ShopproOrdersSyncService::shouldRequestInvoice()` (nowy) — flexible parser sprawdzajacy `wants_invoice`, `invoice_required`, `invoice.required`, `buyer.wants_invoice`, `buyer.invoice` w raw payload. Dziala tylko przy pierwszym imporcie.
|
||||
- 4 nowe widoki:
|
||||
- `resources/views/accounting/invoice_form.php` — formularz wystawiania (config select z label "Lokalnie/Fakturownia: {prefix}", NIP prefilled z payload, manualne nadpisanie buyer fields, podglad pozycji + sprzedawca; confirm dialog dla powtornego wystawienia uzywa `OrderProAlerts.confirm` z options-object API).
|
||||
- `resources/views/accounting/invoice_preview.php` — HTML preview faktury z badgem trybu (Lokalnie/Fakturownia: {prefix}) + dual-button PDF.
|
||||
- `resources/views/accounting/invoice_pdf.php` — Dompdf template "FAKTURA VAT" z parties section, items table (netto/brutto/VAT per pozycja), termin platnosci, konto bankowe.
|
||||
- `resources/views/accounting/invoices_issued_list.php` — lista wystawionych faktur z filtrami (search/config/mode/date_from/date_to) + paginacja + badge Tryb (Lokalnie / Fakturownia: {prefix}).
|
||||
- `resources/views/orders/show.php` — header dostal `data-invoice-button-wrap` z przyciskiem "Wystaw fakture" (visible tylko gdy `invoice_requested=1`); pod headerem checkbox `data-invoice-requested-toggle` (auto-bound przez `invoice-requested-toggle.js`); pod tabem "Documents" nowa sekcja "Faktury" z badgem Tryb i akcjami Podglad/PDF.
|
||||
- `resources/views/settings/accounting.php` — link "Faktury wystawione" w karcie Faktury.
|
||||
- `public/assets/js/modules/invoice-requested-toggle.js` (nowy) — vanilla JS, AJAX POST z `_token` przy `change`, rollback checkbox state przy bledzie, optimistic show/hide `data-invoice-button-wrap`. Idempotent guard `data-bound`.
|
||||
- `resources/views/layouts/app.php` — rejestracja `invoice-requested-toggle.js` z cache-busting.
|
||||
- 6 nowych routes: `POST /orders/{id}/invoice-requested/toggle`, `GET /orders/{id}/invoice/create`, `POST /orders/{id}/invoice/store`, `GET /orders/{id}/invoice/{invoiceId}`, `GET /orders/{id}/invoice/{invoiceId}/pdf`, `GET /settings/accounting/invoices/issued`. Wszystkie pod `AuthMiddleware`.
|
||||
- DI wiring w `routes/web.php`: `InvoiceRepository`, `InvoiceService`, `InvoiceController`. `OrdersController` instantiation rozszerzona o 2 params (`$invoiceRepository`, `$invoiceConfigRepository`).
|
||||
- Docs: `architecture.md` (nowa sekcja "Phase 115"), `tech_changelog.md`, `todo.md` (INVOICE-IDEMP-115).
|
||||
|
||||
**Dlaczego:**
|
||||
- Plan zatwierdzony jako pelny vertical slice — Phase 113 (DB) + 114 (configs CRUD) byly fundamentem; bez wystawiania z zamowienia funkcja byla bezuzyteczna operacjonalnie.
|
||||
- Kolejnosc "POST do Fakturowni najpierw, INSERT lokalny po sukcesie" (per user decision) gwarantuje ze nie ma orphan rows w `invoices` gdy API padnie. Trade-off: brak idempotencji przy double-POST -> notatka INVOICE-IDEMP-115.
|
||||
- Auto-set `invoice_requested` z importu pozwala operatorowi nie myslec o flagi dla typowych przypadkow (Allegro `invoice.required=true`, shopPRO klienci wpisujacy NIP). Manualny toggle dla edge cases / korekty.
|
||||
- Dual PDF flow (Dompdf lokalny / redirect 302 do Fakturowni) — operator dostaje "natywny" PDF z Fakturowni dla delegowanych (ten sam co ksiegowy widzi w Fakturowni), brak ryzyka rozjazdu wygladu/numeracji.
|
||||
- `is_delegated=0` config zachowany jako pelny tryb lokalny — dla scenariuszy gdzie operator nie chce/nie ma konta Fakturowni.
|
||||
|
||||
**BREAKING:** Brak. `OrdersController` ctor dostal 2 NEW optional params (default null) — backwards compatible.
|
||||
|
||||
**Szczegolne uwagi:**
|
||||
- `invoice-config-form.js` (Phase 114) i `invoice-requested-toggle.js` (Phase 115) — dwa rozne moduly, oba w `layouts/app.php`. Nazwa "invoice-*" ale rozne kontekstowo (jeden formularz config, drugi zamowienie).
|
||||
- `OrderProAlerts.confirm` w `invoice_form.php` uzywa options-object API zgodnie z memory (Phase 114 fix).
|
||||
- Migracje no-op nie potrzebne — Phase 113 dostarczyla `orders.invoice_requested`, `invoice_*` tabele i `fakturownia_integration_settings`.
|
||||
- Skill audit: brak required skills dla tego planu.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-10 - Phase 114 Plan 01: Accounting Configs Refactor
|
||||
|
||||
**Co zrobiono:**
|
||||
|
||||
@@ -2,6 +2,23 @@
|
||||
|
||||
> Lista nieformalnych zadan do zrobienia pozniej. Kazdy wpis ma wlasny tag (np. `STAT-NET`) zeby mozna go bylo zlinkowac z komentarzy w kodzie.
|
||||
|
||||
## INVOICE-IDEMP-115 — idempotencja podwojnego POST do Fakturowni (data: 2026-05-10)
|
||||
|
||||
### Kontekst
|
||||
- Phase 115-01 — wystawianie faktury z zamowienia (delegacja Fakturownia).
|
||||
- Flow: `InvoiceService::issueDelegated()` -> POST do Fakturowni -> on success INSERT do `invoices`.
|
||||
- Edge case: faktura zostala utworzona w Fakturowni, ale odpowiedz nie dotarla (timeout, network). Operator widzi blad, klika "Wystaw fakture" ponownie -> drugi POST -> Fakturownia tworzy DRUGA fakture.
|
||||
|
||||
### Zadania
|
||||
1. Dorzucic idempotency-key (np. UUID per attempt zachowany w sesji albo w `invoices` ze statusem `pending_external`).
|
||||
2. Sprawdzic czy Fakturownia API wspiera nag/lowek `Idempotency-Key` lub deduplikacje po referencji.
|
||||
3. Alternatywa: po bledzie API, przed kolejnym POST, query Fakturowni `GET /invoices.json?q=<order_reference>` zeby sprawdzic czy faktura juz istnieje.
|
||||
|
||||
### Status
|
||||
- Odlozone — operator musi recznie zweryfikowac w panelu Fakturowni przy bledach API.
|
||||
|
||||
---
|
||||
|
||||
## STAT-NET — netto zamowien w statystykach (data: 2026-04-19)
|
||||
|
||||
### Kontekst
|
||||
|
||||
359
.paul/phases/115-invoice-from-order/115-01-PLAN.md
Normal file
359
.paul/phases/115-invoice-from-order/115-01-PLAN.md
Normal file
@@ -0,0 +1,359 @@
|
||||
---
|
||||
phase: 115-invoice-from-order
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/Modules/Accounting/InvoiceRepository.php
|
||||
- src/Modules/Accounting/InvoiceService.php
|
||||
- src/Modules/Accounting/InvoiceIssueException.php
|
||||
- src/Modules/Accounting/InvoiceController.php
|
||||
- src/Modules/Accounting/InvoicePdfRenderer.php
|
||||
- src/Modules/Settings/FakturowniaApiClient.php
|
||||
- src/Modules/Orders/OrdersController.php
|
||||
- src/Modules/Orders/OrdersRepository.php
|
||||
- src/Modules/Allegro/AllegroOrderImportService.php
|
||||
- src/Modules/Settings/ShopPro/ShopproOrdersSyncService.php
|
||||
- resources/views/orders/show.php
|
||||
- resources/views/accounting/invoices_list.php
|
||||
- resources/views/accounting/invoice_form.php
|
||||
- resources/views/accounting/invoice_preview.php
|
||||
- public/assets/js/modules/invoice-requested-toggle.js
|
||||
- routes/web.php
|
||||
- bootstrap/app.php
|
||||
autonomous: false
|
||||
delegation: off
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Wystawianie faktur dla klientow z NIP bezposrednio z zamowienia, w dwoch trybach (lokalny Dompdf z `invoice_number_counters` lub delegowany do Fakturowni z importem rezultatu), z auto-prefillem NIP, manualnym togglem flagi `orders.invoice_requested`, lista faktur w sekcji Ksiegowosc i podgladem/PDF analogicznym do paragonow.
|
||||
|
||||
## Purpose
|
||||
Domknieciem milestone v3.7 jest zamiana fundamentu z faz 113-114 na uzyteczna funkcjonalnosc operatorska: jedna sciezka biznesowa "zamowienie z NIP -> wystawiona faktura w PDF" z opcjonalnym outsourcingiem numeracji do Fakturowni.
|
||||
|
||||
## Output
|
||||
Operator widzi przycisk "Wystaw fakture" w szczegolach zamowienia tylko gdy `invoice_requested=1`, otwiera formularz (NIP auto z payload_json + manualny override), wybiera config (lokalny lub delegowany), generuje fakture; w trybie delegowanym faktura tworzy sie najpierw w Fakturowni a potem importuje do `invoices`. Lista `/settings/accounting/invoices/issued` pokazuje wystawione dokumenty z filtrami i podgladem PDF.
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
<clarifications>
|
||||
- **Trigger UI** — Jak ma wygladac trigger wystawiania faktury w szczegolach zamowienia?
|
||||
→ Odpowiedz: Przycisk 'Wystaw fakture' widoczny tylko gdy `orders.invoice_requested=1`.
|
||||
- **NIP nabywcy** — Skad ma byc pobierany NIP nabywcy?
|
||||
→ Odpowiedz: Auto z `orders.payload_json` (Allegro `invoice.address.taxId` / shopPRO equivalent) z manualnym polem fallback w formularzu.
|
||||
- **Delegacja** — Jak ma dzialac flow gdy `invoice_configs.is_delegated=1`?
|
||||
→ Odpowiedz: Wszystko ma byc robione w Fakturowni (POST najpierw), a po sukcesie importowane do orderPRO (INSERT do `invoices` z `external_invoice_id`, numerem zwroconym przez Fakturownie i `external_pdf_url`).
|
||||
- **invoice_requested** — Jak operator ma ustawiac flage `orders.invoice_requested=1`?
|
||||
→ Odpowiedz: Toggle/checkbox w szczegolach zamowienia (manualny, AJAX) + auto z importu gdy zrodlo zglasza zadanie faktury (Allegro `invoice.required` / shopPRO `wants_invoice`-equivalent w payload).
|
||||
- **Lokalny tryb** — Co dzieje sie dla configs gdzie `is_delegated=0`?
|
||||
→ Odpowiedz: Lokalna numeracja przez `invoice_number_counters` + Dompdf PDF, analogicznie do paragonow (reuse pattern z `ReceiptService`).
|
||||
- **Zakres** — Czy plan obejmuje liste faktur i podglad/PDF?
|
||||
→ Odpowiedz: Tak - dolacz wystawianie + lista faktur + podglad/PDF (vertical slice).
|
||||
</clarifications>
|
||||
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
@.paul/codebase/architecture.md
|
||||
@.paul/codebase/db_schema.md
|
||||
|
||||
## Prior Work
|
||||
@.paul/phases/113-fakturownia-integration/113-01-SUMMARY.md
|
||||
@.paul/phases/114-accounting-configs-refactor/114-01-SUMMARY.md
|
||||
|
||||
## Source Files (reuse patterns)
|
||||
@src/Modules/Accounting/ReceiptService.php
|
||||
@src/Modules/Accounting/ReceiptRepository.php
|
||||
@src/Modules/Accounting/ReceiptController.php
|
||||
@src/Modules/Settings/FakturowniaApiClient.php
|
||||
@src/Modules/Settings/FakturowniaIntegrationRepository.php
|
||||
@src/Modules/Settings/InvoiceConfigRepository.php
|
||||
@resources/views/accounting/receipts_list.php
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Flaga invoice_requested ustawia widocznosc przycisku
|
||||
```gherkin
|
||||
Given zamowienie #X ma `orders.invoice_requested=0`
|
||||
When operator otwiera /orders/X
|
||||
Then przycisk "Wystaw fakture" NIE jest widoczny w headerze, a checkbox/toggle "Klient prosi o fakture" jest widoczny i mozna go wlaczyc
|
||||
|
||||
Given operator klika toggle "Klient prosi o fakture"
|
||||
When AJAX POST /orders/X/invoice-requested/toggle zwraca 200
|
||||
Then `orders.invoice_requested` zmienia sie na 1, przycisk "Wystaw fakture" pojawia sie bez przeladowania strony (lub po reloadzie), a `order_activity_log` zapisuje wpis `invoice_requested_changed`
|
||||
|
||||
Given Allegro order import ma payload `order.invoice.required=true` (lub shopPRO `wants_invoice=1`)
|
||||
When OrderImportService zapisuje zamowienie pierwszy raz
|
||||
Then `orders.invoice_requested=1` ustawione automatycznie (delta-only re-import nie nadpisuje rozni-od-DB - flaga manualna pozostaje stabilna)
|
||||
```
|
||||
|
||||
## AC-2: Wystawienie faktury - tryb lokalny
|
||||
```gherkin
|
||||
Given config #C ma `is_delegated=0`, zamowienie #X ma `invoice_requested=1` i NIP w payload_json
|
||||
When operator otwiera /orders/X/invoice/create i wybiera config #C
|
||||
Then formularz pre-fillem pokazuje NIP nabywcy z payload_json, dane firmy z company_settings, items z order_items (z snapshotu jak w receipts), i operator moze nadpisac NIP
|
||||
|
||||
When operator submituje form
|
||||
Then InvoiceService:
|
||||
- generuje numer lokalny przez `InvoiceNumberCounter` (atomowy INSERT ON DUPLICATE KEY) z formatu `invoice_configs.number_format`
|
||||
- INSERT do `invoices` z snapshotami JSON (`seller_data_json`, `buyer_data_json` z NIP, `items_json`)
|
||||
- generuje PDF przez Dompdf (reuse Dompdf wiring z `ReceiptService`) i nie zapisuje na dysku (PDF on-demand z snapshotu)
|
||||
- zwraca redirect na /orders/X/invoice/{invoiceId} z flash success
|
||||
```
|
||||
|
||||
## AC-3: Wystawienie faktury - tryb delegowany (Fakturownia)
|
||||
```gherkin
|
||||
Given config #C ma `is_delegated=1` i wskazuje na konto Fakturownia (integration_id), zamowienie #X ma NIP
|
||||
When operator submituje formularz invoice/create dla #C
|
||||
Then InvoiceService:
|
||||
- WOLA `FakturowniaApiClient::createInvoice(prefix, token, payload)` PRZED jakimkolwiek INSERT lokalnym
|
||||
- po sukcesie API (HTTP 2xx z `id` i `number` w response) INSERT do `invoices` z `external_invoice_id`, `external_pdf_url`, `invoice_number` zwroconym przez Fakturownie i lokalnymi snapshotami (`seller_data_json`/`buyer_data_json`/`items_json` zamrozone w orderPRO dla audytu)
|
||||
- przy bledzie API (HTTP non-2xx, timeout, JSON parse fail) NIE robi INSERT, zwraca redirect na form z bledem `Flash::set('invoice.error', ...)` i czytelnym opisem (status code + message z Fakturowni)
|
||||
- `invoice_number_counters` NIE jest dotykany (numer pochodzi z Fakturowni)
|
||||
|
||||
Given faktura zostala juz utworzona w Fakturowni ale POST sie wywalil sieciowo po stronie Fakturowni
|
||||
When operator probuje ponownie submit form (idempotencja na poziomie aplikacji)
|
||||
Then aplikacja nie zabezpiecza tego automatycznie w tym planie - operator musi recznie pojsc do Fakturowni i sprawdzic; ten przypadek odnotowany w `.paul/codebase/todo.md` jako INVOICE-IDEMP-115
|
||||
```
|
||||
|
||||
## AC-4: Lista, podglad i PDF
|
||||
```gherkin
|
||||
Given istnieja faktury wystawione (lokalne i delegowane)
|
||||
When operator otwiera /settings/accounting/invoices/issued
|
||||
Then widzi tabele analogiczna do `/settings/accounting/receipts/issued`:
|
||||
- kolumny: numer, data wystawienia, sprzedawca, nabywca (z NIP), kwota brutto, tryb (Lokalny/Fakturownia), zamowienie (link)
|
||||
- filtry: zakres dat (issue_date), config, tryb (lokalny/delegowany)
|
||||
- sortowanie po dacie DESC
|
||||
|
||||
Given lokalna faktura #I
|
||||
When operator klika "Podglad"
|
||||
Then otwiera sie /accounting/invoices/{I}/preview - HTML render z snapshotow (analogicznie do `receipts/{id}`)
|
||||
|
||||
Given lokalna faktura #I
|
||||
When operator klika "PDF"
|
||||
Then GET /accounting/invoices/{I}/pdf zwraca `Content-Type: application/pdf` wygenerowany przez Dompdf z snapshotow
|
||||
|
||||
Given delegowana faktura #I (ma `external_pdf_url`)
|
||||
When operator klika "PDF"
|
||||
Then redirect 302 na `external_pdf_url` (PDF z Fakturowni, nie cache'ujemy lokalnie w tym planie)
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Backend - InvoiceService + InvoiceRepository + Fakturownia API client</name>
|
||||
<files>
|
||||
src/Modules/Accounting/InvoiceRepository.php (nowy),
|
||||
src/Modules/Accounting/InvoiceService.php (nowy),
|
||||
src/Modules/Accounting/InvoiceIssueException.php (nowy),
|
||||
src/Modules/Accounting/InvoicePdfRenderer.php (nowy),
|
||||
src/Modules/Settings/FakturowniaApiClient.php,
|
||||
bootstrap/app.php
|
||||
</files>
|
||||
<action>
|
||||
1. **InvoiceRepository** (mirror `ReceiptRepository`):
|
||||
- `insertLocal(array $data): int` - INSERT do `invoices` (numer lokalny, brak external_*).
|
||||
- `insertDelegated(array $data): int` - INSERT z `external_invoice_id`, `external_pdf_url`, numerem z Fakturowni.
|
||||
- `findById(int): ?array`, `listAll(array $filters): array` (filtry: dateFrom/To, configId, mode), `countAll(array $filters): int` dla paginacji.
|
||||
- `nextLocalNumber(int $configId, DateTimeImmutable $issueDate, string $format, string $type): string` - atomowy INSERT ON DUPLICATE KEY UPDATE na `invoice_number_counters` (reuse wzorca z `ReceiptRepository::nextNumber`); placeholder `%N` zerro-padded width zachowany.
|
||||
2. **InvoiceService**:
|
||||
- `issue(int $orderId, int $configId, array $formInput): int` - orchestrator:
|
||||
- laduje order, items, addresses, company_settings;
|
||||
- buduje `seller_data_json`, `buyer_data_json` (z NIP merged: payload_json prefill + manual override z `formInput['buyer_tax_number']`), `items_json` (reuse `buildItemsSnapshot` pattern);
|
||||
- jezeli `is_delegated=0`: alokuje numer lokalny -> `insertLocal` -> zwraca id;
|
||||
- jezeli `is_delegated=1`: pobiera `account_prefix`, `apiToken` przez `FakturowniaIntegrationRepository::getDecryptedToken` -> `FakturowniaApiClient::createInvoice` -> on success `insertDelegated` z `external_invoice_id`, `external_pdf_url`, `invoice_number` z odpowiedzi -> zwraca id; on failure rzuca `InvoiceIssueException` z czytelnym message.
|
||||
- Zera tolerancji: brak `is_delegated=1 && integration_id=NULL` (rzuca exception).
|
||||
3. **FakturowniaApiClient::createInvoice(string $prefix, string $apiToken, array $payload): array**:
|
||||
- POST `https://{prefix}.fakturownia.pl/invoices.json` z body `{api_token, invoice: {...}}` (zgodnie z https://app.fakturownia.pl/api - mapping: `kind`, `seller_*` (z snapshot or omitted gdy konto Fakturowni ma juz dane), `buyer_*`, `positions: [...]`, `payment_to_kind`, `department_id`).
|
||||
- cURL z timeoutem (timeout_seconds z `integrations.timeout_seconds`), `SslCertificateResolver::resolve()`.
|
||||
- Zwraca `['id' => int, 'number' => string, 'view_url' => string, 'raw' => array]` lub rzuca `RuntimeException` przy non-2xx z message: `"Fakturownia API HTTP {code}: {error}"`.
|
||||
4. **FakturowniaApiClient::downloadPdfUrl(string $prefix, int $invoiceId): string**:
|
||||
- Konstruuje URL `https://{prefix}.fakturownia.pl/invoices/{id}.pdf?api_token={token}` - jako string-builder, bez fetcha (uzywany w redirect 302).
|
||||
5. **InvoicePdfRenderer** - reuse Dompdf wiring z `ReceiptService` (jezeli jest extracted) lub mirror; renderuje template `invoice_pdf.php` z snapshotow.
|
||||
6. **Bootstrap wiring** w `bootstrap/app.php`: rejestracja `InvoiceRepository`, `InvoiceService` (deps: InvoiceRepository, InvoiceConfigRepository, FakturowniaIntegrationRepository, FakturowniaApiClient, OrdersRepository, CompanySettingsRepository).
|
||||
</action>
|
||||
<verify>
|
||||
`php -l src/Modules/Accounting/InvoiceService.php` (0 errors).
|
||||
Manual: `php -r "require 'bootstrap/app.php'; var_dump(\$app->make(InvoiceService::class));"` (gdy XAMPP online; opcjonalne).
|
||||
</verify>
|
||||
<done>AC-2 (lokalny insert), AC-3 (delegated POST najpierw, INSERT po sukcesie) - infrastruktura backend gotowa.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: UI zamowienia - toggle invoice_requested + form wystawiania + endpointy + auto-import flagi</name>
|
||||
<files>
|
||||
src/Modules/Orders/OrdersController.php,
|
||||
src/Modules/Orders/OrdersRepository.php,
|
||||
src/Modules/Accounting/InvoiceController.php (nowy),
|
||||
src/Modules/Allegro/AllegroOrderImportService.php,
|
||||
src/Modules/Settings/ShopPro/ShopproOrdersSyncService.php,
|
||||
resources/views/orders/show.php,
|
||||
resources/views/accounting/invoice_form.php (nowy),
|
||||
public/assets/js/modules/invoice-requested-toggle.js (nowy),
|
||||
routes/web.php
|
||||
</files>
|
||||
<action>
|
||||
1. **OrdersController + Repository**:
|
||||
- `OrdersRepository::setInvoiceRequested(int $orderId, bool $value): void` - UPDATE + insert do `order_activity_log` (`event_type='invoice_requested_changed'`, summary "Klient prosi o fakture: tak/nie").
|
||||
- `OrdersController::toggleInvoiceRequested(Request)` - POST `/orders/{id}/invoice-requested/toggle` z `_token`; przyjmuje `invoice_requested=0|1`; zwraca JSON `{success: true, invoice_requested: int}`.
|
||||
2. **InvoiceController** (`src/Modules/Accounting/InvoiceController.php`):
|
||||
- `create(Request)` - GET `/orders/{id}/invoice/create` - lista activnych `invoice_configs` (z label trybu), prefill NIP z `orders.payload_json` (helper `extractBuyerTaxNumber($order)` parsuje Allegro: `payload_json.invoice.address.taxId`, shopPRO: `payload_json.buyer.tax_number` lub fallback `''`); render `invoice_form.php`.
|
||||
- `store(Request)` - POST `/orders/{id}/invoice/store` - walidacja (`config_id required`, `buyer_tax_number required gdy is_delegated=1 lub gdy config wymaga NIP`); wywoluje `InvoiceService::issue()`; flash success/error; redirect na `/orders/{id}/invoice/{invoiceId}` (preview) lub z powrotem na form przy bledzie.
|
||||
- `show(Request)` - GET `/orders/{id}/invoice/{invoiceId}` - HTML preview (mirror `ReceiptController::show`).
|
||||
- `pdf(Request)` - GET `/orders/{id}/invoice/{invoiceId}/pdf` - jezeli `external_pdf_url` istnieje, redirect 302; inaczej Dompdf stream przez `InvoicePdfRenderer`.
|
||||
3. **resources/views/orders/show.php**:
|
||||
- Header: po `Wystaw paragon` dodaj `<?php if ($order['invoice_requested'] ?? 0): ?>` -> `<a href="/orders/.../invoice/create" class="btn btn--secondary">Wystaw fakture</a>`.
|
||||
- Sekcja akcji ponizej: checkbox `[data-invoice-requested-toggle]` z label "Klient prosi o fakture" - vanilla JS modul `invoice-requested-toggle.js` POSTuje `_token` przy `change`, ukrywa/pokazuje `[data-invoice-button-wrap]` bez reloadu.
|
||||
- Po zakladce paragonow dodaj liste faktur dla zamowienia (analogicznie do paragonow) z kolumna Tryb i `external_pdf_url`-aware przyciskiem PDF.
|
||||
4. **invoice_form.php** (analogicznie do `receipt_form.php`):
|
||||
- Select configa (label: `name (Lokalny)` lub `name (Fakturownia: {integration_name})`).
|
||||
- Pola: `buyer_tax_number` (prefill, edytowalne), `buyer_name`, `buyer_address`, items (readonly podglad z order_items), data wystawienia (default today), data sprzedazy (z `sale_date_source` z config).
|
||||
- CSRF `_token`, button submit.
|
||||
5. **AllegroOrderImportService** + **ShopproOrdersSyncService**:
|
||||
- W `upsertOrderAggregate` dodaj `'invoice_requested' => $this->extractInvoiceRequestedFromPayload($payload)` - tylko przy `created=true` (delta-only re-import nie nadpisuje manualnej flagi).
|
||||
- Allegro: `payload.invoice.required === true` -> 1, inaczej 0.
|
||||
- shopPRO: parser zalezny od formatu payload (`buyer.wants_invoice`, `invoice.requested`, lub fallback 0 - dokladny klucz weryfikowany na payloadzie podczas implementacji).
|
||||
6. **routes/web.php**: nowe routy `/orders/{id}/invoice-requested/toggle` (POST), `/orders/{id}/invoice/create` (GET), `/orders/{id}/invoice/store` (POST), `/orders/{id}/invoice/{invoiceId}` (GET), `/orders/{id}/invoice/{invoiceId}/pdf` (GET). Wszystkie pod `AuthMiddleware`. POST z CSRF `_token`.
|
||||
</action>
|
||||
<verify>
|
||||
`php -l` na zmienionych plikach.
|
||||
Smoke (XAMPP online): toggle invoice_requested -> reload -> przycisk widoczny -> klik -> form -> submit lokalny -> preview otwiera sie.
|
||||
</verify>
|
||||
<done>AC-1 (toggle + auto-import), AC-2 (lokalny flow do preview), AC-3 (delegated form path).</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Lista wystawionych faktur w sekcji Ksiegowosc + podglad/PDF</name>
|
||||
<files>
|
||||
src/Modules/Accounting/InvoiceController.php,
|
||||
resources/views/accounting/invoices_issued_list.php (nowy),
|
||||
resources/views/accounting/invoice_preview.php (nowy),
|
||||
resources/views/accounting/invoice_pdf.php (nowy),
|
||||
resources/views/accounting/_hub.php (lub komponent "Faktury wystawione" link),
|
||||
routes/web.php
|
||||
</files>
|
||||
<action>
|
||||
1. **InvoiceController::issuedList(Request)**:
|
||||
- GET `/settings/accounting/invoices/issued`
|
||||
- Filtry: `date_from`, `date_to`, `config_id`, `mode` (`local`/`delegated`/`all`), `page` (paginacja, 50 per page).
|
||||
- Wywoluje `InvoiceRepository::listAll($filters)` + `countAll`.
|
||||
- Renderuje `invoices_issued_list.php` (mirror `receipts_list.php` UI: filter form, table, paginacja).
|
||||
2. **invoices_issued_list.php**:
|
||||
- Kolumny: Numer (link to preview), Data wystawienia, Nabywca (z NIP), Kwota brutto, Tryb (badge `Lokalny`/`Fakturownia: {prefix}`), Zamowienie (link `/orders/{id}`), Akcje (Podglad, PDF).
|
||||
- PDF dla `external_pdf_url` -> `<a href="{external_pdf_url}" target="_blank">PDF (Fakturownia)</a>`; dla lokalnych -> `<a href="/accounting/invoices/{id}/pdf">PDF</a>`.
|
||||
3. **invoice_preview.php** (HTML preview z snapshotow):
|
||||
- Header z numerem, data wystawienia, sprzedawca (z `seller_data_json`), nabywca (z `buyer_data_json` w tym NIP), tabela items (z `items_json`), suma netto/VAT/brutto, dane platnosci (`payment_due_date`).
|
||||
- Przycisk "Pobierz PDF" (lokalny -> Dompdf endpoint, delegowany -> external_pdf_url).
|
||||
- Mode-aware footer (lokalna faktura: "Wystawione lokalnie w orderPRO"; delegowana: "Wystawione w Fakturowni - id {external_invoice_id}").
|
||||
4. **invoice_pdf.php** - template renderowany przez Dompdf, mirror `receipt_pdf.php` z dodatkowymi polami faktury VAT (NIP nabywcy, termin platnosci, sposob platnosci, podsumowanie VAT per stawka).
|
||||
5. **_hub.php / accounting hub view**: dodaj link "Faktury wystawione" obok "Faktury (konfiguracja)" w karcie Faktury (analogicznie do paragonow).
|
||||
6. **routes/web.php**: `/settings/accounting/invoices/issued` (GET), `/accounting/invoices/{id}/preview` i `/accounting/invoices/{id}/pdf` (GET) pod AuthMiddleware.
|
||||
</action>
|
||||
<verify>
|
||||
`php -l` na zmienionych plikach. Smoke test: po wystawieniu lokalnej + delegowanej faktury - obie widoczne na liscie z poprawnym Trybem; klik "Podglad" otwiera preview; klik "PDF" lokalny -> Dompdf inline; "PDF" delegowany -> redirect do Fakturowni.
|
||||
</verify>
|
||||
<done>AC-4 (lista + podglad + PDF dual-path).</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3b: GUS lookup — pobieranie danych nabywcy z NIP przez Fakturownie</name>
|
||||
<files>
|
||||
src/Modules/Settings/FakturowniaApiClient.php,
|
||||
src/Modules/Accounting/InvoiceController.php,
|
||||
resources/views/accounting/invoice_form.php,
|
||||
routes/web.php
|
||||
</files>
|
||||
<action>
|
||||
1. **FakturowniaApiClient::lookupClientByNip(string $prefix, string $apiToken, string $nip): array**:
|
||||
- GET `https://{prefix}.fakturownia.pl/clients/gus.json?nip={nip}&api_token={token}`
|
||||
- Reuse istniejacy `httpGet()` (z `SslCertificateResolver`). Parsowanie JSON na `{name, tax_no, street, post_code, city, country, ...}`.
|
||||
- Rzuca `RuntimeException` przy non-2xx lub niepoprawnej odpowiedzi.
|
||||
2. **InvoiceController::gusLookup(Request)**:
|
||||
- GET `/api/fakturownia/gus-lookup?nip=...&config_id=...` pod AuthMiddleware.
|
||||
- Walidacja NIP: 10 cyfr (po usunieciu spacji/myslnikow). Niepoprawny -> JSON `{success:false, error:...}` 422.
|
||||
- Resolwer integration_id: jezeli `config_id` wskazuje config z `is_delegated=1` -> jego `integration_id`. Inaczej fallback na pierwsza aktywna integracje `type=fakturownia` z `is_active=1`. Brak -> JSON `{success:false, error:'Brak skonfigurowanego konta Fakturownia.'}`.
|
||||
- Pobranie tokenu przez `FakturowniaIntegrationRepository::getDecryptedToken()`. Brak tokenu -> JSON error.
|
||||
- Wywolanie `FakturowniaApiClient::lookupClientByNip()`. On success zwraca JSON `{success:true, data:{company_name, street, postal_code, city, country, ...}}` znormalizowany na klucze formularza.
|
||||
3. **invoice_form.php**:
|
||||
- Przycisk "Pobierz dane z GUS" obok pola `buyer_tax_number`. Klik -> czyta NIP, wywoluje `/api/fakturownia/gus-lookup`, wypelnia `buyer_company_name/street/city/postal_code`. Loading state na buttonie. Bledy przez `OrderProAlerts.error`.
|
||||
- Inline `<script>` (vanilla JS, bez nowego modulu — specyficzne dla widoku).
|
||||
4. **routes/web.php**: `GET /api/fakturownia/gus-lookup` -> `[$invoiceController, 'gusLookup']` z AuthMiddleware.
|
||||
</action>
|
||||
<verify>
|
||||
`php -l` na zmienionych plikach. Smoke: NIP istniejacej firmy (np. swojej) + klik "Pobierz z GUS" -> formularz wypelnia sie nazwa/adresem; nieprawidlowy NIP -> czerwony alert.
|
||||
</verify>
|
||||
<done>AC-2 rozszerzone o auto-fill nabywcy z NIP — operator nie musi wpisywac danych firmy recznie.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>
|
||||
Pelny flow wystawiania faktur z zamowienia: toggle invoice_requested, formularz, dwa tryby (lokalny + delegowany), lista wystawionych faktur, podglad, PDF (Dompdf lokalny + redirect do Fakturowni dla delegowanych), auto-import flagi z Allegro/shopPRO.
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
1. Pobrac fresh z brancha, `php bin/migrate.php` (Phase 113-114 migracje juz w DB).
|
||||
2. (Opcjonalnie) Skonfigurowac konto Fakturowni testowej: `/settings/integrations/fakturownia/edit` -> dodac konto, test connection.
|
||||
3. W `/settings/accounting/invoices` upewnic sie ze istnieja: 1 config lokalny (`is_delegated=0`, np. seed `Domyslny VAT`) i 1 config delegowany (`is_delegated=1` -> integration_id konta Fakturownia).
|
||||
4. Otworzyc dowolne zamowienie z NIP w payload_json (Allegro lub shopPRO testowe). Sprawdzic checkbox "Klient prosi o fakture".
|
||||
- Toggle on -> przycisk "Wystaw fakture" pojawia sie. Toggle off -> znika.
|
||||
5. Klik "Wystaw fakture" -> formularz, NIP prefilled z payload_json. Wybrac config lokalny -> submit -> sprawdzic preview, kliknac PDF -> Dompdf otwiera fakture VAT.
|
||||
6. Wystawic kolejna na tym samym zamowieniu - tym razem config delegowany -> sprawdzic ze:
|
||||
- Pojawia sie w `/settings/accounting/invoices/issued` z badgem "Fakturownia",
|
||||
- Klik PDF -> redirect na external_pdf_url (Fakturownia),
|
||||
- W panelu Fakturowni faktura widoczna z tym samym numerem.
|
||||
7. Sprawdzic blad-path: w UI ustawic config delegowany, ale w `/settings/integrations/fakturownia` zepsuc api_token (typo) -> probuje wystawic -> czerwony flash z message zawierajacym HTTP code i `invoices` ZADEN nowy wiersz.
|
||||
8. Re-import zamowienia (Allegro/shopPRO) - sprawdzic ze manualne `invoice_requested` nie zostaje nadpisane (delta-only).
|
||||
</how-to-verify>
|
||||
<resume-signal>Wpisz "approved" by zamknac plan, lub opisz problemy do naprawy</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- `src/Modules/Settings/FakturowniaIntegrationRepository.php` - kontrakt z Phase 113 stabilny (testConnection + getDecryptedToken).
|
||||
- `src/Modules/Settings/InvoiceConfigRepository.php` - kontrakt z Phase 114 stabilny.
|
||||
- `database/migrations/*` - schema z Phase 113 wystarcza, nie dodajemy migracji w tym planie.
|
||||
- `src/Modules/Accounting/Receipt*` - paragonowe sciezki niezmienione.
|
||||
- `OrderImportRepository::upsertOrderAggregate` delta-only logic z Phase 112 - tylko dodanie `invoice_requested` jako kolejnego pola w aggregate (przy `created=true`), bez zmiany kontraktu delta-only.
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Brak `invoice.created` eventu automatyzacji (decyzja z Phase 113 PROJECT.md - odlozone).
|
||||
- Brak idempotencji aplikacyjnej dla podwojnego POST do Fakturowni (notatka INVOICE-IDEMP-115 w `.paul/codebase/todo.md`).
|
||||
- Brak download+cache PDF z Fakturowni do storage/ - tylko redirect 302 do `external_pdf_url`.
|
||||
- Brak edycji wystawionej faktury (immutable po wystawieniu).
|
||||
- Brak korekt/anulowania faktur w tym planie.
|
||||
- Brak eksportu XLSX listy faktur (analogiczne do paragonow - moze byc dodane pozniej).
|
||||
- Auto-import shopPRO `wants_invoice` zalezny od dokladnego klucza w payload - jezeli payload nie ma takiego pola, parser zwraca 0 (manualny toggle nadal dziala).
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] `php -l` przechodzi dla wszystkich nowych/zmienionych plikow.
|
||||
- [ ] AC-1: toggle dziala, log jest tworzony, przycisk pojawia/znika; auto-import flagi dziala dla Allegro przy nowym zamowieniu.
|
||||
- [ ] AC-2: lokalna faktura tworzy sie, ma numer wg `number_format`, snapshoty zamrozone, PDF Dompdf renderuje sie.
|
||||
- [ ] AC-3: delegowana faktura - POST PRZED INSERT; przy bledzie API zaden wiersz w `invoices`; przy sukcesie `external_invoice_id`/`external_pdf_url` zapisane.
|
||||
- [ ] AC-4: lista pokazuje oba tryby, filtry dzialaja, podglad otwiera sie, PDF lokalny i PDF delegowany (redirect) dzialaja.
|
||||
- [ ] Re-import nie nadpisuje manualnej flagi `invoice_requested`.
|
||||
- [ ] Smoke test (checkpoint Task 4) zaakceptowany przez operatora.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Wszystkie tasks (1-3) zaimplementowane, checkpoint zaakceptowany.
|
||||
- Brak nowych warningow PHP.
|
||||
- `.paul/codebase/architecture.md` zaktualizowany (sekcja "Phase 115 - Invoice From Order").
|
||||
- `.paul/codebase/db_schema.md` bez zmian (Phase 113 wystarcza).
|
||||
- `.paul/codebase/tech_changelog.md` ma wpis Phase 115.
|
||||
- INVOICE-IDEMP-115 dodany do `.paul/codebase/todo.md`.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/115-invoice-from-order/115-01-SUMMARY.md`.
|
||||
</output>
|
||||
229
.paul/phases/115-invoice-from-order/115-01-SUMMARY.md
Normal file
229
.paul/phases/115-invoice-from-order/115-01-SUMMARY.md
Normal file
@@ -0,0 +1,229 @@
|
||||
---
|
||||
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*
|
||||
Reference in New Issue
Block a user