Corporate Actions
Add, list, update, and delete corporate actions — bonus, right issue, IPO, FPO, and auction events that change unit count or cost basis
Four endpoints under the Portfolio Corporate Actions Swagger tag manage non-trade events that affect unit counts or cost basis.
| Method | Path | Service method |
|---|---|---|
POST | /portfolios/{id}/companies/{symbol}/actions | actions.Service.AddCorporateAction |
GET | /portfolios/{id}/companies/{symbol}/actions | actions.Service.FindCorporateActionsByCompany |
PATCH | /portfolios/{id}/companies/{symbol}/actions/{actionId} | actions.Service.UpdateCorporateAction |
DELETE | /portfolios/{id}/companies/{symbol}/actions/{actionId} | actions.Service.DeleteCorporateAction |
Supported action types
NEPSE corporate-action vocabulary supported by this module:
| Type | Effect on units | Effect on cost basis | Lot source |
|---|---|---|---|
BONUS | +qty | none | BONUS (zero cost) |
RIGHT_ISSUE | +subscribed_qty | +price × subscribed_qty + fees | RIGHT_SUBSCRIBED |
IPO | +qty | +price × qty + fees | IPO |
FPO | +qty | +price × qty + fees | FPO |
AUCTION | +qty | +price × qty + fees | AUCTION |
The validation layer currently accepts only BONUS and RIGHT_ISSUE from the public DTO (oneof=BONUS RIGHT_ISSUE). IPO, FPO, AUCTION are supported in the lot-source schema and the replay engine, but they're sourced via internal admin tooling rather than user submissions. See actions/types.go for the DTO enum.
POST /portfolios/{id}/companies/{symbol}/actions — AddCorporateAction
Records a corporate action.
Request body
type CreateCorporateActionRequest = {
type: "BONUS" | "RIGHT_ISSUE";
quantity: number; // ≥ 0 (RIGHT_ISSUE: this is the subscribed qty, can be 0 if rejected)
pricePerUnit?: number; // RIGHT_ISSUE / IPO / FPO / AUCTION
fees?: number; // optional
ratioNumerator?: number; // BONUS / RIGHT_ISSUE — display only, e.g. 10:3 bonus
ratioDenominator?: number;
entitledQty?: number; // RIGHT_ISSUE — entitlement cap
subscribedQty?: number; // RIGHT_ISSUE — informational; the actual subscribed count is `quantity`
bookClosureDate?: string; // YYYY-MM-DD
effectiveDate: string; // ISO 8601 — required
notes?: string; // max 500
};Validation rules per type
Implemented in actions/validation.go → validateCorporateAction:
quantity≥ 1 (must add bonus shares).pricePerUnitignored / forced to 0 in the persistednepse_portfolio_lots.cost_per_unit.feestypically 0; if non-zero it doesn't affect the lot tree (bonus lot is always zero-cost).ratioNumerator/ratioDenominatorare display-only — the engine only usesquantity.
quantity≥ 0 — this is the subscribed quantity.0= rejected, no lot is created.pricePerUnitrequired ifquantity > 0.feesoptional.entitledQty,subscribedQtyare informational — the engine readsquantityas the actual subscribed count.- Cost lot per the formula
cost = price × qty + fees.
- Same shape as RIGHT_ISSUE:
quantityunits allotted atpricePerUnit + fees. - Distinguished only by the lot's
sourceso the WACC breakdown can split them in the company-detail view. - Currently sourced via internal admin tooling; not part of the public DTO
oneofwhitelist.
Success (201)
{
"message": "Corporate action added successfully",
"data": {
"id": "...",
"portfolioId": "...",
"companyId": "...",
"sym": "NABIL",
"type": "BONUS",
"quantity": 30,
"pricePerUnit": 0,
"fees": 0,
"ratioNumerator": 10,
"ratioDenominator": 3,
"entitledQty": null,
"subscribedQty": null,
"bookClosureDate": "2026-02-01",
"effectiveDate": "2026-02-14T00:00:00.000Z",
"notes": "10:3 bonus shares",
"createdAt": "...",
"updatedAt": "..."
}
}Behaviour
- Ownership guard → resolve symbol →
validateCorporateAction(req, type)→CreateCorporateAction→holdings.Rebuild(portfolio, company)→ cache bust. - Cache scope:
CacheScopeDetailAndValuation. Corporate actions move both units (affectsdayPnlnext read) and cost basis (affects valuation series). - The replay engine sorts the new action into the chronological event list by
effectiveDate, thencreatedAt, thenid— back-dated actions are integrated correctly.
Errors
| Status | Cause |
|---|---|
| 400 | type not in [BONUS, RIGHT_ISSUE], quantity < 0, malformed effectiveDate, malformed bookClosureDate, validation rule failed |
| 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 '{
"type":"BONUS",
"quantity":30,
"ratioNumerator":10,"ratioDenominator":3,
"effectiveDate":"2026-02-14T00:00:00.000Z",
"notes":"10:3 bonus"
}' \
"$BASE/portfolios/$PID/companies/NABIL/actions"curl -X POST -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-d '{
"type":"RIGHT_ISSUE",
"quantity":10, "pricePerUnit":100,
"entitledQty":20, "subscribedQty":10,
"effectiveDate":"2026-03-01T00:00:00.000Z",
"notes":"Subscribed 10/20"
}' \
"$BASE/portfolios/$PID/companies/NABIL/actions"# Subscribed = 0. No lot is created; the row is logged for audit only.
curl -X POST -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-d '{
"type":"RIGHT_ISSUE",
"quantity":0, "pricePerUnit":100,
"entitledQty":20, "subscribedQty":0,
"effectiveDate":"2026-03-01T00:00:00.000Z",
"notes":"Did not subscribe"
}' \
"$BASE/portfolios/$PID/companies/NABIL/actions"GET /portfolios/{id}/companies/{symbol}/actions — ListCorporateActions
Paginated list of corporate actions for one company.
Query params
| Param | Default | Notes |
|---|---|---|
page | 1 | |
size | 20 | max 100 |
Success (200)
Standard paginated envelope wrapping []CorporateActionResponse. Soft-deleted rows excluded. Ordered by effective_date DESC, created_at DESC.
PATCH /portfolios/{id}/companies/{symbol}/actions/{actionId} — UpdateCorporateAction
Partial update with the same shape as Add; all fields optional. Re-runs the replay.
Behaviour
FindCorporateActionForUpdateto capture the original company.- If the user changes
type,quantity,pricePerUnit,fees, oreffectiveDate, the replay sees a structurally different event and the lot tree changes accordingly. - Cache scope:
CacheScopeDetailAndValuation.
Errors
Same as Add, plus 404 if actionId doesn't belong to this (portfolio, company).
DELETE /portfolios/{id}/companies/{symbol}/actions/{actionId} — DeleteCorporateAction
Soft-deletes the action and rebuilds the affected company's holdings.
Behaviour
SoftDeleteCorporateActionthenRebuild. The replay walks events without this row, leaving the lot tree as if the action never happened.- Cache scope:
CacheScopeDetailAndValuation.
Success
204 No Content.
Lot-source mapping summary
After replay, a company's open lots show their origin in the source column. The company-detail endpoint surfaces this via waccBreakdown:
{
"boughtCost": 50000.00, "boughtQty": 100,
"bonusQty": 30,
"rightSubscribedCost": 1000.00, "rightSubscribedQty": 10,
"ipoCost": 0.00, "ipoQty": 0,
"fpoCost": 0.00, "fpoQty": 0,
"auctionCost": 0.00, "auctionQty": 0,
"totalCost": 51000.00, "totalQty": 140,
"wacc": 364.29
}WACC is the open-lot cost ÷ open-lot units. Bonus dilutes; right/IPO/FPO/AUCTION add cost and units.
Implementation
- Handler:
internal/modules/portfolio/actions/handler.go - Service:
internal/modules/portfolio/actions/service.go - Validation:
internal/modules/portfolio/actions/validation.go—validateCorporateAction,corporateActionCreateParams,corporateActionUpdateParams - Mappers:
internal/modules/portfolio/actions/mappers.go - DTOs:
internal/modules/portfolio/actions/types.go - Replay (lot derivation):
internal/modules/portfolio/holdings/replay.go - sqlc queries:
internal/platform/database/queries/portfolio_corporate_actions.sql - Tests:
actions/handler_test.go,actions/service_test.go