- 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
2338 lines
74 KiB
Markdown
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 |
|