npm package: @superapp_men/submit-assessment-results
Description: Submit assessment results from SuperApp partner applications via iframe/Capacitor
Author: SuperApp Team
Latest version: 1.1.2

# @superapp_men/submit-assessment-results

**Version:** 1.0.9
**License:** MIT
**Author:** SuperApp Team

Package for **partner applications** (running inside the SuperApp iframe or Capacitor WebView) to submit assessment results to the SuperApp. The SuperApp forwards the payload to the `SubmitAssessmentResults` backend endpoint (adding `academicYearId` and `periodWeekSubjectId`) and returns the **exact backend response** to the partner.

---

## Table of Contents

- [Features](#features)
- [Installation](#installation)
- [Architecture Overview](#architecture-overview)
- [Data Dictionary](#data-dictionary)
  - [SubmitAssessmentResultsPartnerPayload](#1-submitassessmentresultspartnerpayload)
  - [SubmittedSkillResultPayload](#2-submittedskillresultpayload)
  - [SubmittedQuestionResultPayload](#3-submittedquestionresultpayload)
  - [QuestionRole Enum](#4-questionrole-enum)
- [Validation Rules](#validation-rules)
  - [Payload-Level Rules](#payload-level-validation-rules)
  - [ASER-Specific Payload Rules](#aser-specific-payload-rules)
  - [Skill-Level Rules](#skill-level-validation-rules)
  - [ASER-Specific Skill Rules](#aser-specific-skill-rules)
  - [Question-Level Rules](#question-level-validation-rules)
  - [Cross-Field Rules](#cross-field-validation-rules)
- [Builder API](#builder-api)
  - [AssessmentSubmission.builder()](#assessmentsubmissionbuilder)
  - [AssessmentSubmission.skill()](#assessmentsubmissionskillskillcode-skillorder-passed)
  - [AssessmentSubmission.question()](#assessmentsubmissionquestionquestioncode-iscorrect-questionrole)
- [Client API](#client-api)
  - [AssessmentSubmissionClient](#assessmentsubmissionclient)
  - [Validation-Only API](#validation-only-api)
- [Response Types](#response-types)
  - [SubmitAssessmentResultsApiResult](#submitassessmentresultsapiresult)
  - [SubmitAssessmentResultsResponse (200 Success)](#submitassessmentresultsresponse-200-success)
  - [ValidationProblemDetails (400)](#validationproblemdetails-400)
  - [ProblemDetails (404/409/422/429/500)](#problemdetails-404409422429500)
- [Usage Examples](#usage-examples)
- [Communication Protocol](#communication-protocol)
- [SuperApp Integration](#superapp-integration)
- [Exported Members](#exported-members)
- [Constants](#constants)

---

## Features

- **Builder / factory** to construct skills and questions data in a type-safe way.
- **Client-side validation** before calling the SuperApp: the same rules as the backend `SubmitAssessmentResultsValidator` run first. Invalid payloads get immediate `400`-style errors with field-level messages without a round-trip.
- **No `academicYearId` or `periodWeekSubjectId`** in the partner payload — the SuperApp adds them when calling the API.
- **Exact backend response** returned: success (200), validation errors (400), functional errors (422), and technical errors (404, 409, 429, 500).
- **Dual transport support:** works in both iframe (postMessage to parent) and Capacitor WebView environments.

**Current limitation (upcoming feature):** `subSkills` must be `null` or an empty array `[]`. Sub-skills are not yet supported; each skill must use `questions` only. Support for nested sub-skills will be added in a future release.

---

## Installation

```bash
npm install @superapp_men/submit-assessment-results
```

---

## Architecture Overview

```
Partner App (iframe / Capacitor WebView)
  │
  ├── AssessmentSubmission (builder)   → Constructs payload
  ├── validateSubmitAssessmentPayload  → Client-side validation (mirrors backend)
  └── AssessmentSubmissionClient       → submit(payload)
        │
        ├── 1. Runs client-side validation
        │     └── If invalid → returns { ok: false, statusCode: 400, body: ValidationProblemDetails }
        │
        └── 2. If valid → sends postMessage to SuperApp
              │
              SuperApp (host)
              ├── Listens for "assessment:submit-request"
              ├── Enriches with academicYearId + periodWeekSubjectId
              ├── Calls POST api/v1.0/submit-results
              └── Returns "assessment:submit-response" with exact backend HTTP status + body
```

---

## Data Dictionary

### 1. `SubmitAssessmentResultsPartnerPayload`

The root payload sent by partner apps. The SuperApp enriches it with `academicYearId` and `periodWeekSubjectId` before forwarding to the backend.

| Property | Type | Required | Max Length | Description |
|----------|------|----------|------------|-------------|
| `partnerCode` | `string` | Yes | 50 chars | Partner application code. Must be one of: `"bewise"`, `"ltm"`, `"cantoo"`, `"ekinox"`, `"math-scan"` (case-insensitive). |
| `attemptId` | `string` | Yes | 100 chars | Unique attempt identifier used as an idempotency key. Prevents duplicate submissions. |
| `isAser` | `boolean` | Yes | — | Set `true` for ASER (Annual Status of Education Report) positioning assessments; `false` (default) for regular week assessments. Enables additional validation rules when `true`. |
| `totalSkillsInWeek` | `number` | Yes | — | Total number of skills for the week. Must be >= 1 and >= the number of submitted skills. |
| `skills` | `SubmittedSkillResultPayload[]` | Yes | — | Array of skill results. Must contain at least one skill. |

### 2. `SubmittedSkillResultPayload`

Represents a single skill result within the submission.

| Property | Type | Required | Constraints | Description |
|----------|------|----------|-------------|-------------|
| `skillCode` | `string` | Yes | Non-empty | Unique code identifying the skill in the curriculum (e.g. `"MATH_ADD_01"`). Must match an existing skill code in the system. |
| `skillOrder` | `number` | Yes | >= 0 (1–6 for ASER) | Display order of the skill within its parent context. For ASER assessments: must be between 1 and 6 inclusive. |
| `passed` | `boolean` | Yes | `true` or `false` | Whether the student passed this skill based on the partner's evaluation. |
| `titleFr` | `string` | Conditional | — | French title of the skill. **At least one of `titleFr` or `titleAr` is required for ASER assessments.** Optional for non-ASER. |
| `titleAr` | `string` | Conditional | — | Arabic title of the skill. **At least one of `titleFr` or `titleAr` is required for ASER assessments.** Optional for non-ASER. |
| `descriptionFr` | `string` | Conditional | — | French description of the skill. **At least one of `descriptionFr` or `descriptionAr` is required for ASER assessments.** Optional for non-ASER. |
| `descriptionAr` | `string` | Conditional | — | Arabic description of the skill. **At least one of `descriptionFr` or `descriptionAr` is required for ASER assessments.** Optional for non-ASER. |
| `questions` | `SubmittedQuestionResultPayload[]` | Yes* | >= 1 item | List of question results for this skill. *Required until subSkills feature is released. |
| `subSkills` | `SubmittedSkillResultPayload[]` | No | Must be `null` or `[]` | **Not yet supported (upcoming feature).** Must be `null` or empty array. Non-empty subSkills will fail validation. |

### 3. `SubmittedQuestionResultPayload`

Represents a single question result within a skill.

| Property | Type | Required | Constraints | Description |
|----------|------|----------|-------------|-------------|
| `questionCode` | `string` | Yes | Non-empty | Unique code identifying the question (e.g. `"Q001"`). Must be unique within the same skill (case-insensitive). |
| `isCorrect` | `boolean` | Yes | `true` or `false` | Whether the student answered the question correctly. |
| `questionRole` | `QuestionRole` (number) | Yes | `1`, `2`, `3`, or `4` | Pedagogical role of the question. See [QuestionRole Enum](#4-questionrole-enum). |
| `questionOrder` | `number` | No | 0–255 | Display order of the question within the skill. Defaults to `0` in the builder. |
| `responseTime` | `number \| string` | No | 0–3,600,000 ms (1 hour) | Time taken to answer. **Number** = milliseconds (e.g. `5000`); **String** = `"hh:mm:ss"` format (e.g. `"00:00:05"`). |
| `attemptsCount` | `number` | No | >= 0 | Number of attempts the student made on this question. |
| `questionTextFr` | `string` | No | — | French text of the question (used for auto-creation if the question doesn't exist). |
| `questionTextAr` | `string` | No | — | Arabic text of the question (used for auto-creation if the question doesn't exist). |
| `tags` | `string` | No | — | Comma-separated keywords or labels (e.g. `"algebra,addition,level1"`). |
| `metadata` | `string` | No | — | JSON string containing arbitrary metadata (e.g. `JSON.stringify({ difficulty: "easy", chapter: 3 })`). |

### 4. `QuestionRole` Enum

Pedagogical role of the question in the assessment. **Required** for every question.

| Value | Name | Description |
|-------|------|-------------|
| `1` | `POSITIONING` | Used to determine initial skill level (typically in ASER assessments). |
| `2` | `VALIDATION` | Used to validate skill mastery (default for most questions in regular assessments). |
| `3` | `REMEDIATION` | Used after failed validation for practice and reinforcement. |
| `4` | `BONUS` | Optional challenge questions beyond the core assessment. |

```ts
import { QuestionRole } from "@superapp_men/submit-assessment-results";

QuestionRole.POSITIONING  // 1
QuestionRole.VALIDATION   // 2
QuestionRole.REMEDIATION  // 3
QuestionRole.BONUS        // 4
```

---

## Validation Rules

The client-side validator mirrors the backend `SubmitAssessmentResultsValidator`. All rules are checked **before** sending to the SuperApp.

### Payload-Level Validation Rules

| Field | Rules | Error Messages |
|-------|-------|----------------|
| `partnerCode` | Required, non-empty. Must be one of: `bewise`, `ltm`, `cantoo`, `ekinox`, `math-scan` (case-insensitive). Max 50 characters. | `"PartnerCode is required."` / `"PartnerCode must be one of: bewise, ltm, cantoo, ekinox, math-scan."` / `"PartnerCode must not exceed 50 characters."` |
| `attemptId` | Required, non-empty. Max 100 characters. | `"AttemptId is required."` / `"AttemptId must not exceed 100 characters."` |
| `totalSkillsInWeek` | Must be a number >= 1. Must be >= the number of submitted skills. | `"TotalSkillsInWeek must be at least 1."` / `"TotalSkillsInWeek must be greater than or equal to the number of submitted skills."` |
| `skills` | Required, non-null. Must be a non-empty array. No duplicate SkillCodes (case-insensitive). | `"Skills collection is required."` / `"At least one skill must be submitted."` / `"Duplicate SkillCode detected in submitted skills."` |

### ASER-Specific Payload Rules

When `isAser` is `true`, additional constraints apply:

| Field | Rules | Error Messages |
|-------|-------|----------------|
| `skills` | Minimum 1 skill. Maximum 6 skills. | `"ASER assessments require at least 1 skills."` / `"ASER assessments must not exceed 6 skills."` |
| `totalSkillsInWeek` | Must be between 1 and 6. Must equal the number of submitted skills. | `"For ASER assessments, TotalSkillsInWeek must be between 1 and 6."` / `"For ASER assessments, TotalSkillsInWeek must equal the number of submitted skills."` |

### Skill-Level Validation Rules

| Field | Rule | Error Message |
|-------|------|---------------|
| `skillCode` | Required, non-empty | `"SkillCode is required."` |
| `skillOrder` | Must be a number >= 0 | `"SkillOrder must be non-negative."` |
| `passed` | Required, must be `true` or `false` | `"Passed is required and must be true or false."` |
| `subSkills` | Must be null or empty array | `"SubSkills are not yet supported (upcoming feature). SubSkills must be null or an empty array. Use Questions for now."` |
| (skill) | Must contain either SubSkills or Questions | `"A skill must contain either SubSkills or Questions."` |
| `questions` | No duplicate QuestionCodes within the same skill (case-insensitive) | `"Duplicate QuestionCode detected within the same skill."` |

### ASER-Specific Skill Rules

When `isAser` is `true`, additional constraints apply per skill:

| Field | Rule | Error Message |
|-------|------|---------------|
| `skillOrder` | Must be between 1 and 6 (inclusive) | `"For ASER assessments, SkillOrder must be between 1 and 6."` |
| `titleFr` / `titleAr` | At least one must be non-empty | `"At least one of TitleFr or TitleAr is required for ASER assessments."` |
| `descriptionFr` / `descriptionAr` | At least one must be non-empty | `"At least one of DescriptionFr or DescriptionAr is required for ASER assessments."` |

### Question-Level Validation Rules

| Field | Rules | Error Messages |
|-------|-------|----------------|
| `questionCode` | Required, non-empty | `"QuestionCode is required."` |
| `isCorrect` | Required, must be `true` or `false` | `"IsCorrect is required and must be true or false."` |
| `questionOrder` | If provided, must be a number between 0 and 255 | `"QuestionOrder must be non-negative and not exceed 255."` |
| `questionRole` | Required, non-null. Must be 1, 2, 3, or 4. | `"QuestionRole is required."` / `"QuestionRole must be a valid value: POSITIONING (1), VALIDATION (2), REMEDIATION (3), or BONUS (4)."` |
| `responseTime` | If provided, must be a number (ms) or string `"hh:mm:ss"`. Must be 0–3,600,000 ms (1 hour). | `"ResponseTime must be a number (milliseconds) or a string in 'hh:mm:ss' format."` / `"ResponseTime must be non-negative and not exceed 3600000 milliseconds (1 hour)."` |
| `attemptsCount` | If provided, must be a number >= 0 | `"AttemptsCount must be non-negative."` |

### Cross-Field Validation Rules

| Rule | Description |
|------|-------------|
| `totalSkillsInWeek >= skills.length` | The total skills declared for the week must be at least the number of skills submitted. |
| ASER: `totalSkillsInWeek === skills.length` | For ASER assessments, all skills must be submitted (total must equal count). |
| No duplicate `skillCode` (case-insensitive) | All skill codes in the skills array must be unique. |
| No duplicate `questionCode` per skill (case-insensitive) | Within each skill, all question codes must be unique. |
| ASER: at least one of `titleFr` / `titleAr` per skill | For ASER, each skill must have at least a French or Arabic title. |
| ASER: at least one of `descriptionFr` / `descriptionAr` per skill | For ASER, each skill must have at least a French or Arabic description. |

---

## Builder API

### `AssessmentSubmission.builder()`

Creates a fluent builder for the full partner payload. Chain methods then call `.build()`.

| Method | Required | Description |
|--------|----------|-------------|
| `.setPartnerCode(code: string)` | Yes | Partner application code. Must be one of: `"bewise"`, `"ltm"`, `"cantoo"`, `"ekinox"`, `"math-scan"`. |
| `.setAttemptId(attemptId: string)` | Yes | Unique attempt id (idempotency key). Max 100 chars. |
| `.setAser(isAser: boolean)` | No | Set `true` for ASER positioning assessments. Default: `false`. |
| `.setTotalSkillsInWeek(n: number)` | Yes | Total number of skills for the week (>= 1, >= skills count). |
| `.addSkill(skill)` | Yes* | Add a single skill (built via `AssessmentSubmission.skill()`). |
| `.addSkills(skills[])` | Yes* | Add multiple skills at once. |
| `.build()` | — | Returns `SubmitAssessmentResultsPartnerPayload`. |

*At least one skill must be added via `addSkill()` or `addSkills()`.

### `AssessmentSubmission.skill(skillCode, skillOrder, passed)`

Creates a `SkillResultBuilder` for one skill.

**Constructor parameters (required):**

| Parameter | Type | Description |
|-----------|------|-------------|
| `skillCode` | `string` | Unique skill code (e.g. `"MATH_ADD_01"`). |
| `skillOrder` | `number` | Display order (>= 0; 1–6 for ASER). |
| `passed` | `boolean` | Whether the student passed this skill. |

**Chained methods (optional):**

| Method | Description |
|--------|-------------|
| `.title(titleFr?: string, titleAr?: string)` | Set French and/or Arabic title. At least one required for ASER. |
| `.description(descriptionFr?: string, descriptionAr?: string)` | Set French and/or Arabic description. At least one required for ASER. |
| `.questions(questions[])` | Set question results array. Required (until subSkills is supported). |
| `.subSkills(subSkills[])` | Set sub-skills array. **Not yet supported** — will fail validation if non-empty. |
| `.build()` | Returns `SubmittedSkillResultPayload`. |

### `AssessmentSubmission.question(questionCode, isCorrect, questionRole)`

Creates a `QuestionResultBuilder` for one question.

**Constructor parameters (required):**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `questionCode` | `string` | — | Unique question code (e.g. `"Q001"`). |
| `isCorrect` | `boolean` | — | Whether the student answered correctly. |
| `questionRole` | `QuestionRole` | `VALIDATION` (2) | Pedagogical role (1–4). |

**Chained methods (optional):**

| Method | Description |
|--------|-------------|
| `.order(questionOrder: number)` | Display order within the skill (0–255). Default: `0`. |
| `.responseTime(value: number \| string)` | Time to answer: ms (number) or `"hh:mm:ss"` (string). Max 1 hour. |
| `.attemptsCount(count: number)` | Number of attempts (>= 0). |
| `.questionText(fr?: string, ar?: string)` | French and/or Arabic question text. |
| `.tags(tags: string)` | Comma-separated keywords (e.g. `"algebra,addition"`). |
| `.metadata(metadata: string)` | JSON string with arbitrary metadata. |
| `.build()` | Returns `SubmittedQuestionResultPayload`. |

---

## Client API

### `AssessmentSubmissionClient`

```ts
import { AssessmentSubmissionClient } from "@superapp_men/submit-assessment-results";

const client = new AssessmentSubmissionClient({ timeout: 30000, debug: true });
```

**Constructor config:**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `timeout` | `number` | `30000` | Request timeout in milliseconds. |
| `debug` | `boolean` | `false` | Enable debug console logging. |

**Methods:**

| Method | Returns | Description |
|--------|---------|-------------|
| `submit(payload)` | `Promise<SubmitAssessmentResultsApiResult>` | Validates payload client-side, then sends to SuperApp via postMessage. Returns the exact backend response. |
| `setDebug(enabled: boolean)` | `void` | Toggle debug logging at runtime. |
| `destroy()` | `void` | Clean up listeners and pending timeouts. Call when done. |

**`submit()` behavior:**
1. Runs client-side validation (mirrors backend `SubmitAssessmentResultsValidator`).
2. If validation fails: returns immediately with `{ ok: false, statusCode: 400, body: ValidationProblemDetails }`.
3. If validation passes: sends payload to SuperApp via postMessage and waits for the backend response.

### Validation-Only API

Run validation without submitting (e.g. to show errors in the UI before submit):

```ts
import {
  validateSubmitAssessmentPayload,
  toValidationProblemDetails,
} from "@superapp_men/submit-assessment-results";

const validation = validateSubmitAssessmentPayload(payload);
if (!validation.valid) {
  // validation.errors: Array<{ key: string, message: string }>
  const details = toValidationProblemDetails(validation.errors);
  // details.errors: Record<string, string[]> (backend-compatible format)
}
```

**`validateSubmitAssessmentPayload(payload)`** returns:
```ts
{ valid: boolean; errors: Array<{ key: string; message: string }> }
```

**`toValidationProblemDetails(errors)`** converts to backend-style format:
```ts
{ status: 400; title: string; errors: Record<string, string[]> }
```

---

## Response Types

### `SubmitAssessmentResultsApiResult`

Union type returned by `client.submit()`:

```ts
// Success
{ ok: true;  statusCode: number; body: SubmitAssessmentResultsResponse }

// Error
{ ok: false; statusCode: number; body: ValidationProblemDetails | ProblemDetails }
```

### `SubmitAssessmentResultsResponse` (200 Success)

| Field | Type | Description |
|-------|------|-------------|
| `attemptId` | `string` | The attempt ID echoed back. |
| `isDuplicate` | `boolean` | `true` if this attemptId was already submitted (idempotent). |
| `status` | `string` | Status of the submission. |
| `message` | `string \| null` | Optional message from the backend. |
| `serverTimeUtc` | `string` | Server timestamp (UTC). |

### `ValidationProblemDetails` (400)

| Field | Type | Description |
|-------|------|-------------|
| `type` | `string?` | Problem type URI. |
| `title` | `string?` | Short description (e.g. `"One or more validation errors occurred."`). |
| `status` | `number` | HTTP status code (`400`). |
| `detail` | `string?` | Detailed error message. |
| `instance` | `string?` | URI identifying the specific occurrence. |
| `errors` | `Record<string, string[]>?` | Map of field paths to error messages (e.g. `{ "Skills[0].SkillCode": ["SkillCode is required."] }`). |

### `ProblemDetails` (404/409/422/429/500)

| Field | Type | Description |
|-------|------|-------------|
| `type` | `string?` | Problem type URI. |
| `title` | `string?` | Short description. |
| `status` | `number?` | HTTP status code. |
| `detail` | `string?` | Detailed error message. |
| `instance` | `string?` | URI identifying the specific occurrence. |
| `[key: string]` | `unknown` | Additional properties may be present. |

**HTTP status codes and meanings:**

| Status | Meaning | Description |
|--------|---------|-------------|
| `200` | Success | Assessment results submitted successfully. |
| `400` | Validation Error | Payload failed validation (client-side or backend). |
| `404` | Not Found | Student, skill, or question not found. |
| `409` | Conflict | Conflicting state (e.g. concurrent submission). |
| `422` | Unprocessable Entity | Functional/business rule error. |
| `429` | Too Many Requests | Rate limit exceeded. |
| `500` | Internal Server Error | Unexpected backend error. |

---

## Usage Examples

### Regular Assessment (non-ASER)

```ts
import {
  AssessmentSubmissionClient,
  AssessmentSubmission,
  QuestionRole,
} from "@superapp_men/submit-assessment-results";

const skill1 = AssessmentSubmission.skill("MATH_ADD_01", 1, true)
  .questions([
    AssessmentSubmission.question("Q001", true, QuestionRole.VALIDATION)
      .order(1)
      .responseTime(5000)
      .build(),
    AssessmentSubmission.question("Q002", false, QuestionRole.VALIDATION)
      .order(2)
      .responseTime("00:00:08")
      .attemptsCount(2)
      .build(),
  ])
  .build();

const payload = AssessmentSubmission.builder()
  .setPartnerCode("bewise")
  .setAttemptId("unique-attempt-id-12345")
  .setAser(false)
  .setTotalSkillsInWeek(10)
  .addSkill(skill1)
  .build();

const client = new AssessmentSubmissionClient({ timeout: 30000, debug: true });
const result = await client.submit(payload);

if (result.ok) {
  console.log("Success:", result.body.attemptId, result.body.status);
} else {
  console.log("Error:", result.statusCode, result.body);
  if (result.statusCode === 400 && "errors" in result.body) {
    console.log("Validation errors:", result.body.errors);
  }
}

// Clean up when done
client.destroy();
```

### ASER Assessment (positioning)

```ts
const aserSkills = [
  AssessmentSubmission.skill("ASER_SKILL_1", 1, true)
    .title("Addition simple", "الجمع البسيط")
    .description("Additionner deux nombres < 10", "جمع عددين أصغر من ١٠")
    .questions([
      AssessmentSubmission.question("Q001", true, QuestionRole.POSITIONING).order(1).build(),
    ])
    .build(),
  AssessmentSubmission.skill("ASER_SKILL_2", 2, true)
    .title("Soustraction", "الطرح")
    .description("Soustraire deux nombres", "طرح عددين")
    .questions([
      AssessmentSubmission.question("Q002", true, QuestionRole.POSITIONING).order(1).build(),
    ])
    .build(),
  AssessmentSubmission.skill("ASER_SKILL_3", 3, false)
    .title(undefined, "الضرب")               // Arabic only — valid
    .description("Multiplier deux nombres")   // French only — valid
    .questions([
      AssessmentSubmission.question("Q003", false, QuestionRole.POSITIONING).order(1).build(),
    ])
    .build(),
];

const payload = AssessmentSubmission.builder()
  .setPartnerCode("bewise")
  .setAttemptId("aser-attempt-001")
  .setAser(true)           // ASER mode
  .setTotalSkillsInWeek(3) // Must equal skills.length for ASER
  .addSkills(aserSkills)
  .build();
```

### Question with All Optional Fields

```ts
AssessmentSubmission.question("Q001", true, QuestionRole.VALIDATION)
  .order(1)
  .responseTime(5000)                                           // milliseconds
  // .responseTime("00:00:05")                                  // or "hh:mm:ss" string
  .attemptsCount(1)
  .questionText("Combien font 2 + 3 ?", "ما ناتج ٢ + ٣؟")
  .tags("algebra,addition,level1")
  .metadata(JSON.stringify({ difficulty: "easy", chapter: 3 }))
  .build();
```

### Validate Without Submitting

```ts
import {
  validateSubmitAssessmentPayload,
  toValidationProblemDetails,
} from "@superapp_men/submit-assessment-results";

const validation = validateSubmitAssessmentPayload(payload);
if (!validation.valid) {
  const details = toValidationProblemDetails(validation.errors);
  console.log("Validation errors:", details.errors);
  // Example output:
  // {
  //   "Skills[0].TitleFr": ["At least one of TitleFr or TitleAr is required for ASER assessments."],
  //   "Skills[0].SkillCode": ["SkillCode is required."]
  // }
}
```

---

## Communication Protocol

### Message Types

| Enum | Value | Direction | Description |
|------|-------|-----------|-------------|
| `AssessmentMessageType.REQUEST` | `"assessment:submit-request"` | Partner -> SuperApp | Partner sends assessment data. |
| `AssessmentMessageType.RESPONSE` | `"assessment:submit-response"` | SuperApp -> Partner | SuperApp returns backend response. |

### Request Message (`AssessmentSubmitRequestMessage`)

| Field | Type | Description |
|-------|------|-------------|
| `type` | `"assessment:submit-request"` | Message type constant. |
| `requestId` | `string` | Unique ID generated per request (for matching response). |
| `payload` | `SubmitAssessmentResultsPartnerPayload` | The assessment payload. |
| `timestamp` | `number` | `Date.now()` at time of sending. |

### Response Message (`AssessmentSubmitResponseMessage`)

| Field | Type | Description |
|-------|------|-------------|
| `type` | `"assessment:submit-response"` | Message type constant. |
| `requestId` | `string` | Matches the request's `requestId`. |
| `statusCode` | `number` | HTTP status code from the backend. |
| `body` | `SubmitAssessmentResultsResponse \| ValidationProblemDetails \| ProblemDetails` | Exact backend response body. |
| `timestamp` | `number` | `Date.now()` at time of response. |

---

## SuperApp Integration

The SuperApp must:

1. Listen for messages with `type: "assessment:submit-request"`.
2. Enrich the payload with `academicYearId` and `periodWeekSubjectId`.
3. Call `POST api/v1.0/submit-results` with the enriched body.
4. Forward the HTTP status and response body back to the partner via postMessage with `type: "assessment:submit-response"`.

**SuperApp imports:**

```ts
import type {
  AssessmentSubmitRequestMessage,
  AssessmentSubmitResponseMessage,
  SubmitAssessmentResultsPartnerPayload,
} from "@superapp_men/submit-assessment-results/superapp";
import { AssessmentMessageType } from "@superapp_men/submit-assessment-results/superapp";
```

---

## Exported Members

### Main entry (`@superapp_men/submit-assessment-results`)

| Export | Kind | Description |
|--------|------|-------------|
| `AssessmentSubmissionClient` | Class | Client for submitting assessments (validates + sends via postMessage). |
| `AssessmentSubmissionClientConfig` | Type | Config interface for the client (`timeout`, `debug`). |
| `AssessmentSubmission` | Object | Factory with `.builder()`, `.skill()`, `.question()` methods. |
| `AssessmentSubmissionBuilder` | Class | Fluent builder for the partner payload. |
| `SkillResultBuilder` | Class | Fluent builder for a skill result. |
| `QuestionResultBuilder` | Class | Fluent builder for a question result. |
| `QuestionRole` | Enum | `POSITIONING` (1), `VALIDATION` (2), `REMEDIATION` (3), `BONUS` (4). |
| `AssessmentMessageType` | Enum | `REQUEST`, `RESPONSE` message type constants. |
| `validateSubmitAssessmentPayload` | Function | Validate payload without submitting. Returns `{ valid, errors }`. |
| `toValidationProblemDetails` | Function | Convert `ValidationError[]` to backend-style `{ status, title, errors }`. |
| `ValidationResult` | Type | `{ valid: boolean; errors: ValidationError[] }`. |
| `ValidationError` | Type | `{ key: string; message: string }`. |
| `SubmitAssessmentResultsPartnerPayload` | Type | Root payload interface. |
| `SubmittedSkillResultPayload` | Type | Skill result interface. |
| `SubmittedQuestionResultPayload` | Type | Question result interface. |
| `SubmitAssessmentResultsResponse` | Type | Backend success response (200). |
| `SubmitAssessmentResultsApiResult` | Type | Union result type from `submit()`. |
| `ValidationProblemDetails` | Type | Backend validation error response (400). |
| `ProblemDetails` | Type | Backend error response (404/409/422/429/500). |
| `ResponseTimeInput` | Type | `number \| string` type alias for response time. |

### SuperApp entry (`@superapp_men/submit-assessment-results/superapp`)

| Export | Kind | Description |
|--------|------|-------------|
| `AssessmentMessageType` | Enum | Message type constants. |
| `QuestionRole` | Enum | Question role constants. |
| All payload/response types | Types | Same type exports as main entry (types only, no builders/client). |

---

## Constants

| Constant | Value | Used In |
|----------|-------|---------|
| `MAX_ATTEMPT_ID_LENGTH` | `100` | `attemptId` max length validation. |
| `MAX_PARTNER_CODE_LENGTH` | `50` | `partnerCode` max length validation. |
| `MIN_ASER_SKILL_COUNT` | `1` | Minimum skills for ASER assessments. |
| `MAX_ASER_SKILL_COUNT` | `6` | Maximum skills for ASER assessments. |
| `MAX_RESPONSE_TIME_MS` | `3,600,000` (1 hour) | Maximum response time per question. |
| `VALID_PARTNER_CODES` | `["bewise", "ltm", "cantoo", "ekinox", "math-scan"]` | Allowed partner code values (case-insensitive). |
| `VALID_QUESTION_ROLES` | `[1, 2, 3, 4]` | Allowed QuestionRole values. |
| Default `timeout` | `30,000` ms | Client request timeout. |