--- 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 --- ## 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. ## 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 - 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 | %). ## 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). Brak `.paul/SPECIAL-FLOWS.md` — sekcja skills pominięta. ## 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 ``` Task 1: Schemat DB + modele + seeder instrumentów 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 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). php spark migrate && php spark db:seed DefaultInstrumentsSeeder — 3 tabele utworzone, 2 instrumenty AC-1 (schemat instrumentów), AC-2 (schemat operacji), AC-3 (schemat wycen), AC-4 (logika zarobku w withStats) Task 2: Kontrolery CRUD + widoki + routing + nawigacja 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 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". php spark serve + curl: dodanie instrumentu, wpłaty, wypłaty, wyceny; filtr po instrumencie; blokada usunięcia instrumentu z operacjami AC-1, AC-2, AC-3 (pełny CRUD i filtry przez UI/HTTP) Task 3: Pulpit inwestycji — agregacja, wykres zbiorczy, tabela per instrument app/Controllers/Investments.php, app/Views/investments/index.php 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 = Σ(deposit−withdraw) 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. curl /investments: kafelki i tabela zgodne z ręcznym wyliczeniem (ETF: netto 800, wartość 950, zarobek 150); JSON serii wykresu poprawny AC-4 (zarobek netto), AC-5 (pulpit: kafelki + wykres + tabela) ## 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. - [ ] `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. - [ ] Wszystkie AC (AC-1..AC-5) Pass. - [ ] Weryfikacja runtime kompletna (migrate/seed/serve/curl). - [ ] STATE.md wskazuje ten plan; radar delta zapisana w UNIFY. SUMMARY.md path: `.paul/plans/20260706-1430-modul-inwestycji/SUMMARY.md`