Shop It Docs
Stock Analysis (Fundamentals)

Errors

Validation rules, status codes, and the unified error envelope across all stock-analysis endpoints

Every endpoint uses the standard project envelope (internal/http/response.JSON) and the same response.Error* helpers as the rest of the API.

Error envelope

{
  "message": "<human-friendly summary>"
}

errors[] (the per-field validation array used elsewhere) is not populated by this module — query params are clamped/normalised rather than validated, so the only 400-class errors here carry plain message strings.

Content-Type: application/json always.

Status code map

CodeMeaning
200OK — including degenerate cases (available: false, tier4 missing, all rows upstream_missing)
400Query-param error after a value couldn't be normalised (e.g. tab=foo, type=cashflowwwww)
404Symbol not in nepse_companies. Always uppercased before lookup.
429Rate limit exceeded (30 req/min/IP under the rl30 group)
500DB error or programmer mistake. The recoverer middleware turns panics into 500.

There is no 401/403 — the module is fully public.

Soft-failure philosophy

Where the API can produce a useful page despite missing/dirty input, it does. Silent normalisation is preferred over a 4xx that would block the entire response:

EndpointSoft-failure rule
peersUnknown / cross-sector / duplicate symbols → excludedPeers[] instead of 4xx
peersOverflow past 4 peer symbols → silently dropped (over-specification isn't worth the error)
announcementsUnknown chip in ?types= → silently dropped
announcementsAll-unknown ?types= → expands back to all 5 canonical types so the timeline still renders
announcementsUnparseable ?from= / ?to= → treated as absent
dividendsyears, upcomingDays outside range → clamped to bounds
statements / ratiosEmpty ?type= / ?tab= → reverts to default (income/valuation)

This means a fat-fingered URL in the FE almost never produces a 400 — it produces a "best effort" response with a hint surfaced via excludedPeers, note, or implicit defaulting.

Per-endpoint quick lookup

Endpoint200400404500
GET /snapshotalways (upstream_missing rows OK)symbol unknownDB
GET /ownershipalways (empty BOD/Phase-A overrides OK)symbol unknownDB
Endpoint200400404500
GET /statementsalways (available:false for cashflow is 200)type or period outside whitelistsymbol unknownDB
GET /ratiosalwaystab outside whitelistsymbol unknownDB
Endpoint200400404500
GET /dividendsalways (empty rows OK)— (params clamped)symbol unknownDB
GET /upcoming-book-closesalways (empty rows OK)— (params clamped)DB
Endpoint200400404500
GET /peersalways (cross-sector → excludedPeers)focal symbol unknownDB
GET /announcementsalways (unknown chips dropped)— (params clamped)symbol unknownDB
Endpoint200400404500
GET /fair-valuealways (both methods may collapse to invalid)symbol unknownDB

400 messages

These surface when a value can't be normalised back to a whitelist:

EndpointCauseMessage
statements?type not in income/balance/cashflow`invalid type; expected income
statements?period not in quarterly/annual`invalid period; expected quarterly
ratios?tab not in valuation/profitability/solvency/growth`invalid tab; expected valuation

Symbol path-param validation: an empty {symbol} returns 400 symbol is required. The chi route pattern rejects empty path segments before the handler runs in normal usage, but the explicit check guards against unusual middleware ordering.

404 messages

Every "symbol unknown" error uses the same message regardless of which endpoint surfaces it:

{ "message": "Company not found" }

The 404 is raised the first time the handler hits Store.GetFundamentalsSnapshotCore (or GetFundamentalsOwnershipCore for ownership) and the row is missing. Several endpoints reuse the snapshot core query as a 404 probe before doing more expensive work — this is cheap (one row from fundamentals_snapshot) and saves a multi-query trip for non-existent symbols.

Examples

$ curl -i "$BASE/api/nepse/companies/NOTASYMBOL/fundamentals/snapshot"
HTTP/1.1 404 Not Found
{"message":"Company not found"}
$ curl -i "$BASE/api/nepse/companies/NABIL/fundamentals/ratios?tab=foo"
HTTP/1.1 400 Bad Request
{"message":"invalid tab; expected valuation | profitability | solvency | growth"}
# NABIL is a Commercial Bank; NTC is Telecommunications.
$ curl "$BASE/api/nepse/companies/NABIL/fundamentals/peers?symbols=NTC"
HTTP/1.1 200 OK
{
  "data": {
    "companies": [ { "symbol": "NABIL", "isFocal": true, } ],
    "excludedPeers": [ { "symbol": "NTC", "reason": "different_sector" } ],

  }
}
$ curl "$BASE/api/nepse/companies/NABIL/fundamentals/statements?type=cashflow"
HTTP/1.1 200 OK
{
  "data": {
    "type": "cashflow",
    "available": false,
    "reason": "Cash flow statements are not currently provided by upstream (NEPSE MDP). Will be enabled once available.",
    "rows": [],

  }
}

Implementation

  • Envelope: internal/http/response/response.go
  • Validation helpers: response.NewValidation(...), response.NewNotFound(...)
  • URL param case-fold: handlerutil.URLParamUpper(r, "symbol")
  • Query int clamping: fundhandlerutil.ClampedQueryInt(r, name, default, lo, hi)

The "soft normalisation over 4xx" policy is intentional. FE prototypes routinely send fat-fingered URLs while users explore filters; returning a useful page with a note or excludedPeers is better UX than a red error banner.