first commit

This commit is contained in:
2026-07-06 20:16:00 +02:00
commit 352e6d9e22
147 changed files with 12242 additions and 0 deletions
@@ -0,0 +1,288 @@
---
plan_id: 20260706-1430-modul-inwestycji
title: Moduł inwestycji — instrumenty, wpłaty/wypłaty, ręczne wyceny, pulpit
storage: plan-first
legacy_phase: null
created: 2026-07-06T14:30:00+02:00
status: planned
type: execute
autonomous: true
delegation: auto
files_modified:
- app/Database/Migrations/2026-07-06-140001_CreateInvInstruments.php
- app/Database/Migrations/2026-07-06-140002_CreateInvOperations.php
- app/Database/Migrations/2026-07-06-140003_CreateInvValuations.php
- app/Database/Seeds/DefaultInstrumentsSeeder.php
- app/Models/InstrumentModel.php
- app/Models/InvOperationModel.php
- app/Models/ValuationModel.php
- app/Controllers/Instruments.php
- app/Controllers/InvOperations.php
- app/Controllers/Valuations.php
- app/Controllers/Investments.php
- app/Views/instruments/index.php
- app/Views/instruments/form.php
- app/Views/inv_operations/index.php
- app/Views/inv_operations/form.php
- app/Views/valuations/index.php
- app/Views/valuations/form.php
- app/Views/investments/index.php
- app/Config/Routes.php
- app/Views/layout/main.php
quality_radar: degraded
---
<objective>
## Goal
Dodać do finansePRO moduł inwestycji: płaska lista instrumentów (kategorie główne, np. obligacje, ETF), operacje wpłat/wypłat per instrument, ręczne snapshoty wyceny per instrument oraz pulpit inwestycji z wykresem zbiorczym w czasie i tabelą rozbicia per instrument.
## Purpose
Domknięcie Planu 2 z roadmapy. Użytkownik chce śledzić, ile realnie włożył w inwestycje (netto), ile są teraz warte i ile zarobił — bez integracji z API kursów (wyceny ręczne).
## Output
3 migracje (`inv_instruments`, `inv_operations`, `inv_valuations`), seeder instrumentów, 3 modele, 4 kontrolery (CRUD + pulpit), widoki, trasy i pozycja w nawigacji.
</objective>
<context>
## Project Docs
@.paul/PROJECT.md
@.paul/STATE.md
@.paul/codebase/impact_map.md
@.paul/codebase/quality_risks.md
@.paul/plans/20260706-1248-fundament-wydatki-przychody/SUMMARY.md
## Source Files (wzorce do naśladowania)
@app/Models/OperationModel.php
@app/Controllers/Operations.php
@app/Controllers/Dashboard.php
@app/Config/Routes.php
@app/Views/layout/main.php
@app/Views/dashboard/index.php
@app/Database/Migrations/2026-07-06-100002_CreateOperations.php
</context>
<clarifications>
- Instrumenty: płaska lista kategorii głównych (obligacje, ETF, …), bez podkategorii i bez rozróżniania konkretnych funduszy.
- Zarobek per instrument: `zarobek = ostatnia_wycena (Σwpłat Σwypłat)`. Wypłaty zmniejszają zainwestowany kapitał (zainwestowane netto).
- Wyceny: wyłącznie ręczne snapshoty (`inv_valuations`), brak integracji z API kursów.
- Pulpit: jeden wykres zbiorczy w czasie (Σwpłat_netto | wartość | zarobek) + tabela rozbicia per instrument (wpłacono | wartość | zarobek | %).
</clarifications>
<impact_scan>
## Quality Radar
**Status:** degraded (weryfikacja runtime + odczyt plików źródłowych; pełny `codebase-memory-mcp` odłożony — jscpd/ast-grep wyłączone polityką w `config.md`)
**Tools:** codebase-memory-mcp (nieindeksowany), fallback: bezpośredni odczyt plików
## Affected Areas
- Domena inwestycji (nowa): `app/Models/{Instrument,InvOperation,Valuation}Model.php`, `app/Controllers/{Instruments,InvOperations,Valuations,Investments}.php`, migracje `inv_*`, widoki `instruments/`, `inv_operations/`, `valuations/`, `investments/`.
- Routing: `app/Config/Routes.php` — nowa grupa tras w bloku `auth`.
- Nawigacja: `app/Views/layout/main.php` — jedna pozycja „Inwestycje”.
- Baza: nowe tabele `inv_instruments`, `inv_operations`, `inv_valuations` (FK do `inv_instruments`).
## Duplicate / Hardcoded Risks
- Ryzyko duplikacji logiki filtrów/sum wobec `OperationModel` — świadomie osobne modele (inna domena: brak `type income/expense`, dochodzą wyceny). Wzorce zapytań kopiowane celowo, kod trzymany krótki. Obsłużone: brak współdzielonego „drugiego źródła prawdy” dla wartości domenowych.
- Typ operacji inwestycyjnej `deposit|withdraw` jako ENUM w migracji + reguła walidacji `in_list` w modelu — jedno źródło (ENUM DB), walidacja to strażnik wejścia. Obsłużone w Task 1.
- Definicja zarobku (netto) liczona w jednym miejscu — `InstrumentModel`/`Investments` (metoda agregująca), nie powielana w widokach. Obsłużone w Task 3.
## Explicit Deferrals
- Pełna indeksacja `codebase-memory-mcp` — odłożona do `$paul-map-codebase` (spójne z Planem 1).
- Integracja kursów z API, waluty obce, podatek Belki — poza zakresem (wyceny ręczne w PLN).
</impact_scan>
<skills>
Brak `.paul/SPECIAL-FLOWS.md` — sekcja skills pominięta.
</skills>
<acceptance_criteria>
## AC-1: Instrumenty (CRUD, płaska lista)
```gherkin
Given zalogowany użytkownik
When otwiera /instruments i dodaje instrument "Obligacje"
Then instrument zapisuje się i pojawia na liście
And próba usunięcia instrumentu z operacjami lub wycenami jest zablokowana komunikatem
```
## AC-2: Operacje inwestycyjne (wpłaty/wypłaty)
```gherkin
Given istnieje instrument "ETF"
When użytkownik dodaje wpłatę 1000 zł i wypłatę 200 zł na "ETF"
Then obie operacje zapisują się z poprawnym typem (deposit/withdraw)
And lista pokazuje je z nazwą instrumentu, filtrowanie po instrumencie zwraca właściwe wiersze
```
## AC-3: Ręczne wyceny (snapshoty)
```gherkin
Given istnieje instrument "ETF" z wpłatami
When użytkownik dodaje wycenę 950 zł na dziś
Then wycena zapisuje się per instrument z datą
And ostatnia wycena instrumentu jest używana do wyliczenia zarobku
```
## AC-4: Zarobek per instrument (netto)
```gherkin
Given "ETF": Σwpłat = 1000, Σwypłat = 200, ostatnia wycena = 950
When system liczy zarobek
Then wpłacone_netto = 800 (1000 200)
And zarobek = 150 (950 800)
```
## AC-5: Pulpit inwestycji (wykres zbiorczy + tabela)
```gherkin
Given istnieją instrumenty z operacjami i wycenami
When użytkownik otwiera /investments
Then widzi kafelki (wpłacono netto, wartość, zarobek) dla całego portfela
And wykres w czasie z 3 seriami: Σwpłat_netto, wartość, zarobek
And tabelę per instrument: wpłacono | wartość | zarobek | % zwrotu
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Schemat DB + modele + seeder instrumentów</name>
<files>
app/Database/Migrations/2026-07-06-140001_CreateInvInstruments.php,
app/Database/Migrations/2026-07-06-140002_CreateInvOperations.php,
app/Database/Migrations/2026-07-06-140003_CreateInvValuations.php,
app/Database/Seeds/DefaultInstrumentsSeeder.php,
app/Models/InstrumentModel.php,
app/Models/InvOperationModel.php,
app/Models/ValuationModel.php
</files>
<action>
Migracje (wzorzec: CreateOperations — InnoDB, timestamps DATETIME null, indeksy):
- `inv_instruments`: id, name VARCHAR(120), created_at/updated_at. Unikalny indeks na `name`.
- `inv_operations`: id, instrument_id (unsigned, NOT NULL), date DATE, amount DECIMAL(12,2),
type ENUM('deposit','withdraw'), description VARCHAR(255) null, timestamps.
Indeksy: date, instrument_id, type. FK instrument_id → inv_instruments(id) ON DELETE RESTRICT
(nie kasujemy instrumentu z operacjami — spójne z blokadą w kontrolerze).
- `inv_valuations`: id, instrument_id (unsigned, NOT NULL), date DATE, value DECIMAL(12,2), timestamps.
Indeksy: date, instrument_id. FK instrument_id → inv_instruments(id) ON DELETE RESTRICT.
Modele (returnType array, useTimestamps true, validationRules):
- InstrumentModel (tabela inv_instruments): allowedFields=['name']; reguła name required|max_length[120].
Metoda `withStats()`: zwraca instrumenty z Σwpłat, Σwypłat, wpłacone_netto, ostatnia wycena (value
z max(date) per instrument — podzapytanie/JOIN), zarobek = ostatnia_wycena wpłacone_netto,
procent = wpłacone_netto>0 ? zarobek/wpłacone_netto*100 : null. Metoda `hasChildren($id)`:
true jeśli instrument ma operacje lub wyceny (do blokady usunięcia).
- InvOperationModel (tabela inv_operations): allowedFields=['instrument_id','date','amount','type','description'];
reguły: instrument_id required|is_natural_no_zero, date required|valid_date[Y-m-d],
amount required|decimal|greater_than[0], type required|in_list[deposit,withdraw],
description permit_empty|max_length[255]. Metoda `filtered($f)`: JOIN nazwa instrumentu,
opcjonalny filtr instrument_id/type/from/to, orderBy date DESC.
- ValuationModel (tabela inv_valuations): allowedFields=['instrument_id','date','value'];
reguły: instrument_id required|is_natural_no_zero, date required|valid_date[Y-m-d],
value required|decimal|greater_than_equal_to[0]. Metoda `filtered($f)`: JOIN nazwa instrumentu,
orderBy date DESC.
Seeder DefaultInstrumentsSeeder: wstaw "Obligacje" i "ETF" (idempotentnie — pomiń jeśli name istnieje).
</action>
<verify>php spark migrate && php spark db:seed DefaultInstrumentsSeeder — 3 tabele utworzone, 2 instrumenty</verify>
<done>AC-1 (schemat instrumentów), AC-2 (schemat operacji), AC-3 (schemat wycen), AC-4 (logika zarobku w withStats)</done>
</task>
<task type="auto">
<name>Task 2: Kontrolery CRUD + widoki + routing + nawigacja</name>
<files>
app/Controllers/Instruments.php,
app/Controllers/InvOperations.php,
app/Controllers/Valuations.php,
app/Views/instruments/index.php,
app/Views/instruments/form.php,
app/Views/inv_operations/index.php,
app/Views/inv_operations/form.php,
app/Views/valuations/index.php,
app/Views/valuations/form.php,
app/Config/Routes.php,
app/Views/layout/main.php
</files>
<action>
Kontrolery (wzorzec: Operations/Categories — metoda model(), index/new/create/edit/update/delete,
persist() z redirectem i flashdata):
- Instruments: CRUD instrumentów. delete() blokuje gdy hasChildren($id) → redirect z error.
- InvOperations: CRUD wpłat/wypłat. index z filtrami (instrument_id/type/from/to), przekazuje listę
instrumentów do selecta. type walidowany do deposit|withdraw.
- Valuations: CRUD wycen. index z filtrem instrument_id, select instrumentów w formularzu.
Widoki (wzorzec: operations/index + form, Bootstrap 5, flashdata z layoutu):
- instruments/index: tabela instrumentów + link do withStats? (statystyki zostają na pulpicie — tu prosty CRUD).
- inv_operations/index: filtry (instrument, typ, zakres dat) + tabela (data, instrument, typ badge,
kwota, opis, akcje). form: select instrument, date, amount, radio/select typ deposit/withdraw, opis.
- valuations/index: filtr instrument + tabela (data, instrument, wartość, akcje). form: select instrument, date, value.
Routes.php: w grupie 'auth' dodać trasy REST-lite (jak operations) dla:
instruments, investments/operations (kontroler InvOperations), investments/valuations (kontroler Valuations),
oraz investments (Investments::index z Task 3). Prefiksy URL czytelne, bez kolizji z istniejącym /operations.
layout/main.php: dodać jedną pozycję nav "Inwestycje" → site_url('investments') między "Operacje" a "Kategorie".
</action>
<verify>php spark serve + curl: dodanie instrumentu, wpłaty, wypłaty, wyceny; filtr po instrumencie; blokada usunięcia instrumentu z operacjami</verify>
<done>AC-1, AC-2, AC-3 (pełny CRUD i filtry przez UI/HTTP)</done>
</task>
<task type="auto">
<name>Task 3: Pulpit inwestycji — agregacja, wykres zbiorczy, tabela per instrument</name>
<files>
app/Controllers/Investments.php,
app/Views/investments/index.php
</files>
<action>
Investments::index (wzorzec: Dashboard::index):
- Kafelki portfela: Σwpłacone_netto, Σwartość (suma ostatnich wycen per instrument), Σzarobek.
- Tabela per instrument z InstrumentModel::withStats(): nazwa | wpłacono netto | wartość | zarobek | % zwrotu
(zarobek kolorem: dodatni income / ujemny expense — klasy z layoutu).
- Szereg czasowy zbiorczy do wykresu: zbierz wszystkie daty wycen (distinct). Dla każdej daty:
wartość = suma po instrumentach ostatniej wyceny o dacie ≤ ta data; wpłacone_netto = Σ(depositwithdraw)
o dacie ≤ ta data; zarobek = wartość wpłacone_netto. Policz w PHP (metoda w Investments lub
InstrumentModel::portfolioTimeline()) — nie duplikuj logiki zarobku w widoku.
// ponytail: timeline liczony w PHP z kilku zapytań, bez okien SQL; przy setkach wycen i tak trywialne.
Widok investments/index (wzorzec dashboard/index + ApexCharts z layoutu):
- 3 kafelki, wykres liniowy (area/line) z 3 seriami: "Wpłacono netto", "Wartość", "Zarobek",
xaxis = daty wycen; noData gdy pusto.
- tabela per instrument pod wykresem.
</action>
<verify>curl /investments: kafelki i tabela zgodne z ręcznym wyliczeniem (ETF: netto 800, wartość 950, zarobek 150); JSON serii wykresu poprawny</verify>
<done>AC-4 (zarobek netto), AC-5 (pulpit: kafelki + wykres + tabela)</done>
</task>
</tasks>
<boundaries>
## Do Not Change
- Moduł finansów bieżących (categories, operations, dashboard) — działa, nie ruszać poza dodaniem 1 pozycji nav.
- `app/Config/Database.php`, `.env`, auth/filtry — bez zmian.
- Istniejące migracje `categories`/`operations` i seeder kategorii.
## Scope Limits
- Bez integracji API kursów, walut obcych, podatków, prowizji.
- Bez podkategorii instrumentów (płaska lista).
- Bez edycji wielu instrumentów jednym formularzem / importu CSV.
</boundaries>
<verification>
- [ ] `php spark migrate` — 3 nowe tabele `inv_*` na bazie hostido.
- [ ] `php spark db:seed DefaultInstrumentsSeeder` — Obligacje, ETF.
- [ ] CRUD instrumentów/operacji/wycen przez HTTP (create/edit/delete, filtry).
- [ ] Blokada usunięcia instrumentu z operacjami/wycenami (FK RESTRICT + kontroler).
- [ ] Zarobek per instrument = ostatnia wycena (Σwpłat Σwypłat); przykład ETF = 150.
- [ ] Pulpit /investments: kafelki + wykres 3 serie + tabela per instrument.
- [ ] Nawigacja pokazuje "Inwestycje"; brak regresji w istniejących modułach.
- [ ] Quality Radar relevant risks handled or deferred.
</verification>
<success_criteria>
- [ ] Wszystkie AC (AC-1..AC-5) Pass.
- [ ] Weryfikacja runtime kompletna (migrate/seed/serve/curl).
- [ ] STATE.md wskazuje ten plan; radar delta zapisana w UNIFY.
</success_criteria>
<output>
SUMMARY.md path: `.paul/plans/20260706-1430-modul-inwestycji/SUMMARY.md`
</output>