Shop It Docs
Portfolio Module

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.

MethodPathService method
POST/portfolioscore.Service.Create
GET/portfolioscore.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:

  1. UpdatePortfolio on the target row.
  2. UnsetDefaultForOtherPortfolios to clear is_default on 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.