fix: harden terminal-scoped analytics contracts
Keep JSON and bounded CSV exports on one canonical predicate, replace replayable offsets with encrypted tenant/filter-bound keyset cursors, and suppress merchant-grain peers under terminal scope. Validate period filters and align OpenAPI and capability specs with runtime behavior.
This commit is contained in:
@@ -58,11 +58,13 @@ Every request, query parameter, cursor, run id, event id, and export request is
|
||||
| `GET` | `/api/v1/auth/session` | Return the current merchant-user session summary |
|
||||
| `DELETE` | `/api/v1/auth/session` | Revoke the current session and clear its cookie |
|
||||
| `GET` | `/api/v1/analytics/periods` | List approved period choices and the default period |
|
||||
| `GET` | `/api/v1/analytics/terminals` | List terminals owned by the authenticated merchant |
|
||||
| `POST` | `/api/v1/analytics/runs` | Idempotently create or return the current run |
|
||||
| `GET` | `/api/v1/analytics/runs/{runId}` | Return the current run/section snapshot |
|
||||
| `GET` | `/api/v1/analytics/runs/{runId}/events` | Stream authorized SSE run events |
|
||||
| `POST` | `/api/v1/analytics/runs/{runId}/sections/{sectionId}/retry-attempts` | Retry one exhausted failed section |
|
||||
| `GET` | `/api/v1/analytics/session-details` | List authorized merchant-owned session evidence |
|
||||
| `GET` | `/api/v1/analytics/sessions/{sessionId}` | Return one merchant-owned session and ordered attempts |
|
||||
| `GET` | `/api/v1/analytics/session-details.csv` | Export the same authorized detail view as CSV |
|
||||
|
||||
The request/response schemas, required headers, status codes, examples, and stable `operationId` values are defined in `docs/openapi.yaml`.
|
||||
@@ -77,11 +79,11 @@ Clients select one allowlisted `periodId` returned by `GET /analytics/periods`;
|
||||
- `timezone: Asia/Tehran` and `jalaliLabelFa`, ready for display;
|
||||
- `isComplete` and `isDefault` flags.
|
||||
|
||||
The default is the latest complete Jalali month containing source data. Weekly periods start Saturday at `00:00` and end at the following Saturday `00:00` in Tehran. The server rejects stale or fabricated period ids instead of accepting client-computed dates.
|
||||
The default is the latest complete Jalali month containing source data. Weekly periods start Saturday at `00:00` and end at the following Saturday `00:00` in Tehran. The optional `periodType` query is allowlisted and filters the returned descriptors. The server rejects stale or fabricated period ids and invalid period types instead of accepting client-computed dates.
|
||||
|
||||
## Analytics-run contract
|
||||
|
||||
`POST /analytics/runs` accepts `periodId` and an `Idempotency-Key` representing one dashboard-start intent. The server binds the key to the authenticated user, merchant, canonical request, immutable dataset snapshot, and metric-definition version.
|
||||
`POST /analytics/runs` accepts `periodId`, an optional merchant-owned `terminalId`, and an `Idempotency-Key` representing one dashboard-start intent. An omitted terminal selects the whole merchant. The server resolves a supplied terminal under the session merchant before calculation and binds the key to the authenticated user, merchant, canonical request, immutable dataset snapshot, and metric-definition version.
|
||||
|
||||
- The same key and same canonical request replays the original status/body without duplicate jobs.
|
||||
- The same key with a different request returns `422 IDEMPOTENCY_KEY_REUSED`.
|
||||
@@ -89,7 +91,7 @@ The default is the latest complete Jalali month containing source data. Weekly p
|
||||
- An in-flight duplicate returns the current `202` run snapshot rather than starting another effect.
|
||||
- Intent records outlive the one-hour event replay window and the maximum job retry chain; the concrete retention value is an implementation parameter that must be documented before implementation.
|
||||
|
||||
A run snapshot identifies the selected period, immutable dataset snapshot, metric-definition version, creation/update times, overall state, and all four sections. Section states are `QUEUED`, `CALCULATING`, `READY`, `FAILED`, or `SUPPRESSED`. A failure includes only a safe stable error code and retry eligibility. Ready section payloads use the typed schemas in OpenAPI. A suppressed peer section contains threshold evidence but no peer metric values.
|
||||
A run snapshot identifies the selected period, optional terminal scope, immutable dataset snapshot, metric-definition version, creation/update times, overall state, and all four sections. Section states are `QUEUED`, `CALCULATING`, `READY`, `FAILED`, or `SUPPRESSED`. A failure includes only a safe stable error code and retry eligibility. Ready section payloads use the typed schemas in OpenAPI. Because the approved peer cohort is merchant-grain, terminal-scoped runs return peer comparison as `SUPPRESSED` with `TERMINAL_SCOPE_UNSUPPORTED`; they never place merchant-wide peer values under a terminal scope.
|
||||
|
||||
## Metric response rules
|
||||
|
||||
@@ -104,13 +106,14 @@ A run snapshot identifies the selected period, immutable dataset snapshot, metri
|
||||
|
||||
`GET /analytics/session-details` requires an approved `view`:
|
||||
|
||||
- `ALL`: all merchant-owned sessions in the selected scope;
|
||||
- `UNVERIFIED_PAID`: sessions currently in `Paid`;
|
||||
- `FAILED`: sessions currently in `Failed`, optionally filtered by `failureStage=NO_ATTEMPT|REACHED_BANK|OTHER_OBSERVED`;
|
||||
- `RETRIED`: sessions with more than one normalized attempt.
|
||||
|
||||
All views require one approved `periodId`. Optional terminal and observable status/stage filters are validated inside the merchant boundary. There is no merchant-id, peer, payer-token, free-text, response-code, PSP, bank, or arbitrary date filter in the MVP.
|
||||
All views require one approved `periodId`. Optional `terminalId`, `status`, `failureStage`, `minAmountRial`, and `maxAmountRial` filters are validated inside the merchant boundary. The failure-stage filter is valid only for `FAILED`, and amount bounds must be non-negative and ascending. There is no merchant-id, peer, payer-token, free-text, response-code, PSP, bank, or arbitrary date filter in the MVP.
|
||||
|
||||
Ordering is stable and deterministic: `createdAt DESC, sessionId DESC`. The opaque cursor binds the merchant, view, canonical filters, ordering, and last row. It must be integrity-protected, expire after a bounded interval, and reveal no internal ids or filter data when decoded by a client.
|
||||
Ordering is stable and deterministic: `createdAt DESC, sessionId DESC`. The authenticated-encrypted cursor binds the merchant, period, view, canonical filters, ordering, and last row, expires after 24 hours, and reveals no internal ids or filter data when decoded by a client. The encryption key comes from the independently configured `CURSOR_SECRET` and is never returned or logged.
|
||||
|
||||
- `limit` defaults to 25 and is limited to 1–100.
|
||||
- Responses contain `items`, `pageInfo.limit`, `pageInfo.hasNextPage`, and nullable `pageInfo.nextCursor`.
|
||||
@@ -119,6 +122,8 @@ Ordering is stable and deterministic: `createdAt DESC, sessionId DESC`. The opaq
|
||||
|
||||
Detail rows expose only fields available in the authorized view: opaque merchant-local `sessionId`, the existing opaque source terminal reference labeled `شناسه پایانه`, status, amount/fee in Rial, timestamps, attempt count, and a bounded ordered attempt summary where relevant. The terminal reference is resolved and displayed only inside the authenticated merchant boundary and never grants authorization. Rows exclude source merchant keys, payer tokens, credentials, raw session tokens, peer data, internal database keys, and undocumented response-code meanings.
|
||||
|
||||
`GET /analytics/sessions/{sessionId}` applies the same session-derived merchant predicate before resolving the opaque session id. Missing and other-merchant ids return the same safe `404`; successful responses include ordered attempt summaries but never payer tokens or response-code fields.
|
||||
|
||||
## CSV export
|
||||
|
||||
`GET /analytics/session-details.csv` accepts exactly the same approved view and filters as the JSON detail endpoint, except cursor and page limit. The server reauthorizes and re-resolves the merchant at export time. It executes one immutable logical snapshot so rows do not drift between authorization, limit validation, and streaming.
|
||||
|
||||
Reference in New Issue
Block a user