289 lines
14 KiB
Markdown
289 lines
14 KiB
Markdown
---
|
||
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 = Σ(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.
|
||
</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>
|