Shop It Docs
Portfolio Module

Errors

Validation rules, status codes, and the unified error envelope across all portfolio endpoints

Every portfolio endpoint returns errors via the standard project envelope (internal/http/response.JSON). Unlike the TradingView UDF surface, there is no UDF-style root-level error — portfolio is a normal authenticated REST API.

Error envelope

{
  "message": "<human-friendly summary>",
  "errors":  [{ "field": "<path>", "message": "<rule>" }]
}

errors[] is populated for validation failures. Other failures (404, 500) carry only message.

Content-Type: application/json always.

Status code map

CodeMeaning
400Validation error — bad UUID, malformed body, decode failure, body-too-large, unknown field, validate-tag rule violated, business-rule rejected at request layer (e.g., cgtAmount on a BUY).
401Missing or invalid JWT. The auth middleware rejects before the handler runs.
404Resource not found. Always also returned for "not yours" — the server never reveals whether a UUID exists for another customer.
422Business rule rejected after parse — used for SELL-quantity-exceeds-holding, currently the only 422 in the module.
500Unexpected DB or programming error. The recoverer middleware turns panics into 500.

There is no 403 in this module. "Not yours" is 404 by design.

Common 400 messages

These all surface from the request-decode layer (handlerutil.DecodeAndValidate) before any service code runs:

CauseMessage shape
id path param not a UUIDInvalid <param> format
Body fails to JSON-decodeMalformed JSON
Body exceeds BodyLimitRequest body too large
Body contains _unknown fieldunknown field <name>
validate:"required" field missing<field> is required
validate:"max=N" exceeded<field> must not exceed N characters
validate:"min=N" violated<field> must be at least N
validate:"oneof=A B" violated<field> must be one of: A B
validate:"uuid" violated<field> must be a valid UUID
validate:"iso8601" violated<field> must be a valid ISO 8601 timestamp

Per-endpoint quick lookup

Endpoint400401404500
POST /portfoliosname missing/too long, description too long, body issuesyesDB
PATCH /portfolios/{id}bad UUID, name/description too longyesnot yoursDB
DELETE /portfolios/{id}bad UUIDyesnot yoursDB
Endpoint400401404422500
POST /portfolios/{id}/tradestype, qty, price, totalAmount mismatch, tradedAt malformed/future, cgt-on-BUYyesportfolioSELL exceeds holdingDB
GET /portfolios/{id}/tradesbad pagination, bad type/from/toyesportfolioDB
PATCH /portfolios/{id}/trades/{tradeId}same as Add (after merge)yesportfolio or tradeSELL exceeds holdingDB
DELETE /portfolios/{id}/trades/{tradeId}bad UUIDsyesportfolio or tradeDB

Total-amount mismatch message: totalAmount mismatch: expected X.XX. Reject threshold is NPR 1.

CGT mismatch is not a 400 — it surfaces as cgtMismatch: true on the response and is informational.

Endpoint400401404500
POST /.../dividendsamount < 0, receivedAt malformed, bodyyesportfolio or symbolDB
GET /.../dividendsbad paginationyesportfolioDB
GET /portfolios/{id}/dividendsbad pagination/from/toyesportfolioDB
PATCH /.../dividends/{dividendId}same as Addyesany of portfolio/symbol/dividendDB
DELETE /.../dividends/{dividendId}bad UUIDsyesany of portfolio/symbol/dividendDB
Endpoint400401404500
POST /.../actionstype not in oneof, qty/price/fees invalid, dates malformed, type-specific ruleyesportfolio or symbolDB
GET /.../actionsbad paginationyesportfolioDB
PATCH /.../actions/{actionId}same as Addyesany of portfolio/symbol/actionDB
DELETE /.../actions/{actionId}bad UUIDsyesany of portfolio/symbol/actionDB
Endpoint400401404
GET /portfoliosbad paginationyes
GET /portfolios/{id}bad UUIDyesnot yours
GET /portfolios/{id}/summarybad UUIDyesnot yours
GET /portfolios/{id}/holdingsbad UUID, paginationyesnot yours
GET /portfolios/{id}/distributionbad UUIDyesnot yours
GET /portfolios/{id}/valuationbad UUID, range not in whitelistyesnot yours
GET /portfolios/{id}/companies/{symbol}bad UUID, empty symbolyesportfolio or holding for company

Validation rule sources

The struct tags drive the bulk of validation. Examples from trades/types.go:

type AddTradeRequest struct {
    CompanyID    string  `json:"companyId"    validate:"required,uuid"`
    Type         string  `json:"type"         validate:"required,oneof=BUY SELL"`
    Quantity     int     `json:"quantity"     validate:"required,min=1"`
    PricePerUnit float64 `json:"pricePerUnit" validate:"required,gt=0"`
    TotalAmount  float64 `json:"totalAmount"  validate:"required,gte=0"`
    TradedAt     string  `json:"tradedAt"     validate:"required,iso8601"`
    Notes        *string `json:"notes"        validate:"omitempty,max=500"`
    // ...
}

Beyond struct tags, three layers of business validation kick in within services:

  1. Trade fee mathcompleteTradeFees + computedTradeTotal reject totalAmount deviation > NPR 1.
  2. Sell quantity checkholdings.CurrentUnitsForHolding rejects oversell with 422.
  3. Type-specific corporate-action validationvalidateCorporateAction enforces "BONUS needs qty>=1", "RIGHT_ISSUE needs price if qty>0", etc.

Soft warnings vs errors

Some "wrong" inputs are accepted with a warning rather than rejected:

CaseBehaviour
tradedAt is not a NEPSE trading day (Mon–Fri or holiday)Trade saved; warnings: ["traded_at <date> is not a NEPSE trading day"] on the response.
cgtAmount differs from server-computed cgtComputedTrade saved; cgtMismatch: true on the response.
Trade in the future by ≤ 1 minuteAllowed (skews TZ-tolerant).
to > now on trade list filterAllowed.

These are deliberate UX calls. The server holds opinions about what's plausible but doesn't reject straight-line user data unless the FIFO math would actually be wrong.

Examples

$ curl -i -H "Authorization: Bearer $JWT" \
    "$BASE/portfolios/not-a-uuid"
HTTP/1.1 400 Bad Request
Content-Type: application/json
{"message":"Invalid id format"}
$ curl -X POST -i -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
    -d '{"companyId":"...","type":"BUY","quantity":10,"pricePerUnit":100,"cgtAmount":50,"totalAmount":1000,"tradedAt":"2026-04-01T00:00:00.000Z"}' \
    "$BASE/portfolios/$PID/trades"
HTTP/1.1 400 Bad Request
{"message":"cgtAmount is only valid on SELL trades"}
$ curl -X POST -i -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
    -d '{"companyId":"...","type":"BUY","quantity":100,"pricePerUnit":1250.00,"totalAmount":99999.00,"tradedAt":"2026-04-01T00:00:00.000Z"}' \
    "$BASE/portfolios/$PID/trades"
HTTP/1.1 400 Bad Request
{"message":"totalAmount mismatch: expected 125481.25"}
# Portfolio holds 50 units of NABIL; try to sell 100
$ curl -X POST -i ... -d '{"type":"SELL","quantity":100,...}' "$BASE/portfolios/$PID/trades"
HTTP/1.1 422 Unprocessable Entity
{"message":"SELL quantity exceeds current holding (50 units)"}
# The portfolio exists, but it belongs to another customer
$ curl -i -H "Authorization: Bearer $JWT" \
    "$BASE/portfolios/0192d4e5-6f7a-7b8c-9d0e-1f2a3b4c5d6e"
HTTP/1.1 404 Not Found
{"message":"Portfolio not found"}

Implementation

  • Envelope: internal/http/response/response.go
  • Decode + validate: internal/http/handlerutil
  • Validator tags: registered in internal/http/validator
  • Domain-specific errors: response.NewValidation, response.NewNotFound, response.NewStatus(http.StatusUnprocessableEntity, ...)