# Android Questionnaire API Integration

This document describes the API contract the Android app can use to fetch questionnaires, fetch translations, and upload completed questionnaire answers.

Use the front-controller routes under `/api/...`. The clean route `/api/app_questionnaires` is preferred because it returns the unified response envelope used by the web app and supports `POST` uploads.

## Base Requirements

- Base URL: `https://<server-host>/api`
- JSON header: `Content-Type: application/json; charset=UTF-8`
- Auth header after login: `Authorization: Bearer <token>`
- All timestamps are integer Unix timestamps. Use seconds unless the server contract is changed.
- Successful front-controller responses use:

```json
{
  "ok": true,
  "data": {}
}
```

- Error responses use:

```json
{
  "ok": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human readable message"
  }
}
```

Some low-level auth failures can still return a legacy body such as `{"error":"Missing Bearer token"}` or `{"error":"Invalid or expired token"}`.

## Authentication

### Login

`POST /api/auth/login`

Request:

```json
{
  "username": "coach1",
  "password": "secret"
}
```

Success:

```json
{
  "ok": true,
  "data": {
    "token": "64-character-hex-token",
    "user": "coach1",
    "role": "coach"
  }
}
```

If the account must change its password first, the response contains `mustChangePassword: true` and a temporary token. That temporary token cannot access questionnaire endpoints until the password is changed.

## Fetch Questionnaires

### Fetch Active Questionnaire List

`GET /api/app_questionnaires`

Headers:

```http
Authorization: Bearer <token>
```

Success:

```json
{
  "ok": true,
  "data": [
    {
      "id": "questionnaire_1_demographic_information",
      "name": "demographic information",
      "showPoints": false,
      "condition": {}
    }
  ]
}
```

Fields:

- `id`: stable questionnaire identifier. Use this when fetching the questionnaire details and uploading answers.
- `name`: display/admin name.
- `showPoints`: whether the app may show the points result.
- `condition`: JSON condition object. Empty conditions are returned as `{}`.

Only questionnaires with `state = "active"` are returned, ordered by server `orderIndex`.

### Fetch One Questionnaire

`GET /api/app_questionnaires?id=<questionnaireID>`

Example:

`GET /api/app_questionnaires?id=questionnaire_1_demographic_information`

Success:

```json
{
  "ok": true,
  "data": {
    "meta": {
      "id": "questionnaire_1_demographic_information"
    },
    "questions": [
      {
        "id": "q1",
        "layout": "radio_question",
        "question": "consent_instruction",
        "options": [
          {
            "key": "consent_signed",
            "nextQuestionId": "q6"
          },
          {
            "key": "consent_not_signed",
            "nextQuestionId": "q2"
          }
        ],
        "pointsMap": {
          "consent_signed": 0,
          "consent_not_signed": 0
        }
      }
    ]
  }
}
```

Question fields:

- `id`: short question ID used by the Android app and upload payload, for example `q1`.
- `layout`: question renderer type, for example `radio_question`, `multi_check_box_question`, `string_spinner`, `value_spinner`, `client_coach_code_question`, or `last_page`.
- `question`: translation key or default text for the question.
- `options`: answer choices for choice questions and spinner options.
- `options[].key`: answer option key. Send this back as `answerOptionKey` during upload.
- `options[].nextQuestionId`: optional conditional navigation target.
- `pointsMap`: optional map from answer option key to point value.
- `textKey`, `textKey1`, `textKey2`, `hint`, `hint1`, `hint2`, `symptoms`, `range`, `constraints`, `minSelection`: optional layout-specific config fields.

Important ID rule: the fetch response returns short question IDs such as `q1`. The server resolves these back to full database IDs during upload, so the Android app should upload the same short IDs it received.

## Fetch Translations

`GET /api/app_questionnaires?translations=1`

**Authentication:** not required (login screen).

Success:

```json
{
  "ok": true,
  "data": {
    "translations": {
      "en": {
        "consent_instruction": "Consent instruction text",
        "consent_signed": "Consent signed",
        "client_code": "Client code"
      },
      "de": {
        "consent_instruction": "Einwilligungstext",
        "consent_signed": "Einwilligung unterschrieben",
        "client_code": "Klientencode"
      }
    }
  }
}
```

The translation map is grouped by language code. Each language contains a flat key-value map of **app UI strings only**. Questionnaire content translations are returned on each `GET ?id=<questionnaireID>` response in a sibling `translations` object.

Android lookup process:

1. Load `translations[activeLanguage]`.
2. For each questionnaire string, look up the string value as a key.
3. If a translation is missing, fall back to the raw key from the questionnaire JSON.

## Recommended Android Sync Flow

1. On app start (login screen), fetch app UI translations with `GET /api/app_questionnaires?translations=1` (no token).
2. Login with `/api/auth/login` and store the returned token securely.
3. Fetch the active questionnaire list with `GET /api/app_questionnaires`.
4. For each list item, fetch details with `GET /api/app_questionnaires?id=<id>`.
5. Cache translations, the list, and questionnaire details locally for offline use.
6. When a questionnaire is completed, upload answers with `POST /api/app_questionnaires` (plaintext JSON over HTTPS).

There is currently no version or `updatedAt` field in the app fetch response, so the safest refresh strategy is to re-fetch translations, list, and details at login or app start when network is available.

## Android local data security

The Android app encrypts sensitive data **at rest on the device**:

- **Answer text** and **client codes** in the local Room database use AES-256-GCM with keys in Android Keystore (ciphertext on disk; plaintext only inside the running app).
- **Assigned client list** cache and **session token** use the same encryption / EncryptedSharedPreferences respectively.
- **Questionnaire definitions** and UI translations cached from this API are not encrypted (no user answers).

Sensitive API traffic uses **HTTPS** plus **AES-256-CBC envelopes** keyed from the Bearer token (`clientsPayload` on login, encrypted `?clients=1` responses, encrypted upload POST). Questionnaire definitions and UI translations stay plain JSON. See `nat-as-server/docs/android-questionnaire-api.md` for the wire format.

Re-uploading the same `clientCode` + `questionnaireID` updates server rows in place (supports correction / re-edit workflows).

## Upload Questionnaire Data

`POST /api/app_questionnaires`

Allowed roles: `admin`, `supervisor`, `coach`.

The authenticated user must be authorized for the submitted `clientCode`. A coach can upload for their own clients. A supervisor can upload for clients of their coaches. An admin can upload for any client.

Request:

```json
{
  "questionnaireID": "questionnaire_1_demographic_information",
  "clientCode": "CLIENT-001",
  "startedAt": 1714471200,
  "completedAt": 1714471800,
  "answers": [
    {
      "questionID": "q1",
      "answerOptionKey": "consent_signed",
      "answeredAt": 1714471210
    },
    {
      "questionID": "q9",
      "numericValue": 34,
      "answeredAt": 1714471300
    },
    {
      "questionID": "q12",
      "freeTextValue": "Free text answer",
      "answeredAt": 1714471400
    }
  ]
}
```

Required top-level fields:

- `questionnaireID`: questionnaire ID from the list/detail endpoints.
- `clientCode`: existing client code.
- `answers`: array of answer objects.

Optional top-level fields:

- `startedAt`: questionnaire start timestamp.
- `completedAt`: questionnaire completion timestamp.

Answer fields:

- `questionID`: required per useful answer. Use the short question ID returned by the detail endpoint.
- `answerOptionKey`: for choice answers. Use `options[].key` from the detail endpoint.
- `freeTextValue`: for text answers.
- `numericValue`: for numeric/range answers.
- `answeredAt`: timestamp for this answer.

Current upload behavior:

- Unknown or empty `questionID` values are skipped.
- Unknown `answerOptionKey` values are stored as no selected option for that question.
- Re-uploading the same `clientCode` and `questionID` overwrites the previous answer.
- Re-uploading the same `clientCode` and `questionnaireID` overwrites the completed questionnaire status and point sum.
- `sumPoints` is calculated from recognized `answerOptionKey` values.
- The current database stores one selected answer option per question. If the Android UI allows multiple selections for a layout, coordinate a server change before uploading multiple option keys for one question.

Success:

```json
{
  "ok": true,
  "data": {
    "submitted": true,
    "sumPoints": 0
  }
}
```

Common failures:

- `400 INVALID_BODY`: request body is not valid JSON.
- `400 MISSING_FIELDS`: one or more required fields are missing.
- `400 INVALID_FIELD`: `answers` is not an array.
- `401`: missing token or invalid credentials.
- `403`: expired token, password change required, or insufficient role.
- `404 NOT_FOUND`: questionnaire does not exist, client does not exist, or user is not authorized for that client.
- `405 METHOD_NOT_ALLOWED`: wrong HTTP method.

## Retrofit Shape

Example Kotlin data shapes:

```kotlin
data class ApiEnvelope<T>(
    val ok: Boolean,
    val data: T? = null,
    val error: ApiError? = null
)

data class ApiError(
    val code: String? = null,
    val message: String
)

data class QuestionnaireListItem(
    val id: String,
    val name: String,
    val showPoints: Boolean,
    val condition: Map<String, Any?> = emptyMap()
)

data class QuestionnaireDetail(
    val meta: QuestionnaireMeta,
    val questions: List<Question>
)

data class QuestionnaireMeta(
    val id: String
)

data class Question(
    val id: String,
    val layout: String,
    val question: String,
    val options: List<QuestionOption>? = null,
    val pointsMap: Map<String, Int>? = null,
    val textKey: String? = null,
    val textKey1: String? = null,
    val textKey2: String? = null,
    val hint: String? = null,
    val hint1: String? = null,
    val hint2: String? = null,
    val symptoms: List<String>? = null,
    val range: Map<String, Double>? = null,
    val constraints: Map<String, Any?>? = null,
    val minSelection: Int? = null
)

data class QuestionOption(
    val key: String,
    val nextQuestionId: String? = null
)

data class TranslationsResponse(
    val translations: Map<String, Map<String, String>>
)

data class SubmitQuestionnaireRequest(
    val questionnaireID: String,
    val clientCode: String,
    val startedAt: Long? = null,
    val completedAt: Long? = null,
    val answers: List<SubmitAnswer>
)

data class SubmitAnswer(
    val questionID: String,
    val answerOptionKey: String? = null,
    val freeTextValue: String? = null,
    val numericValue: Double? = null,
    val answeredAt: Long? = null
)

data class SubmitQuestionnaireResponse(
    val submitted: Boolean,
    val sumPoints: Int
)
```

Example Retrofit service:

```kotlin
interface QuestionnaireApi {
    @POST("auth/login")
    suspend fun login(@Body body: LoginRequest): ApiEnvelope<LoginResponse>

    @GET("app_questionnaires")
    suspend fun getQuestionnaires(
        @Header("Authorization") bearerToken: String
    ): ApiEnvelope<List<QuestionnaireListItem>>

    @GET("app_questionnaires")
    suspend fun getQuestionnaire(
        @Header("Authorization") bearerToken: String,
        @Query("id") questionnaireId: String
    ): ApiEnvelope<QuestionnaireDetail>

    @GET("app_questionnaires")
    suspend fun getTranslations(
        @Header("Authorization") bearerToken: String,
        @Query("translations") translations: Int = 1
    ): ApiEnvelope<TranslationsResponse>

    @POST("app_questionnaires")
    suspend fun submitQuestionnaire(
        @Header("Authorization") bearerToken: String,
        @Body body: SubmitQuestionnaireRequest
    ): ApiEnvelope<SubmitQuestionnaireResponse>
}
```

Pass the token as `Bearer $token`.
