React 18 + TypeScript + Vite 5 + Tailwind CSS + shadcn/ui frontend: - Multi-role auth: student, teacher, admin, corporate, agent, developer - Student portal: dashboard, courses, adaptive learning, exams, placement - Teacher portal: courses, assignments, grading, attendance, AI workbench - Admin portal: 60+ management pages (LMS, exams, users, entities, AI) - AI-powered features: study coach, generation page, gap analysis, TTS - Exam system: session, auto-save, results, grading queue - Full IELTS workflow: reading/listening/writing/speaking modules Made-with: Cursor
74 KiB
EnCoach Platform -- Odoo 19 Migration SRS
SUPERSEDED -- This document has been replaced by
ENCOACH_ODOO19_BACKEND_SRS.md(v3.0) andENCOACH_UNIFIED_SRS.md(v2.0). All content below is historical. The migration is complete and the system is deployed athttp://5.189.151.117:8069.
Software Requirements Specification
Version: 1.0
Date: March 11, 2026
Status: Draft SUPERSEDED
Table of Contents
- Executive Summary
- Odoo Module Plan
- Data Models
- REST API Specification
- Authentication & Authorization
- ielts-be Integration Specification
- Payment Integration
- Business Rules & Workflows
- Data Migration Plan
- 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/andielts-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:
[
{
"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:
[
{
"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:
[
{
"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:
[
{
"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:
[
{
"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:
{
"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:
{
"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:
{
"email": "user@example.com",
"password": "secret"
}
Response (200):
{
"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):
{
"type": "individual",
"email": "user@example.com",
"password": "secret",
"name": "John Doe",
"code": "OPTIONAL_REG_CODE"
}
Request (corporate):
{
"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):
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
- Create
encoach.evaluationrecord with statuspending - Forward request to ielts-be
POST /api/exam/grade/writing/{task} - 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):
{
"status": "completed",
"result": { ...gradingResult }
}
POST /api/grading/multiple
Grade multiple short-answer exercises at once. Proxied to ielts-be.
Request:
{
"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:
{
"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:
{
"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:
- Frontend sends
POST /api/loginwith email + password - Odoo validates credentials against
res.users - Odoo generates a JWT token (using a library like
PyJWT) - Frontend stores the JWT and sends it as
Authorization: Bearer <token>on every request - Odoo middleware validates the JWT on each request and loads the user context
JWT payload:
{
"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:
- Keep Firebase Auth for identity management
- Frontend authenticates with Firebase, gets Firebase ID token
- Odoo validates Firebase token using Google's public keys
- Odoo maps Firebase UID to
res.usersvialegacy_idfield
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
adminordevelopertype - Endpoints that manage classrooms and assignments require
teacher,corporate, oradmintype - Endpoints that create payments require
corporate,agent, oradmintype - 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
requestsorhttpxfor 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:
- Odoo creates
encoach.evaluationrecord with statuspending - Odoo sends grading request to ielts-be with
userId,sessionId,exerciseId - ielts-be processes asynchronously and writes result directly to the shared database (current pattern) OR calls back to Odoo (new pattern)
- Frontend polls
GET /api/evaluate/{sessionId}/{exerciseId}until status iscompleted
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/callbackto 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:
- Frontend calls
POST /api/stripewith{ email, days, key, checkout } - Odoo creates a Stripe Checkout session via Stripe API
- Returns checkout URL to frontend
- Customer completes payment on Stripe
- Stripe sends webhook to
POST /api/stripe/webhook
Webhook handling (POST /api/stripe/webhook):
- Verify Stripe signature
- On
checkout.session.completed:- If user exists with that email: extend
subscription_expiration_datebydays - If user does not exist: create/update
encoach.codewith the email and expiry info, send registration email
- If user exists with that email: extend
- Create
encoach.subscription.paymentrecord
7.2 PayPal
Create order (POST /api/paypal):
- Call PayPal API to create order
- Return order ID to frontend
Capture payment (POST /api/paypal/approve):
- Call PayPal API to capture order
- On success:
- Find user by tracking ID
- Calculate new expiry:
max(current_expiry, now) + duration - Update
subscription_expiration_dateand setstatus = active - Create
encoach.subscription.paymentrecord - For corporate users: propagate subscription to all entity participants
7.3 Paymob
Create intention (POST /api/paymob):
- Call Paymob API to create payment intention
- Return client secret to frontend
Webhook (POST /api/paymob/webhook):
- Verify Paymob transaction signature
- On success:
- Find user from transaction metadata
- Extend subscription (same logic as PayPal)
- Create
encoach.subscription.paymentrecord - For entity payments: update entity
expiry_dateand propagate to entity users
7.4 Corporate Payment Flow
Corporate payments are manually tracked (bank transfers, not online):
- Admin/agent creates
encoach.paymentrecord with corporate, entity, agent details - When payment is confirmed (
is_paidset totrue):- Set corporate user's
statustoactive - Optionally extend entity expiry
- Set corporate user's
8. Business Rules & Workflows
8.1 Registration Flow
Individual Registration (with or without code)
- Validate email is not already registered
- Create
res.userswithencoach_type = 'student',encoach_status = 'active' - Set defaults:
is_first_login = True,is_verified = False,focus = 'academic', levels all 0, desired levels all 9 - If a registration
codeis provided: a. Look upencoach.codeby code string b. Validate code is not expired and not already used c. Set user'ssubscription_expiration_datefrom code'sexpiry_dated. If code has anentity_id: add user to that entity with the default role e. Mark code as used (user_id= new user) - Send verification email
Corporate Registration
- Validate email is not already registered
- Create
res.userswithencoach_type = 'corporate' - Create
encoach.entityfor the corporate - Create 3 default
encoach.grouprecords under the entity:- "Teachers" (admin = corporate user)
- "Students" (admin = corporate user)
- "Corporate" (admin = corporate user)
- 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:
new_expiry = max(current_expiry or now(), now()) + timedelta(days=days)
8.3 Entity Licensing
- Entities have a
licensescount (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_dateis 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
- Created: Teacher creates assignment with exams and assignees
- Started:
auto_starttriggers atstart_date, or teacher manually calls/start - In Progress: Students can take the exams. Stats are recorded
- Released: Teacher calls
/release. Students can view their results - Archived: Teacher calls
/archive. Assignment hidden from active lists
8.5 Grading Polling
- Student submits writing/speaking answer
- Frontend calls
POST /api/evaluate/writingor/speaking - Odoo creates
encoach.evaluationwithstatus = 'pending' - Odoo forwards to ielts-be
- Frontend polls
GET /api/evaluate/{sessionId}/{exerciseId}every 3-5 seconds - When ielts-be finishes (via callback or direct DB update), evaluation status becomes
completed - Frontend receives the grading result and displays scores + feedback
8.6 Corporate Payment Activation
- Corporate user or agent creates payment record
- Admin verifies bank transfer received
- Admin updates payment:
is_paid = true - System sets corporate user's
status = 'active' - System extends
subscription_expiration_datefor 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
- Export MongoDB data to JSON files (one per collection)
- Resolve references: Build a mapping of
MongoDB _id / legacy_id -> Odoo IDas records are imported - Import order: Users first (to resolve references), then entities, roles, groups, exams, then dependent records
- 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)
- File migration: Migrate files from Firebase Storage to Odoo
ir.attachmentor a configured file storage - Validation: After import, run integrity checks:
- All Many2one references resolve to existing records
- User counts match between MongoDB and PostgreSQL
- Exam
partsJSON is valid and parseable - Subscription dates are preserved correctly
9.4 Suggested Import Order
res.users/res.partner(fromusers)encoach.entity(fromentities)encoach.role(fromroles)encoach.user.entity.rel(fromusers.entitiesarray)encoach.group(fromgroups)encoach.exam(fromreading,listening,writing,speaking,level)encoach.package(frompackages)encoach.discount(fromdiscounts)encoach.code(fromcodes)encoach.permission(frompermissions)encoach.assignment(fromassignments)encoach.session(fromsessions)encoach.stat(fromstats)encoach.evaluation(fromevaluation)encoach.payment(frompayments)encoach.subscription.payment(frompaypalpayments)encoach.ticket(fromtickets)encoach.invite(frominvites)encoach.training(fromtraining)encoach.walkthrough(fromwalkthrough)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,nullor{}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: trueAccess-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONSAccess-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 |