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>
360 lines
25 KiB
Markdown
360 lines
25 KiB
Markdown
---
|
|
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>
|