Portfolio CRUD
Create, list, get, update, and delete portfolios — the core lifecycle endpoints
Five endpoints under the Portfolio Swagger tag manage the portfolio aggregate itself. All require a JWT.
| Method | Path | Service method |
|---|---|---|
POST | /portfolios | core.Service.Create |
GET | /portfolios | core.Service.FindAll |
GET | /portfolios/{id} | core.Service.FindDetail |
PATCH | /portfolios/{id} | core.Service.Update |
DELETE | /portfolios/{id} | core.Service.Delete |
POST /portfolios
Creates a new portfolio. The first portfolio per customer is automatically marked default.
Request
type CreatePortfolioRequest = {
name: string; // required, max 100 chars
description?: string; // optional, max 500 chars
};Success (201)
{
"message": "Portfolio created successfully",
"data": {
"id": "0192d4e5-6f7a-7b8c-9d0e-1f2a3b4c5d6e",
"name": "My Main Portfolio",
"description": "Long-term equity holdings",
"isDefault": true,
"createdAt": "2026-04-28T10:30:00.000Z",
"updatedAt": "2026-04-28T10:30:00.000Z"
}
}GET /portfolios
Paginated list. Standard page / size query params.
{
"message": "Portfolios fetched successfully",
"data": [ /* Portfolio[] */ ],
"meta": { "page": 1, "size": 20, "total": 3 }
}The rows + count run inside one REPEATABLE READ snapshot so concurrent writes can't yield inconsistent pagination.
GET /portfolios/{id}
Returns the bundled dashboard payload — header + summary + suspendedSummary + distribution (byCompany + bySector, both LTP × current units) + positions (lightweight per-active-stock projection for heat maps).
The full holdings table lives at /portfolios/{id}/holdings. Broker analysis is intentionally excluded from this bundle — fetch separately at /portfolios/{id}/broker-analysis.
{
"message": "Portfolio fetched successfully",
"data": {
"id": "...",
"name": "...",
"description": "...",
"isDefault": true,
"lastFetchedAt": "2026-04-28T10:31:00.000Z",
"createdAt": "...",
"updatedAt": "...",
"summary": { /* see Summary doc */ },
"suspendedSummary": null,
"distribution": {
"byCompany": [ { "label": "NABIL", "v": 60000, "pct": 60.0 } ],
"bySector": [ { "label": "Commercial Banks", "v": 60000, "pct": 60.0 } ]
},
"positions": [
{
"sym": "NABIL", "name": "Nabil Bank Limited", "icon": "...", "sec": "Commercial Banks",
"curInv": 50000, "curVal": 60000, "dayPnl": 200,
"pch": 1.5, "holdingPct": 60.0
}
]
}
}Cached at portfolio:detail:{id} for 30s. Busted on every transaction mutation in that portfolio.
PATCH /portfolios/{id}
Patches a portfolio. All fields optional — send only what changes.
type UpdatePortfolioRequest = {
name?: string; // max 100
description?: string; // max 500
isDefault?: true; // setting true atomically demotes other defaults
};Setting isDefault: false is rejected with 400 — promote a different portfolio to default instead, which atomically swaps and preserves the single-default invariant.
When isDefault: true is sent, the service runs two queries inside one tx:
UpdatePortfolioon the target row.UnsetDefaultForOtherPortfoliosto clearis_defaulton every other portfolio of the customer.
Both succeed or both roll back. Detail cache is busted.
DELETE /portfolios/{id}
Soft-deletes the portfolio and all its transactions. Both rows remain in the database for audit but are excluded from every subsequent read. 204 No Content on success. Detail and valuation caches are busted.