# EnCoach Platform -- Odoo 19 Migration SRS > **SUPERSEDED** -- This document has been replaced by `ENCOACH_ODOO19_BACKEND_SRS.md` (v3.0) and `ENCOACH_UNIFIED_SRS.md` (v2.0). All content below is historical. The migration is complete and the system is deployed at `http://5.189.151.117:8069`. ## Software Requirements Specification **Version:** 1.0 **Date:** March 11, 2026 **Status:** ~~Draft~~ **SUPERSEDED** --- ## Table of Contents 1. [Executive Summary](#1-executive-summary) 2. [Odoo Module Plan](#2-odoo-module-plan) 3. [Data Models](#3-data-models-odoo-models) 4. [REST API Specification](#4-rest-api-specification) 5. [Authentication & Authorization](#5-authentication--authorization) 6. [ielts-be Integration Specification](#6-ielts-be-integration-specification) 7. [Payment Integration](#7-payment-integration) 8. [Business Rules & Workflows](#8-business-rules--workflows) 9. [Data Migration Plan](#9-data-migration-plan) 10. [Non-Functional Requirements](#10-non-functional-requirements) --- ## 1. Executive Summary ### 1.1 Platform Overview EnCoach is an IELTS preparation and English proficiency testing platform serving students, teachers, corporate clients, and administrators. The platform provides: - Full IELTS exam simulation (Reading, Listening, Writing, Speaking, Level) - AI-powered grading of writing and speaking responses - AI-powered exam content generation - Multi-tenant entity (organization) management - Classroom and assignment management - Training and personalized recommendations - Subscription-based access with multiple payment providers - Support ticket system ### 1.2 Migration Rationale Replace the current custom Node.js/MongoDB backend with Odoo 19 to leverage Odoo's built-in ERP capabilities for user management, subscriptions, payments, and helpdesk while retaining the specialized AI/ML microservice. ### 1.3 Scope Boundaries **In Scope (Odoo replaces):** - All MongoDB direct access from ielts-ui API routes (22+ collections, 60+ endpoints) - Firebase Auth for user authentication - All business logic in `ielts-ui/src/pages/api/` and `ielts-ui/src/utils/*.be.ts` - Payment processing (Stripe, PayPal, Paymob) - File storage (currently Firebase Storage) **Out of Scope (unchanged):** - ielts-ui browser/frontend code (React components, Zustand stores, pages) - ielts-be (FastAPI) AI/ML microservice (grading, generation, TTS, transcription, training) - Strapi CMS (encoachcms) - Landing page (encoach-landing-page) ### 1.4 Current Architecture ``` ielts-ui (Next.js 14) ├── Browser UI (React + Zustand + Tailwind) ├── API Routes (/pages/api/) ──── MongoDB (22+ collections) │ └─── Firebase Auth └── Proxy routes ─────────────── ielts-be (FastAPI) ├── OpenAI GPT-4o ├── Whisper (local STT) ├── AWS Polly (TTS) ├── ELAI (AI Video) └── GPTZero (AI Detection) ``` ### 1.5 Target Architecture ``` ielts-ui (Next.js 14) └── Browser UI (unchanged) │ ▼ Odoo 19 (Python + PostgreSQL) ├── REST API Controllers (JSON) ├── Business Logic (custom modules) ├── PostgreSQL (all data) ├── Odoo Attachments (file storage) ├── Payment Providers (Stripe, PayPal, Paymob) └── HTTP Client ──────────────── ielts-be (FastAPI, unchanged) ├── OpenAI GPT-4o ├── Whisper (local STT) ├── AWS Polly (TTS) ├── ELAI (AI Video) └── GPTZero (AI Detection) ``` The ielts-ui frontend will be updated to point its API calls at Odoo instead of its own `/api/` routes. The Next.js API routes layer is eliminated entirely. Odoo becomes the sole backend, proxying AI/ML requests to the unchanged ielts-be service. --- ## 2. Odoo Module Plan ### 2.1 Standard Odoo Modules to Leverage | Odoo Module | Usage | |-------------|-------| | `base` / `res.users` / `res.partner` | User accounts, extended with EnCoach-specific fields | | `payment` | Payment provider framework for Stripe, PayPal, Paymob | | `product` | Subscription packages as products | | `helpdesk` (or custom) | Support ticket management | | `mail` | Email notifications (password reset, verification, invites) | ### 2.2 Custom Modules to Develop | Module | Depends On | Complexity | Description | |--------|------------|------------|-------------| | `encoach_core` | `base`, `mail` | Medium | User type extensions, entity management, roles, permissions, codes, invites | | `encoach_exam` | `encoach_core` | High | Exam models for 5 modules (reading, listening, writing, speaking, level) with complex nested exercise structures | | `encoach_classroom` | `encoach_core` | Low | Group/classroom management with participant tracking | | `encoach_assignment` | `encoach_core`, `encoach_exam` | Medium | Assignment lifecycle (create, start, release, archive) linking exams to students | | `encoach_stats` | `encoach_core`, `encoach_exam` | Medium | Exam sessions, per-exercise statistics, score tracking | | `encoach_evaluation` | `encoach_core`, `encoach_exam` | Medium | Async grading records for writing/speaking, integration with ielts-be | | `encoach_training` | `encoach_core` | Low | Training content storage, walkthrough state, proxy to ielts-be | | `encoach_subscription` | `encoach_core`, `product`, `payment` | Medium | Packages, discounts, subscription expiry management, Stripe/PayPal/Paymob | | `encoach_registration` | `encoach_core` | Medium | Multi-path registration (individual, corporate, admin-created), registration codes | | `encoach_ticket` | `encoach_core` | Low | Support tickets (or extend `helpdesk`) | | `encoach_api` | All above | High | REST JSON controllers exposing ~60 endpoints for frontend consumption | ### 2.3 Module Dependency Graph ``` encoach_api ├── encoach_core │ ├── base / res.users / res.partner │ └── mail ├── encoach_exam │ └── encoach_core ├── encoach_classroom │ └── encoach_core ├── encoach_assignment │ ├── encoach_core │ └── encoach_exam ├── encoach_stats │ ├── encoach_core │ └── encoach_exam ├── encoach_evaluation │ ├── encoach_core │ └── encoach_exam ├── encoach_training │ └── encoach_core ├── encoach_subscription │ ├── encoach_core │ ├── product │ └── payment ├── encoach_registration │ └── encoach_core └── encoach_ticket └── encoach_core ``` --- ## 3. Data Models (Odoo Models) This section specifies every Odoo model required, with field-level detail. Each model maps from one or more current MongoDB collections. ### 3.1 `encoach.user` (extends `res.users`) **Source:** MongoDB `users` collection Extend `res.users` with the following fields. The standard `res.users` already provides `login` (email), `name`, `password`, and `active`. | Field | Type | Required | Description | |-------|------|----------|-------------| | `encoach_type` | Selection | Yes | One of: `student`, `teacher`, `corporate`, `admin`, `developer`, `agent`, `mastercorporate` | | `encoach_status` | Selection | Yes | One of: `active`, `disabled`, `payment_due`. Default: `active` | | `profile_picture` | Binary / URL | No | Profile image. Default: `/defaultAvatar.png` | | `bio` | Text | No | User biography | | `focus` | Selection | No | `academic` or `general`. Default: `academic` | | `is_first_login` | Boolean | No | Default: `True` | | `is_verified` | Boolean | No | Email verification status. Default: `False` | | `subscription_expiration_date` | Datetime | No | When the subscription expires | | `registration_date` | Datetime | Yes | Account creation date | | `last_login` | Datetime | No | Last login timestamp | | `level_reading` | Float | No | Current reading level (0-9). Default: 0 | | `level_listening` | Float | No | Current listening level (0-9). Default: 0 | | `level_writing` | Float | No | Current writing level (0-9). Default: 0 | | `level_speaking` | Float | No | Current speaking level (0-9). Default: 0 | | `desired_level_reading` | Float | No | Target reading level. Default: 9 | | `desired_level_listening` | Float | No | Target listening level. Default: 9 | | `desired_level_writing` | Float | No | Target writing level. Default: 9 | | `desired_level_speaking` | Float | No | Target speaking level. Default: 9 | | `student_id` | Char | No | External student ID (student type only) | | `average_level` | Float | No | Computed average across modules (student only) | | `preferred_gender` | Selection | No | `male` or `female` (student only) | | `preferred_topics` | Text (JSON) | No | JSON array of preferred topics (student only) | | `phone` | Char | No | Phone number (demographic info) | | `passport_id` | Char | No | Passport/national ID | | `country_code` | Char | No | Country code | | `company_name` | Char | No | Company name (corporate/agent) | | `commercial_registration` | Char | No | Commercial registration number (agent) | | `company_arab_name` | Char | No | Company name in Arabic (agent) | | `entity_ids` | Many2many | No | Link to `encoach.entity` via `encoach.user.entity.rel` | | `permission_ids` | Many2many | No | Link to `encoach.permission` | | `legacy_id` | Char | No | Original MongoDB/Firebase UID for migration | ### 3.2 `encoach.user.entity.rel` **Purpose:** Many-to-many relationship between users and entities with a role. | Field | Type | Required | Description | |-------|------|----------|-------------| | `user_id` | Many2one(`res.users`) | Yes | The user | | `entity_id` | Many2one(`encoach.entity`) | Yes | The entity | | `role_id` | Many2one(`encoach.role`) | Yes | The user's role within this entity | ### 3.3 `encoach.entity` **Source:** MongoDB `entities` collection | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | Char | Yes | Entity display name (`label` in current system) | | `licenses` | Integer | No | Number of licensed seats | | `expiry_date` | Datetime | No | Entity license expiry | | `payment_status` | Selection | No | `paid`, `unpaid`, `trial` | | `role_ids` | One2many(`encoach.role`) | No | Roles defined for this entity | | `user_rel_ids` | One2many(`encoach.user.entity.rel`) | No | Users in this entity | | `group_ids` | One2many(`encoach.group`) | No | Groups belonging to this entity | | `legacy_id` | Char | No | Original MongoDB ID | ### 3.4 `encoach.role` **Source:** MongoDB `roles` collection | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | Char | Yes | Role display name (`label` in current system) | | `entity_id` | Many2one(`encoach.entity`) | Yes | Parent entity | | `is_default` | Boolean | No | Whether this is the default role for new entity members | | `permissions` | Text (JSON) | No | JSON object of granular permission flags | | `legacy_id` | Char | No | Original MongoDB ID | ### 3.5 `encoach.group` **Source:** MongoDB `groups` collection | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | Char | Yes | Group/classroom name | | `admin_id` | Many2one(`res.users`) | Yes | Group administrator | | `entity_id` | Many2one(`encoach.entity`) | No | Owning entity | | `participant_ids` | Many2many(`res.users`) | No | Group members | | `disable_editing` | Boolean | No | Whether members can be modified. Default: `False` | | `legacy_id` | Char | No | Original MongoDB ID | ### 3.6 `encoach.exam` **Source:** MongoDB `reading`, `listening`, `writing`, `speaking`, `level` collections | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | Char | No | Display label | | `module` | Selection | Yes | `reading`, `listening`, `writing`, `speaking`, `level` | | `min_timer` | Integer | No | Minimum time in seconds | | `is_diagnostic` | Boolean | No | Diagnostic exam flag. Default: `False` | | `variant` | Char | No | Exam variant identifier | | `difficulty` | Selection | No | `A1`, `A2`, `B1`, `B2`, `C1`, `C2` | | `access` | Selection | Yes | `public`, `private`, `confidential`. Default: `public` | | `shuffle` | Boolean | No | Whether to shuffle questions. Default: `False` | | `requires_approval` | Boolean | No | Needs admin approval before use. Default: `False` | | `approved` | Boolean | No | Approval status. Default: `False` | | `owner_ids` | Many2many(`res.users`) | No | Exam owners/creators | | `entity_ids` | Many2many(`encoach.entity`) | No | Entities with access | | `parts` | Text (JSON) | Yes | **Full exam structure as JSON** (see Section 3.6.1) | | `legacy_id` | Char | No | Original MongoDB ID | #### 3.6.1 Exam `parts` JSON Structure Exams use a deeply nested JSON structure for their content. Modeling this as relational tables would be extremely complex (exercises contain sub-questions, solutions, options, etc.). Store as a JSON field and validate at the application level. **Reading exam `parts` example:** ```json [ { "text": { "title": "Passage Title", "content": "Full passage text..." }, "exercises": [ { "id": "uuid", "type": "multipleChoice", "prompt": "Choose the correct answer", "questions": [ { "id": "uuid", "prompt": "What does the author suggest?", "options": [ { "id": "a", "text": "Option A" }, { "id": "b", "text": "Option B" } ], "solution": "a" } ] }, { "id": "uuid", "type": "trueFalse", "prompt": "Are these statements true, false, or not given?", "questions": [ { "id": "uuid", "prompt": "Statement X", "solution": "TRUE" } ] }, { "id": "uuid", "type": "fillBlanks", "prompt": "Complete the summary", "text": "The study found that ___ were ___", "solutions": [{ "id": "1", "solution": "researchers" }], "words": [{ "letter": "A", "word": "researchers" }], "allowRepetition": false }, { "id": "uuid", "type": "writeBlanks", "prompt": "Complete with no more than 3 words", "maxWords": 3, "text": "Scientists discovered ___", "solutions": [{ "id": "1", "solution": ["the answer", "an answer"] }] }, { "id": "uuid", "type": "matchSentences", "prompt": "Match the headings", "variant": "HEADING", "options": [{ "id": "i", "sentence": "Heading A" }], "sentences": [{ "id": "1", "sentence": "Paragraph 1 summary", "solution": "i" }] } ] } ] ``` **Listening exam `parts` example:** ```json [ { "script": [ { "name": "Speaker A", "gender": "male", "text": "Hello..." }, { "name": "Speaker B", "gender": "female", "text": "Hi..." } ], "exercises": [ { "id": "uuid", "type": "multipleChoice", "prompt": "Choose the correct answer", "questions": [ { "id": "uuid", "prompt": "What is the main topic?", "options": [{ "id": "a", "text": "Weather" }], "solution": "a" } ] }, { "id": "uuid", "type": "writeBlanks", "prompt": "Complete the form", "maxWords": 2, "variant": "FORM", "questions": [ { "id": "uuid", "prompt": "Name: ___", "solution": ["John Smith"] } ] } ] } ] ``` **Writing exam `parts` example:** ```json [ { "exercises": [ { "id": "uuid", "type": "writing", "task": 1, "prompt": "Describe the chart below...", "attachment": "url-to-image-or-null" } ] }, { "exercises": [ { "id": "uuid", "type": "writing", "task": 2, "prompt": "Some people believe that..." } ] } ] ``` **Speaking exam `parts` example:** ```json [ { "exercises": [ { "id": "uuid", "type": "interactiveSpeaking", "task": 1, "prompts": [ "Do you enjoy reading?", "What kind of books do you prefer?" ] } ] }, { "exercises": [ { "id": "uuid", "type": "speaking", "task": 2, "text": "Describe a place you have visited..." } ] } ] ``` **Level exam `parts` example:** ```json [ { "text": { "title": "Passage", "content": "Read the following..." }, "exercises": [ { "id": "uuid", "type": "multipleChoice", "prompt": "Answer the questions", "questions": [ { "id": "uuid", "prompt": "What is correct?", "variant": "text", "options": [{ "id": "a", "text": "Option A" }], "solution": "a" } ] }, { "id": "uuid", "type": "fillBlanks", "variant": "mc", "prompt": "Fill in the blanks", "text": "She ___ to school every day", "solutions": [{ "id": "1", "solution": "goes" }], "words": [{ "letter": "A", "word": "goes" }] } ] } ] ``` ### 3.7 `encoach.assignment` **Source:** MongoDB `assignments` collection | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | Char | Yes | Assignment name | | `assigner_id` | Many2one(`res.users`) | Yes | Creator (teacher/admin) | | `assignee_ids` | Many2many(`res.users`) | Yes | Assigned students | | `teacher_ids` | Many2many(`res.users`) | No | Supervising teachers | | `exam_ids` | Many2many(`encoach.exam`) | Yes | Exams included | | `instructor_gender` | Selection | No | `male` or `female` | | `start_date` | Datetime | No | When assignment becomes available | | `end_date` | Datetime | No | Assignment deadline | | `auto_start` | Boolean | No | Auto-start at start_date. Default: `False` | | `started` | Boolean | No | Whether assignment has been started. Default: `False` | | `released` | Boolean | No | Whether results are released. Default: `False` | | `archived` | Boolean | No | Archived status. Default: `False` | | `entity_id` | Many2one(`encoach.entity`) | No | Owning entity | | `results` | Text (JSON) | No | JSON object of student results | | `legacy_id` | Char | No | Original MongoDB ID | ### 3.8 `encoach.session` **Source:** MongoDB `sessions` collection | Field | Type | Required | Description | |-------|------|----------|-------------| | `user_id` | Many2one(`res.users`) | Yes | Student | | `exam_id` | Many2one(`encoach.exam`) | No | Exam being taken | | `assignment_id` | Many2one(`encoach.assignment`) | No | Related assignment (if any) | | `start_time` | Datetime | Yes | Session start | | `end_time` | Datetime | No | Session end | | `status` | Selection | Yes | `in_progress`, `completed`, `abandoned` | | `legacy_id` | Char | No | Original MongoDB ID | ### 3.9 `encoach.stat` **Source:** MongoDB `stats` collection | Field | Type | Required | Description | |-------|------|----------|-------------| | `user_id` | Many2one(`res.users`) | Yes | Student | | `exam_id` | Many2one(`encoach.exam`) | Yes | Exam attempted | | `exercise_id` | Char | Yes | Exercise UUID within the exam | | `session_id` | Many2one(`encoach.session`) | Yes | Parent session | | `assignment_id` | Many2one(`encoach.assignment`) | No | Related assignment | | `date` | Datetime | Yes | Attempt timestamp | | `module` | Selection | Yes | `reading`, `listening`, `writing`, `speaking`, `level` | | `solutions` | Text (JSON) | No | JSON of user's answers | | `exercise_type` | Char | No | Exercise type string | | `time_spent` | Integer | No | Seconds spent | | `inactivity` | Integer | No | Seconds of inactivity | | `score` | Float | No | Score achieved | | `is_disabled` | Boolean | No | Whether stat is disabled. Default: `False` | | `shuffle_maps` | Text (JSON) | No | JSON mapping of shuffled question order | | `pdf` | Binary | No | Generated PDF report | | `is_practice` | Boolean | No | Practice attempt flag. Default: `False` | | `legacy_id` | Char | No | Original MongoDB ID | ### 3.10 `encoach.evaluation` **Source:** MongoDB `evaluation` collection | Field | Type | Required | Description | |-------|------|----------|-------------| | `user_id` | Many2one(`res.users`) | Yes | Student being evaluated | | `session_id` | Many2one(`encoach.session`) | Yes | Parent session | | `exercise_id` | Char | Yes | Exercise UUID | | `eval_type` | Selection | Yes | `writing`, `speaking`, `speaking_interactive` | | `task` | Integer | Yes | Task number (1, 2, or 3) | | `status` | Selection | Yes | `pending`, `in_progress`, `completed`, `error`. Default: `pending` | | `result` | Text (JSON) | No | Grading result JSON (see Section 3.10.1) | | `error` | Text | No | Error message if grading failed | | `legacy_id` | Char | No | Original MongoDB ID | #### 3.10.1 Evaluation `result` JSON Structure **Writing result:** ```json { "comment": "Overall assessment of the student's writing...", "overall": 6.5, "task_response": { "Task Achievement": { "grade": 6.0, "comment": "..." }, "Coherence and Cohesion": { "grade": 7.0, "comment": "..." }, "Lexical Resource": { "grade": 6.5, "comment": "..." }, "Grammatical Range and Accuracy": { "grade": 6.5, "comment": "..." } }, "perfect_answer": "A model answer text...", "fixed_text": "The student's text with corrections...", "ai_detection": { "probability": 0.12 } } ``` **Speaking result:** ```json { "comment": "Overall assessment of the student's speaking...", "overall": 7.0, "task_response": { "Fluency and Coherence": { "grade": 7.0, "comment": "..." }, "Lexical Resource": { "grade": 7.0, "comment": "..." }, "Grammatical Range and Accuracy": { "grade": 6.5, "comment": "..." }, "Pronunciation": { "grade": 7.5, "comment": "..." } }, "transcript": "Transcribed student speech...", "fixed_text": "Corrected transcript...", "perfect_answer": "A model answer...", "solutions": ["url-to-audio-file"] } ``` ### 3.11 `encoach.package` **Source:** MongoDB `packages` collection | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | Char | No | Display name | | `currency` | Char | Yes | Currency code (e.g., `USD`, `SAR`) | | `duration` | Integer | Yes | Subscription duration value | | `duration_unit` | Selection | Yes | `days`, `months`, `years` | | `price` | Float | Yes | Package price | | `active` | Boolean | No | Whether package is available. Default: `True` | | `legacy_id` | Char | No | Original MongoDB ID | ### 3.12 `encoach.payment` **Source:** MongoDB `payments` collection (corporate/agent payments) | Field | Type | Required | Description | |-------|------|----------|-------------| | `corporate_id` | Many2one(`res.users`) | Yes | Corporate user | | `entity_id` | Many2one(`encoach.entity`) | No | Related entity | | `agent_id` | Many2one(`res.users`) | No | Sales agent | | `agent_commission` | Float | No | Agent commission percentage | | `agent_value` | Float | No | Agent commission amount | | `currency` | Char | Yes | Currency code | | `value` | Float | Yes | Payment amount | | `is_paid` | Boolean | No | Payment status. Default: `False` | | `date` | Datetime | Yes | Payment date | | `corporate_transfer` | Char | No | Corporate bank transfer reference | | `commission_transfer` | Char | No | Commission transfer reference | | `legacy_id` | Char | No | Original MongoDB ID | ### 3.13 `encoach.subscription.payment` **Source:** MongoDB `paypalpayments` collection (individual Stripe/PayPal/Paymob payments) | Field | Type | Required | Description | |-------|------|----------|-------------| | `user_id` | Many2one(`res.users`) | Yes | Paying user | | `provider` | Selection | Yes | `stripe`, `paypal`, `paymob` | | `order_id` | Char | Yes | Provider's order/session ID | | `status` | Selection | Yes | `pending`, `completed`, `failed` | | `value` | Float | Yes | Amount paid | | `currency` | Char | Yes | Currency code | | `subscription_duration` | Integer | No | Duration value | | `subscription_duration_unit` | Selection | No | `days`, `months`, `years` | | `subscription_expiration_date` | Datetime | No | New expiry date after payment | | `created_at` | Datetime | Yes | Payment timestamp | | `checkout_data` | Text (JSON) | No | Raw provider checkout data | | `legacy_id` | Char | No | Original MongoDB ID | ### 3.14 `encoach.ticket` **Source:** MongoDB `tickets` collection | Field | Type | Required | Description | |-------|------|----------|-------------| | `subject` | Char | Yes | Ticket subject | | `description` | Text | Yes | Ticket description | | `status` | Selection | Yes | `open`, `in_progress`, `resolved`, `closed`. Default: `open` | | `ticket_type` | Char | No | Ticket category | | `reporter_id` | Many2one(`res.users`) | No | Reporting user (null for public tickets) | | `reported_from` | Char | No | Source (`platform`, `landing_page`) | | `assigned_to_id` | Many2one(`res.users`) | No | Assigned support agent | | `exam_information` | Text (JSON) | No | Related exam context | | `date` | Datetime | Yes | Creation date | | `legacy_id` | Char | No | Original MongoDB ID | ### 3.15 `encoach.code` **Source:** MongoDB `codes` collection | Field | Type | Required | Description | |-------|------|----------|-------------| | `code` | Char | Yes | Registration code string (unique) | | `creator_id` | Many2one(`res.users`) | No | User who created the code | | `entity_id` | Many2one(`encoach.entity`) | No | Entity this code belongs to | | `code_type` | Selection | No | Code type category | | `expiry_date` | Datetime | No | When the code expires | | `creation_date` | Datetime | Yes | When the code was created | | `user_id` | Many2one(`res.users`) | No | User who redeemed the code | | `email` | Char | No | Email associated with this code | | `name` | Char | No | Name associated with this code | | `passport_id` | Char | No | Passport ID associated | | `checkout` | Text (JSON) | No | Stripe checkout session data | | `legacy_id` | Char | No | Original MongoDB ID | ### 3.16 `encoach.invite` **Source:** MongoDB `invites` collection | Field | Type | Required | Description | |-------|------|----------|-------------| | `entity_id` | Many2one(`encoach.entity`) | Yes | Entity being invited to | | `from_user_id` | Many2one(`res.users`) | Yes | Inviter | | `to_user_id` | Many2one(`res.users`) | Yes | Invitee | | `status` | Selection | Yes | `pending`, `accepted`, `declined`. Default: `pending` | | `created_at` | Datetime | Yes | Invite creation date | | `legacy_id` | Char | No | Original MongoDB ID | ### 3.17 `encoach.permission` **Source:** MongoDB `permissions` collection | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | Char | Yes | Permission display name | | `permission_type` | Char | Yes | Permission type key | | `topic` | Char | No | Permission topic/category | | `user_ids` | Many2many(`res.users`) | No | Users granted this permission | | `legacy_id` | Char | No | Original MongoDB ID | ### 3.18 `encoach.discount` **Source:** MongoDB `discounts` collection | Field | Type | Required | Description | |-------|------|----------|-------------| | `code` | Char | Yes | Discount code string | | `percentage` | Float | Yes | Discount percentage (0-100) | | `domain` | Char | No | Domain restriction | | `valid_until` | Datetime | No | Expiry date | | `active` | Boolean | No | Default: `True` | | `legacy_id` | Char | No | Original MongoDB ID | ### 3.19 `encoach.training` **Source:** MongoDB `training` collection | Field | Type | Required | Description | |-------|------|----------|-------------| | `user_id` | Many2one(`res.users`) | Yes | Student | | `created_at` | Datetime | Yes | Training generation date | | `exams` | Text (JSON) | No | JSON array of exam performance summaries | | `tips` | Text (JSON) | No | JSON object with categorized tips | | `weak_areas` | Text (JSON) | No | JSON array: `[{ "area": "...", "comment": "..." }]` | | `legacy_id` | Char | No | Original MongoDB ID | ### 3.20 `encoach.walkthrough` **Source:** MongoDB `walkthrough` collection | Field | Type | Required | Description | |-------|------|----------|-------------| | `user_id` | Many2one(`res.users`) | Yes | User | | `state` | Text (JSON) | No | JSON object tracking walkthrough progress | | `legacy_id` | Char | No | Original MongoDB ID | ### 3.21 `encoach.approval.workflow` **Source:** MongoDB `configured-workflows` and `active-workflows` collections | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | Char | Yes | Workflow name | | `workflow_type` | Selection | Yes | `configured`, `active` | | `config` | Text (JSON) | No | Workflow configuration | | `status` | Selection | No | `pending`, `approved`, `rejected` | | `entity_id` | Many2one(`encoach.entity`) | No | Related entity | | `legacy_id` | Char | No | Original MongoDB ID | --- ## 4. REST API Specification All endpoints must return JSON. The `encoach_api` module implements Odoo JSON controllers (inheriting `http.Controller`) with `type='json'` or `type='http'` routes. **Base URL:** `https://{odoo-host}/api/` **Authentication:** All endpoints require a valid session/JWT unless marked `[PUBLIC]`. **Response format convention:** Match the current frontend expectations. All responses are JSON objects. Lists are returned as arrays. Errors use HTTP status codes with `{ "error": "message" }`. ### 4.1 Authentication Endpoints #### `POST /api/login` Login with email and password. Returns user object and sets session. **Request:** ```json { "email": "user@example.com", "password": "secret" } ``` **Response (200):** ```json { "id": 1, "email": "user@example.com", "name": "John Doe", "type": "student", "status": "active", "profilePicture": "/defaultAvatar.png", "isFirstLogin": false, "isVerified": true, "focus": "academic", "levels": { "reading": 6.5, "listening": 7.0, "writing": 6.0, "speaking": 6.5 }, "desiredLevels": { "reading": 9, "listening": 9, "writing": 9, "speaking": 9 }, "subscriptionExpirationDate": "2026-12-31T00:00:00Z", "registrationDate": "2025-01-15T00:00:00Z", "entities": [{ "id": 5, "role": "student" }], "permissions": [] } ``` **Error (401):** `{ "error": "Invalid credentials" }` #### `POST /api/register` Register a new user. Supports two registration types. **Request (individual):** ```json { "type": "individual", "email": "user@example.com", "password": "secret", "name": "John Doe", "code": "OPTIONAL_REG_CODE" } ``` **Request (corporate):** ```json { "type": "corporate", "email": "corp@example.com", "password": "secret", "name": "Corp Admin", "corporateInformation": { "companyName": "Acme Inc" } } ``` **Response (201):** User object (same shape as login response) **Business logic:** See Section 8.1 for full registration flow. #### `POST /api/logout` Destroy current session. **Response (200):** `{ "ok": true }` #### `GET /api/user` Get current authenticated user. **Response (200):** User object (same shape as login response) #### `POST /api/reset/sendVerification` Send email verification link. **Request:** `{ "email": "user@example.com" }` **Response (200):** `{ "ok": true }` #### `POST /api/reset/confirm` Reset password via token. **Request:** `{ "token": "reset-token", "newPassword": "new-secret" }` **Response (200):** `{ "ok": true }` ### 4.2 User Endpoints #### `GET /api/users/list` List users linked to the current user (by entity, group, or admin scope). **Query params:** `page`, `limit`, `type`, `entity`, `group`, `search` **Response (200):** ```json { "users": [ { ...userObject } ], "total": 150 } ``` #### `GET /api/users/{id}` Get a single user by ID. **Response (200):** User object #### `GET /api/users/search` Full-text search users by name or email. **Query params:** `q` (search string), `limit` **Response (200):** `[ { ...userObject } ]` #### `PATCH /api/users/update` Update user fields. Admins can update other users. Users can update their own profile. **Request:** ```json { "id": 1, "name": "Updated Name", "bio": "New bio", "focus": "general", "levels": { "reading": 7.0 }, "status": "active", "subscriptionExpirationDate": "2027-01-01T00:00:00Z" } ``` **Response (200):** Updated user object #### `DELETE /api/user` Delete the current user's account. **Response (200):** `{ "ok": true }` #### `GET /api/users/balance` Get user's subscription balance/remaining days. **Response (200):** `{ "balance": 45, "expirationDate": "2026-05-01T00:00:00Z" }` #### `GET /api/users/controller` Admin user CRUD controller. **Query params:** `action` (`list`, `create`, `update`, `delete`), relevant fields **Response:** Depends on action #### `POST /api/users/controller` Admin user creation/modification. **Request:** User creation/update payload **Response:** User object or confirmation #### `POST /api/make_user` Admin/corporate create a user (with auto-generated password). **Request:** ```json { "email": "student@example.com", "name": "Student Name", "type": "student", "entity": 5, "group": 10, "expiryDate": "2027-01-01", "passport_id": "AB123456" } ``` **Response (201):** `{ "user": { ...userObject }, "code": { ...codeObject } }` #### `POST /api/batch_users` Bulk import users. Odoo forwards to ielts-be for Firebase-compatible processing, then stores the resulting users. **Request:** `{ "makerID": "admin-id", "users": [ { ...userDTO } ] }` **Response (200):** `{ "ok": true }` **Integration:** Calls ielts-be `POST /api/user/import` (see Section 6). #### `GET /api/users/agents` `[PUBLIC]` List sales agent users. Used by the landing page. **Response (200):** `[ { "id": 1, "name": "Agent", "companyName": "...", "country": "SA" } ]` #### `GET /api/users/agents/{code}` Get agent by country code. **Response (200):** Agent user object ### 4.3 Entity Endpoints #### `GET /api/entities` List entities the current user has access to. **Response (200):** `[ { ...entityObject } ]` #### `POST /api/entities` Create a new entity. **Request:** ```json { "label": "ABC School", "licenses": 100, "expiryDate": "2027-01-01" } ``` **Response (201):** Entity object #### `GET /api/entities/{id}` Get entity by ID with roles and user counts. **Response (200):** Entity object with nested `roles` and `userCount` #### `PATCH /api/entities/{id}` Update entity fields. **Request:** Partial entity object **Response (200):** Updated entity #### `GET /api/entities/{id}/users` List users belonging to an entity. **Query params:** `page`, `limit`, `role` **Response (200):** `{ "users": [...], "total": 50 }` #### `GET /api/entities/{id}/groups` List groups in an entity. **Response (200):** `[ { ...groupObject } ]` ### 4.4 Group Endpoints #### `GET /api/groups` List groups accessible to the current user. **Query params:** `entity` **Response (200):** `[ { ...groupObject, "participantCount": 25 } ]` #### `POST /api/groups` Create a group. **Request:** ```json { "name": "Class A", "entity": 5, "participants": [1, 2, 3] } ``` **Response (201):** Group object #### `GET /api/groups/{id}` Get group with participant list. **Response (200):** Group object with `participants` array #### `PATCH /api/groups/{id}` Update group (add/remove participants, rename). **Request:** Partial group object **Response (200):** Updated group ### 4.5 Role Endpoints #### `GET /api/roles` List roles for an entity. **Query params:** `entity` **Response (200):** `[ { ...roleObject } ]` #### `POST /api/roles` Create a role. **Request:** `{ "label": "Teacher", "entityID": 5, "permissions": { ... }, "isDefault": false }` **Response (201):** Role object #### `GET /api/roles/{id}` Get role details. #### `PATCH /api/roles/{id}` Update role permissions. #### `GET /api/roles/{id}/users` List users with this role. ### 4.6 Permission Endpoints #### `GET /api/permissions` List all permission definitions. **Response (200):** `[ { "id": 1, "type": "exam_create", "topic": "exams", "users": [1,2,3] } ]` #### `POST /api/permissions/bootstrap` Initialize default permissions for an entity. #### `PATCH /api/permissions/{id}` Update a permission (add/remove users). ### 4.7 Invite Endpoints #### `GET /api/invites` List invites for the current user (sent and received). **Response (200):** `[ { ...inviteObject } ]` #### `POST /api/invites` Send an invite. **Request:** `{ "entity": 5, "to": "user@example.com" }` **Response (201):** Invite object #### `POST /api/invites/accept/{id}` Accept an invite. **Response (200):** `{ "ok": true }` **Business logic:** Add user to entity with the default role. #### `POST /api/invites/decline/{id}` Decline an invite. **Response (200):** `{ "ok": true }` ### 4.8 Code Endpoints #### `GET /api/code` List registration codes. **Query params:** `entity`, `creator` **Response (200):** `[ { ...codeObject } ]` #### `POST /api/code` Create a registration code. **Request:** `{ "entity": 5, "expiryDate": "2027-01-01", "type": "student" }` **Response (201):** Code object #### `GET /api/code/{id}` Get code details. #### `DELETE /api/code/{id}` Delete a code. #### `GET /api/code/entities` List codes grouped by entity. ### 4.9 Exam Endpoints #### `GET /api/exam/{module}` List exams for a module. **Path params:** `module` = `reading` | `listening` | `writing` | `speaking` | `level` **Query params:** `entity`, `access`, `difficulty`, `page`, `limit` **Response (200):** `[ { ...examObject (without full parts for listing) } ]` #### `GET /api/exam/{module}/{id}` Get a single exam with full `parts` JSON. **Response (200):** Full exam object including nested exercises #### `POST /api/exam/upload` Upload an exam document for parsing. Proxied to ielts-be. **Request:** Multipart form with file **Response (200):** Parsed exam object **Integration:** Forwards to ielts-be endpoint (see Section 6). #### `POST /api/exam/generate/{module}` Generate exam content using AI. Proxied to ielts-be. **Path params:** `module` = `reading` | `listening` | `writing` | `speaking` | `level` **Request:** Module-specific generation parameters (varies) **Response (200):** Generated content **Integration:** Forwards to ielts-be endpoint (see Section 6). #### `POST /api/exam/media/{module}` Generate media (audio/video) for an exam. Proxied to ielts-be. **Request:** Module-specific media parameters **Response (200):** Media file (audio/video bytes) or URL **Integration:** Forwards to ielts-be endpoint (see Section 6). #### `GET /api/exam/avatars` List available speaking avatars. Proxied to ielts-be. **Response (200):** `[ { "id": "avatar_1", "name": "...", "thumbnail": "..." } ]` #### `POST /api/exam/{module}/import` Import an exam from file. Proxied to ielts-be. **Request:** Multipart form with `exercises` and optional `solutions` files **Response (200):** Parsed exam object. Odoo should then persist the exam to `encoach.exam`. #### `POST /api/exam` (save) Save an exam (new or updated) to the database. **Request:** Full exam object with `parts` JSON **Response (201/200):** Saved exam object with ID ### 4.10 Assignment Endpoints #### `GET /api/assignments` List assignments. **Query params:** `assigner`, `assignee`, `entity`, `archived`, `page`, `limit` **Response (200):** `[ { ...assignmentObject } ]` #### `POST /api/assignments` Create an assignment. **Request:** ```json { "name": "Week 1 Practice", "assignees": [1, 2, 3], "exams": [10, 11], "startDate": "2026-04-01", "endDate": "2026-04-07", "instructorGender": "female", "entity": 5, "teachers": [4], "autoStart": true } ``` **Response (201):** Assignment object #### `GET /api/assignments/{id}` Get assignment details with participant results. #### `PATCH /api/assignments/{id}` Update assignment. #### `POST /api/assignments/{id}/start` Start an assignment (make it available to students). **Response (200):** `{ "ok": true }` #### `POST /api/assignments/{id}/release` Release assignment results to students. **Response (200):** `{ "ok": true }` #### `POST /api/assignments/{id}/archive` Archive an assignment. #### `POST /api/assignments/{id}/unarchive` Unarchive an assignment. #### `GET /api/assignments/corporate` List assignments for corporate dashboard. ### 4.11 Session Endpoints #### `GET /api/sessions` List sessions for a user. **Query params:** `user`, `exam`, `assignment` **Response (200):** `[ { ...sessionObject } ]` #### `POST /api/sessions` Create a new exam session. **Request:** `{ "exam": 10, "assignment": 5 }` **Response (201):** Session object with ID #### `GET /api/sessions/{id}` Get session details. #### `PATCH /api/sessions/{id}` Update session (e.g., mark as completed). ### 4.12 Stats Endpoints #### `GET /api/stats` List stats. **Query params:** `user`, `exam`, `session`, `module`, `assignment`, `page`, `limit` **Response (200):** `[ { ...statObject } ]` #### `POST /api/stats` Create a stat record (exercise attempt result). **Request:** ```json { "user": 1, "exam": 10, "exercise": "uuid-string", "session": 20, "module": "reading", "solutions": { ... }, "type": "multipleChoice", "timeSpent": 120, "inactivity": 5, "score": 0.85, "assignment": 5, "isPractice": false } ``` **Response (201):** Stat object #### `GET /api/stats/{id}` Get a single stat. #### `PATCH /api/stats/{id}` Update a stat (e.g., disable, set PDF). #### `GET /api/stats/user/{userId}` Get all stats for a user. #### `GET /api/stats/session/{sessionId}` Get all stats for a session. #### `PATCH /api/stats/disabled` Bulk disable stats. #### `GET /api/stats/{id}/{exportType}/pdf` Generate a PDF report for a stat. Calls ielts-be for grading summary. **Response:** PDF binary or `{ "url": "..." }` **Integration:** Calls ielts-be `POST /api/exam/grade/summary` for section-level evaluation. ### 4.13 Grading / Evaluation Endpoints #### `POST /api/evaluate/writing` Submit a writing answer for AI grading. Creates an evaluation record and proxies to ielts-be. **Request:** ```json { "userId": 1, "sessionId": 20, "exerciseId": "uuid", "question": "Write about...", "answer": "Student's essay text...", "task": 1, "attachment": "optional-image-url" } ``` **Response (200):** `{ "ok": true }` (grading happens asynchronously) **Business logic:** 1. Create `encoach.evaluation` record with status `pending` 2. Forward request to ielts-be `POST /api/exam/grade/writing/{task}` 3. ielts-be processes asynchronously and updates the evaluation record #### `POST /api/evaluate/speaking` Submit speaking audio for AI grading. **Request:** Multipart form with `userId`, `sessionId`, `exerciseId`, `task`, and audio files (`audio_1`, `audio_2`, etc.) **Response (200):** `{ "ok": true }` (grading happens asynchronously) **Business logic:** Same pattern as writing grading. #### `POST /api/evaluate/interactiveSpeaking` Submit interactive speaking (multiple Q&A pairs). **Request:** Same as speaking but with `question_N` and `audio_N` pairs. **Response (200):** `{ "ok": true }` #### `GET /api/evaluate/{sessionId}/{exerciseId}` Poll for evaluation result. **Response (200):** ```json { "status": "completed", "result": { ...gradingResult } } ``` #### `POST /api/grading/multiple` Grade multiple short-answer exercises at once. Proxied to ielts-be. **Request:** ```json { "text": "passage text", "questions": ["Q1", "Q2"], "answers": ["A1", "A2"] } ``` **Response (200):** `{ "exercises": [{ "id": "...", "correct": true, "correct_answer": "..." }] }` ### 4.14 Training Endpoints #### `POST /api/training` Generate training content for a user. Proxied to ielts-be. **Request:** ```json { "userID": 1, "stats": [ { ...statSummary } ] } ``` **Response (200):** `{ "id": "training-record-id" }` **Business logic:** Forward to ielts-be; store returned content in `encoach.training`. #### `POST /api/training/tips` Fetch personalized tips. Proxied to ielts-be. **Request:** `{ "context": "...", "question": "...", "answer": "...", "correct_answer": "..." }` **Response (200):** `{ "tips": "..." }` #### `GET /api/training/user/{userId}` Get stored training content for a user. **Response (200):** Training object with tips and weak areas #### `GET /api/training/walkthrough` Get walkthrough state. **Response (200):** `{ "state": { ... } }` #### `PATCH /api/training/walkthrough` Update walkthrough state. **Request:** `{ "state": { ... } }` ### 4.15 Payment Endpoints #### `POST /api/stripe` Create a Stripe checkout session. **Request:** `{ "email": "user@example.com", "days": 30, "key": "package-key" }` **Response (200):** `{ "url": "https://checkout.stripe.com/..." }` **Webhook: `POST /api/stripe/webhook`** Handle Stripe webhook events (payment completed). See Section 7.1. #### `POST /api/paypal` Create a PayPal order. **Request:** `{ "amount": 99.00, "currency": "USD" }` **Response (200):** `{ "orderId": "PAYPAL_ORDER_ID" }` #### `POST /api/paypal/approve` Capture a PayPal payment after customer approval. **Request:** `{ "id": "PAYPAL_ORDER_ID", "duration": 30, "duration_unit": "days", "trackingId": "..." }` **Response (200):** `{ "ok": true }` **Business logic:** See Section 7.2. #### `POST /api/paymob` Create a Paymob payment intention. **Request:** `{ "amount": 500, "currency": "SAR" }` **Response (200):** `{ "clientSecret": "..." }` #### `POST /api/paymob/webhook` Handle Paymob transaction webhook. **Business logic:** See Section 7.3. #### `GET /api/payments` List corporate/agent payments. **Query params:** `corporate`, `entity`, `agent`, `isPaid` **Response (200):** `[ { ...paymentObject } ]` #### `POST /api/payments` Create a corporate payment record. **Request:** Payment object fields **Response (201):** Payment object #### `GET /api/payments/{id}` Get payment details. #### `PATCH /api/payments/{id}` Update payment (mark as paid, update transfers). **Business logic:** When `isPaid` is set to `true`, activate the corporate user's subscription (see Section 8.4). #### `DELETE /api/payments/{id}` Delete a payment record. #### `GET /api/payments/assigned` List payments assigned to an agent. ### 4.16 Package Endpoints #### `GET /api/packages` `[PUBLIC]` List available subscription packages. Used by both the platform and landing page. **Response (200):** `[ { "id": 1, "currency": "USD", "duration": 30, "duration_unit": "days", "price": 29.99 } ]` #### `POST /api/packages` Create a package (admin only). **Request:** Package fields **Response (201):** Package object #### `GET /api/packages/{id}` Get package details. #### `PATCH /api/packages/{id}` Update a package. #### `DELETE /api/packages/{id}` Delete a package. ### 4.17 Discount Endpoints #### `GET /api/discounts` List discount codes. **Response (200):** `[ { ...discountObject } ]` #### `POST /api/discounts` Create a discount code. **Request:** `{ "code": "SAVE20", "percentage": 20, "domain": "encoach.com", "validUntil": "2027-01-01" }` **Response (201):** Discount object #### `DELETE /api/discounts/{id}` Delete a discount code. ### 4.18 Ticket Endpoints #### `GET /api/tickets` List tickets. **Query params:** `status`, `assignedTo`, `reporter`, `page`, `limit` **Response (200):** `[ { ...ticketObject } ]` #### `POST /api/tickets` `[PUBLIC]` Create a ticket. Used by both the platform and landing page contact form. **Request:** ```json { "subject": "Cannot access exam", "description": "When I click...", "type": "bug", "reportedFrom": "platform", "examInformation": { "examId": 10, "module": "reading" } } ``` **Response (201):** Ticket object #### `GET /api/tickets/{id}` Get ticket details. #### `PATCH /api/tickets/{id}` Update ticket (assign, change status). #### `GET /api/tickets/assignedToUser` List tickets assigned to the current support agent. ### 4.19 Storage Endpoints #### `POST /api/storage/insert` Upload a file. Use Odoo's `ir.attachment` system. **Request:** Multipart form with file **Response (200):** `{ "url": "https://odoo-host/web/content/attachment_id" }` #### `GET /api/storage` List files for a context (e.g., user profile pictures, exam attachments). **Query params:** `context`, `user` **Response (200):** `[ { "name": "file.png", "url": "..." } ]` #### `DELETE /api/storage/{id}` Delete a file. ### 4.20 Transcription Endpoint #### `POST /api/transcribe` Transcribe audio. Proxied to ielts-be. **Request:** Multipart form with audio file **Response (200):** Dialog/transcript object ### 4.21 Approval Workflow Endpoints #### `GET /api/approval-workflows` List configured and active approval workflows. #### `POST /api/approval-workflows` Create or configure an approval workflow. #### `PATCH /api/approval-workflows/{id}` Approve or reject a workflow. ### 4.22 Statistical Endpoint #### `GET /api/statistical` Get platform-wide statistics (admin dashboard). Proxied to ielts-be if needed. --- ## 5. Authentication & Authorization ### 5.1 Recommended Approach: Odoo Native Auth with JWT Use Odoo's built-in `res.users` authentication with a JWT token layer for the SPA frontend. **Flow:** 1. Frontend sends `POST /api/login` with email + password 2. Odoo validates credentials against `res.users` 3. Odoo generates a JWT token (using a library like `PyJWT`) 4. Frontend stores the JWT and sends it as `Authorization: Bearer ` on every request 5. Odoo middleware validates the JWT on each request and loads the user context **JWT payload:** ```json { "uid": 1, "email": "user@example.com", "type": "student", "exp": 1735689600 } ``` **JWT secret:** Configured via Odoo system parameter `encoach.jwt_secret`. **Token expiry:** 24 hours (configurable). Frontend should handle token refresh. ### 5.2 Alternative: Firebase Token Validation If Firebase Auth migration is too disruptive: 1. Keep Firebase Auth for identity management 2. Frontend authenticates with Firebase, gets Firebase ID token 3. Odoo validates Firebase token using Google's public keys 4. Odoo maps Firebase UID to `res.users` via `legacy_id` field This approach requires maintaining Firebase Auth as a dependency. ### 5.3 Role-Based Access Control Map the 7 user types to Odoo security groups: | User Type | Odoo Group | Access Level | |-----------|------------|-------------| | `student` | `encoach_group_student` | Read own data, take exams, view own stats | | `teacher` | `encoach_group_teacher` | Manage classrooms, assignments, view student stats in their groups | | `corporate` | `encoach_group_corporate` | Manage entity, create users, view entity-wide data | | `admin` | `encoach_group_admin` | Full platform access | | `developer` | `encoach_group_developer` | Full access + system tools | | `agent` | `encoach_group_agent` | View assigned payments, agent dashboard | | `mastercorporate` | `encoach_group_mastercorporate` | Multi-entity corporate management | **Record rules (`ir.rule`):** - Students see only their own stats, sessions, evaluations, training - Teachers see stats/sessions for students in their groups - Corporate users see data for their entity - Admins see all data - Agents see their assigned payments ### 5.4 API-Level Authorization Each endpoint should validate the user's type/group before processing: - Endpoints that create entities, manage users globally, or access admin dashboards require `admin` or `developer` type - Endpoints that manage classrooms and assignments require `teacher`, `corporate`, or `admin` type - Endpoints that create payments require `corporate`, `agent`, or `admin` type - Public endpoints (packages, tickets/create, agents) require no authentication --- ## 6. ielts-be Integration Specification Odoo communicates with the ielts-be microservice over HTTP. The ielts-be service is unchanged and expects a Bearer JWT token for authentication. ### 6.1 Configuration | Odoo System Parameter | Description | |----------------------|-------------| | `encoach.ielts_be_url` | Base URL of ielts-be service (e.g., `http://ielts-be:8000/api`) | | `encoach.ielts_be_jwt` | JWT token for authenticating with ielts-be | ### 6.2 HTTP Client Implement a service class `EncoachAIClient` that: - Uses `requests` or `httpx` for HTTP calls - Sets `Authorization: Bearer {jwt}` header on all requests - Handles timeouts (30s for sync, 120s for generation endpoints) - Handles errors and retries - Logs all requests/responses for debugging ### 6.3 Endpoint Mapping #### Exam Generation | Odoo calls | ielts-be endpoint | Method | Request | Response | |-----------|-------------------|--------|---------|----------| | Generate reading passage | `GET /api/exam/reading/{passage}` | GET | Query: `topic`, `word_count` | `{ "title": "...", "text": "..." }` | | Generate reading exercises | `POST /api/exam/reading/` | POST | `ReadingDTO` JSON | `{ "exercises": [...] }` | | Generate listening dialog | `GET /api/exam/listening/{section}` | GET | Query: `difficulty`, `topic` | `{ "dialog": {...} }` | | Generate listening MP3 | `POST /api/exam/listening/media` | POST | `Dialog` JSON | MP3 binary | | Transcribe audio | `POST /api/exam/listening/transcribe` | POST | Multipart: `audio` file | Dialog object | | Generate listening instructions | `POST /api/exam/listening/instructions` | POST | `{ "text": "..." }` | MP3 binary | | Generate listening exercises | `POST /api/exam/listening/` | POST | `ListeningExercisesDTO` JSON | `{ "exercises": [...] }` | | Get speaking task | `GET /api/exam/speaking/{task}` | GET | Query: `topic`, `difficulty` | Speaking content | | Generate speaking video | `POST /api/exam/speaking/media` | POST | `{ "text": "...", "avatar": "..." }` | `{ "status": "...", "result": "..." }` | | Poll speaking video | `GET /api/exam/speaking/media/{vid_id}` | GET | - | `{ "status": "...", "result": "url" }` | | Get speaking avatars | `GET /api/exam/speaking/avatars` | GET | - | Avatars list | | Get writing task | `GET /api/exam/writing/{task}` | GET | Query: `difficulty`, `topic` | Writing prompt | | Get writing task (academic) | `POST /api/exam/writing/{task}/attachment` | POST | Multipart: `file`, `difficulty` | Writing prompt with attachment | | Generate level exercises | `POST /api/exam/level/` | POST | `LevelExercisesDTO` JSON | Generated exercises | | Get level exam | `GET /api/exam/level/` | GET | - | Level exam | | Get UTAS level exam | `GET /api/exam/level/utas` | GET | - | UTAS level exam | | Import reading exam | `POST /api/exam/reading/import` | POST | Multipart: `exercises`, `solutions` | Parsed exam | | Import listening exam | `POST /api/exam/listening/import` | POST | Multipart: `exercises`, `solutions` | Parsed exam + dialog | | Import level exam | `POST /api/exam/level/import/` | POST | Multipart: `exercises`, `solutions` | Parsed level exam | | Custom level exam | `POST /api/exam/level/custom/` | POST | JSON body | Custom level exam | #### Grading | Odoo calls | ielts-be endpoint | Method | Request | Response | Async? | |-----------|-------------------|--------|---------|----------|--------| | Grade writing | `POST /api/exam/grade/writing/{task}` | POST | `WritingGradeTaskDTO` JSON | `200 OK` | Yes | | Grade speaking | `POST /api/exam/grade/speaking/{task}` | POST | Multipart form | `200 OK` | Yes | | Grade short answers | `POST /api/exam/grade/short_answers` | POST | JSON | `{ "exercises": [...] }` | No | | Grading summary | `POST /api/exam/grade/summary` | POST | `{ "sections": [...] }` | `{ "sections": [...] }` | No | **Async grading flow:** 1. Odoo creates `encoach.evaluation` record with status `pending` 2. Odoo sends grading request to ielts-be with `userId`, `sessionId`, `exerciseId` 3. ielts-be processes asynchronously and writes result directly to the shared database (current pattern) OR calls back to Odoo (new pattern) 4. Frontend polls `GET /api/evaluate/{sessionId}/{exerciseId}` until status is `completed` **Important decision:** Currently ielts-be writes directly to the same MongoDB. With Odoo on PostgreSQL, one of these approaches is needed: - **Option A (recommended):** ielts-be calls an Odoo webhook `POST /api/evaluate/callback` to update the evaluation record when grading is done. Minimal change to ielts-be -- just change where it writes the result. - **Option B:** Odoo polls ielts-be for grading status. Requires ielts-be to expose a status endpoint. - **Option C:** ielts-be writes to a shared message queue (Redis, RabbitMQ) and Odoo consumes it. #### Training | Odoo calls | ielts-be endpoint | Method | Request | Response | |-----------|-------------------|--------|---------|----------| | Get training content | `POST /api/training/` | POST | Training request JSON | `{ "id": "..." }` | | Get tips | `POST /api/training/tips` | POST | `FetchTipsDTO` JSON | Tips text | #### User Import | Odoo calls | ielts-be endpoint | Method | Request | Response | |-----------|-------------------|--------|---------|----------| | Batch import users | `POST /api/user/import` | POST | `BatchUsersDTO` JSON | `{ "ok": true }` | **Note:** With the migration away from Firebase Auth, the batch user import flow will need to be redesigned. Instead of importing to Firebase, users should be created directly in Odoo's `res.users`. The ielts-be batch import endpoint may become unnecessary unless Firebase Auth is retained (Option B in Section 5). --- ## 7. Payment Integration ### 7.1 Stripe **Odoo modules:** `payment`, `payment_stripe` (standard), or custom controller. **Checkout flow:** 1. Frontend calls `POST /api/stripe` with `{ email, days, key, checkout }` 2. Odoo creates a Stripe Checkout session via Stripe API 3. Returns checkout URL to frontend 4. Customer completes payment on Stripe 5. Stripe sends webhook to `POST /api/stripe/webhook` **Webhook handling (`POST /api/stripe/webhook`):** 1. Verify Stripe signature 2. On `checkout.session.completed`: - If user exists with that email: extend `subscription_expiration_date` by `days` - If user does not exist: create/update `encoach.code` with the email and expiry info, send registration email 3. Create `encoach.subscription.payment` record ### 7.2 PayPal **Create order (`POST /api/paypal`):** 1. Call PayPal API to create order 2. Return order ID to frontend **Capture payment (`POST /api/paypal/approve`):** 1. Call PayPal API to capture order 2. On success: - Find user by tracking ID - Calculate new expiry: `max(current_expiry, now) + duration` - Update `subscription_expiration_date` and set `status = active` - Create `encoach.subscription.payment` record - For corporate users: propagate subscription to all entity participants ### 7.3 Paymob **Create intention (`POST /api/paymob`):** 1. Call Paymob API to create payment intention 2. Return client secret to frontend **Webhook (`POST /api/paymob/webhook`):** 1. Verify Paymob transaction signature 2. On success: - Find user from transaction metadata - Extend subscription (same logic as PayPal) - Create `encoach.subscription.payment` record - For entity payments: update entity `expiry_date` and propagate to entity users ### 7.4 Corporate Payment Flow Corporate payments are manually tracked (bank transfers, not online): 1. Admin/agent creates `encoach.payment` record with corporate, entity, agent details 2. When payment is confirmed (`is_paid` set to `true`): - Set corporate user's `status` to `active` - Optionally extend entity expiry --- ## 8. Business Rules & Workflows ### 8.1 Registration Flow #### Individual Registration (with or without code) 1. Validate email is not already registered 2. Create `res.users` with `encoach_type = 'student'`, `encoach_status = 'active'` 3. Set defaults: `is_first_login = True`, `is_verified = False`, `focus = 'academic'`, levels all 0, desired levels all 9 4. If a registration `code` is provided: a. Look up `encoach.code` by code string b. Validate code is not expired and not already used c. Set user's `subscription_expiration_date` from code's `expiry_date` d. If code has an `entity_id`: add user to that entity with the default role e. Mark code as used (`user_id` = new user) 5. Send verification email #### Corporate Registration 1. Validate email is not already registered 2. Create `res.users` with `encoach_type = 'corporate'` 3. Create `encoach.entity` for the corporate 4. Create 3 default `encoach.group` records under the entity: - "Teachers" (admin = corporate user) - "Students" (admin = corporate user) - "Corporate" (admin = corporate user) 5. Add the corporate user to the entity with an admin role ### 8.2 Subscription Management **Status transitions:** ``` payment received payment_due ────────────────────────> active ▲ │ │ │ subscription expired │ ▼ └──────────────────────────── disabled (can be reactivated) ``` **Automated checks (cron job):** - Run daily: check all users where `subscription_expiration_date < now()` - Set `encoach_status = 'disabled'` for expired users - Optionally send expiry warning emails 7 days before expiry **Subscription extension logic:** ```python new_expiry = max(current_expiry or now(), now()) + timedelta(days=days) ``` ### 8.3 Entity Licensing - Entities have a `licenses` count (max users) - When adding a user to an entity, check: `current_user_count < entity.licenses` - If limit reached, reject the addition with an error - Entity `expiry_date` is separate from individual user subscriptions - When entity expires, all entity users lose access (optional business rule) ### 8.4 Assignment Lifecycle ``` Created ──> Started ──> In Progress ──> Released ──> Archived │ └──> Unarchived ──> Released ``` 1. **Created:** Teacher creates assignment with exams and assignees 2. **Started:** `auto_start` triggers at `start_date`, or teacher manually calls `/start` 3. **In Progress:** Students can take the exams. Stats are recorded 4. **Released:** Teacher calls `/release`. Students can view their results 5. **Archived:** Teacher calls `/archive`. Assignment hidden from active lists ### 8.5 Grading Polling 1. Student submits writing/speaking answer 2. Frontend calls `POST /api/evaluate/writing` or `/speaking` 3. Odoo creates `encoach.evaluation` with `status = 'pending'` 4. Odoo forwards to ielts-be 5. Frontend polls `GET /api/evaluate/{sessionId}/{exerciseId}` every 3-5 seconds 6. When ielts-be finishes (via callback or direct DB update), evaluation status becomes `completed` 7. Frontend receives the grading result and displays scores + feedback ### 8.6 Corporate Payment Activation 1. Corporate user or agent creates payment record 2. Admin verifies bank transfer received 3. Admin updates payment: `is_paid = true` 4. System sets corporate user's `status = 'active'` 5. System extends `subscription_expiration_date` for the corporate and optionally all entity members ### 8.7 User Change Propagation When a user's entity membership or subscription changes, propagate to related records: - When a user is added to an entity: auto-add to the entity's default group (if configured) - When a corporate's subscription is extended: optionally extend all participants' subscriptions - When an entity's expiry is extended: optionally extend all entity users' subscriptions --- ## 9. Data Migration Plan ### 9.1 Migration Strategy Perform a one-time data migration from MongoDB + Firebase Auth to Odoo 19 (PostgreSQL). ### 9.2 Collection-to-Model Mapping | MongoDB Collection | Odoo Model | Notes | |-------------------|------------|-------| | `users` | `res.users` + `res.partner` | Map `id` (Firebase UID) to `legacy_id`. Create Odoo-native passwords. | | `entities` | `encoach.entity` | Direct mapping | | `roles` | `encoach.role` | Direct mapping | | `groups` | `encoach.group` | Resolve `admin` and `participants` to Odoo user IDs via `legacy_id` | | `reading` | `encoach.exam` (module=reading) | `parts` stored as JSON | | `listening` | `encoach.exam` (module=listening) | `parts` stored as JSON | | `writing` | `encoach.exam` (module=writing) | `parts` stored as JSON | | `speaking` | `encoach.exam` (module=speaking) | `parts` stored as JSON | | `level` | `encoach.exam` (module=level) | `parts` stored as JSON | | `assignments` | `encoach.assignment` | Resolve user references | | `sessions` | `encoach.session` | Resolve user and exam references | | `stats` | `encoach.stat` | Resolve user, exam, session references | | `evaluation` | `encoach.evaluation` | Resolve user, session references | | `packages` | `encoach.package` | Direct mapping | | `payments` | `encoach.payment` | Resolve user references | | `paypalpayments` | `encoach.subscription.payment` | Resolve user references | | `tickets` | `encoach.ticket` | Resolve user references | | `codes` | `encoach.code` | Resolve user, entity references | | `invites` | `encoach.invite` | Resolve user, entity references | | `permissions` | `encoach.permission` | Resolve user references | | `discounts` | `encoach.discount` | Direct mapping | | `training` | `encoach.training` | Resolve user references | | `walkthrough` | `encoach.walkthrough` | Resolve user references | | `active-workflows` | `encoach.approval.workflow` | Direct mapping | | `configured-workflows` | `encoach.approval.workflow` | Direct mapping | ### 9.3 Migration Script Requirements 1. **Export MongoDB data** to JSON files (one per collection) 2. **Resolve references:** Build a mapping of `MongoDB _id / legacy_id -> Odoo ID` as records are imported 3. **Import order:** Users first (to resolve references), then entities, roles, groups, exams, then dependent records 4. **Password migration:** - If using Odoo native auth: generate temporary passwords and force password reset on first login - If keeping Firebase: no password migration needed (users authenticate via Firebase) 5. **File migration:** Migrate files from Firebase Storage to Odoo `ir.attachment` or a configured file storage 6. **Validation:** After import, run integrity checks: - All Many2one references resolve to existing records - User counts match between MongoDB and PostgreSQL - Exam `parts` JSON is valid and parseable - Subscription dates are preserved correctly ### 9.4 Suggested Import Order 1. `res.users` / `res.partner` (from `users`) 2. `encoach.entity` (from `entities`) 3. `encoach.role` (from `roles`) 4. `encoach.user.entity.rel` (from `users.entities` array) 5. `encoach.group` (from `groups`) 6. `encoach.exam` (from `reading`, `listening`, `writing`, `speaking`, `level`) 7. `encoach.package` (from `packages`) 8. `encoach.discount` (from `discounts`) 9. `encoach.code` (from `codes`) 10. `encoach.permission` (from `permissions`) 11. `encoach.assignment` (from `assignments`) 12. `encoach.session` (from `sessions`) 13. `encoach.stat` (from `stats`) 14. `encoach.evaluation` (from `evaluation`) 15. `encoach.payment` (from `payments`) 16. `encoach.subscription.payment` (from `paypalpayments`) 17. `encoach.ticket` (from `tickets`) 18. `encoach.invite` (from `invites`) 19. `encoach.training` (from `training`) 20. `encoach.walkthrough` (from `walkthrough`) 21. `encoach.approval.workflow` (from workflows) --- ## 10. Non-Functional Requirements ### 10.1 API Response Format All API responses must match the JSON structure the frontend currently expects. The frontend uses Axios and SWR to consume these APIs. - Success responses: HTTP 200/201 with JSON body - Error responses: HTTP 4xx/5xx with `{ "error": "Human-readable message" }` - List responses should support pagination: `{ "items": [...], "total": N }` or flat arrays depending on the current endpoint behavior - Empty results: return `[]` for lists, `null` or `{}` for single objects ### 10.2 CORS Configuration The following origins need CORS access: | Origin | Endpoints | |--------|-----------| | `https://platform.encoach.com` | All authenticated endpoints | | `https://encoach.com` | `GET /api/packages`, `POST /api/tickets`, `GET /api/users/agents` | | `http://localhost:3000` | All (development) | CORS headers must include: - `Access-Control-Allow-Origin` (per origin, not `*`) - `Access-Control-Allow-Credentials: true` - `Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS` - `Access-Control-Allow-Headers: Content-Type, Authorization` ### 10.3 File Storage Replace Firebase Storage with Odoo's `ir.attachment` system or configure an external storage backend (S3, GCS). **Files stored:** - User profile pictures - Exam audio files (listening MP3s) - Exam image attachments (writing Task 1 charts) - Speaking video files - PDF reports **Requirements:** - Files must be accessible via URL (for the frontend to load) - Support for audio (MP3), image (PNG/JPG), video (MP4), and PDF formats - Maximum file size: 50 MB (audio/video), 10 MB (images), 5 MB (PDFs) ### 10.4 Performance Requirements | Operation | Target Response Time | |-----------|---------------------| | Login / Auth | < 500ms | | User list (paginated) | < 1s | | Exam load (full JSON) | < 2s | | Stat creation | < 500ms | | Evaluation poll | < 300ms | | Package list (public) | < 500ms | | Ticket creation | < 500ms | | File upload | < 5s (for files < 10 MB) | **Grading latency** is handled by ielts-be and is inherently slow (10-30s for writing, 30-60s for speaking). The polling pattern handles this. ### 10.5 Scalability - Support at least 10,000 concurrent users - Support 500,000+ stat records per exam module - Efficient pagination for all list endpoints - JSON fields (exam `parts`, solutions) indexed appropriately in PostgreSQL ### 10.6 Security - All API communication over HTTPS in production - JWT tokens with short expiry (24h) and secure signing - Rate limiting on login endpoint (max 10 attempts per minute per IP) - Input validation on all endpoints (prevent SQL injection, XSS) - Stripe/PayPal/Paymob webhook signature verification - No sensitive data in JWT payload (no passwords, no payment details) ### 10.7 Logging & Monitoring - Log all API requests with timestamp, user, endpoint, response status - Log all ielts-be integration calls with request/response - Log all payment events - Error alerting for failed grading callbacks, payment failures ### 10.8 Testing - Unit tests for all business logic (registration, subscription, payments) - Integration tests for all API endpoints - End-to-end tests for critical flows (login -> take exam -> get graded) - Load tests for concurrent exam-taking scenarios --- ## Appendix A: Exercise Type Reference Complete list of exercise types used across all exam modules. ### Reading Exercise Types | Type | Fields | Description | |------|--------|-------------| | `multipleChoice` | `questions[]` with `prompt`, `options[]`, `solution` | Select correct answer | | `trueFalse` | `questions[]` with `prompt`, `solution` (TRUE/FALSE/NOT_GIVEN) | True/False/Not Given | | `fillBlanks` | `text`, `solutions[]`, `words[]`, `allowRepetition` | Drag words into blanks | | `writeBlanks` | `text`, `maxWords`, `solutions[]` | Type answers into blanks | | `matchSentences` (HEADING) | `options[]`, `sentences[]` with `solution` | Match headings to paragraphs | | `matchSentences` (IDEAMATCH) | `options[]`, `sentences[]` with `solution` | Match ideas to paragraphs | ### Listening Exercise Types | Type | Fields | Description | |------|--------|-------------| | `multipleChoice` | `questions[]` | Standard MC | | `multipleChoice3Options` | `questions[]` | MC with 3 options | | `writeBlanks` (FILL) | `text`, `maxWords`, `solutions[]` | Fill in blanks from audio | | `writeBlanks` (FORM) | `questions[]`, `maxWords` | Complete a form from audio | | `writeBlanks` (QUESTIONS) | `questions[]`, `maxWords` | Answer questions from audio | | `trueFalse` | `questions[]` | True/False from audio | ### Writing Exercise Types | Type | Fields | Description | |------|--------|-------------| | `writing` (task 1) | `prompt`, optional `attachment` | Letter or chart description | | `writing` (task 2) | `prompt` | Essay | ### Speaking Exercise Types | Type | Fields | Description | |------|--------|-------------| | `interactiveSpeaking` (part 1) | `prompts[]` | Short Q&A | | `speaking` (part 2) | `text` | Long turn monologue | | `interactiveSpeaking` (part 3) | `prompts[]` | Discussion | ### Level Exercise Types | Type | Fields | Description | |------|--------|-------------| | `multipleChoice` | `questions[]` with `variant` | MC with text/image variants | | `fillBlanks` (mc) | `text`, `solutions[]`, `words[]` | Fill blanks with MC options | --- ## Appendix B: Grading Rubric Details ### Writing Rubric (IELTS Band Descriptors) | Criterion | Scale | Description | |-----------|-------|-------------| | Task Achievement / Task Response | 0-9 | How well the task requirements are addressed | | Coherence and Cohesion | 0-9 | Logical organization and paragraph linking | | Lexical Resource | 0-9 | Vocabulary range and accuracy | | Grammatical Range and Accuracy | 0-9 | Grammar variety and correctness | | **Overall** | 0-9 | Average of the four criteria | ### Speaking Rubric (IELTS Band Descriptors) | Criterion | Scale | Description | |-----------|-------|-------------| | Fluency and Coherence | 0-9 | Flow, pace, logical connection | | Lexical Resource | 0-9 | Vocabulary range and appropriateness | | Grammatical Range and Accuracy | 0-9 | Grammar variety and correctness | | Pronunciation | 0-9 | Clarity, stress, intonation | | **Overall** | 0-9 | Average of the four criteria | --- ## Appendix C: Environment Variables | Variable | Description | Example | |----------|-------------|---------| | `ENCOACH_JWT_SECRET` | JWT signing secret | Random 256-bit string | | `ENCOACH_IELTS_BE_URL` | ielts-be base URL | `http://ielts-be:8000/api` | | `ENCOACH_IELTS_BE_JWT` | JWT for ielts-be auth | Pre-shared token | | `STRIPE_SECRET_KEY` | Stripe secret key | `sk_live_...` | | `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret | `whsec_...` | | `PAYPAL_CLIENT_ID` | PayPal client ID | `AX...` | | `PAYPAL_CLIENT_SECRET` | PayPal client secret | `EL...` | | `PAYPAL_ACCESS_TOKEN_URL` | PayPal OAuth URL | `https://api.paypal.com/v1/oauth2/token` | | `PAYMOB_API_KEY` | Paymob API key | `ZXlK...` | | `PAYMOB_SECRET` | Paymob webhook secret | `...` | --- ## Appendix D: Glossary | Term | Definition | |------|-----------| | **Entity** | An organization (school, company) that manages users under a shared license | | **Group** | A classroom or user grouping within an entity | | **Module** | An IELTS exam skill: Reading, Listening, Writing, Speaking, or Level | | **Session** | A single exam-taking attempt by a student | | **Stat** | The result of a single exercise attempt within a session | | **Evaluation** | An AI-graded writing or speaking assessment | | **Code** | A registration/invite code that grants access or subscription time | | **Package** | A purchasable subscription plan | | **ielts-be** | The AI/ML microservice that handles content generation and grading | | **BFF** | Backend-for-Frontend; the current ielts-ui API routes layer being replaced |