# EnCoach Platform -- Odoo 19 Migration SRS

> **SUPERSEDED** -- This document has been replaced by `ENCOACH_ODOO19_BACKEND_SRS.md` (v3.0) and `ENCOACH_UNIFIED_SRS.md` (v2.0). All content below is historical. The migration is complete and the system is deployed at `http://5.189.151.117:8069`.

## Software Requirements Specification

**Version:** 1.0
**Date:** March 11, 2026
**Status:** ~~Draft~~ **SUPERSEDED**

---

## Table of Contents

1. [Executive Summary](#1-executive-summary)
2. [Odoo Module Plan](#2-odoo-module-plan)
3. [Data Models](#3-data-models-odoo-models)
4. [REST API Specification](#4-rest-api-specification)
5. [Authentication & Authorization](#5-authentication--authorization)
6. [ielts-be Integration Specification](#6-ielts-be-integration-specification)
7. [Payment Integration](#7-payment-integration)
8. [Business Rules & Workflows](#8-business-rules--workflows)
9. [Data Migration Plan](#9-data-migration-plan)
10. [Non-Functional Requirements](#10-non-functional-requirements)

---

## 1. Executive Summary

### 1.1 Platform Overview

EnCoach is an IELTS preparation and English proficiency testing platform serving students, teachers, corporate clients, and administrators. The platform provides:

- Full IELTS exam simulation (Reading, Listening, Writing, Speaking, Level)
- AI-powered grading of writing and speaking responses
- AI-powered exam content generation
- Multi-tenant entity (organization) management
- Classroom and assignment management
- Training and personalized recommendations
- Subscription-based access with multiple payment providers
- Support ticket system

### 1.2 Migration Rationale

Replace the current custom Node.js/MongoDB backend with Odoo 19 to leverage Odoo's built-in ERP capabilities for user management, subscriptions, payments, and helpdesk while retaining the specialized AI/ML microservice.

### 1.3 Scope Boundaries

**In Scope (Odoo replaces):**

- All MongoDB direct access from ielts-ui API routes (22+ collections, 60+ endpoints)
- Firebase Auth for user authentication
- All business logic in `ielts-ui/src/pages/api/` and `ielts-ui/src/utils/*.be.ts`
- Payment processing (Stripe, PayPal, Paymob)
- File storage (currently Firebase Storage)

**Out of Scope (unchanged):**

- ielts-ui browser/frontend code (React components, Zustand stores, pages)
- ielts-be (FastAPI) AI/ML microservice (grading, generation, TTS, transcription, training)
- Strapi CMS (encoachcms)
- Landing page (encoach-landing-page)

### 1.4 Current Architecture

```
ielts-ui (Next.js 14)
  ├── Browser UI (React + Zustand + Tailwind)
  ├── API Routes (/pages/api/) ──── MongoDB (22+ collections)
  │                             └─── Firebase Auth
  └── Proxy routes ─────────────── ielts-be (FastAPI)
                                      ├── OpenAI GPT-4o
                                      ├── Whisper (local STT)
                                      ├── AWS Polly (TTS)
                                      ├── ELAI (AI Video)
                                      └── GPTZero (AI Detection)
```

### 1.5 Target Architecture

```
ielts-ui (Next.js 14)
  └── Browser UI (unchanged)
        │
        ▼
Odoo 19 (Python + PostgreSQL)
  ├── REST API Controllers (JSON)
  ├── Business Logic (custom modules)
  ├── PostgreSQL (all data)
  ├── Odoo Attachments (file storage)
  ├── Payment Providers (Stripe, PayPal, Paymob)
  └── HTTP Client ──────────────── ielts-be (FastAPI, unchanged)
                                      ├── OpenAI GPT-4o
                                      ├── Whisper (local STT)
                                      ├── AWS Polly (TTS)
                                      ├── ELAI (AI Video)
                                      └── GPTZero (AI Detection)
```

The ielts-ui frontend will be updated to point its API calls at Odoo instead of its own `/api/` routes. The Next.js API routes layer is eliminated entirely. Odoo becomes the sole backend, proxying AI/ML requests to the unchanged ielts-be service.

---

## 2. Odoo Module Plan

### 2.1 Standard Odoo Modules to Leverage

| Odoo Module | Usage |
|-------------|-------|
| `base` / `res.users` / `res.partner` | User accounts, extended with EnCoach-specific fields |
| `payment` | Payment provider framework for Stripe, PayPal, Paymob |
| `product` | Subscription packages as products |
| `helpdesk` (or custom) | Support ticket management |
| `mail` | Email notifications (password reset, verification, invites) |

### 2.2 Custom Modules to Develop

| Module | Depends On | Complexity | Description |
|--------|------------|------------|-------------|
| `encoach_core` | `base`, `mail` | Medium | User type extensions, entity management, roles, permissions, codes, invites |
| `encoach_exam` | `encoach_core` | High | Exam models for 5 modules (reading, listening, writing, speaking, level) with complex nested exercise structures |
| `encoach_classroom` | `encoach_core` | Low | Group/classroom management with participant tracking |
| `encoach_assignment` | `encoach_core`, `encoach_exam` | Medium | Assignment lifecycle (create, start, release, archive) linking exams to students |
| `encoach_stats` | `encoach_core`, `encoach_exam` | Medium | Exam sessions, per-exercise statistics, score tracking |
| `encoach_evaluation` | `encoach_core`, `encoach_exam` | Medium | Async grading records for writing/speaking, integration with ielts-be |
| `encoach_training` | `encoach_core` | Low | Training content storage, walkthrough state, proxy to ielts-be |
| `encoach_subscription` | `encoach_core`, `product`, `payment` | Medium | Packages, discounts, subscription expiry management, Stripe/PayPal/Paymob |
| `encoach_registration` | `encoach_core` | Medium | Multi-path registration (individual, corporate, admin-created), registration codes |
| `encoach_ticket` | `encoach_core` | Low | Support tickets (or extend `helpdesk`) |
| `encoach_api` | All above | High | REST JSON controllers exposing ~60 endpoints for frontend consumption |

### 2.3 Module Dependency Graph

```
encoach_api
  ├── encoach_core
  │     ├── base / res.users / res.partner
  │     └── mail
  ├── encoach_exam
  │     └── encoach_core
  ├── encoach_classroom
  │     └── encoach_core
  ├── encoach_assignment
  │     ├── encoach_core
  │     └── encoach_exam
  ├── encoach_stats
  │     ├── encoach_core
  │     └── encoach_exam
  ├── encoach_evaluation
  │     ├── encoach_core
  │     └── encoach_exam
  ├── encoach_training
  │     └── encoach_core
  ├── encoach_subscription
  │     ├── encoach_core
  │     ├── product
  │     └── payment
  ├── encoach_registration
  │     └── encoach_core
  └── encoach_ticket
        └── encoach_core
```

---

## 3. Data Models (Odoo Models)

This section specifies every Odoo model required, with field-level detail. Each model maps from one or more current MongoDB collections.

### 3.1 `encoach.user` (extends `res.users`)

**Source:** MongoDB `users` collection

Extend `res.users` with the following fields. The standard `res.users` already provides `login` (email), `name`, `password`, and `active`.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `encoach_type` | Selection | Yes | One of: `student`, `teacher`, `corporate`, `admin`, `developer`, `agent`, `mastercorporate` |
| `encoach_status` | Selection | Yes | One of: `active`, `disabled`, `payment_due`. Default: `active` |
| `profile_picture` | Binary / URL | No | Profile image. Default: `/defaultAvatar.png` |
| `bio` | Text | No | User biography |
| `focus` | Selection | No | `academic` or `general`. Default: `academic` |
| `is_first_login` | Boolean | No | Default: `True` |
| `is_verified` | Boolean | No | Email verification status. Default: `False` |
| `subscription_expiration_date` | Datetime | No | When the subscription expires |
| `registration_date` | Datetime | Yes | Account creation date |
| `last_login` | Datetime | No | Last login timestamp |
| `level_reading` | Float | No | Current reading level (0-9). Default: 0 |
| `level_listening` | Float | No | Current listening level (0-9). Default: 0 |
| `level_writing` | Float | No | Current writing level (0-9). Default: 0 |
| `level_speaking` | Float | No | Current speaking level (0-9). Default: 0 |
| `desired_level_reading` | Float | No | Target reading level. Default: 9 |
| `desired_level_listening` | Float | No | Target listening level. Default: 9 |
| `desired_level_writing` | Float | No | Target writing level. Default: 9 |
| `desired_level_speaking` | Float | No | Target speaking level. Default: 9 |
| `student_id` | Char | No | External student ID (student type only) |
| `average_level` | Float | No | Computed average across modules (student only) |
| `preferred_gender` | Selection | No | `male` or `female` (student only) |
| `preferred_topics` | Text (JSON) | No | JSON array of preferred topics (student only) |
| `phone` | Char | No | Phone number (demographic info) |
| `passport_id` | Char | No | Passport/national ID |
| `country_code` | Char | No | Country code |
| `company_name` | Char | No | Company name (corporate/agent) |
| `commercial_registration` | Char | No | Commercial registration number (agent) |
| `company_arab_name` | Char | No | Company name in Arabic (agent) |
| `entity_ids` | Many2many | No | Link to `encoach.entity` via `encoach.user.entity.rel` |
| `permission_ids` | Many2many | No | Link to `encoach.permission` |
| `legacy_id` | Char | No | Original MongoDB/Firebase UID for migration |

### 3.2 `encoach.user.entity.rel`

**Purpose:** Many-to-many relationship between users and entities with a role.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `user_id` | Many2one(`res.users`) | Yes | The user |
| `entity_id` | Many2one(`encoach.entity`) | Yes | The entity |
| `role_id` | Many2one(`encoach.role`) | Yes | The user's role within this entity |

### 3.3 `encoach.entity`

**Source:** MongoDB `entities` collection

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | Char | Yes | Entity display name (`label` in current system) |
| `licenses` | Integer | No | Number of licensed seats |
| `expiry_date` | Datetime | No | Entity license expiry |
| `payment_status` | Selection | No | `paid`, `unpaid`, `trial` |
| `role_ids` | One2many(`encoach.role`) | No | Roles defined for this entity |
| `user_rel_ids` | One2many(`encoach.user.entity.rel`) | No | Users in this entity |
| `group_ids` | One2many(`encoach.group`) | No | Groups belonging to this entity |
| `legacy_id` | Char | No | Original MongoDB ID |

### 3.4 `encoach.role`

**Source:** MongoDB `roles` collection

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | Char | Yes | Role display name (`label` in current system) |
| `entity_id` | Many2one(`encoach.entity`) | Yes | Parent entity |
| `is_default` | Boolean | No | Whether this is the default role for new entity members |
| `permissions` | Text (JSON) | No | JSON object of granular permission flags |
| `legacy_id` | Char | No | Original MongoDB ID |

### 3.5 `encoach.group`

**Source:** MongoDB `groups` collection

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | Char | Yes | Group/classroom name |
| `admin_id` | Many2one(`res.users`) | Yes | Group administrator |
| `entity_id` | Many2one(`encoach.entity`) | No | Owning entity |
| `participant_ids` | Many2many(`res.users`) | No | Group members |
| `disable_editing` | Boolean | No | Whether members can be modified. Default: `False` |
| `legacy_id` | Char | No | Original MongoDB ID |

### 3.6 `encoach.exam`

**Source:** MongoDB `reading`, `listening`, `writing`, `speaking`, `level` collections

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | Char | No | Display label |
| `module` | Selection | Yes | `reading`, `listening`, `writing`, `speaking`, `level` |
| `min_timer` | Integer | No | Minimum time in seconds |
| `is_diagnostic` | Boolean | No | Diagnostic exam flag. Default: `False` |
| `variant` | Char | No | Exam variant identifier |
| `difficulty` | Selection | No | `A1`, `A2`, `B1`, `B2`, `C1`, `C2` |
| `access` | Selection | Yes | `public`, `private`, `confidential`. Default: `public` |
| `shuffle` | Boolean | No | Whether to shuffle questions. Default: `False` |
| `requires_approval` | Boolean | No | Needs admin approval before use. Default: `False` |
| `approved` | Boolean | No | Approval status. Default: `False` |
| `owner_ids` | Many2many(`res.users`) | No | Exam owners/creators |
| `entity_ids` | Many2many(`encoach.entity`) | No | Entities with access |
| `parts` | Text (JSON) | Yes | **Full exam structure as JSON** (see Section 3.6.1) |
| `legacy_id` | Char | No | Original MongoDB ID |

#### 3.6.1 Exam `parts` JSON Structure

Exams use a deeply nested JSON structure for their content. Modeling this as relational tables would be extremely complex (exercises contain sub-questions, solutions, options, etc.). Store as a JSON field and validate at the application level.

**Reading exam `parts` example:**

```json
[
  {
    "text": { "title": "Passage Title", "content": "Full passage text..." },
    "exercises": [
      {
        "id": "uuid",
        "type": "multipleChoice",
        "prompt": "Choose the correct answer",
        "questions": [
          {
            "id": "uuid",
            "prompt": "What does the author suggest?",
            "options": [
              { "id": "a", "text": "Option A" },
              { "id": "b", "text": "Option B" }
            ],
            "solution": "a"
          }
        ]
      },
      {
        "id": "uuid",
        "type": "trueFalse",
        "prompt": "Are these statements true, false, or not given?",
        "questions": [
          { "id": "uuid", "prompt": "Statement X", "solution": "TRUE" }
        ]
      },
      {
        "id": "uuid",
        "type": "fillBlanks",
        "prompt": "Complete the summary",
        "text": "The study found that ___ were ___",
        "solutions": [{ "id": "1", "solution": "researchers" }],
        "words": [{ "letter": "A", "word": "researchers" }],
        "allowRepetition": false
      },
      {
        "id": "uuid",
        "type": "writeBlanks",
        "prompt": "Complete with no more than 3 words",
        "maxWords": 3,
        "text": "Scientists discovered ___",
        "solutions": [{ "id": "1", "solution": ["the answer", "an answer"] }]
      },
      {
        "id": "uuid",
        "type": "matchSentences",
        "prompt": "Match the headings",
        "variant": "HEADING",
        "options": [{ "id": "i", "sentence": "Heading A" }],
        "sentences": [{ "id": "1", "sentence": "Paragraph 1 summary", "solution": "i" }]
      }
    ]
  }
]
```

**Listening exam `parts` example:**

```json
[
  {
    "script": [
      { "name": "Speaker A", "gender": "male", "text": "Hello..." },
      { "name": "Speaker B", "gender": "female", "text": "Hi..." }
    ],
    "exercises": [
      {
        "id": "uuid",
        "type": "multipleChoice",
        "prompt": "Choose the correct answer",
        "questions": [
          {
            "id": "uuid",
            "prompt": "What is the main topic?",
            "options": [{ "id": "a", "text": "Weather" }],
            "solution": "a"
          }
        ]
      },
      {
        "id": "uuid",
        "type": "writeBlanks",
        "prompt": "Complete the form",
        "maxWords": 2,
        "variant": "FORM",
        "questions": [
          { "id": "uuid", "prompt": "Name: ___", "solution": ["John Smith"] }
        ]
      }
    ]
  }
]
```

**Writing exam `parts` example:**

```json
[
  {
    "exercises": [
      {
        "id": "uuid",
        "type": "writing",
        "task": 1,
        "prompt": "Describe the chart below...",
        "attachment": "url-to-image-or-null"
      }
    ]
  },
  {
    "exercises": [
      {
        "id": "uuid",
        "type": "writing",
        "task": 2,
        "prompt": "Some people believe that..."
      }
    ]
  }
]
```

**Speaking exam `parts` example:**

```json
[
  {
    "exercises": [
      {
        "id": "uuid",
        "type": "interactiveSpeaking",
        "task": 1,
        "prompts": [
          "Do you enjoy reading?",
          "What kind of books do you prefer?"
        ]
      }
    ]
  },
  {
    "exercises": [
      {
        "id": "uuid",
        "type": "speaking",
        "task": 2,
        "text": "Describe a place you have visited..."
      }
    ]
  }
]
```

**Level exam `parts` example:**

```json
[
  {
    "text": { "title": "Passage", "content": "Read the following..." },
    "exercises": [
      {
        "id": "uuid",
        "type": "multipleChoice",
        "prompt": "Answer the questions",
        "questions": [
          {
            "id": "uuid",
            "prompt": "What is correct?",
            "variant": "text",
            "options": [{ "id": "a", "text": "Option A" }],
            "solution": "a"
          }
        ]
      },
      {
        "id": "uuid",
        "type": "fillBlanks",
        "variant": "mc",
        "prompt": "Fill in the blanks",
        "text": "She ___ to school every day",
        "solutions": [{ "id": "1", "solution": "goes" }],
        "words": [{ "letter": "A", "word": "goes" }]
      }
    ]
  }
]
```

### 3.7 `encoach.assignment`

**Source:** MongoDB `assignments` collection

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | Char | Yes | Assignment name |
| `assigner_id` | Many2one(`res.users`) | Yes | Creator (teacher/admin) |
| `assignee_ids` | Many2many(`res.users`) | Yes | Assigned students |
| `teacher_ids` | Many2many(`res.users`) | No | Supervising teachers |
| `exam_ids` | Many2many(`encoach.exam`) | Yes | Exams included |
| `instructor_gender` | Selection | No | `male` or `female` |
| `start_date` | Datetime | No | When assignment becomes available |
| `end_date` | Datetime | No | Assignment deadline |
| `auto_start` | Boolean | No | Auto-start at start_date. Default: `False` |
| `started` | Boolean | No | Whether assignment has been started. Default: `False` |
| `released` | Boolean | No | Whether results are released. Default: `False` |
| `archived` | Boolean | No | Archived status. Default: `False` |
| `entity_id` | Many2one(`encoach.entity`) | No | Owning entity |
| `results` | Text (JSON) | No | JSON object of student results |
| `legacy_id` | Char | No | Original MongoDB ID |

### 3.8 `encoach.session`

**Source:** MongoDB `sessions` collection

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `user_id` | Many2one(`res.users`) | Yes | Student |
| `exam_id` | Many2one(`encoach.exam`) | No | Exam being taken |
| `assignment_id` | Many2one(`encoach.assignment`) | No | Related assignment (if any) |
| `start_time` | Datetime | Yes | Session start |
| `end_time` | Datetime | No | Session end |
| `status` | Selection | Yes | `in_progress`, `completed`, `abandoned` |
| `legacy_id` | Char | No | Original MongoDB ID |

### 3.9 `encoach.stat`

**Source:** MongoDB `stats` collection

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `user_id` | Many2one(`res.users`) | Yes | Student |
| `exam_id` | Many2one(`encoach.exam`) | Yes | Exam attempted |
| `exercise_id` | Char | Yes | Exercise UUID within the exam |
| `session_id` | Many2one(`encoach.session`) | Yes | Parent session |
| `assignment_id` | Many2one(`encoach.assignment`) | No | Related assignment |
| `date` | Datetime | Yes | Attempt timestamp |
| `module` | Selection | Yes | `reading`, `listening`, `writing`, `speaking`, `level` |
| `solutions` | Text (JSON) | No | JSON of user's answers |
| `exercise_type` | Char | No | Exercise type string |
| `time_spent` | Integer | No | Seconds spent |
| `inactivity` | Integer | No | Seconds of inactivity |
| `score` | Float | No | Score achieved |
| `is_disabled` | Boolean | No | Whether stat is disabled. Default: `False` |
| `shuffle_maps` | Text (JSON) | No | JSON mapping of shuffled question order |
| `pdf` | Binary | No | Generated PDF report |
| `is_practice` | Boolean | No | Practice attempt flag. Default: `False` |
| `legacy_id` | Char | No | Original MongoDB ID |

### 3.10 `encoach.evaluation`

**Source:** MongoDB `evaluation` collection

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `user_id` | Many2one(`res.users`) | Yes | Student being evaluated |
| `session_id` | Many2one(`encoach.session`) | Yes | Parent session |
| `exercise_id` | Char | Yes | Exercise UUID |
| `eval_type` | Selection | Yes | `writing`, `speaking`, `speaking_interactive` |
| `task` | Integer | Yes | Task number (1, 2, or 3) |
| `status` | Selection | Yes | `pending`, `in_progress`, `completed`, `error`. Default: `pending` |
| `result` | Text (JSON) | No | Grading result JSON (see Section 3.10.1) |
| `error` | Text | No | Error message if grading failed |
| `legacy_id` | Char | No | Original MongoDB ID |

#### 3.10.1 Evaluation `result` JSON Structure

**Writing result:**

```json
{
  "comment": "Overall assessment of the student's writing...",
  "overall": 6.5,
  "task_response": {
    "Task Achievement": { "grade": 6.0, "comment": "..." },
    "Coherence and Cohesion": { "grade": 7.0, "comment": "..." },
    "Lexical Resource": { "grade": 6.5, "comment": "..." },
    "Grammatical Range and Accuracy": { "grade": 6.5, "comment": "..." }
  },
  "perfect_answer": "A model answer text...",
  "fixed_text": "The student's text with corrections...",
  "ai_detection": { "probability": 0.12 }
}
```

**Speaking result:**

```json
{
  "comment": "Overall assessment of the student's speaking...",
  "overall": 7.0,
  "task_response": {
    "Fluency and Coherence": { "grade": 7.0, "comment": "..." },
    "Lexical Resource": { "grade": 7.0, "comment": "..." },
    "Grammatical Range and Accuracy": { "grade": 6.5, "comment": "..." },
    "Pronunciation": { "grade": 7.5, "comment": "..." }
  },
  "transcript": "Transcribed student speech...",
  "fixed_text": "Corrected transcript...",
  "perfect_answer": "A model answer...",
  "solutions": ["url-to-audio-file"]
}
```

### 3.11 `encoach.package`

**Source:** MongoDB `packages` collection

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | Char | No | Display name |
| `currency` | Char | Yes | Currency code (e.g., `USD`, `SAR`) |
| `duration` | Integer | Yes | Subscription duration value |
| `duration_unit` | Selection | Yes | `days`, `months`, `years` |
| `price` | Float | Yes | Package price |
| `active` | Boolean | No | Whether package is available. Default: `True` |
| `legacy_id` | Char | No | Original MongoDB ID |

### 3.12 `encoach.payment`

**Source:** MongoDB `payments` collection (corporate/agent payments)

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `corporate_id` | Many2one(`res.users`) | Yes | Corporate user |
| `entity_id` | Many2one(`encoach.entity`) | No | Related entity |
| `agent_id` | Many2one(`res.users`) | No | Sales agent |
| `agent_commission` | Float | No | Agent commission percentage |
| `agent_value` | Float | No | Agent commission amount |
| `currency` | Char | Yes | Currency code |
| `value` | Float | Yes | Payment amount |
| `is_paid` | Boolean | No | Payment status. Default: `False` |
| `date` | Datetime | Yes | Payment date |
| `corporate_transfer` | Char | No | Corporate bank transfer reference |
| `commission_transfer` | Char | No | Commission transfer reference |
| `legacy_id` | Char | No | Original MongoDB ID |

### 3.13 `encoach.subscription.payment`

**Source:** MongoDB `paypalpayments` collection (individual Stripe/PayPal/Paymob payments)

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `user_id` | Many2one(`res.users`) | Yes | Paying user |
| `provider` | Selection | Yes | `stripe`, `paypal`, `paymob` |
| `order_id` | Char | Yes | Provider's order/session ID |
| `status` | Selection | Yes | `pending`, `completed`, `failed` |
| `value` | Float | Yes | Amount paid |
| `currency` | Char | Yes | Currency code |
| `subscription_duration` | Integer | No | Duration value |
| `subscription_duration_unit` | Selection | No | `days`, `months`, `years` |
| `subscription_expiration_date` | Datetime | No | New expiry date after payment |
| `created_at` | Datetime | Yes | Payment timestamp |
| `checkout_data` | Text (JSON) | No | Raw provider checkout data |
| `legacy_id` | Char | No | Original MongoDB ID |

### 3.14 `encoach.ticket`

**Source:** MongoDB `tickets` collection

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `subject` | Char | Yes | Ticket subject |
| `description` | Text | Yes | Ticket description |
| `status` | Selection | Yes | `open`, `in_progress`, `resolved`, `closed`. Default: `open` |
| `ticket_type` | Char | No | Ticket category |
| `reporter_id` | Many2one(`res.users`) | No | Reporting user (null for public tickets) |
| `reported_from` | Char | No | Source (`platform`, `landing_page`) |
| `assigned_to_id` | Many2one(`res.users`) | No | Assigned support agent |
| `exam_information` | Text (JSON) | No | Related exam context |
| `date` | Datetime | Yes | Creation date |
| `legacy_id` | Char | No | Original MongoDB ID |

### 3.15 `encoach.code`

**Source:** MongoDB `codes` collection

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `code` | Char | Yes | Registration code string (unique) |
| `creator_id` | Many2one(`res.users`) | No | User who created the code |
| `entity_id` | Many2one(`encoach.entity`) | No | Entity this code belongs to |
| `code_type` | Selection | No | Code type category |
| `expiry_date` | Datetime | No | When the code expires |
| `creation_date` | Datetime | Yes | When the code was created |
| `user_id` | Many2one(`res.users`) | No | User who redeemed the code |
| `email` | Char | No | Email associated with this code |
| `name` | Char | No | Name associated with this code |
| `passport_id` | Char | No | Passport ID associated |
| `checkout` | Text (JSON) | No | Stripe checkout session data |
| `legacy_id` | Char | No | Original MongoDB ID |

### 3.16 `encoach.invite`

**Source:** MongoDB `invites` collection

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `entity_id` | Many2one(`encoach.entity`) | Yes | Entity being invited to |
| `from_user_id` | Many2one(`res.users`) | Yes | Inviter |
| `to_user_id` | Many2one(`res.users`) | Yes | Invitee |
| `status` | Selection | Yes | `pending`, `accepted`, `declined`. Default: `pending` |
| `created_at` | Datetime | Yes | Invite creation date |
| `legacy_id` | Char | No | Original MongoDB ID |

### 3.17 `encoach.permission`

**Source:** MongoDB `permissions` collection

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | Char | Yes | Permission display name |
| `permission_type` | Char | Yes | Permission type key |
| `topic` | Char | No | Permission topic/category |
| `user_ids` | Many2many(`res.users`) | No | Users granted this permission |
| `legacy_id` | Char | No | Original MongoDB ID |

### 3.18 `encoach.discount`

**Source:** MongoDB `discounts` collection

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `code` | Char | Yes | Discount code string |
| `percentage` | Float | Yes | Discount percentage (0-100) |
| `domain` | Char | No | Domain restriction |
| `valid_until` | Datetime | No | Expiry date |
| `active` | Boolean | No | Default: `True` |
| `legacy_id` | Char | No | Original MongoDB ID |

### 3.19 `encoach.training`

**Source:** MongoDB `training` collection

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `user_id` | Many2one(`res.users`) | Yes | Student |
| `created_at` | Datetime | Yes | Training generation date |
| `exams` | Text (JSON) | No | JSON array of exam performance summaries |
| `tips` | Text (JSON) | No | JSON object with categorized tips |
| `weak_areas` | Text (JSON) | No | JSON array: `[{ "area": "...", "comment": "..." }]` |
| `legacy_id` | Char | No | Original MongoDB ID |

### 3.20 `encoach.walkthrough`

**Source:** MongoDB `walkthrough` collection

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `user_id` | Many2one(`res.users`) | Yes | User |
| `state` | Text (JSON) | No | JSON object tracking walkthrough progress |
| `legacy_id` | Char | No | Original MongoDB ID |

### 3.21 `encoach.approval.workflow`

**Source:** MongoDB `configured-workflows` and `active-workflows` collections

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | Char | Yes | Workflow name |
| `workflow_type` | Selection | Yes | `configured`, `active` |
| `config` | Text (JSON) | No | Workflow configuration |
| `status` | Selection | No | `pending`, `approved`, `rejected` |
| `entity_id` | Many2one(`encoach.entity`) | No | Related entity |
| `legacy_id` | Char | No | Original MongoDB ID |

---

## 4. REST API Specification

All endpoints must return JSON. The `encoach_api` module implements Odoo JSON controllers (inheriting `http.Controller`) with `type='json'` or `type='http'` routes.

**Base URL:** `https://{odoo-host}/api/`

**Authentication:** All endpoints require a valid session/JWT unless marked `[PUBLIC]`.

**Response format convention:** Match the current frontend expectations. All responses are JSON objects. Lists are returned as arrays. Errors use HTTP status codes with `{ "error": "message" }`.

### 4.1 Authentication Endpoints

#### `POST /api/login`

Login with email and password. Returns user object and sets session.

**Request:**
```json
{
  "email": "user@example.com",
  "password": "secret"
}
```

**Response (200):**
```json
{
  "id": 1,
  "email": "user@example.com",
  "name": "John Doe",
  "type": "student",
  "status": "active",
  "profilePicture": "/defaultAvatar.png",
  "isFirstLogin": false,
  "isVerified": true,
  "focus": "academic",
  "levels": { "reading": 6.5, "listening": 7.0, "writing": 6.0, "speaking": 6.5 },
  "desiredLevels": { "reading": 9, "listening": 9, "writing": 9, "speaking": 9 },
  "subscriptionExpirationDate": "2026-12-31T00:00:00Z",
  "registrationDate": "2025-01-15T00:00:00Z",
  "entities": [{ "id": 5, "role": "student" }],
  "permissions": []
}
```

**Error (401):** `{ "error": "Invalid credentials" }`

#### `POST /api/register`

Register a new user. Supports two registration types.

**Request (individual):**
```json
{
  "type": "individual",
  "email": "user@example.com",
  "password": "secret",
  "name": "John Doe",
  "code": "OPTIONAL_REG_CODE"
}
```

**Request (corporate):**
```json
{
  "type": "corporate",
  "email": "corp@example.com",
  "password": "secret",
  "name": "Corp Admin",
  "corporateInformation": {
    "companyName": "Acme Inc"
  }
}
```

**Response (201):** User object (same shape as login response)

**Business logic:** See Section 8.1 for full registration flow.

#### `POST /api/logout`

Destroy current session.

**Response (200):** `{ "ok": true }`

#### `GET /api/user`

Get current authenticated user.

**Response (200):** User object (same shape as login response)

#### `POST /api/reset/sendVerification`

Send email verification link.

**Request:** `{ "email": "user@example.com" }`

**Response (200):** `{ "ok": true }`

#### `POST /api/reset/confirm`

Reset password via token.

**Request:** `{ "token": "reset-token", "newPassword": "new-secret" }`

**Response (200):** `{ "ok": true }`

### 4.2 User Endpoints

#### `GET /api/users/list`

List users linked to the current user (by entity, group, or admin scope).

**Query params:** `page`, `limit`, `type`, `entity`, `group`, `search`

**Response (200):**
```json
{
  "users": [ { ...userObject } ],
  "total": 150
}
```

#### `GET /api/users/{id}`

Get a single user by ID.

**Response (200):** User object

#### `GET /api/users/search`

Full-text search users by name or email.

**Query params:** `q` (search string), `limit`

**Response (200):** `[ { ...userObject } ]`

#### `PATCH /api/users/update`

Update user fields. Admins can update other users. Users can update their own profile.

**Request:**
```json
{
  "id": 1,
  "name": "Updated Name",
  "bio": "New bio",
  "focus": "general",
  "levels": { "reading": 7.0 },
  "status": "active",
  "subscriptionExpirationDate": "2027-01-01T00:00:00Z"
}
```

**Response (200):** Updated user object

#### `DELETE /api/user`

Delete the current user's account.

**Response (200):** `{ "ok": true }`

#### `GET /api/users/balance`

Get user's subscription balance/remaining days.

**Response (200):** `{ "balance": 45, "expirationDate": "2026-05-01T00:00:00Z" }`

#### `GET /api/users/controller`

Admin user CRUD controller.

**Query params:** `action` (`list`, `create`, `update`, `delete`), relevant fields

**Response:** Depends on action

#### `POST /api/users/controller`

Admin user creation/modification.

**Request:** User creation/update payload

**Response:** User object or confirmation

#### `POST /api/make_user`

Admin/corporate create a user (with auto-generated password).

**Request:**
```json
{
  "email": "student@example.com",
  "name": "Student Name",
  "type": "student",
  "entity": 5,
  "group": 10,
  "expiryDate": "2027-01-01",
  "passport_id": "AB123456"
}
```

**Response (201):** `{ "user": { ...userObject }, "code": { ...codeObject } }`

#### `POST /api/batch_users`

Bulk import users. Odoo forwards to ielts-be for Firebase-compatible processing, then stores the resulting users.

**Request:** `{ "makerID": "admin-id", "users": [ { ...userDTO } ] }`

**Response (200):** `{ "ok": true }`

**Integration:** Calls ielts-be `POST /api/user/import` (see Section 6).

#### `GET /api/users/agents` `[PUBLIC]`

List sales agent users. Used by the landing page.

**Response (200):** `[ { "id": 1, "name": "Agent", "companyName": "...", "country": "SA" } ]`

#### `GET /api/users/agents/{code}`

Get agent by country code.

**Response (200):** Agent user object

### 4.3 Entity Endpoints

#### `GET /api/entities`

List entities the current user has access to.

**Response (200):** `[ { ...entityObject } ]`

#### `POST /api/entities`

Create a new entity.

**Request:**
```json
{
  "label": "ABC School",
  "licenses": 100,
  "expiryDate": "2027-01-01"
}
```

**Response (201):** Entity object

#### `GET /api/entities/{id}`

Get entity by ID with roles and user counts.

**Response (200):** Entity object with nested `roles` and `userCount`

#### `PATCH /api/entities/{id}`

Update entity fields.

**Request:** Partial entity object

**Response (200):** Updated entity

#### `GET /api/entities/{id}/users`

List users belonging to an entity.

**Query params:** `page`, `limit`, `role`

**Response (200):** `{ "users": [...], "total": 50 }`

#### `GET /api/entities/{id}/groups`

List groups in an entity.

**Response (200):** `[ { ...groupObject } ]`

### 4.4 Group Endpoints

#### `GET /api/groups`

List groups accessible to the current user.

**Query params:** `entity`

**Response (200):** `[ { ...groupObject, "participantCount": 25 } ]`

#### `POST /api/groups`

Create a group.

**Request:**
```json
{
  "name": "Class A",
  "entity": 5,
  "participants": [1, 2, 3]
}
```

**Response (201):** Group object

#### `GET /api/groups/{id}`

Get group with participant list.

**Response (200):** Group object with `participants` array

#### `PATCH /api/groups/{id}`

Update group (add/remove participants, rename).

**Request:** Partial group object

**Response (200):** Updated group

### 4.5 Role Endpoints

#### `GET /api/roles`

List roles for an entity.

**Query params:** `entity`

**Response (200):** `[ { ...roleObject } ]`

#### `POST /api/roles`

Create a role.

**Request:** `{ "label": "Teacher", "entityID": 5, "permissions": { ... }, "isDefault": false }`

**Response (201):** Role object

#### `GET /api/roles/{id}`

Get role details.

#### `PATCH /api/roles/{id}`

Update role permissions.

#### `GET /api/roles/{id}/users`

List users with this role.

### 4.6 Permission Endpoints

#### `GET /api/permissions`

List all permission definitions.

**Response (200):** `[ { "id": 1, "type": "exam_create", "topic": "exams", "users": [1,2,3] } ]`

#### `POST /api/permissions/bootstrap`

Initialize default permissions for an entity.

#### `PATCH /api/permissions/{id}`

Update a permission (add/remove users).

### 4.7 Invite Endpoints

#### `GET /api/invites`

List invites for the current user (sent and received).

**Response (200):** `[ { ...inviteObject } ]`

#### `POST /api/invites`

Send an invite.

**Request:** `{ "entity": 5, "to": "user@example.com" }`

**Response (201):** Invite object

#### `POST /api/invites/accept/{id}`

Accept an invite.

**Response (200):** `{ "ok": true }`

**Business logic:** Add user to entity with the default role.

#### `POST /api/invites/decline/{id}`

Decline an invite.

**Response (200):** `{ "ok": true }`

### 4.8 Code Endpoints

#### `GET /api/code`

List registration codes.

**Query params:** `entity`, `creator`

**Response (200):** `[ { ...codeObject } ]`

#### `POST /api/code`

Create a registration code.

**Request:** `{ "entity": 5, "expiryDate": "2027-01-01", "type": "student" }`

**Response (201):** Code object

#### `GET /api/code/{id}`

Get code details.

#### `DELETE /api/code/{id}`

Delete a code.

#### `GET /api/code/entities`

List codes grouped by entity.

### 4.9 Exam Endpoints

#### `GET /api/exam/{module}`

List exams for a module.

**Path params:** `module` = `reading` | `listening` | `writing` | `speaking` | `level`

**Query params:** `entity`, `access`, `difficulty`, `page`, `limit`

**Response (200):** `[ { ...examObject (without full parts for listing) } ]`

#### `GET /api/exam/{module}/{id}`

Get a single exam with full `parts` JSON.

**Response (200):** Full exam object including nested exercises

#### `POST /api/exam/upload`

Upload an exam document for parsing. Proxied to ielts-be.

**Request:** Multipart form with file

**Response (200):** Parsed exam object

**Integration:** Forwards to ielts-be endpoint (see Section 6).

#### `POST /api/exam/generate/{module}`

Generate exam content using AI. Proxied to ielts-be.

**Path params:** `module` = `reading` | `listening` | `writing` | `speaking` | `level`

**Request:** Module-specific generation parameters (varies)

**Response (200):** Generated content

**Integration:** Forwards to ielts-be endpoint (see Section 6).

#### `POST /api/exam/media/{module}`

Generate media (audio/video) for an exam. Proxied to ielts-be.

**Request:** Module-specific media parameters

**Response (200):** Media file (audio/video bytes) or URL

**Integration:** Forwards to ielts-be endpoint (see Section 6).

#### `GET /api/exam/avatars`

List available speaking avatars. Proxied to ielts-be.

**Response (200):** `[ { "id": "avatar_1", "name": "...", "thumbnail": "..." } ]`

#### `POST /api/exam/{module}/import`

Import an exam from file. Proxied to ielts-be.

**Request:** Multipart form with `exercises` and optional `solutions` files

**Response (200):** Parsed exam object. Odoo should then persist the exam to `encoach.exam`.

#### `POST /api/exam` (save)

Save an exam (new or updated) to the database.

**Request:** Full exam object with `parts` JSON

**Response (201/200):** Saved exam object with ID

### 4.10 Assignment Endpoints

#### `GET /api/assignments`

List assignments.

**Query params:** `assigner`, `assignee`, `entity`, `archived`, `page`, `limit`

**Response (200):** `[ { ...assignmentObject } ]`

#### `POST /api/assignments`

Create an assignment.

**Request:**
```json
{
  "name": "Week 1 Practice",
  "assignees": [1, 2, 3],
  "exams": [10, 11],
  "startDate": "2026-04-01",
  "endDate": "2026-04-07",
  "instructorGender": "female",
  "entity": 5,
  "teachers": [4],
  "autoStart": true
}
```

**Response (201):** Assignment object

#### `GET /api/assignments/{id}`

Get assignment details with participant results.

#### `PATCH /api/assignments/{id}`

Update assignment.

#### `POST /api/assignments/{id}/start`

Start an assignment (make it available to students).

**Response (200):** `{ "ok": true }`

#### `POST /api/assignments/{id}/release`

Release assignment results to students.

**Response (200):** `{ "ok": true }`

#### `POST /api/assignments/{id}/archive`

Archive an assignment.

#### `POST /api/assignments/{id}/unarchive`

Unarchive an assignment.

#### `GET /api/assignments/corporate`

List assignments for corporate dashboard.

### 4.11 Session Endpoints

#### `GET /api/sessions`

List sessions for a user.

**Query params:** `user`, `exam`, `assignment`

**Response (200):** `[ { ...sessionObject } ]`

#### `POST /api/sessions`

Create a new exam session.

**Request:** `{ "exam": 10, "assignment": 5 }`

**Response (201):** Session object with ID

#### `GET /api/sessions/{id}`

Get session details.

#### `PATCH /api/sessions/{id}`

Update session (e.g., mark as completed).

### 4.12 Stats Endpoints

#### `GET /api/stats`

List stats.

**Query params:** `user`, `exam`, `session`, `module`, `assignment`, `page`, `limit`

**Response (200):** `[ { ...statObject } ]`

#### `POST /api/stats`

Create a stat record (exercise attempt result).

**Request:**
```json
{
  "user": 1,
  "exam": 10,
  "exercise": "uuid-string",
  "session": 20,
  "module": "reading",
  "solutions": { ... },
  "type": "multipleChoice",
  "timeSpent": 120,
  "inactivity": 5,
  "score": 0.85,
  "assignment": 5,
  "isPractice": false
}
```

**Response (201):** Stat object

#### `GET /api/stats/{id}`

Get a single stat.

#### `PATCH /api/stats/{id}`

Update a stat (e.g., disable, set PDF).

#### `GET /api/stats/user/{userId}`

Get all stats for a user.

#### `GET /api/stats/session/{sessionId}`

Get all stats for a session.

#### `PATCH /api/stats/disabled`

Bulk disable stats.

#### `GET /api/stats/{id}/{exportType}/pdf`

Generate a PDF report for a stat. Calls ielts-be for grading summary.

**Response:** PDF binary or `{ "url": "..." }`

**Integration:** Calls ielts-be `POST /api/exam/grade/summary` for section-level evaluation.

### 4.13 Grading / Evaluation Endpoints

#### `POST /api/evaluate/writing`

Submit a writing answer for AI grading. Creates an evaluation record and proxies to ielts-be.

**Request:**
```json
{
  "userId": 1,
  "sessionId": 20,
  "exerciseId": "uuid",
  "question": "Write about...",
  "answer": "Student's essay text...",
  "task": 1,
  "attachment": "optional-image-url"
}
```

**Response (200):** `{ "ok": true }` (grading happens asynchronously)

**Business logic:**
1. Create `encoach.evaluation` record with status `pending`
2. Forward request to ielts-be `POST /api/exam/grade/writing/{task}`
3. ielts-be processes asynchronously and updates the evaluation record

#### `POST /api/evaluate/speaking`

Submit speaking audio for AI grading.

**Request:** Multipart form with `userId`, `sessionId`, `exerciseId`, `task`, and audio files (`audio_1`, `audio_2`, etc.)

**Response (200):** `{ "ok": true }` (grading happens asynchronously)

**Business logic:** Same pattern as writing grading.

#### `POST /api/evaluate/interactiveSpeaking`

Submit interactive speaking (multiple Q&A pairs).

**Request:** Same as speaking but with `question_N` and `audio_N` pairs.

**Response (200):** `{ "ok": true }`

#### `GET /api/evaluate/{sessionId}/{exerciseId}`

Poll for evaluation result.

**Response (200):**
```json
{
  "status": "completed",
  "result": { ...gradingResult }
}
```

#### `POST /api/grading/multiple`

Grade multiple short-answer exercises at once. Proxied to ielts-be.

**Request:**
```json
{
  "text": "passage text",
  "questions": ["Q1", "Q2"],
  "answers": ["A1", "A2"]
}
```

**Response (200):** `{ "exercises": [{ "id": "...", "correct": true, "correct_answer": "..." }] }`

### 4.14 Training Endpoints

#### `POST /api/training`

Generate training content for a user. Proxied to ielts-be.

**Request:**
```json
{
  "userID": 1,
  "stats": [ { ...statSummary } ]
}
```

**Response (200):** `{ "id": "training-record-id" }`

**Business logic:** Forward to ielts-be; store returned content in `encoach.training`.

#### `POST /api/training/tips`

Fetch personalized tips. Proxied to ielts-be.

**Request:** `{ "context": "...", "question": "...", "answer": "...", "correct_answer": "..." }`

**Response (200):** `{ "tips": "..." }`

#### `GET /api/training/user/{userId}`

Get stored training content for a user.

**Response (200):** Training object with tips and weak areas

#### `GET /api/training/walkthrough`

Get walkthrough state.

**Response (200):** `{ "state": { ... } }`

#### `PATCH /api/training/walkthrough`

Update walkthrough state.

**Request:** `{ "state": { ... } }`

### 4.15 Payment Endpoints

#### `POST /api/stripe`

Create a Stripe checkout session.

**Request:** `{ "email": "user@example.com", "days": 30, "key": "package-key" }`

**Response (200):** `{ "url": "https://checkout.stripe.com/..." }`

**Webhook: `POST /api/stripe/webhook`**

Handle Stripe webhook events (payment completed). See Section 7.1.

#### `POST /api/paypal`

Create a PayPal order.

**Request:** `{ "amount": 99.00, "currency": "USD" }`

**Response (200):** `{ "orderId": "PAYPAL_ORDER_ID" }`

#### `POST /api/paypal/approve`

Capture a PayPal payment after customer approval.

**Request:** `{ "id": "PAYPAL_ORDER_ID", "duration": 30, "duration_unit": "days", "trackingId": "..." }`

**Response (200):** `{ "ok": true }`

**Business logic:** See Section 7.2.

#### `POST /api/paymob`

Create a Paymob payment intention.

**Request:** `{ "amount": 500, "currency": "SAR" }`

**Response (200):** `{ "clientSecret": "..." }`

#### `POST /api/paymob/webhook`

Handle Paymob transaction webhook.

**Business logic:** See Section 7.3.

#### `GET /api/payments`

List corporate/agent payments.

**Query params:** `corporate`, `entity`, `agent`, `isPaid`

**Response (200):** `[ { ...paymentObject } ]`

#### `POST /api/payments`

Create a corporate payment record.

**Request:** Payment object fields

**Response (201):** Payment object

#### `GET /api/payments/{id}`

Get payment details.

#### `PATCH /api/payments/{id}`

Update payment (mark as paid, update transfers).

**Business logic:** When `isPaid` is set to `true`, activate the corporate user's subscription (see Section 8.4).

#### `DELETE /api/payments/{id}`

Delete a payment record.

#### `GET /api/payments/assigned`

List payments assigned to an agent.

### 4.16 Package Endpoints

#### `GET /api/packages` `[PUBLIC]`

List available subscription packages. Used by both the platform and landing page.

**Response (200):** `[ { "id": 1, "currency": "USD", "duration": 30, "duration_unit": "days", "price": 29.99 } ]`

#### `POST /api/packages`

Create a package (admin only).

**Request:** Package fields

**Response (201):** Package object

#### `GET /api/packages/{id}`

Get package details.

#### `PATCH /api/packages/{id}`

Update a package.

#### `DELETE /api/packages/{id}`

Delete a package.

### 4.17 Discount Endpoints

#### `GET /api/discounts`

List discount codes.

**Response (200):** `[ { ...discountObject } ]`

#### `POST /api/discounts`

Create a discount code.

**Request:** `{ "code": "SAVE20", "percentage": 20, "domain": "encoach.com", "validUntil": "2027-01-01" }`

**Response (201):** Discount object

#### `DELETE /api/discounts/{id}`

Delete a discount code.

### 4.18 Ticket Endpoints

#### `GET /api/tickets`

List tickets.

**Query params:** `status`, `assignedTo`, `reporter`, `page`, `limit`

**Response (200):** `[ { ...ticketObject } ]`

#### `POST /api/tickets` `[PUBLIC]`

Create a ticket. Used by both the platform and landing page contact form.

**Request:**
```json
{
  "subject": "Cannot access exam",
  "description": "When I click...",
  "type": "bug",
  "reportedFrom": "platform",
  "examInformation": { "examId": 10, "module": "reading" }
}
```

**Response (201):** Ticket object

#### `GET /api/tickets/{id}`

Get ticket details.

#### `PATCH /api/tickets/{id}`

Update ticket (assign, change status).

#### `GET /api/tickets/assignedToUser`

List tickets assigned to the current support agent.

### 4.19 Storage Endpoints

#### `POST /api/storage/insert`

Upload a file. Use Odoo's `ir.attachment` system.

**Request:** Multipart form with file

**Response (200):** `{ "url": "https://odoo-host/web/content/attachment_id" }`

#### `GET /api/storage`

List files for a context (e.g., user profile pictures, exam attachments).

**Query params:** `context`, `user`

**Response (200):** `[ { "name": "file.png", "url": "..." } ]`

#### `DELETE /api/storage/{id}`

Delete a file.

### 4.20 Transcription Endpoint

#### `POST /api/transcribe`

Transcribe audio. Proxied to ielts-be.

**Request:** Multipart form with audio file

**Response (200):** Dialog/transcript object

### 4.21 Approval Workflow Endpoints

#### `GET /api/approval-workflows`

List configured and active approval workflows.

#### `POST /api/approval-workflows`

Create or configure an approval workflow.

#### `PATCH /api/approval-workflows/{id}`

Approve or reject a workflow.

### 4.22 Statistical Endpoint

#### `GET /api/statistical`

Get platform-wide statistics (admin dashboard). Proxied to ielts-be if needed.

---

## 5. Authentication & Authorization

### 5.1 Recommended Approach: Odoo Native Auth with JWT

Use Odoo's built-in `res.users` authentication with a JWT token layer for the SPA frontend.

**Flow:**

1. Frontend sends `POST /api/login` with email + password
2. Odoo validates credentials against `res.users`
3. Odoo generates a JWT token (using a library like `PyJWT`)
4. Frontend stores the JWT and sends it as `Authorization: Bearer <token>` on every request
5. Odoo middleware validates the JWT on each request and loads the user context

**JWT payload:**

```json
{
  "uid": 1,
  "email": "user@example.com",
  "type": "student",
  "exp": 1735689600
}
```

**JWT secret:** Configured via Odoo system parameter `encoach.jwt_secret`.

**Token expiry:** 24 hours (configurable). Frontend should handle token refresh.

### 5.2 Alternative: Firebase Token Validation

If Firebase Auth migration is too disruptive:

1. Keep Firebase Auth for identity management
2. Frontend authenticates with Firebase, gets Firebase ID token
3. Odoo validates Firebase token using Google's public keys
4. Odoo maps Firebase UID to `res.users` via `legacy_id` field

This approach requires maintaining Firebase Auth as a dependency.

### 5.3 Role-Based Access Control

Map the 7 user types to Odoo security groups:

| User Type | Odoo Group | Access Level |
|-----------|------------|-------------|
| `student` | `encoach_group_student` | Read own data, take exams, view own stats |
| `teacher` | `encoach_group_teacher` | Manage classrooms, assignments, view student stats in their groups |
| `corporate` | `encoach_group_corporate` | Manage entity, create users, view entity-wide data |
| `admin` | `encoach_group_admin` | Full platform access |
| `developer` | `encoach_group_developer` | Full access + system tools |
| `agent` | `encoach_group_agent` | View assigned payments, agent dashboard |
| `mastercorporate` | `encoach_group_mastercorporate` | Multi-entity corporate management |

**Record rules (`ir.rule`):**

- Students see only their own stats, sessions, evaluations, training
- Teachers see stats/sessions for students in their groups
- Corporate users see data for their entity
- Admins see all data
- Agents see their assigned payments

### 5.4 API-Level Authorization

Each endpoint should validate the user's type/group before processing:

- Endpoints that create entities, manage users globally, or access admin dashboards require `admin` or `developer` type
- Endpoints that manage classrooms and assignments require `teacher`, `corporate`, or `admin` type
- Endpoints that create payments require `corporate`, `agent`, or `admin` type
- Public endpoints (packages, tickets/create, agents) require no authentication

---

## 6. ielts-be Integration Specification

Odoo communicates with the ielts-be microservice over HTTP. The ielts-be service is unchanged and expects a Bearer JWT token for authentication.

### 6.1 Configuration

| Odoo System Parameter | Description |
|----------------------|-------------|
| `encoach.ielts_be_url` | Base URL of ielts-be service (e.g., `http://ielts-be:8000/api`) |
| `encoach.ielts_be_jwt` | JWT token for authenticating with ielts-be |

### 6.2 HTTP Client

Implement a service class `EncoachAIClient` that:

- Uses `requests` or `httpx` for HTTP calls
- Sets `Authorization: Bearer {jwt}` header on all requests
- Handles timeouts (30s for sync, 120s for generation endpoints)
- Handles errors and retries
- Logs all requests/responses for debugging

### 6.3 Endpoint Mapping

#### Exam Generation

| Odoo calls | ielts-be endpoint | Method | Request | Response |
|-----------|-------------------|--------|---------|----------|
| Generate reading passage | `GET /api/exam/reading/{passage}` | GET | Query: `topic`, `word_count` | `{ "title": "...", "text": "..." }` |
| Generate reading exercises | `POST /api/exam/reading/` | POST | `ReadingDTO` JSON | `{ "exercises": [...] }` |
| Generate listening dialog | `GET /api/exam/listening/{section}` | GET | Query: `difficulty`, `topic` | `{ "dialog": {...} }` |
| Generate listening MP3 | `POST /api/exam/listening/media` | POST | `Dialog` JSON | MP3 binary |
| Transcribe audio | `POST /api/exam/listening/transcribe` | POST | Multipart: `audio` file | Dialog object |
| Generate listening instructions | `POST /api/exam/listening/instructions` | POST | `{ "text": "..." }` | MP3 binary |
| Generate listening exercises | `POST /api/exam/listening/` | POST | `ListeningExercisesDTO` JSON | `{ "exercises": [...] }` |
| Get speaking task | `GET /api/exam/speaking/{task}` | GET | Query: `topic`, `difficulty` | Speaking content |
| Generate speaking video | `POST /api/exam/speaking/media` | POST | `{ "text": "...", "avatar": "..." }` | `{ "status": "...", "result": "..." }` |
| Poll speaking video | `GET /api/exam/speaking/media/{vid_id}` | GET | - | `{ "status": "...", "result": "url" }` |
| Get speaking avatars | `GET /api/exam/speaking/avatars` | GET | - | Avatars list |
| Get writing task | `GET /api/exam/writing/{task}` | GET | Query: `difficulty`, `topic` | Writing prompt |
| Get writing task (academic) | `POST /api/exam/writing/{task}/attachment` | POST | Multipart: `file`, `difficulty` | Writing prompt with attachment |
| Generate level exercises | `POST /api/exam/level/` | POST | `LevelExercisesDTO` JSON | Generated exercises |
| Get level exam | `GET /api/exam/level/` | GET | - | Level exam |
| Get UTAS level exam | `GET /api/exam/level/utas` | GET | - | UTAS level exam |
| Import reading exam | `POST /api/exam/reading/import` | POST | Multipart: `exercises`, `solutions` | Parsed exam |
| Import listening exam | `POST /api/exam/listening/import` | POST | Multipart: `exercises`, `solutions` | Parsed exam + dialog |
| Import level exam | `POST /api/exam/level/import/` | POST | Multipart: `exercises`, `solutions` | Parsed level exam |
| Custom level exam | `POST /api/exam/level/custom/` | POST | JSON body | Custom level exam |

#### Grading

| Odoo calls | ielts-be endpoint | Method | Request | Response | Async? |
|-----------|-------------------|--------|---------|----------|--------|
| Grade writing | `POST /api/exam/grade/writing/{task}` | POST | `WritingGradeTaskDTO` JSON | `200 OK` | Yes |
| Grade speaking | `POST /api/exam/grade/speaking/{task}` | POST | Multipart form | `200 OK` | Yes |
| Grade short answers | `POST /api/exam/grade/short_answers` | POST | JSON | `{ "exercises": [...] }` | No |
| Grading summary | `POST /api/exam/grade/summary` | POST | `{ "sections": [...] }` | `{ "sections": [...] }` | No |

**Async grading flow:**

1. Odoo creates `encoach.evaluation` record with status `pending`
2. Odoo sends grading request to ielts-be with `userId`, `sessionId`, `exerciseId`
3. ielts-be processes asynchronously and writes result directly to the shared database (current pattern) OR calls back to Odoo (new pattern)
4. Frontend polls `GET /api/evaluate/{sessionId}/{exerciseId}` until status is `completed`

**Important decision:** Currently ielts-be writes directly to the same MongoDB. With Odoo on PostgreSQL, one of these approaches is needed:

- **Option A (recommended):** ielts-be calls an Odoo webhook `POST /api/evaluate/callback` to update the evaluation record when grading is done. Minimal change to ielts-be -- just change where it writes the result.
- **Option B:** Odoo polls ielts-be for grading status. Requires ielts-be to expose a status endpoint.
- **Option C:** ielts-be writes to a shared message queue (Redis, RabbitMQ) and Odoo consumes it.

#### Training

| Odoo calls | ielts-be endpoint | Method | Request | Response |
|-----------|-------------------|--------|---------|----------|
| Get training content | `POST /api/training/` | POST | Training request JSON | `{ "id": "..." }` |
| Get tips | `POST /api/training/tips` | POST | `FetchTipsDTO` JSON | Tips text |

#### User Import

| Odoo calls | ielts-be endpoint | Method | Request | Response |
|-----------|-------------------|--------|---------|----------|
| Batch import users | `POST /api/user/import` | POST | `BatchUsersDTO` JSON | `{ "ok": true }` |

**Note:** With the migration away from Firebase Auth, the batch user import flow will need to be redesigned. Instead of importing to Firebase, users should be created directly in Odoo's `res.users`. The ielts-be batch import endpoint may become unnecessary unless Firebase Auth is retained (Option B in Section 5).

---

## 7. Payment Integration

### 7.1 Stripe

**Odoo modules:** `payment`, `payment_stripe` (standard), or custom controller.

**Checkout flow:**

1. Frontend calls `POST /api/stripe` with `{ email, days, key, checkout }`
2. Odoo creates a Stripe Checkout session via Stripe API
3. Returns checkout URL to frontend
4. Customer completes payment on Stripe
5. Stripe sends webhook to `POST /api/stripe/webhook`

**Webhook handling (`POST /api/stripe/webhook`):**

1. Verify Stripe signature
2. On `checkout.session.completed`:
   - If user exists with that email: extend `subscription_expiration_date` by `days`
   - If user does not exist: create/update `encoach.code` with the email and expiry info, send registration email
3. Create `encoach.subscription.payment` record

### 7.2 PayPal

**Create order (`POST /api/paypal`):**

1. Call PayPal API to create order
2. Return order ID to frontend

**Capture payment (`POST /api/paypal/approve`):**

1. Call PayPal API to capture order
2. On success:
   - Find user by tracking ID
   - Calculate new expiry: `max(current_expiry, now) + duration`
   - Update `subscription_expiration_date` and set `status = active`
   - Create `encoach.subscription.payment` record
   - For corporate users: propagate subscription to all entity participants

### 7.3 Paymob

**Create intention (`POST /api/paymob`):**

1. Call Paymob API to create payment intention
2. Return client secret to frontend

**Webhook (`POST /api/paymob/webhook`):**

1. Verify Paymob transaction signature
2. On success:
   - Find user from transaction metadata
   - Extend subscription (same logic as PayPal)
   - Create `encoach.subscription.payment` record
   - For entity payments: update entity `expiry_date` and propagate to entity users

### 7.4 Corporate Payment Flow

Corporate payments are manually tracked (bank transfers, not online):

1. Admin/agent creates `encoach.payment` record with corporate, entity, agent details
2. When payment is confirmed (`is_paid` set to `true`):
   - Set corporate user's `status` to `active`
   - Optionally extend entity expiry

---

## 8. Business Rules & Workflows

### 8.1 Registration Flow

#### Individual Registration (with or without code)

1. Validate email is not already registered
2. Create `res.users` with `encoach_type = 'student'`, `encoach_status = 'active'`
3. Set defaults: `is_first_login = True`, `is_verified = False`, `focus = 'academic'`, levels all 0, desired levels all 9
4. If a registration `code` is provided:
   a. Look up `encoach.code` by code string
   b. Validate code is not expired and not already used
   c. Set user's `subscription_expiration_date` from code's `expiry_date`
   d. If code has an `entity_id`: add user to that entity with the default role
   e. Mark code as used (`user_id` = new user)
5. Send verification email

#### Corporate Registration

1. Validate email is not already registered
2. Create `res.users` with `encoach_type = 'corporate'`
3. Create `encoach.entity` for the corporate
4. Create 3 default `encoach.group` records under the entity:
   - "Teachers" (admin = corporate user)
   - "Students" (admin = corporate user)
   - "Corporate" (admin = corporate user)
5. Add the corporate user to the entity with an admin role

### 8.2 Subscription Management

**Status transitions:**

```
                    payment received
  payment_due ────────────────────────> active
       ▲                                   │
       │                                   │ subscription expired
       │                                   ▼
       └──────────────────────────── disabled
                (can be reactivated)
```

**Automated checks (cron job):**

- Run daily: check all users where `subscription_expiration_date < now()`
- Set `encoach_status = 'disabled'` for expired users
- Optionally send expiry warning emails 7 days before expiry

**Subscription extension logic:**

```python
new_expiry = max(current_expiry or now(), now()) + timedelta(days=days)
```

### 8.3 Entity Licensing

- Entities have a `licenses` count (max users)
- When adding a user to an entity, check: `current_user_count < entity.licenses`
- If limit reached, reject the addition with an error
- Entity `expiry_date` is separate from individual user subscriptions
- When entity expires, all entity users lose access (optional business rule)

### 8.4 Assignment Lifecycle

```
Created ──> Started ──> In Progress ──> Released ──> Archived
                                            │
                                            └──> Unarchived ──> Released
```

1. **Created:** Teacher creates assignment with exams and assignees
2. **Started:** `auto_start` triggers at `start_date`, or teacher manually calls `/start`
3. **In Progress:** Students can take the exams. Stats are recorded
4. **Released:** Teacher calls `/release`. Students can view their results
5. **Archived:** Teacher calls `/archive`. Assignment hidden from active lists

### 8.5 Grading Polling

1. Student submits writing/speaking answer
2. Frontend calls `POST /api/evaluate/writing` or `/speaking`
3. Odoo creates `encoach.evaluation` with `status = 'pending'`
4. Odoo forwards to ielts-be
5. Frontend polls `GET /api/evaluate/{sessionId}/{exerciseId}` every 3-5 seconds
6. When ielts-be finishes (via callback or direct DB update), evaluation status becomes `completed`
7. Frontend receives the grading result and displays scores + feedback

### 8.6 Corporate Payment Activation

1. Corporate user or agent creates payment record
2. Admin verifies bank transfer received
3. Admin updates payment: `is_paid = true`
4. System sets corporate user's `status = 'active'`
5. System extends `subscription_expiration_date` for the corporate and optionally all entity members

### 8.7 User Change Propagation

When a user's entity membership or subscription changes, propagate to related records:

- When a user is added to an entity: auto-add to the entity's default group (if configured)
- When a corporate's subscription is extended: optionally extend all participants' subscriptions
- When an entity's expiry is extended: optionally extend all entity users' subscriptions

---

## 9. Data Migration Plan

### 9.1 Migration Strategy

Perform a one-time data migration from MongoDB + Firebase Auth to Odoo 19 (PostgreSQL).

### 9.2 Collection-to-Model Mapping

| MongoDB Collection | Odoo Model | Notes |
|-------------------|------------|-------|
| `users` | `res.users` + `res.partner` | Map `id` (Firebase UID) to `legacy_id`. Create Odoo-native passwords. |
| `entities` | `encoach.entity` | Direct mapping |
| `roles` | `encoach.role` | Direct mapping |
| `groups` | `encoach.group` | Resolve `admin` and `participants` to Odoo user IDs via `legacy_id` |
| `reading` | `encoach.exam` (module=reading) | `parts` stored as JSON |
| `listening` | `encoach.exam` (module=listening) | `parts` stored as JSON |
| `writing` | `encoach.exam` (module=writing) | `parts` stored as JSON |
| `speaking` | `encoach.exam` (module=speaking) | `parts` stored as JSON |
| `level` | `encoach.exam` (module=level) | `parts` stored as JSON |
| `assignments` | `encoach.assignment` | Resolve user references |
| `sessions` | `encoach.session` | Resolve user and exam references |
| `stats` | `encoach.stat` | Resolve user, exam, session references |
| `evaluation` | `encoach.evaluation` | Resolve user, session references |
| `packages` | `encoach.package` | Direct mapping |
| `payments` | `encoach.payment` | Resolve user references |
| `paypalpayments` | `encoach.subscription.payment` | Resolve user references |
| `tickets` | `encoach.ticket` | Resolve user references |
| `codes` | `encoach.code` | Resolve user, entity references |
| `invites` | `encoach.invite` | Resolve user, entity references |
| `permissions` | `encoach.permission` | Resolve user references |
| `discounts` | `encoach.discount` | Direct mapping |
| `training` | `encoach.training` | Resolve user references |
| `walkthrough` | `encoach.walkthrough` | Resolve user references |
| `active-workflows` | `encoach.approval.workflow` | Direct mapping |
| `configured-workflows` | `encoach.approval.workflow` | Direct mapping |

### 9.3 Migration Script Requirements

1. **Export MongoDB data** to JSON files (one per collection)
2. **Resolve references:** Build a mapping of `MongoDB _id / legacy_id -> Odoo ID` as records are imported
3. **Import order:** Users first (to resolve references), then entities, roles, groups, exams, then dependent records
4. **Password migration:**
   - If using Odoo native auth: generate temporary passwords and force password reset on first login
   - If keeping Firebase: no password migration needed (users authenticate via Firebase)
5. **File migration:** Migrate files from Firebase Storage to Odoo `ir.attachment` or a configured file storage
6. **Validation:** After import, run integrity checks:
   - All Many2one references resolve to existing records
   - User counts match between MongoDB and PostgreSQL
   - Exam `parts` JSON is valid and parseable
   - Subscription dates are preserved correctly

### 9.4 Suggested Import Order

1. `res.users` / `res.partner` (from `users`)
2. `encoach.entity` (from `entities`)
3. `encoach.role` (from `roles`)
4. `encoach.user.entity.rel` (from `users.entities` array)
5. `encoach.group` (from `groups`)
6. `encoach.exam` (from `reading`, `listening`, `writing`, `speaking`, `level`)
7. `encoach.package` (from `packages`)
8. `encoach.discount` (from `discounts`)
9. `encoach.code` (from `codes`)
10. `encoach.permission` (from `permissions`)
11. `encoach.assignment` (from `assignments`)
12. `encoach.session` (from `sessions`)
13. `encoach.stat` (from `stats`)
14. `encoach.evaluation` (from `evaluation`)
15. `encoach.payment` (from `payments`)
16. `encoach.subscription.payment` (from `paypalpayments`)
17. `encoach.ticket` (from `tickets`)
18. `encoach.invite` (from `invites`)
19. `encoach.training` (from `training`)
20. `encoach.walkthrough` (from `walkthrough`)
21. `encoach.approval.workflow` (from workflows)

---

## 10. Non-Functional Requirements

### 10.1 API Response Format

All API responses must match the JSON structure the frontend currently expects. The frontend uses Axios and SWR to consume these APIs.

- Success responses: HTTP 200/201 with JSON body
- Error responses: HTTP 4xx/5xx with `{ "error": "Human-readable message" }`
- List responses should support pagination: `{ "items": [...], "total": N }` or flat arrays depending on the current endpoint behavior
- Empty results: return `[]` for lists, `null` or `{}` for single objects

### 10.2 CORS Configuration

The following origins need CORS access:

| Origin | Endpoints |
|--------|-----------|
| `https://platform.encoach.com` | All authenticated endpoints |
| `https://encoach.com` | `GET /api/packages`, `POST /api/tickets`, `GET /api/users/agents` |
| `http://localhost:3000` | All (development) |

CORS headers must include:
- `Access-Control-Allow-Origin` (per origin, not `*`)
- `Access-Control-Allow-Credentials: true`
- `Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS`
- `Access-Control-Allow-Headers: Content-Type, Authorization`

### 10.3 File Storage

Replace Firebase Storage with Odoo's `ir.attachment` system or configure an external storage backend (S3, GCS).

**Files stored:**
- User profile pictures
- Exam audio files (listening MP3s)
- Exam image attachments (writing Task 1 charts)
- Speaking video files
- PDF reports

**Requirements:**
- Files must be accessible via URL (for the frontend to load)
- Support for audio (MP3), image (PNG/JPG), video (MP4), and PDF formats
- Maximum file size: 50 MB (audio/video), 10 MB (images), 5 MB (PDFs)

### 10.4 Performance Requirements

| Operation | Target Response Time |
|-----------|---------------------|
| Login / Auth | < 500ms |
| User list (paginated) | < 1s |
| Exam load (full JSON) | < 2s |
| Stat creation | < 500ms |
| Evaluation poll | < 300ms |
| Package list (public) | < 500ms |
| Ticket creation | < 500ms |
| File upload | < 5s (for files < 10 MB) |

**Grading latency** is handled by ielts-be and is inherently slow (10-30s for writing, 30-60s for speaking). The polling pattern handles this.

### 10.5 Scalability

- Support at least 10,000 concurrent users
- Support 500,000+ stat records per exam module
- Efficient pagination for all list endpoints
- JSON fields (exam `parts`, solutions) indexed appropriately in PostgreSQL

### 10.6 Security

- All API communication over HTTPS in production
- JWT tokens with short expiry (24h) and secure signing
- Rate limiting on login endpoint (max 10 attempts per minute per IP)
- Input validation on all endpoints (prevent SQL injection, XSS)
- Stripe/PayPal/Paymob webhook signature verification
- No sensitive data in JWT payload (no passwords, no payment details)

### 10.7 Logging & Monitoring

- Log all API requests with timestamp, user, endpoint, response status
- Log all ielts-be integration calls with request/response
- Log all payment events
- Error alerting for failed grading callbacks, payment failures

### 10.8 Testing

- Unit tests for all business logic (registration, subscription, payments)
- Integration tests for all API endpoints
- End-to-end tests for critical flows (login -> take exam -> get graded)
- Load tests for concurrent exam-taking scenarios

---

## Appendix A: Exercise Type Reference

Complete list of exercise types used across all exam modules.

### Reading Exercise Types

| Type | Fields | Description |
|------|--------|-------------|
| `multipleChoice` | `questions[]` with `prompt`, `options[]`, `solution` | Select correct answer |
| `trueFalse` | `questions[]` with `prompt`, `solution` (TRUE/FALSE/NOT_GIVEN) | True/False/Not Given |
| `fillBlanks` | `text`, `solutions[]`, `words[]`, `allowRepetition` | Drag words into blanks |
| `writeBlanks` | `text`, `maxWords`, `solutions[]` | Type answers into blanks |
| `matchSentences` (HEADING) | `options[]`, `sentences[]` with `solution` | Match headings to paragraphs |
| `matchSentences` (IDEAMATCH) | `options[]`, `sentences[]` with `solution` | Match ideas to paragraphs |

### Listening Exercise Types

| Type | Fields | Description |
|------|--------|-------------|
| `multipleChoice` | `questions[]` | Standard MC |
| `multipleChoice3Options` | `questions[]` | MC with 3 options |
| `writeBlanks` (FILL) | `text`, `maxWords`, `solutions[]` | Fill in blanks from audio |
| `writeBlanks` (FORM) | `questions[]`, `maxWords` | Complete a form from audio |
| `writeBlanks` (QUESTIONS) | `questions[]`, `maxWords` | Answer questions from audio |
| `trueFalse` | `questions[]` | True/False from audio |

### Writing Exercise Types

| Type | Fields | Description |
|------|--------|-------------|
| `writing` (task 1) | `prompt`, optional `attachment` | Letter or chart description |
| `writing` (task 2) | `prompt` | Essay |

### Speaking Exercise Types

| Type | Fields | Description |
|------|--------|-------------|
| `interactiveSpeaking` (part 1) | `prompts[]` | Short Q&A |
| `speaking` (part 2) | `text` | Long turn monologue |
| `interactiveSpeaking` (part 3) | `prompts[]` | Discussion |

### Level Exercise Types

| Type | Fields | Description |
|------|--------|-------------|
| `multipleChoice` | `questions[]` with `variant` | MC with text/image variants |
| `fillBlanks` (mc) | `text`, `solutions[]`, `words[]` | Fill blanks with MC options |

---

## Appendix B: Grading Rubric Details

### Writing Rubric (IELTS Band Descriptors)

| Criterion | Scale | Description |
|-----------|-------|-------------|
| Task Achievement / Task Response | 0-9 | How well the task requirements are addressed |
| Coherence and Cohesion | 0-9 | Logical organization and paragraph linking |
| Lexical Resource | 0-9 | Vocabulary range and accuracy |
| Grammatical Range and Accuracy | 0-9 | Grammar variety and correctness |
| **Overall** | 0-9 | Average of the four criteria |

### Speaking Rubric (IELTS Band Descriptors)

| Criterion | Scale | Description |
|-----------|-------|-------------|
| Fluency and Coherence | 0-9 | Flow, pace, logical connection |
| Lexical Resource | 0-9 | Vocabulary range and appropriateness |
| Grammatical Range and Accuracy | 0-9 | Grammar variety and correctness |
| Pronunciation | 0-9 | Clarity, stress, intonation |
| **Overall** | 0-9 | Average of the four criteria |

---

## Appendix C: Environment Variables

| Variable | Description | Example |
|----------|-------------|---------|
| `ENCOACH_JWT_SECRET` | JWT signing secret | Random 256-bit string |
| `ENCOACH_IELTS_BE_URL` | ielts-be base URL | `http://ielts-be:8000/api` |
| `ENCOACH_IELTS_BE_JWT` | JWT for ielts-be auth | Pre-shared token |
| `STRIPE_SECRET_KEY` | Stripe secret key | `sk_live_...` |
| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret | `whsec_...` |
| `PAYPAL_CLIENT_ID` | PayPal client ID | `AX...` |
| `PAYPAL_CLIENT_SECRET` | PayPal client secret | `EL...` |
| `PAYPAL_ACCESS_TOKEN_URL` | PayPal OAuth URL | `https://api.paypal.com/v1/oauth2/token` |
| `PAYMOB_API_KEY` | Paymob API key | `ZXlK...` |
| `PAYMOB_SECRET` | Paymob webhook secret | `...` |

---

## Appendix D: Glossary

| Term | Definition |
|------|-----------|
| **Entity** | An organization (school, company) that manages users under a shared license |
| **Group** | A classroom or user grouping within an entity |
| **Module** | An IELTS exam skill: Reading, Listening, Writing, Speaking, or Level |
| **Session** | A single exam-taking attempt by a student |
| **Stat** | The result of a single exercise attempt within a session |
| **Evaluation** | An AI-graded writing or speaking assessment |
| **Code** | A registration/invite code that grants access or subscription time |
| **Package** | A purchasable subscription plan |
| **ielts-be** | The AI/ML microservice that handles content generation and grading |
| **BFF** | Backend-for-Frontend; the current ielts-ui API routes layer being replaced |
