Files
encoach_frontend_new_v3/frontend/docs/ODOO_MIGRATION_SRS.md
Yamen Ahmad f1c4953a63 feat(v3): restructure project + add complete frontend
- Restructure: move backend from new_project/ to backend/
- Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks)
- Add docs/ with SRS specs, user stories, and workflow documentation
- Update .gitignore for new directory layout

Workflows implemented:
  WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration,
  WF4 General English Exam, WF5 Course Generation,
  WF6 Entity Student Onboarding, AI Course Generation,
  Adaptive Learning Engine UI, White-Label Branding, Score Release

Made-with: Cursor
2026-04-10 17:26:42 +04:00

74 KiB

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
  2. Odoo Module Plan
  3. Data Models
  4. REST API Specification
  5. Authentication & Authorization
  6. ielts-be Integration Specification
  7. Payment Integration
  8. Business Rules & Workflows
  9. Data Migration Plan
  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:

[
  {
    "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:

  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):

{
  "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

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:

{
  "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:

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