Files
2026-07-06 20:16:00 +02:00

289 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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>