Shop It Docs
Portfolio Module

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.

MethodPathService method
POST/portfolios/{id}/companies/{symbol}/dividendsdividends.Service.AddDividend
GET/portfolios/{id}/companies/{symbol}/dividendsdividends.Service.FindDividendsByCompany
GET/portfolios/{id}/dividendsdividends.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.
  • receivedAt parses as YYYY-MM-DD.
  • The portfolio must own at least one trade for symbol (otherwise the WACC math doesn't have a basis); validated implicitly by FindPortfolioCompanyBySymbol returning 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 affect summary.div and summary.totalPnl but not the value time-series, so the valuation cache is not busted.
  • holdings.Rebuild is not called — dividends don't move units or cost. Instead, the next Rebuild on the company (triggered by a future trade or action) will pick up the new dividend via FindDividendsForReplay and accumulate it into dividendNet.

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

StatusCause
400bad id, empty symbol, amount < 0, receivedAt malformed, body invalid
401JWT missing/invalid
404portfolio 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

ParamDefaultNotes
page1
size20max 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

ParamDefaultNotes
page1
size20max 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 (matching id + portfolio_id + company_id) → cache bust.
  • Cache scope: CacheScopeDetail.
  • Symbol mismatch (URL symbol doesn't match the persisted company_id) → 404.

Errors

StatusCause
400bad UUIDs, malformed receivedAt, body invalid
401JWT missing/invalid
404not 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 Rebuild for this company picks up the deletion (the replay query already filters deleted_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