14 KiB
plan_id, title, storage, legacy_phase, created, status, type, autonomous, delegation, files_modified, quality_radar
| plan_id | title | storage | legacy_phase | created | status | type | autonomous | delegation | files_modified | quality_radar | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 20260706-1430-modul-inwestycji | Moduł inwestycji — instrumenty, wpłaty/wypłaty, ręczne wyceny, pulpit | plan-first | null | 2026-07-06T14:30:00+02:00 | planned | execute | true | auto |
|
degraded |
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.
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 | %).<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, migracjeinv_*, widokiinstruments/,inv_operations/,valuations/,investments/. - Routing:
app/Config/Routes.php— nowa grupa tras w blokuauth. - Nawigacja:
app/Views/layout/main.php— jedna pozycja „Inwestycje”. - Baza: nowe tabele
inv_instruments,inv_operations,inv_valuations(FK doinv_instruments).
Duplicate / Hardcoded Risks
- Ryzyko duplikacji logiki filtrów/sum wobec
OperationModel— świadomie osobne modele (inna domena: braktype 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|withdrawjako ENUM w migracji + reguła walidacjiin_listw 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>
<acceptance_criteria>
AC-1: Instrumenty (CRUD, płaska lista)
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)
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)
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)
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)
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>
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.
<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>