Dividends
Add, list, update, and delete cash dividend records — affect totalPnl but not the lot tree
Five endpoints under the Portfolio Dividends Swagger tag manage cash dividend receipts. Dividends affect totalPnl and the holding's dividend field but don't touch the FIFO lot tree — they're a flat counter aggregated during holdings.Rebuild.
| Method | Path | Service method |
|---|---|---|
POST | /portfolios/{id}/companies/{symbol}/dividends | dividends.Service.AddDividend |
GET | /portfolios/{id}/companies/{symbol}/dividends | dividends.Service.FindDividendsByCompany |
GET | /portfolios/{id}/dividends | dividends.Service.FindDividendsByPortfolio |
PATCH | /portfolios/{id}/companies/{symbol}/dividends/{dividendId} | dividends.Service.UpdateDividend |
DELETE | /portfolios/{id}/companies/{symbol}/dividends/{dividendId} | dividends.Service.DeleteDividend |
Cash dividends only. Stock dividends ("bonus shares") are a corporate action — see Corporate actions.
POST /portfolios/{id}/companies/{symbol}/dividends — AddDividend
Records a net cash dividend amount the user received.
Request body
type CreateDividendRequest = {
amount: number; // ≥ 0, NPR
receivedAt: string; // YYYY-MM-DD
notes?: string; // max 500
};Validation
amount≥ 0.receivedAtparses asYYYY-MM-DD.- The portfolio must own at least one trade for
symbol(otherwise the WACC math doesn't have a basis); validated implicitly byFindPortfolioCompanyBySymbolreturning the right company UUID.
Success (201)
{
"message": "Dividend added successfully",
"data": {
"id": "...",
"portfolioId": "...",
"companyId": "...",
"sym": "NABIL",
"amount": 2000.00,
"receivedAt": "2026-03-15",
"notes": "FY 2025/26 cash dividend",
"createdAt": "...",
"updatedAt": "..."
}
}Behaviour
- Ownership guard → resolve symbol →
CreateDividend→ cache bust. - Cache scope:
CacheScopeDetail. Dividends affectsummary.divandsummary.totalPnlbut not the value time-series, so the valuation cache is not busted. holdings.Rebuildis not called — dividends don't move units or cost. Instead, the nextRebuildon the company (triggered by a future trade or action) will pick up the new dividend viaFindDividendsForReplayand accumulate it intodividendNet.
Until the next replay-triggering event, the persisted nepse_portfolio_holdings.dividend_net is stale. The detail/holdings reads still show fresh totals because they recompute dividend from the dividends_received_total SQL aggregation joined into FindHoldingsWithPricesByPortfolio. The detail bust is what makes this consistent.
Errors
| Status | Cause |
|---|---|
| 400 | bad id, empty symbol, amount < 0, receivedAt malformed, body invalid |
| 401 | JWT missing/invalid |
| 404 | portfolio not yours / not found, symbol not in catalog |
cURL
curl -X POST -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-d '{"amount":2000.00,"receivedAt":"2026-03-15","notes":"FY 2025/26 cash dividend"}' \
"$BASE/portfolios/$PID/companies/NABIL/dividends"GET /portfolios/{id}/companies/{symbol}/dividends — ListCompanyDividends
List dividends for one company in the portfolio.
Query params
| Param | Default | Notes |
|---|---|---|
page | 1 | |
size | 20 | max 100 |
Success (200)
{
"message": "Dividends fetched successfully",
"data": [ /* DividendResponse... */ ],
"meta": { "total": 4, "page": 1, "size": 20 }
}Behaviour
- Soft-deleted rows excluded.
- Ordered by
received_at DESC, id DESC. - Not cached.
GET /portfolios/{id}/dividends — ListPortfolioDividends
Aggregated list of dividends across all companies in the portfolio.
Query params
| Param | Default | Notes |
|---|---|---|
page | 1 | |
size | 20 | max 100 |
from | (none) | YYYY-MM-DD lower bound on receivedAt |
to | (none) | YYYY-MM-DD upper bound (server adds +1 day) |
Success (200)
Same shape as the per-company list, with rows from any company. The sym field on each row identifies the company.
Behaviour
- Same soft-delete and ordering rules.
- Used by the audit/tax view of dividend history.
PATCH /portfolios/{id}/companies/{symbol}/dividends/{dividendId} — UpdateDividend
Partial update.
Request body
type UpdateDividendRequest = {
amount?: number;
receivedAt?: string;
notes?: string;
};Behaviour
- Ownership guard → resolve symbol →
UpdateDividend(matchingid + portfolio_id + company_id) → cache bust. - Cache scope:
CacheScopeDetail. - Symbol mismatch (URL
symboldoesn't match the persistedcompany_id) → 404.
Errors
| Status | Cause |
|---|---|
| 400 | bad UUIDs, malformed receivedAt, body invalid |
| 401 | JWT missing/invalid |
| 404 | not yours / not found / symbol mismatch |
DELETE /portfolios/{id}/companies/{symbol}/dividends/{dividendId} — DeleteDividend
Soft-deletes the dividend.
Behaviour
SoftDeleteDividend(deleted_at = now()).- Cache scope:
CacheScopeDetail. - The next
Rebuildfor this company picks up the deletion (the replay query already filtersdeleted_at IS NULL).
Success
204 No Content.
Implementation
- Handler:
internal/modules/portfolio/dividends/handler.go - Service:
internal/modules/portfolio/dividends/service.go - Mappers:
internal/modules/portfolio/dividends/mappers.go— 4 row→DTO converters - DTOs:
internal/modules/portfolio/dividends/types.go - sqlc queries:
internal/platform/database/queries/portfolio_dividends.sql - Tests:
dividends/handler_test.go