Shop It Docs
Portfolio Module

Transactions

One unified ledger for all 8 event types — BUY, SELL, IPO, FPO, RIGHTS, AUCTION, DIVIDEND, BONUS

Four endpoints under the Portfolio Transactions Swagger tag manage the portfolio's event ledger. The unified table replaces the three legacy tables (trades, dividends, corporate-actions) and the three derived ones (holdings, lots, lot_consumptions).

MethodPathService method
POST/portfolios/{id}/transactionstransactions.Service.Add
GET/portfolios/{id}/transactionstransactions.Service.Find
PATCH/portfolios/{id}/transactions/{txId}transactions.Service.Update
DELETE/portfolios/{id}/transactions/{txId}transactions.Service.Delete

Transaction types and required fields

TypeRequired fields
BUYcompanyId, transactedAt, quantity, rate, amount, sebonCommission, brokerCommission, dpFee, netAmount
SELLBUY fields plus basePrice, term (SHORT / LONG), cgtAmount
IPOcompanyId, transactedAt, quantity, rate, amount
FPOcompanyId, transactedAt, quantity, rate, amount
RIGHTScompanyId, transactedAt, quantity, rate, amount
AUCTIONcompanyId, transactedAt, quantity, rate, amount
DIVIDENDcompanyId, transactedAt, amount
BONUScompanyId, transactedAt, quantity

transactedAt accepts three forms — all interpreted as the same instant:

  • 2025-11-10 — date-only, anchored to start of NPT day (UTC+05:45)
  • 2025-11-10T00:00:00.000Z
  • Any RFC3339 timestamp

POST /portfolios/{id}/transactions

The type field discriminates the variant. The backend stores the user-supplied numbers as-is and never recomputes fees, CGT, or net amounts.

Example: BUY

{
  "type": "BUY",
  "companyId": "019d8bf0-9b6e-7af8-a794-9da3c2bd1835",
  "transactedAt": "2025-11-10",
  "quantity": 10,
  "rate": 500,
  "amount": 5000,
  "sebonCommission": 0.75,
  "brokerCommission": 18.50,
  "dpFee": 25,
  "netAmount": 5044.25
}

Example: SELL

{
  "type": "SELL",
  "companyId": "019d8bf0-9b6e-7af8-a794-9da3c2bd1835",
  "transactedAt": "2025-11-12",
  "quantity": 10,
  "rate": 600,
  "amount": 6000,
  "sebonCommission": 0.90,
  "brokerCommission": 22.20,
  "dpFee": 25,
  "basePrice": 500,
  "term": "SHORT",
  "cgtAmount": 75,
  "netAmount": 5876.90
}

Example: IPO

{
  "type": "IPO",
  "companyId": "019d8bf0-9b6e-7af8-a794-9da3c2bd1835",
  "transactedAt": "2025-11-10",
  "quantity": 10,
  "rate": 100,
  "amount": 1000
}

Example: DIVIDEND

{
  "type": "DIVIDEND",
  "companyId": "019d8bf0-9b6e-7af8-a794-9da3c2bd1835",
  "transactedAt": "2025-11-10",
  "amount": 250
}

Example: BONUS

{
  "type": "BONUS",
  "companyId": "019d8bf0-9b6e-7af8-a794-9da3c2bd1835",
  "transactedAt": "2025-11-10",
  "quantity": 3
}

Validation rules

Two layers, both lightweight. The backend never recomputes fees from outside sources (no broker tier tables, no SEBON % derivation, no settlement-date lookup). It only checks that the user-supplied fields agree with each other.

Shape (hard reject)

  • transactedAt valid; not in the future (1-min skew tolerance).
  • companyId references an existing company.
  • quantity > 0 where required by type.
  • rate > 0 where required by type.
  • All monetary fields >= 0.
  • term{SHORT, LONG} on SELL.
  • Per-type required-field shape (DB CHECK + service-layer pre-check for nicer error messages).
  • notes ≤ 500 chars.

Cross-field consistency (hard reject, ±0.01 NPR)

CheckApplies toRule
Gross matches qty × rateBUY, SELL, IPO, FPO, RIGHTS, AUCTION|amount − quantity × rate| ≤ 0.01
Net amount on BUYBUY|netAmount − (amount + sebonCommission + brokerCommission + dpFee)| ≤ 0.01
Net amount on SELLSELL|netAmount − (amount − sebonCommission − brokerCommission − dpFee − cgtAmount)| ≤ 0.01
CGT bandSELLcgtAmount falls within [0, gain × rate(term)] where gain = (rate − basePrice) × quantity and rate(term) = 0.05 (LONG) or 0.075 (SHORT). When gain ≤ 0, require cgtAmount = 0.
OversellSELLquantity ≤ currentUnits(portfolio, company) from a single SUM aggregate query

GET /portfolios/{id}/transactions

Paginated, newest-first listing with optional filters.

Query params

ParamDefaultNotes
page11-indexed
size20max 100
typeBUY / SELL / IPO / FPO / RIGHTS / AUCTION / DIVIDEND / BONUS
frominclusive YYYY-MM-DD
toinclusive YYYY-MM-DD
symbolexact case-insensitive match against the company symbol

Rows + total count run inside one REPEATABLE READ snapshot for pagination consistency.

PATCH /portfolios/{id}/transactions/{txId}

Patches a transaction in place. All fields optional. The transaction's type cannot change — to switch type, delete and re-add.

The same shape and cross-field consistency checks re-run after merging the patch with the persisted row, so a partial update can never violate type invariants.

DELETE /portfolios/{id}/transactions/{txId}

Soft-deletes the transaction. The row stays in the database (audit) but is excluded from all subsequent reads, summaries, valuations, and oversell checks. 204 No Content on success. Detail and valuation caches are busted.

Response shape

Every endpoint returns the same TransactionResponse (PATCH/GET/POST), enriched with company identity for client convenience:

type TransactionResponse = {
  id: string;
  portfolioId: string;
  companyId: string;
  sym: string;            // symbol
  name: string;           // company name
  icon?: string;
  type: 'BUY' | 'SELL' | 'IPO' | 'FPO' | 'RIGHTS' | 'AUCTION' | 'DIVIDEND' | 'BONUS';
  transactedAt: string;   // RFC3339, UTC
  quantity?: number;
  rate?: number;
  amount?: number;
  sebonCommission?: number;
  brokerCommission?: number;
  dpFee?: number;
  netAmount?: number;
  basePrice?: number;     // SELL only
  term?: 'SHORT' | 'LONG'; // SELL only
  cgtAmount?: number;     // SELL only
  notes?: string;
  createdAt: string;
  updatedAt: string;
};

Optional fields are omitted from the JSON when unset.