feat(08-10-receipt-module): phases 08-10 complete — receipt issuing from orders

Phase 08 — DB Foundation:
- 3 new tables: receipt_configs, receipts, receipt_number_counters
- company_settings extended with BDO, REGON, KRS, logo fields

Phase 09 — Receipt Config:
- CRUD for receipt configurations (Settings > Accounting)
- ReceiptConfigController + ReceiptConfigRepository

Phase 10 — Receipt Issuing:
- ReceiptRepository with atomic numbering (INSERT ON DUPLICATE KEY UPDATE)
- ReceiptController with snapshot pattern (seller/buyer/items as JSON)
- "Wystaw paragon" button in order view
- Documents tab showing both receipts and marketplace documents
- Activity log entry on receipt creation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 19:49:06 +01:00
parent 3bccc7a533
commit ed057fc304
31 changed files with 2539 additions and 39 deletions

View File

@@ -0,0 +1,320 @@
---
phase: 10-receipt-issue
plan: 01
type: execute
wave: 1
depends_on: ["09-01"]
files_modified:
- src/Modules/Accounting/ReceiptRepository.php
- src/Modules/Accounting/ReceiptController.php
- src/Modules/Orders/OrdersController.php
- resources/views/orders/show.php
- resources/views/orders/receipt-create.php
- routes/web.php
- resources/lang/pl.php
- resources/scss/shared/_ui-components.scss
- public/assets/css/app.css
- DOCS/ARCHITECTURE.md
autonomous: false
---
<objective>
## Goal
Umozliwienie wystawiania paragonow z poziomu widoku zamowienia — przycisk "Wystaw paragon", formularz wyboru konfiguracji z podgladem pozycji, zapis do bazy z atomowym numerowaniem i snapshotem danych.
## Purpose
Kluczowa funkcjonalnosc modulu Ksiegowosci (v0.3) — sprzedawca moze wystawic paragon bezposrednio z zamowienia, bez opuszczania widoku zamowienia.
## Output
- `ReceiptRepository` — CRUD na `receipts` + atomowe numerowanie przez `receipt_number_counters`
- `ReceiptController` — formularz tworzenia paragonu + zapis
- Przycisk "Wystaw paragon" w widoku zamowienia
- Lista wystawionych paragonow w zakladce "Dokumenty"
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Prior Work
@.paul/phases/08-db-foundation/08-01-SUMMARY.md — schemat tabel receipts, receipt_configs, receipt_number_counters
@.paul/phases/09-receipt-config/09-01-SUMMARY.md — CRUD konfiguracji, ReceiptConfigRepository, wzorce Request/Csrf
## Source Files
@database/migrations/20260315_000051_create_receipts_table.sql
@database/migrations/20260315_000052_create_receipt_number_counters_table.sql
@src/Modules/Settings/ReceiptConfigRepository.php
@src/Modules/Settings/CompanySettingsRepository.php
@src/Modules/Orders/OrdersController.php
@resources/views/orders/show.php
@routes/web.php
</context>
<skills>
## Required Skills (from SPECIAL-FLOWS.md)
| Skill | Priority | When to Invoke | Loaded? |
|-------|----------|----------------|---------|
| sonar-scanner | required | Po APPLY, przed UNIFY | ○ |
## Skill Invocation Checklist
- [ ] sonar-scanner uruchomiony po APPLY
</skills>
<acceptance_criteria>
## AC-1: Przycisk "Wystaw paragon" w widoku zamowienia
```gherkin
Given uzytkownik jest na stronie szczegulow zamowienia /orders/{id}
When sa aktywne konfiguracje paragonow
Then widoczny jest przycisk "Wystaw paragon" w sekcji akcji
And przycisk prowadzi do formularza /orders/{id}/receipt/create
```
## AC-2: Formularz wystawiania paragonu
```gherkin
Given uzytkownik otwiera /orders/{id}/receipt/create
When formularz sie laduje
Then widoczny jest select z aktywnymi konfiguracjami paragonow
And wyswietlona jest tabela pozycji zamowienia (nazwa, ilosc, cena, suma)
And wyswietlone sa pola: data wystawienia (domyslnie dzis), data sprzedazy (wg konfiguracji)
And widoczny jest podglad danych sprzedawcy z company_settings
And przycisk "Wystaw paragon" submituje formularz
```
## AC-3: Zapis paragonu z atomowym numerowaniem
```gherkin
Given uzytkownik wypelnia formularz i klika "Wystaw paragon"
When POST /orders/{id}/receipt/store jest wysylany
Then tworzony jest rekord w tabeli receipts z:
- receipt_number wygenerowanym atomowo z receipt_number_counters (INSERT ON DUPLICATE KEY UPDATE)
- seller_data_json jako snapshot company_settings
- items_json jako snapshot pozycji zamowienia
- total_net i total_gross obliczone z pozycji
- sale_date okreslona wg sale_date_source z konfiguracji
- order_reference_value wypelnione wg order_reference z konfiguracji
And uzytkownik jest przekierowany na /orders/{id} z flash success
```
## AC-4: Lista paragonow w zakladce Dokumenty
```gherkin
Given zamowienie ma wystawione paragony
When uzytkownik klika zakladke "Dokumenty"
Then wyswietlona jest tabela paragonow (numer, data wystawienia, kwota brutto, konfiguracja)
And kazdy paragon ma link do podgladu (na razie placeholder faza 11)
```
## AC-5: Walidacja — brak duplikatow i brak pustych konfiguracji
```gherkin
Given uzytkownik probuje wystawic paragon
When nie ma aktywnych konfiguracji
Then przycisk "Wystaw paragon" nie jest widoczny w widoku zamowienia
Given uzytkownik submituje formularz bez wybranej konfiguracji
When POST jest wysylany
Then zwracany jest blad walidacji i paragon nie jest tworzony
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: ReceiptRepository — CRUD + atomowe numerowanie</name>
<files>src/Modules/Accounting/ReceiptRepository.php</files>
<action>
Utworz nowy modul `src/Modules/Accounting/` z klasa `ReceiptRepository`:
1. **Konstruktor:** przyjmuje `PDO $pdo`
2. **getNextNumber(int $configId, string $numberFormat, string $numberingType): string**
- Atomowe numerowanie przez `INSERT INTO receipt_number_counters (config_id, year, month, last_number) VALUES (:config_id, :year, :month, 1) ON DUPLICATE KEY UPDATE last_number = last_number + 1`
- Odczyt `last_number` przez `SELECT last_number FROM receipt_number_counters WHERE config_id = :config_id AND year = :year AND month = :month`
- Dla `numbering_type = 'yearly'`: month = NULL (w unique key)
- Dla `numbering_type = 'monthly'`: month = biezacy miesiac
- Podmiana w formacie: `%N` → numer (z zerem wiodacym min 3 cyfry), `%M` → miesiac (2 cyfry), `%Y` → rok (4 cyfry)
3. **create(array $data): int**
- INSERT do `receipts` ze wszystkimi polami
- Zwraca `lastInsertId()`
4. **findByOrderId(int $orderId): array**
- SELECT receipts + LEFT JOIN receipt_configs (na nazwe konfiguracji)
- ORDER BY created_at DESC
5. **findById(int $id): ?array**
- SELECT * WHERE id = :id
Wzorzec: analogicznie do ReceiptConfigRepository (PDO, prepared statements, strict types).
Namespace: `App\Modules\Accounting`
</action>
<verify>Klasa parsuje sie bez bledow: `php -l src/Modules/Accounting/ReceiptRepository.php`</verify>
<done>AC-3 backend spelnione: atomowe numerowanie i zapis paragonu</done>
</task>
<task type="auto">
<name>Task 2: ReceiptController + routing + widok formularza</name>
<files>
src/Modules/Accounting/ReceiptController.php,
resources/views/orders/receipt-create.php,
routes/web.php,
resources/lang/pl.php
</files>
<action>
1. **ReceiptController** (`App\Modules\Accounting`):
- Konstruktor: `Template, Translator, AuthService, ReceiptRepository, ReceiptConfigRepository, CompanySettingsRepository, OrdersRepository`
- **create(Request $request): Response** — GET /orders/{id}/receipt/create
- Pobierz zamowienie przez OrdersRepository::findDetails($orderId)
- Pobierz aktywne konfiguracje: ReceiptConfigRepository::listAll() + filtruj is_active = 1
- Pobierz dane sprzedawcy: CompanySettingsRepository::getSettings()
- Renderuj widok `orders/receipt-create`
- **store(Request $request): Response** — POST /orders/{id}/receipt/store
- Walidacja: config_id wymagane, zamowienie istnieje
- CSRF: Csrf::validate()
- Pobierz konfiguracje (findById), zamowienie (findDetails), company settings
- Oblicz sale_date wg `sale_date_source`: order_date → ordered_at, payment_date → z payments, issue_date → dzis
- Oblicz order_reference_value wg `order_reference`: none → NULL, orderpro → internal_order_number, integration → external_order_id
- Zbuduj seller_data_json z company_settings (company_name, tax_number, street, city, postal_code, phone, email, bank_account, bdo_number, regon, court_register)
- Zbuduj buyer_data_json z address (invoice lub customer)
- Zbuduj items_json z pozycji zamowienia (original_name, quantity, original_price_with_tax, sku, ean)
- Oblicz total_gross = suma(qty * price), total_net = total_gross (paragony nie rozdzielaja netto/brutto — wartosc taka sama)
- Wygeneruj numer przez ReceiptRepository::getNextNumber()
- ReceiptRepository::create() ze wszystkimi danymi
- Flash::set('order.success', 'Paragon wystawiony: ' . $receiptNumber)
- Redirect /orders/{id}
2. **Widok receipt-create.php:**
- Layout z naglowkiem "Wystaw paragon" + link powrotny do zamowienia
- Select z aktywnymi konfiguracjami (name + number_format)
- Tabela pozycji zamowienia (readonly): nazwa, ilosc, cena, suma
- Podsumowanie kwoty brutto
- Podglad danych sprzedawcy (readonly z company_settings)
- Pole daty wystawienia (input date, domyslnie dzis)
- Przycisk "Wystaw paragon" + CSRF token
3. **routes/web.php:**
- Dodaj use statements: `ReceiptController`, `ReceiptRepository` (z `App\Modules\Accounting`)
- Utworz instancje: `$receiptRepository = new ReceiptRepository($app->db())`
- Utworz kontroler: `$receiptController = new ReceiptController($template, $translator, $auth, $receiptRepository, $receiptConfigRepository, $companySettingsRepository, new OrdersRepository($app->db()))`
- Zarejestruj trasy:
- `GET /orders/{id}/receipt/create``[$receiptController, 'create']`
- `POST /orders/{id}/receipt/store``[$receiptController, 'store']`
4. **resources/lang/pl.php:**
- Dodaj klucze `receipts.create.title`, `receipts.create.select_config`, `receipts.create.issue_date`, `receipts.create.submit`, `receipts.create.seller_data`, `receipts.create.items`, `receipts.create.total`, `receipts.create.back`
Wzorzec kontrolera: analogicznie do ReceiptConfigController (Csrf::token(), Csrf::validate(), Flash::set/get, Request::input()).
NIE uzywaj natywnych alert()/confirm() — OrderProAlerts juz jest w uzyciu.
</action>
<verify>
- `php -l src/Modules/Accounting/ReceiptController.php`
- `php -l resources/views/orders/receipt-create.php`
- Otworz /orders/{id}/receipt/create w przegladarce — formularz sie wyswietla
</verify>
<done>AC-2, AC-3, AC-5 spelnione: formularz, zapis, walidacja</done>
</task>
<task type="auto">
<name>Task 3: Integracja z widokiem zamowienia — przycisk + zakladka Dokumenty</name>
<files>
src/Modules/Orders/OrdersController.php,
resources/views/orders/show.php,
routes/web.php
</files>
<action>
1. **OrdersController::show():**
- Dodaj zaleznosc: `ReceiptConfigRepository` i `ReceiptRepository` (przez konstruktor lub przekazanie w widoku)
- UWAGA: OrdersController ma juz 5 parametrow konstruktora. Zamiast rozszerzac konstruktor, przekaz dane przez nowe instancje w routes/web.php:
- W web.php: pobierz aktywne configs i receipts w zamknieciu routera GET /orders/{id} LUB
- Prostsze: dodaj `?ReceiptRepository` i `?ReceiptConfigRepository` do konstruktora OrdersController
- W metodzie show(): pobierz `$receiptConfigs = $this->receiptConfigs->listAll()` (filtruj aktywne)
- Pobierz `$receipts = $this->receipts->findByOrderId($orderId)`
- Przekaz do widoku: `'receiptConfigs' => $activeConfigs, 'receipts' => $receipts`
2. **orders/show.php — przycisk "Wystaw paragon":**
- W sekcji `.order-details-actions` (linia ~47-54):
- Dodaj przycisk miedzy "Przygotuj przesylke" a "Platnosc":
```php
<?php if (($receiptConfigs ?? []) !== []): ?>
<a href="/orders/<?= $e((string) ($orderId ?? 0)) ?>/receipt/create" class="btn btn--secondary">Wystaw paragon</a>
<?php endif; ?>
```
- Przycisk widoczny tylko gdy sa aktywne konfiguracje (AC-1, AC-5)
3. **orders/show.php — zakladka Dokumenty:**
- Zastap pusty placeholder w `data-order-tab-panel="documents"` (linia ~516-521):
- Tabela paragonow: Numer, Data wystawienia, Kwota brutto, Konfiguracja, Akcje
- Jesli brak paragonow: "Brak dokumentow"
- Akcja: link "Podglad" (na razie `#` — placeholder do fazy 11)
- Zaktualizuj licznik w zakladce: `Dokumenty (N)` gdzie N = count($receipts)
4. **routes/web.php:**
- Dodaj `use App\Modules\Accounting\ReceiptRepository;` na gorze
- Przekaz `$receiptRepository` i `$receiptConfigRepository` do konstruktora `$ordersController`
5. **DOCS/ARCHITECTURE.md:**
- Dodaj modul Accounting z klasami ReceiptRepository, ReceiptController
- Dodaj trasy GET/POST /orders/{id}/receipt/*
</action>
<verify>
- Otworz /orders/{id} — przycisk "Wystaw paragon" widoczny (jesli sa aktywne configs)
- Kliknij zakladke "Dokumenty" — tabela paragonow wyswietla sie
- Po wystawieniu paragonu — pojawia sie na liscie w zakladce Dokumenty
</verify>
<done>AC-1, AC-4 spelnione: przycisk w widoku zamowienia + lista paragonow w Dokumentach</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>Wystawianie paragonow z poziomu zamowienia — pelny flow: przycisk → formularz → zapis → lista w Dokumentach</what-built>
<how-to-verify>
1. Otworz dowolne zamowienie /orders/{id}
2. Sprawdz: przycisk "Wystaw paragon" jest widoczny w akcjach
3. Kliknij "Wystaw paragon" — formularz sie otwiera
4. Wybierz konfiguracje, sprawdz podglad pozycji i danych sprzedawcy
5. Kliknij "Wystaw paragon" w formularzu
6. Sprawdz: redirect na zamowienie z flash "Paragon wystawiony: PAR/001/03/2026"
7. Kliknij zakladke "Dokumenty" — paragon widoczny na liscie
8. Wystaw drugi paragon — numer powinien byc PAR/002/03/2026
</how-to-verify>
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- database/migrations/* (schemat zablokowany — tabele juz istnieja z fazy 08)
- src/Modules/Settings/ReceiptConfigRepository.php (gotowe z fazy 09)
- src/Modules/Settings/ReceiptConfigController.php (gotowe z fazy 09)
- resources/views/settings/accounting.php (gotowe z fazy 09)
## SCOPE LIMITS
- Brak podgladu/wydruku paragonu — to faza 11
- Brak edycji/anulowania paragonu — poza zakresem v0.3
- Brak generowania PDF — to faza 11
- Paragony nie rozdzielaja netto/brutto (total_net = total_gross) — uproszczenie dla paragonow
</boundaries>
<verification>
Before declaring plan complete:
- [ ] `php -l src/Modules/Accounting/ReceiptRepository.php` — brak bledow
- [ ] `php -l src/Modules/Accounting/ReceiptController.php` — brak bledow
- [ ] Przycisk "Wystaw paragon" widoczny w /orders/{id} gdy sa aktywne konfiguracje
- [ ] Formularz /orders/{id}/receipt/create wyswietla pozycje zamowienia i konfiguracje
- [ ] Po wystawieniu paragonu: redirect + flash + rekord w bazie
- [ ] Numer paragonu generowany atomowo (kolejne numery nie powtarzaja sie)
- [ ] Zakladka Dokumenty wyswietla wystawione paragony
- [ ] Brak bledow w konsoli PHP
- [ ] All acceptance criteria met
</verification>
<success_criteria>
- Wszystkie 3 taski auto + 1 checkpoint ukonczone
- Wszystkie AC-1 do AC-5 spelnione
- Brak bledow PHP (php -l na wszystkich nowych plikach)
- Weryfikacja manualna przez uzytkownika (checkpoint)
</success_criteria>
<output>
After completion, create `.paul/phases/10-receipt-issue/10-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,142 @@
---
phase: 10-receipt-issue
plan: 01
subsystem: accounting
tags: [php, receipts, orders, crud, snapshots, atomic-numbering]
requires:
- phase: 08-db-foundation
provides: receipts, receipt_configs, receipt_number_counters tables
- phase: 09-receipt-config
provides: ReceiptConfigRepository CRUD, active configs
provides:
- Wystawianie paragonow z poziomu zamowienia
- ReceiptRepository (CRUD + atomowe numerowanie)
- ReceiptController (formularz + zapis ze snapshotami)
- Przycisk "Wystaw paragon" w widoku zamowienia
- Lista paragonow + dokumentow zewnetrznych w zakladce Dokumenty
- Activity log entry po wystawieniu paragonu
affects: [11-receipt-print, 12-accounting-list]
tech-stack:
added: []
patterns: [snapshot-json-pattern for seller/buyer/items, atomic-counter-pattern for receipt numbering]
key-files:
created:
- src/Modules/Accounting/ReceiptRepository.php
- src/Modules/Accounting/ReceiptController.php
- resources/views/orders/receipt-create.php
modified:
- src/Modules/Orders/OrdersController.php
- resources/views/orders/show.php
- routes/web.php
- resources/lang/pl.php
- DOCS/ARCHITECTURE.md
key-decisions:
- "ReceiptRepository w App\\Modules\\Accounting (nowy modul, nie w Settings)"
- "Snapshot pattern: seller/buyer/items jako JSON w momencie wystawienia"
- "Atomowe numerowanie: INSERT ON DUPLICATE KEY UPDATE na receipt_number_counters"
- "total_net = total_gross (paragony nie rozdzielaja netto/brutto)"
- "OrdersController rozszerzony o opcjonalne ?ReceiptRepository i ?ReceiptConfigRepository"
patterns-established:
- "Modul Accounting: osobny namespace dla funkcjonalnosci ksiegowych"
- "Snapshot przy tworzeniu dokumentu: dane zamrazane w JSON, niezalezne od przyszlych zmian zrodla"
- "Activity log: recordActivity() po kazdej akcji generujacej dokument"
duration: ~25min
completed: 2026-03-15
---
# Phase 10 Plan 01: Wystawianie paragonow z zamowienia Summary
**Pelny flow wystawiania paragonow: przycisk w zamowieniu, formularz z podgladem pozycji/sprzedawcy, zapis z atomowym numerowaniem i snapshotami, lista w zakladce Dokumenty + wpis w historii.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~25min |
| Completed | 2026-03-15 |
| Tasks | 3 auto + 1 checkpoint |
| Files created | 3 |
| Files modified | 5 |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Przycisk "Wystaw paragon" w widoku zamowienia | Pass | Widoczny tylko gdy sa aktywne konfiguracje |
| AC-2: Formularz wystawiania paragonu | Pass | Select konfiguracji, tabela pozycji, podglad sprzedawcy, data wystawienia |
| AC-3: Zapis paragonu z atomowym numerowaniem | Pass | INSERT ON DUPLICATE KEY UPDATE, snapshoty JSON |
| AC-4: Lista paragonow w zakladce Dokumenty | Pass | Paragony + dokumenty zewnetrzne w osobnych sekcjach |
| AC-5: Walidacja — brak duplikatow i brak pustych konfiguracji | Pass | Przycisk ukryty bez konfiguracji, walidacja config_id |
## Accomplishments
- Nowy modul `App\Modules\Accounting` z ReceiptRepository i ReceiptController
- Atomowe numerowanie paragonow przez receipt_number_counters (INSERT ON DUPLICATE KEY UPDATE)
- Snapshoty seller/buyer/items jako JSON — dane zamrozone w momencie wystawienia
- Zakladka Dokumenty wyswietla zarowno paragony jak i dokumenty zewnetrzne z marketplace
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `src/Modules/Accounting/ReceiptRepository.php` | Created | CRUD na receipts + atomowe numerowanie |
| `src/Modules/Accounting/ReceiptController.php` | Created | Formularz tworzenia + zapis paragonu + activity log |
| `resources/views/orders/receipt-create.php` | Created | Widok formularza wystawiania paragonu |
| `src/Modules/Orders/OrdersController.php` | Modified | Dodano ?ReceiptRepository, ?ReceiptConfigRepository do konstruktora + show() |
| `resources/views/orders/show.php` | Modified | Przycisk "Wystaw paragon", zakladka Dokumenty z paragony + dokumenty zewnetrzne |
| `routes/web.php` | Modified | Instancje ReceiptRepository/ReceiptController, 2 nowe trasy |
| `resources/lang/pl.php` | Modified | Tlumaczenia receipts.create.*, receipts.documents.*, receipt_issued |
| `DOCS/ARCHITECTURE.md` | Modified | Klasy Accounting, przeplyw wystawiania paragonu |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Scope additions | 2 | User feedback — activity log + dokumenty zewnetrzne |
**Total impact:** Dwa uzasadnione rozszerzenia poza planem, poprawiajace UX.
### Scope Additions
**1. Activity log po wystawieniu paragonu**
- **Source:** User feedback podczas checkpoint
- **Issue:** Brak wpisu w historii zmian zamowienia po wystawieniu paragonu
- **Fix:** Dodano recordActivity() z typem `receipt_issued` + tlumaczenie
- **Files:** ReceiptController.php, resources/lang/pl.php
**2. Dokumenty zewnetrzne w zakladce Dokumenty**
- **Source:** User feedback — licznik Dokumenty(2) ale widoczny tylko 1 paragon
- **Issue:** Zakladka wyswietlala tylko paragony, nie dokumenty z order_documents
- **Fix:** Dodano sekcje "Dokumenty zewnetrzne" z tabela order_documents
- **Files:** resources/views/orders/show.php
## Issues Encountered
| Issue | Resolution |
|-------|------------|
| Duplikacja instancji ReceiptConfigRepository/ReceiptRepository w web.php | Przeniesiono tworzenie przed ordersController, usunieto duplikaty |
## Next Phase Readiness
**Ready:**
- ReceiptRepository::findById() gotowe pod podglad paragonu (faza 11)
- Snapshoty JSON (seller, buyer, items) gotowe do renderowania HTML/PDF
- receipt_number unikalne — gotowe do wyswietlania
**Concerns:**
- Brak
**Blockers:**
- Brak
---
*Phase: 10-receipt-issue, Plan: 01*
*Completed: 2026-03-15*