# Call CSAT submissions Backend ownership of post-call rating data. Persists each submission to Postgres, keyed by call + user. ## Endpoint ``` POST /calls/:callId/csat Auth: Bearer JWT (existing app session) ``` Path: | Param | Type | Notes | |-------|------|-------| | `callId` | UUID | Internal `Call.id` returned by `/calls/:callId/init` or `/accept`. Not the `providerCallId`. | Body: ```json { "rating": 1, "selectedIssues": ["audio_mine"], "comments": "audio cut off" } ``` | Field | Type | Rules | |-------|------|-------| | `rating` | integer | 1..5 | | `selectedIssues` | string[] | values in `CallCSATIssue` enum, array length ≤ enum cardinality | | `comments` | string | length ≤ 1000 | Cross-field rule: when `rating === 5`, both `selectedIssues` and `comments` must be empty. Otherwise rejected with 400 `INVALID_DATA`. `CallCSATIssue`: ``` audio_mine | audio_others | audio_echo | audio_cut video_camera_mine | video_camera_others | video_quality ``` ## Responses | Code | Meaning | |------|---------| | 204 | Stored, or duplicate silently ignored (first write wins) | | 400 `INVALID_DATA` | Validation error (range, enum, length, rating=5 with payload) | | 401 | Standard auth middleware | | 403 `CALL_FORBIDDEN` | Submitter is not a participant of the call, or cross-instance | | 404 `CALL_NOT_FOUND` | `callId` unknown | ## Idempotency `(callId, userId)` is unique. Duplicate submits are persisted via `findOrCreate`: the **first write wins** and subsequent submits return 204 without changing the stored row. FE retries are safe. If product later wants "edit rating", switch the adapter from `findOrCreate` to an upsert against the unique index. ## Data model — `CallCSATSubmissions` | Column | Type | Notes | |--------|------|-------| | `id` | UUID | PK | | `instanceId` | INTEGER | FK `Instances(id)` | | `callId` | UUID | FK `Calls(id)` ON DELETE CASCADE | | `userId` | INTEGER | FK `Users(id)` ON DELETE CASCADE | | `rating` | SMALLINT | range enforced at app layer | | `selectedIssues` | TEXT[] | enum enforced at app layer | | `comments` | TEXT | default `''` | | `createdAt` / `updatedAt` | TIMESTAMPTZ | | Indexes: - `UNIQUE (callId, userId)` — backs the upsert - `(instanceId, createdAt)` — BI scans ## Observability Metric `calls.csat.submitted` is emitted on each accepted submission with tags `status`, `rating`, `inserted` (so duplicate retries are distinguishable from first writes).