Files
encoach_frontend_new_v2/docs/ODOO_MIGRATION_SRS.md
devops 4383db7fa1 docs: update all SRS documents to reflect implemented state
- ENCOACH_UNIFIED_SRS.md v2.0: updated header, added 8 new Part VIII-B
  sections (student leave, fees, lessons, gradebook, student progress,
  library, activities, facilities), extended permissions with roles CRUD
  and authority matrix, updated tech specs (93 pages, ~377 API routes,
  41 modules), added implementation traceability to all sections
- ENCOACH_ODOO19_BACKEND_SRS.md v3.0: updated status to implemented,
  added beyond-SRS features section, updated module count to 41,
  endpoint count to ~377
- ODOO_DEVELOPER_HANDOFF.md: rewritten with current repo references
  and implementation status
- ENCOACH_SYSTEM_FEATURES_GUIDE.md: added system features guide
- Superseded notices added to ODOO_BACKEND_SRS_v3.md,
  ODOO_MIGRATION_SRS_v2.md, ODOO_MIGRATION_SRS.md,
  ODOO_MIGRATION_FRONTEND_SRS.md, MATH_IT_ADAPTIVE_LEARNING_SRS.md

Made-with: Cursor
2026-04-06 12:57:52 +04:00

2338 lines
74 KiB
Markdown

# 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 |