Files
encoach_backend_new_v2/docs/PROJECT_SUMMARY.md
Yamen Ahmad 93c530eef2
Some checks failed
CI / Frontend — lint + build + e2e (pull_request) Has been cancelled
CI / Backend — Odoo HttpCase (pull_request) Has been cancelled
chore(ci,docs): GitHub Actions, ADRs, README overhaul, §21 Hardening Release
- .github/workflows/ci.yml: two jobs — frontend (tsc --noEmit, lint, build,
  Playwright) and backend (Postgres 16 + odoo:19 --test-enable
  --test-tags encoach_api) — catches regressions before merge.
- docs/adr/: start an Architecture Decision Record trail with
  0001 canonical directory layout, 0002 JWT refresh flow,
  0003 paginated response envelope, 0004 RAG metadata + chunking.
- docs/PROJECT_SUMMARY.md §21 Hardening Release: full recap of the AI
  quality loop, compliance, Paymob, i18n, and CI work shipped in this
  drop, plus new DB tables, REST routes, frontend routes, verification
  results, and operator-facing configuration.
- README.md refreshed for the v4 split-repo doctrine and the new feature
  surface.
- new_project/DEPRECATED.md: formal retirement notice pointing at
  backend/ as the canonical tree.

Made-with: Cursor
2026-04-19 14:16:47 +04:00

91 KiB
Raw Blame History

EnCoach Platform — Project Summary

Last updated: 2026-04-19 | Canonical repos: encoach_backend_v4 (backend) + encoach_frontend_v4 (frontend), branch main.

This workspace (odoo19/) is a developer monorepo / working tree only — it conveniently contains both halves side-by-side for local development and testing. The two split repos above are the authoritative origins for each half: every change must be published to them (via git subtree split + push) before the team lead can deploy. See §6 Git Remotes & Repositories for the exact workflow.

Latest events:

  • 2026-04-19 (reports section): Built the Reports section end-to-end — the three pages /admin/student-performance, /admin/stats-corporate, /admin/record (previously pure hardcoded-array mocks) are now wired to real aggregated data from encoach.student.attempt. New /api/reports/{student-performance,stats-corporate,record,filters} controller (encoach_lms_api/controllers/reports.py) does the rollups: per-student band averages + CEFR, per-module corporate charts, trend / distribution / entity comparison, and per-user attempt history with search / level / entity / period filters and CSV export. New seed_reports.py completes in-progress attempts and backfills six months of historical attempts so the trend chart and KPI cards are meaningful. 25/25 API smoke passing (test_reports_flows.py), 24/24 Configuration + 29/29 Support + 26/26 Training regressions still green, all three pages verified live in-browser with 28 real attempts showing across 4 tabs. See §20.
  • 2026-04-19 (remote rename): Aligned local remote names with the new doctrine — backend-v4 → origin-backend, frontend-v4 → origin-frontend, origin → mirror-monorepo. The v4 branch now tracks mirror-monorepo/v4. All publishing commands in §6 updated. No remote URLs changed.
  • 2026-04-19 (repos-of-record reorganization): Declared encoach_backend_v4 and encoach_frontend_v4 as the canonical origins for their respective halves; the monorepo encoach_backend_new_v2/v4 is now a secondary working-tree mirror kept only for history/convenience. §6 rewritten around this model.
  • 2026-04-19 (release to VPS repos): Pushed the accumulated institutional + support + training work. Frontend canonical → encoach_frontend_v4/main (b78124bb..435930a8, 403 files, clean fast-forward). Backend canonical → encoach_backend_v4/main (74d83af5..6ec68160, 420 files, clean fast-forward). Monorepo mirror → mirror-monorepo/v4 (98b9837a). .gitignore now excludes pgdata_bak_*/, frontend/.vite/, frontend/dist/, frontend/node_modules/ so the 674 MB of local pgdata backups never leak into a push. See §6.
  • 2026-04-18 (training section): Built the Training section end-to-end — the Vocabulary and Grammar pages (previously pure hard-coded mocks) are now wired to real Odoo data with CRUD, search, level filtering, and per-user progress tracking. New encoach.vocab.item / encoach.vocab.progress / encoach.grammar.rule / encoach.grammar.progress models + /api/training/vocabulary and /api/training/grammar controllers. 26/26 API write-flow actions passing (test_training_flows.py); both pages verified live in-browser. See §19.
  • 2026-04-18 (support section): Built the Support section end-to-end — the Tickets, Payment Record, and Settings pages are now wired to real Odoo data (they were previously mocks/404s). New encoach.ticket model + full CRUD controller, /api/payment-records deriving from op.student.fees.details + account.move, /api/codes + /api/packages + /api/grading-config powering the Settings tabs. 29/29 API write-flow actions passing (test_support_flows.py); all 3 pages verified live in-browser. See §18.
  • 2026-04-18 (institutional pages write-flow): API-level write-flow sweep across all 14 institutional admin pages — 44/44 actions passing (CREATE / EDIT / DELETE / WORKFLOW). Numerous backend gaps fixed: missing DELETE routes added (admission-registers, admissions, result-templates, student-leaves), lesson/library detail endpoints added, custom encoach.asset model introduced to replace the missing op.asset, marksheet generate endpoint aliased for Odoo 19's generate_result, fees create-invoice auto-wires the product income account via Odoo 19's JSONB property storage, academic-year DELETE now cascades dependent terms. See §17.
  • 2026-04-18 (browser E2E): Full end-to-end test across admin, teacher, student user types. All critical pages verified working in browser. Two backend bugs fixed: (1) /api/approval-workflows returned 500 because raw SQL referenced a table that doesn't exist — now gracefully returns empty list; (2) DELETE /api/courses/<id> returned opaque 500 when course had enrollments — now returns 409 with a clear message and supports ?force=true for cascade-delete. See §16.

1. Project Overview

EnCoach is an AI-powered online learning and examination platform built on Odoo 19 (backend) and React + TypeScript + Vite (frontend). It supports IELTS and general English exam generation, student exam sessions, auto-scoring, adaptive learning, and course creation.


2. Tech Stack

Layer Technology
Backend Odoo 19, Python 3.12, PostgreSQL 18
Frontend React 18, TypeScript, Vite 5, Tailwind CSS, Shadcn UI
State Management @tanstack/react-query (mutations + queries)
Auth JWT (issued by Odoo, validated via validate_token helper)
AI OpenAI API (with fallback templates when API key not configured)
Database encoach_v2 (PostgreSQL 18.3)

3. User Credentials

Admin/teacher users use password admin. Student passwords were reset to student123 during end-to-end testing.

ID Login Name Type Password
2 admin Administrator superadmin admin
5 admin@encoach.test Admin User admin admin
6 sarah@encoach.test Sarah Ahmed student student123
7 omar@encoach.test Omar Khan student student123
8 layla@encoach.test Layla Nasser student student123
9 khalid@encoach.test Dr. Khalid teacher admin
10 fatima@encoach.test Ms. Fatima teacher admin

Note: Student passwords were bulk-reset to student123 via a direct psycopg2 + passlib script during testing. If a student can't login, reset their password in psql using passlib.context.CryptContext(['pbkdf2_sha512']).

Login API

# Admin login
curl -X POST http://localhost:8069/api/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin","password":"admin"}'

# Student login
curl -X POST http://localhost:8069/api/login \
  -H "Content-Type: application/json" \
  -d '{"email":"sarah@encoach.test","password":"student123"}'

Returns { token, user, permissions }. The token (JWT) is used as Authorization: Bearer <token> for all subsequent API calls.


4. How to Start the Project

Prerequisites

  • PostgreSQL 18 binary at /Users/yamenahmad/micromamba/envs/odoo19/bin/
  • Python 3.12 (conda env at .conda-envs/odoo19/ or micromamba)
  • Node.js 20 at /Users/yamenahmad/.local/node/bin/

Start PostgreSQL

PGBIN="/Users/yamenahmad/micromamba/envs/odoo19/bin"
PGDATA="/Users/yamenahmad/projects2026/odoo/odoo19/pgdata"
"$PGBIN/pg_ctl" -D "$PGDATA" -l "$PGDATA/logfile" start

Important: The pgdata was initialized with PG 18. The conda env has PG 17.6, which is incompatible. Always use the micromamba PG 18 binary above.

If PG fails with config errors, check pgdata/logfile. The parameter autovacuum_worker_slots (PG 18-only) was commented out in pgdata/postgresql.conf line 687 for compatibility.

Start Odoo Backend

cd /Users/yamenahmad/projects2026/odoo/odoo19
./run.sh

Runs at http://127.0.0.1:8069

Odoo config (odoo.conf) must have db_name = encoach_v2 and dbfilter = ^encoach_v2$, otherwise API routes return 404 ("No database selected").

Start Frontend

export PATH="/Users/yamenahmad/.local/node/bin:$PATH"
cd /Users/yamenahmad/projects2026/odoo/odoo19/frontend
npm run dev

Runs at http://localhost:8080 — Vite proxies /api requests to Odoo :8069.

Stop PostgreSQL

"$PGBIN/pg_ctl" -D "$PGDATA" stop

5. Project Structure

odoo19/
├── backend/
│   ├── custom_addons/          # All EnCoach Odoo modules
│   │   ├── encoach_api/        # Auth (login/logout/user) + base helpers
│   │   ├── encoach_ai/         # AI generation, exam generation/submit
│   │   ├── encoach_ai_course/  # AI course creation pipelines
│   │   ├── encoach_adaptive/   # Adaptive learning engine
│   │   ├── encoach_core/       # Core models (user, entity, role, permission)
│   │   ├── encoach_exam_template/ # Exam CRUD, sessions, rubrics, assignments, schedules
│   │   ├── encoach_scoring/    # Grading queue, score release
│   │   ├── encoach_taxonomy/   # Subject/topic tree
│   │   ├── encoach_lms_api/    # LMS: courses, chapters, materials, progress, enrollment
│   │   ├── encoach_course_gen/ # Course generation
│   │   ├── encoach_branding/   # Entity branding
│   │   ├── encoach_signup/     # Registration, email verification, onboarding
│   │   ├── encoach_placement/  # Placement test
│   │   └── ...
│   └── openeducat_erp-19.0/   # OpenEduCat base modules
├── frontend/
│   ├── src/
│   │   ├── pages/              # Route pages (admin/, student/, etc.)
│   │   ├── components/         # Reusable UI components
│   │   ├── services/           # API service classes
│   │   ├── hooks/              # React Query hooks
│   │   ├── lib/                # api-client, utils
│   │   └── types/              # TypeScript type definitions
│   ├── .env.development        # VITE_API_BASE_URL=/api
│   └── .env.production         # VITE_API_BASE_URL=https://api.encoach.com/api
├── docs/                       # Documentation
├── odoo/                       # Odoo 19 source code
├── odoo.conf                   # Odoo configuration
├── pgdata/                     # PostgreSQL data directory (PG 18)
├── run.sh                      # Start Odoo
├── start.sh                    # Start PG + Odoo
└── stop.sh                     # Stop services

6. Git Remotes & Repositories

Canonical (repos-of-record):

These are the repositories the team lead (Talal / devops) clones on the VPS and builds with docker compose up -d --build. All production / staging code lives here.

This workspace (odoo19/) is a developer monorepo / working tree — a convenience folder that holds both halves side-by-side so a single developer can run the full stack locally. It is not the source of truth: every change must be published to the two canonical repos above via git subtree split + push before it counts as merged.

6.1 Repository Layout

odoo19/                          ← developer working tree (NOT source of truth)
├── backend/                     → published to encoach_backend_v4/main  (authoritative)
│   ├── custom_addons/
│   ├── Dockerfile
│   ├── docker-compose.yml
│   └── requirements.txt
├── frontend/                    → published to encoach_frontend_v4/main (authoritative)
│   ├── src/
│   ├── public/
│   ├── Dockerfile, vite.config.ts, package.json, …
└── docs/                        → lives only in this workspace

6.2 Remotes Configured on This Clone

Listed in order of importance. The two bold rows are the repos of record; everything else is a secondary mirror or legacy.

Remote URL Role
origin-backend https://git.albousalh.com/devops/encoach_backend_v4.git Canonical backend — branch main. VPS deploy target.
origin-frontend https://git.albousalh.com/devops/encoach_frontend_v4.git Canonical frontend — branch main. VPS deploy target.
mirror-monorepo https://git.albousalh.com/devops/encoach_backend_new_v2.git Secondary full-tree mirror (branch v4 tracked here). Useful for history/backup and for docs that live only in the workspace root (e.g. this file). Not the source of truth.
ip-origin https://5.189.151.117/devops/encoach_backend_new_v2.git IP fallback for mirror-monorepo when DNS is flaky.
backend-v3 https://git.albousalh.com/devops/encoach_backend_new_v3.git Legacy v3 monorepo mirror — rarely used.
frontend-v3 https://git.albousalh.com/devops/encoach_frontend_new_v3.git Legacy v3 frontend mirror — rarely used.
frontend https://git.albousalh.com/devops/encoach_frontend_new_v2.git Legacy v2 frontend monorepo — rarely used.

Rename history: backend-v4 → origin-backend, frontend-v4 → origin-frontend, origin → mirror-monorepo (applied 2026-04-19). If you have old scripts or notes referencing the old names, update them or add aliases via git remote rename.

6.3 Feature Development Workflow

Everything is edited inside this workspace, but a change is only merged once it lands on main of the two canonical repos.

# 1. Start from a clean workspace synced with the monorepo mirror
git checkout v4
git pull mirror-monorepo v4               # optional: refresh full-tree mirror

# 2. Create a feature branch
git checkout -b feature/<short-name>

# 3. Make changes, commit small and often
git add <files>
git commit -m "feat(<area>): <short description>"

# 4. Merge back onto v4 (or open a PR if branch protection is enabled)
git checkout v4
git merge --no-ff feature/<short-name>

# 5. Publish to the CANONICAL repos (mandatory — see §6.4)
#    This is what the team lead / CI will see on the VPS.

# 6. (Optional) push the monorepo mirror for a full-tree backup / docs
git push mirror-monorepo v4

6.4 Publishing to the Canonical Repos (MANDATORY)

The canonical repos expect a flat tree (no backend/ or frontend/ prefix). Use git subtree split to carve each subdirectory into its own parallel history, then push to main. These commands produce clean fast-forwards when nothing has been pushed directly to the canonical repos from elsewhere — which is the invariant we enforce.

# ── Publish BACKEND → encoach_backend_v4 ─────────────────────────────────
git subtree split --prefix=backend HEAD -b split-backend-v4
git push origin-backend split-backend-v4:main
git branch -D split-backend-v4            # cleanup the temp branch

# ── Publish FRONTEND → encoach_frontend_v4 ───────────────────────────────
git subtree split --prefix=frontend HEAD -b split-frontend-v4
git push origin-frontend split-frontend-v4:main
git branch -D split-frontend-v4

If a push is rejected as non-fast-forward, fetch and inspect before forcing:

git fetch origin-backend origin-frontend
git log --oneline origin-backend/main..split-backend-v4     # what we're adding
git log --oneline split-backend-v4..origin-backend/main     # divergence, if any

Only use --force after confirming the remote main has no commits worth keeping (e.g. someone pushed directly and it must be overwritten). Since the canonical repos are the source of truth, a force-push there is a destructive production operation — pair it with a Slack heads-up to the team lead.

6.5 What Each Canonical Repo Contains

encoach_backend_v4 (branch main) — everything under backend/ hoisted to repo root:

  • custom_addons/ (all EnCoach Odoo addons + encoach_lms_api)
  • Dockerfile, docker-compose.yml, requirements.txt

Not shipped: openeducat_erp-19.0/ is vendored locally but not tracked in git. The VPS needs OpenEduCat installed separately — either baked into the container image, mounted at /opt/odoo/extra_addons, or installed via pip if a package release is used. backend/odoo.conf is also not currently tracked; add it in a follow-up commit if the VPS container needs a baked-in config.

encoach_frontend_v4 (branch main) — everything under frontend/ hoisted to repo root:

  • src/, public/, docs/ (frontend docs only)
  • package.json, package-lock.json, bun.lock, bun.lockb
  • Dockerfile, docker-compose.yml
  • vite.config.ts, tsconfig.*.json, tailwind.config.ts, eslint.config.js
  • index.html, components.json, .env.example

Not shipped (ignored by frontend/.gitignore): node_modules/, dist/, .vite/, .env, .env.development, .env.production.

6.6 VPS Deploy — Team-Lead Quickstart

# Backend
git clone https://git.albousalh.com/devops/encoach_backend_v4.git
cd encoach_backend_v4 && docker compose up -d --build

# Frontend
git clone https://git.albousalh.com/devops/encoach_frontend_v4.git
cd encoach_frontend_v4 && cp .env.example .env        # set VITE_API_BASE_URL
docker compose up -d --build                          # or: npm ci && npm run build

6.7 Safety Rules (DO NOT COMMIT)

Already excluded via .gitignore — do not force them in:

  • pgdata/, pgdata_bak_*/ — PostgreSQL data directories (up to ~700 MB)
  • frontend/node_modules/, frontend/dist/, frontend/.vite/ — build artifacts
  • .env, .env.development, .env.production — local secrets
  • .venv/, venv/, .conda-envs/ — Python virtualenvs
  • *.zip, *.tar.gz — archives

odoo.conf at the workspace root IS tracked (local Mac dev config, harmless). backend/odoo.conf (container-friendly) is not tracked; add it in a follow-up if the VPS image needs it baked in.

6.8 Git Credentials

Stored in macOS Keychain for git.albousalh.com:

  • Username: yamen
  • Auth: token-based (stored in keychain, not in this file)

7. API Routes Reference

Authentication (encoach_api)

Method Route Auth Description
POST /api/login public Login, returns JWT
GET /api/user JWT Get current user
POST /api/logout public Logout (client clears token)

Exam Generation & AI (encoach_ai)

Method Route Description
POST /api/exam/<module>/generate AI-generate content for a module (reading/listening/writing/speaking)
POST /api/exam/generation/submit Submit generated exam, create questions
POST /api/exam/<module>/generate/save Save generated content

Exam Session (encoach_exam_template)

Method Route Description
GET /api/exam/<exam_id>/session Get exam session (creates attempt if needed)
POST /api/exam/<exam_id>/autosave Autosave student answers
POST /api/exam/<exam_id>/submit Submit exam for scoring
GET /api/exam/<exam_id>/status Get attempt status
GET /api/exam/<exam_id>/results Get exam results

Custom Exams

Method Route Description
GET /api/exam/custom/list List custom exams
POST /api/exam/custom/create Create custom exam
GET/PUT/DELETE /api/exam/custom/<id> CRUD on custom exam

Exam Structures, Templates, Schedules, Assignments

Method Route Description
GET/POST /api/exam-structures List/create exam structures
GET/POST /api/exam/templates List/create exam templates
GET/POST /api/exam-schedules List/create schedules
GET /api/student/my-exams Student's assigned exams
GET/POST /api/assignments List/create assignments

Rubrics & Approval Workflows

Method Route Description
GET/POST /api/rubrics List/create rubrics
GET/POST /api/rubric-groups List/create rubric groups
GET/POST /api/approval-workflows List/create approval workflows
POST /api/approval-requests/<id>/approve Approve request
POST /api/approval-requests/<id>/reject Reject request

Entities

Method Route Description
GET /api/entities List entities

LMS Core (encoach_lms_apilms_core.py)

Method Route Description
GET/POST /api/courses List all courses (paginated) / create course
GET/PUT/DELETE /api/courses/<id> Get / update / delete a single course
GET/POST /api/students List students / create student
GET/PUT/DELETE /api/students/<id> Get / update / delete a student
GET /api/student/my-courses Enrolled courses for logged-in student (with progress)
POST /api/students/<id>/enroll Enroll a student in one or more courses
POST /api/courses/<id>/enroll Bulk-enroll students in a course
GET/POST /api/teachers List / create teachers
GET/PUT/DELETE /api/teachers/<id> Get / update / delete a teacher
GET/POST /api/batches List / create batches
GET/PUT/DELETE /api/batches/<id> Get / update / delete a batch

LMS Courseware (encoach_lms_apicourseware.py)

Method Route Description
GET/POST /api/courses/<id>/chapters List / create chapters for a course
GET/PUT/DELETE /api/chapters/<id> Get / update / delete a chapter
POST /api/chapters/<id>/materials Upload material (JSON or FormData)
GET/PUT/DELETE /api/materials/<id> Get / update / delete a material
GET /api/materials/<id>/download Download material file
GET /api/materials/<id>/content Serve material file inline (for iframe embed, accepts ?token= param)
POST /api/materials/<id>/view Mark material as viewed
POST /api/chapters/<id>/progress/complete Mark chapter as completed
GET /api/courses/<id>/completion Get course completion status (% progress, post-test availability)
GET /api/materials/search Search all materials across courses (for Admin Resources page)

Institutional (encoach_lms_api)

These power the 14 institutional admin pages under /admin/* (see §14) and back the write-flow test in §17. Every route is JWT-protected.

Academic & Departments

Method Route Description
GET / POST /api/academic-years List / create academic years
GET / PATCH / DELETE /api/academic-years/<yid> Get / update / delete year (DELETE cascades terms)
POST /api/academic-years/<yid>/generate-terms Auto-create terms spanning the year (two_sem, three_trim, four_qtr)
GET / POST /api/academic-terms List / create academic terms
GET / PATCH / DELETE /api/academic-terms/<tid> Term CRUD
GET / POST /api/departments List / create departments
GET / PATCH / DELETE /api/departments/<did> Department CRUD

Admissions

Method Route Description
GET / POST /api/admission-registers List / create admission registers
GET / PATCH / DELETE /api/admission-registers/<rid> Register CRUD (DELETE supported)
POST /api/admission-registers/<rid>/confirm Confirm register
GET / POST /api/admissions List / create admissions (application_date must fall inside register window)
GET / PATCH / DELETE /api/admissions/<aid> Admission CRUD (DELETE supported)

Institutional Exams & Marksheets

Method Route Description
GET / POST /api/inst-exam-sessions List / create exam sessions
GET / PATCH / DELETE /api/inst-exam-sessions/<sid> Session CRUD
POST /api/inst-exam-sessions/<sid>/schedule Schedule session
GET / POST /api/result-templates List / create result templates
GET / PATCH / DELETE /api/result-templates/<tid> Template CRUD (DELETE cascades op.marksheet.register)
POST /api/result-templates/<tid>/generate (alias: /generate-marksheets) Generate marksheet registers (tries generate_result then action_generate_marksheet)
GET /api/marksheets List marksheet registers
POST /api/marksheets/<mid>/validate Validate marksheet

Facilities, Assets, Activities

Method Route Description
GET / POST /api/facilities List / create facilities
GET / PATCH / DELETE /api/facilities/<fid> Facility CRUD
GET / POST /api/assets List / create assets (backed by custom encoach.asset model)
GET / PATCH / DELETE /api/assets/<aid> Asset CRUD
GET / POST / PATCH / DELETE /api/activity-types + /<id> Activity-type CRUD
GET / POST / PATCH / DELETE /api/activities + /<id> Activity CRUD

Library, Lessons, Gradebook, Leaves, Fees

Method Route Description
GET / POST /api/library/media List / create media items (authors split into author_ids)
GET / PATCH / DELETE /api/library/media/<mid> Media CRUD — detail endpoint returns author_names + author_ids
GET / POST /api/lessons List / create lessons
GET / PATCH / DELETE /api/lessons/<lid> Lesson CRUD — detail endpoint preserves course_id / batch_id on edits
GET / POST / DELETE /api/grading-assignments + /<id> Grading assignment CRUD (auto-resolves assignment_type and default faculty_id)
GET /api/gradebooks + /api/gradebook-lines?gradebook_id=<id> Gradebook drill-down
GET /api/student-progress + /<id> Student progress list/detail
GET / POST /api/student-leaves List / create leave requests
POST /api/student-leaves/<lid>/approve Approve leave
DELETE /api/student-leaves/<lid> Delete leave
GET / POST /api/fees-plans List / create fee plans
POST /api/fees-plans/<fid>/create-invoice Generate accounting invoice (auto-wires income account on the product template for Odoo 19 JSONB property storage if missing)
POST /api/fees-plans/<fid>/register-payment Register a payment against the invoice

Other

Route Description
/api/taxonomy/* Subject/topic taxonomy tree
/api/adaptive/* Adaptive learning dashboard/signals
/api/placement/* Placement test start/answer/results
/api/grading/* Grading queue, AI suggest
/api/scores/* Score release/reject
/api/course/* Course CRUD, gap analysis, auto-generate
/api/ai-course/* AI course creation pipelines
/api/auth/register User registration

8. Key Odoo Models

Model Table Description
encoach.exam.custom encoach_exam_custom Custom exams (16 records)
encoach.exam.custom.section encoach_exam_custom_section Exam sections (reading/listening/writing/speaking)
encoach.question encoach_question Questions (38 records)
encoach.student.attempt encoach_student_attempt Student exam attempts (10 records)
encoach.student.answer encoach_student_answer Individual answers
encoach.entity encoach_entity Organizations (1: "EnCoach Demo Academy")
encoach.rubric encoach_rubric Grading rubrics
encoach.exam.template encoach_exam_template Exam templates
encoach.exam.structure encoach_exam_structure Exam structures
encoach.exam.schedule encoach_exam_schedule Exam schedules
encoach.exam.assignment encoach_exam_assignment Exam assignments
encoach.course.chapter encoach_course_chapter Course chapters (ordered by sequence)
encoach.chapter.material encoach_chapter_material Materials within chapters (pdf, video, article, link)
encoach.chapter.progress encoach_chapter_progress Per-student chapter progress tracking
encoach.course.completion encoach_course_completion Per-student course completion (% progress, post-test flag)
encoach.resource encoach_resource Learning resources linked to taxonomy (subject, domain, topics, objectives)
encoach.resource.tag encoach_resource_tag Tags for categorizing resources
encoach.subject encoach_subject Taxonomy subjects (e.g. English, Mathematics)
encoach.domain encoach_domain Taxonomy domains under subjects (e.g. Grammar, Listening)
encoach.topic encoach_topic Taxonomy topics under domains
encoach.learning.objective encoach_learning_objective Learning objectives under topics (Bloom levels)
op.course (extended) op_course OpenEduCat course + description, max_capacity, encoach_subject_id, topic_ids, learning_objective_ids
op.student op_student OpenEduCat student model
op.faculty op_faculty OpenEduCat teacher/faculty model
op.batch op_batch OpenEduCat batch model

Institutional models (OpenEduCat + EnCoach)

Model Table Description
op.academic.year op_academic_year Academic year (name unique, term_structure)
op.academic.term op_academic_term Term under a year; FK to year is RESTRICT — delete-year cascades terms first
op.department op_department Department
op.admission.register op_admission_register Admission register window (start_date / end_date)
op.admission op_admission Admission — constrained so application_date ∈ [register.start_date, register.end_date]
op.exam.session op_exam_session Institutional exam session
op.result.template op_result_template Marksheet result template — generate_result() in Odoo 19 (older: action_generate_marksheet) produces op.marksheet.register rows
op.marksheet.register op_marksheet_register Generated marksheet register (lines + validate workflow)
op.facility op_facility Physical facility (rooms, labs, etc.)
encoach.asset encoach_asset New lightweight asset model (replaces missing op.asset): name, code, facility_id, description, active
op.activity.type / op.activity op_activity_type / op_activity Activity categorization + activities
op.media / op.author op_media / op_author Library media + authors (M2M)
encoach.lesson encoach_lesson Lesson tied to course + batch
grading.assignment / grading.assignment.type grading_assignment* Grading assignments — assignment_type and faculty_id are required
encoach.student.leave encoach_student_leave Student leave requests with approve workflow
op.fees.terms + .line op_fees_terms* Fees terms; line_ids must sum to 100%
op.student.fees.details op_student_fees_details Per-student fees; get_invoice() requires an income account on product template (stored as JSONB in Odoo 19)

9. Installed EnCoach Modules

Module Description
encoach_adaptive Adaptive learning engine
encoach_ai AI generation, search, insights
encoach_ai_course AI course creation
encoach_api REST API auth + base helpers
encoach_branding Entity branding
encoach_core Core models (user, entity, role)
encoach_course_gen Course generation
encoach_exam_template Exam CRUD, sessions, rubrics, assignments
encoach_lms_api LMS: courses, students, teachers, batches, chapters, materials, enrollment, progress
encoach_quality_gate Quality gate checks
encoach_resources Learning resources
encoach_scoring Grading and score release
encoach_taxonomy Subject/topic taxonomy
encoach_vector Vector embeddings

10. Exam Lifecycle Flow

1. Admin → /admin/generation page
   ├── Select modules (reading, listening, writing, speaking)
   ├── Set parameters (difficulty, timer, grading, rubric, entity, etc.)
   └── Click "Generate with AI" → POST /api/exam/<module>/generate

2. AI generates content (or fallback templates if OpenAI not configured)

3. Admin reviews generated content → Submit
   ├── "Submit & skip approval" → POST /api/exam/generation/submit (skip_approval=true)
   ├── "Submit for approval" → POST /api/exam/generation/submit (skip_approval=false)
   └── Creates: exam_custom + sections + questions in DB

4. Admin assigns exam → /api/exam/custom/<id>/assign or /api/exam-schedules

5. Student → /student/my-exams → clicks exam → /student/exam/<id>
   ├── GET /api/exam/<id>/session → loads sections, questions, passage_text
   ├── POST /api/exam/<id>/autosave → saves answers periodically
   └── POST /api/exam/<id>/submit → auto-scores objective questions

6. Student → /student/exam/<id>/results → views score breakdown

11. Student Learning Lifecycle Flow

1. Admin/Teacher → /teacher/courses → "New Course"
   ├── Create course (title, code, description, max_capacity)
   └── Course stored in op.course (extended with description, max_capacity)

2. Teacher → /teacher/courses/:courseId/chapters → "Add Chapter"
   ├── Create chapters with sequence ordering
   └── Upload materials (pdf, video, article, link) per chapter
       └── POST /api/chapters/<id>/materials (JSON or FormData)

3. Admin → /admin/courses → "Enroll Students"
   ├── Bulk-enroll students via POST /api/courses/<id>/enroll
   └── Or enroll individually via POST /api/students/<id>/enroll

4. Student → /student/dashboard → sees enrolled courses with progress
   ├── GET /api/student/my-courses → returns courses + chapter_count + progress %
   └── Cards show "Start Learning" or "Continue Learning"

5. Student → /student/courses/:id → course detail with chapter list
   ├── Shows chapter list with material counts and lock status
   └── Student clicks a chapter → /student/courses/:courseId/chapters/:chapterId

6. Student → chapter view → views materials
   ├── POST /api/materials/<id>/view → marks material as viewed
   ├── Progress tracked in encoach.chapter.progress
   └── When all materials viewed → chapter auto-completes

7. Student → clicks "Mark Chapter Complete"
   ├── POST /api/chapters/<id>/progress/complete
   └── Triggers _check_course_completion()

8. Course completion check
   ├── When all chapters completed → encoach.course.completion updated
   ├── progress = 100%, completed_at = now
   └── post_test_available = True → student can take final exam

12. Taxonomy ↔ Resource ↔ Course Relationships

encoach.subject (e.g. English)
 └── encoach.domain (e.g. Grammar, Listening, Reading)
      └── encoach.topic (e.g. Verb Tenses, Relative Clauses)
           └── encoach.learning.objective (Bloom levels: Remember, Understand, Apply...)

encoach.resource ──M2O──→ encoach.subject (subject_id)
                 ──M2O──→ encoach.domain  (domain_id)
                 ──M2M──→ encoach.topic   (topic_ids)
                 ──M2M──→ encoach.learning.objective (learning_objective_ids)

op.course (extended) ──M2O──→ encoach.subject (encoach_subject_id)
                     ──M2M──→ encoach.topic   (topic_ids)
                     ──M2M──→ encoach.learning.objective (learning_objective_ids)

encoach.course.chapter ──M2O──→ encoach.subject (encoach_subject_id)
                       ──M2M──→ encoach.topic   (topic_ids)
                       ──M2M──→ encoach.learning.objective (learning_objective_ids)

Frontend: TaxonomyCascade Component

Reusable cascading picker (frontend/src/components/TaxonomyCascade.tsx) used in:

  • Admin Resource Manager (upload + edit dialogs)
  • Teacher Library (upload dialog)

Provides Subject → Domain → Topics → Learning Objectives selection with auto-fetching and reset-on-parent-change logic.

API endpoints for taxonomy data

Method Route Description
GET /api/subjects List subjects (paginated, returns items array)
GET /api/domains?subject_id=N List domains filtered by subject
GET /api/topics?domain_id=N List topics filtered by domain
GET /api/learning-objectives?topic_id=N List objectives filtered by topic

13. Known Issues & Notes

  1. PostgreSQL version: pgdata is PG 18; must use micromamba pg_ctl (/Users/yamenahmad/micromamba/envs/odoo19/bin/pg_ctl), NOT conda env PG 17.
  2. odoo.conf requires db_name: Without db_name = encoach_v2, all API routes return 404.
  3. npm not in default PATH: Must add /Users/yamenahmad/.local/node/bin to PATH before running frontend.
  4. OpenAI not configured: AI generation uses fallback templates (static mock content). Set OpenAI API key in Odoo system parameters to enable real AI.
  5. Gitea deployment hook: Frontend v3 server has a docker-compose.yml conflict that blocks auto-deploy. SSH and stash/checkout on server to fix.
  6. Student passwords were reset: Student demo users (sarah, omar, layla) now use password student123 (not admin). Passwords were reset via psycopg2 + passlib script. To reset again:
    from passlib.context import CryptContext
    ctx = CryptContext(schemes=['pbkdf2_sha512'])
    new_hash = ctx.hash('student123')
    # UPDATE res_users SET password = '<new_hash>' WHERE login = 'sarah@encoach.test';
    
  7. op.course extended fields: The op.course model (OpenEduCat) was extended via encoach_lms_api/models/course_ext.py to add description (Text) and max_capacity (Integer). If the module is not installed/updated, course creation will fail with ValueError: Invalid field 'description'.
  8. material_count computed field: The material_count on encoach.course.chapter requires @api.depends('material_ids') to recompute when materials are added. If counts show 0 after adding materials, restart Odoo with -u encoach_lms_api.
  9. Taxonomy API response format: Taxonomy list endpoints (/api/subjects, /api/domains, /api/topics) return paginated responses with data under an items key ({"items": [...], "total": N}), not a data key. The frontend taxonomy.service.ts handles both formats.
  10. Resource taxonomy fields via FormData: When uploading resources, topic_ids and learning_objective_ids are sent as comma-separated strings in multipart FormData. The backend create_resource handler parses these into proper Many2many commands [(6, 0, ids)].
  11. Module upgrades for schema changes: After adding new fields to Odoo models (e.g. encoach_subject_id on op.course), you must run ./run.sh -u <module_name> --stop-after-init to apply DB schema changes before the server can use them.

14. Frontend Key Pages

Admin Pages

Route Component Description
/admin/dashboard AdminLmsDashboard Platform overview, AI tips, stats (LMS-focused)
/admin/platform AdminDashboard Alternative platform dashboard
/admin/courses AdminCourses Course table with enroll/assign/delete actions
/admin/students AdminStudents Student management
/admin/teachers AdminTeachers Teacher management
/admin/users UsersPage All platform users + roles (Management section)
/admin/entities EntitiesPage Entity/organization management
/admin/classrooms ClassroomsPage Batches/classrooms with student management
/admin/user-roles UserRoles User role assignments (⚠ NOT /admin/roles)
/admin/roles-permissions RolesPermissions Roles & permissions matrix
/admin/authority-matrix AuthorityMatrix Authority matrix across roles
/admin/generation GenerationPage AI exam generation
/admin/examsList ExamsListPage List all exams (⚠ NOT /admin/exams)
/admin/rubrics RubricsPage Manage rubrics
/admin/assignments AssignmentsPage Manage assignments
/admin/approval-workflows ApprovalWorkflowsPage Approval workflow config
/admin/taxonomy TaxonomyManager Manage subjects, domains, topics, objectives (inline editable)
/admin/resources ResourceManager Upload/edit resources with full taxonomy cascade (Subject→Domain→Topics→Objectives)

Admin — Institutional Pages

Backed by encoach_lms_api + OpenEduCat models. All 14 pages verified CREATE / EDIT / DELETE / WORKFLOW in the write-flow sweep (see §17).

Route Page Backing API Key actions
/admin/academic-years Academic Years /api/academic-years, /generate-terms CREATE, generate-terms, EDIT, DELETE (cascades terms)
/admin/academic-terms Academic Terms /api/academic-terms CRUD
/admin/departments Departments /api/departments CRUD
/admin/admission-registers Admission Registers /api/admission-registers CREATE, confirm, DELETE
/admin/admissions Admissions /api/admissions CREATE (date-constrained), DELETE
/admin/inst-exam-sessions Institutional Exam Sessions /api/inst-exam-sessions CREATE, schedule, EDIT, DELETE
/admin/marksheets Marksheets /api/result-templates, /api/marksheets Create template, generate, validate marksheet, DELETE
/admin/facilities Facilities /api/facilities, /api/assets Facility CRUD + nested assets
/admin/activities Activities /api/activity-types, /api/activities Type + activity CRUD
/admin/library Library /api/library/media Media + authors, detail exposes author links
/admin/lessons Lessons /api/lessons CRUD; edits preserve course_id / batch_id
/admin/gradebook Gradebook /api/grading-assignments, /api/gradebooks, /api/gradebook-lines Assignment CRUD + gradebook drill
/admin/student-progress Student Progress /api/student-progress Drill-through detail
/admin/student-leave Student Leave /api/student-leaves CREATE, approve, DELETE
/admin/fees Fees /api/fees-plans, /create-invoice, /register-payment List seeded plans, generate invoice, register payment

Teacher Pages

Route Component Description
/teacher/dashboard TeacherDashboard Teacher overview
/teacher/courses TeacherCourses Course cards with Edit, Chapters & Materials, AI Workbench
/teacher/courses/new CourseBuilder Create new course
/teacher/courses/:courseId/chapters CourseChapters Manage chapters and materials for a course
/teacher/courses/:courseId/chapters/:chapterId ChapterDetail Chapter detail with material list
/teacher/courses/:courseId/workbench AiWorkbench AI content generation workbench
/teacher/assignments TeacherAssignments Manage assignments
/teacher/library TeacherLibrary Upload resources with full taxonomy cascade (Subject→Domain→Topics→Objectives)

Student Pages

Route Component Description
/student/dashboard StudentDashboard Enrolled courses, progress, grades, quick actions
/student/courses StudentCourses Enrolled courses with progress badges
/student/courses/:id StudentCourseDetail Course chapters, materials, grades
/student/courses/:courseId/chapters/:chapterId StudentChapterView View materials, mark viewed/complete
/student/my-exams StudentExamsPage Student's assigned exams
/student/exam/:id ExamSession Take an exam
/student/exam/:id/results ExamResults View results
/student/assignments StudentAssignments Homework assignments

15. Database Connection

Host: localhost
Port: 5432
Database: encoach_v2
User: yamenahmad
Password: (none)

Quick access:

PGBIN="/Users/yamenahmad/micromamba/envs/odoo19/bin"
"$PGBIN/psql" -U yamenahmad -d encoach_v2

16. QA Test Report — 2026-04-18

A full end-to-end browser test was executed across all three user types (admin, teacher, student), driven by an autonomous browser agent. 32 distinct pages were verified plus an exam session flow.

16.1 Backend API Health — 32/33 endpoints healthy (pre-fix)

All tested with admin JWT unless noted:

Category Endpoints Tested Status
Auth /api/login, /api/user
Users & Roles /api/users/list, /api/users/with-roles, /api/roles, /api/permissions, /api/authority-matrix, /api/user-authority-matrix
Entities /api/entities, /api/entities/<id>/roles
LMS Core /api/courses, /api/students, /api/teachers, /api/batches, /api/batches/<id>/students
Taxonomy /api/subjects, /api/domains, /api/topics, /api/learning-objectives
Resources /api/resources, /api/resource-tags
Exams /api/exam/custom/list, /api/exam/templates, /api/exam-structures, /api/exam-schedules, /api/rubrics, /api/rubric-groups
Assignments /api/assignments
Stats /api/stats, /api/stats/performance, /api/analytics/student, /api/adaptive/dashboard
Courseware /api/courses/<id>/chapters, /api/courses/<id>/completion, /api/materials/search
Grading /api/grading/queue
Student role /api/student/my-courses, /api/student/my-exams, /api/assignments
Approvals /api/approval-workflows, /api/approval-requests, /api/approval-users (after fix)

16.2 Bugs Fixed During This Pass

Bug #1: /api/approval-workflows returned 500

  • Cause: encoach_exam_template/controllers/approval_workflows.py ran raw SQL against tables (encoach_approval_workflow, encoach_approval_request) that are only created by the encoach_approval module — which lives in new_project/ and is NOT installed in the active backend/.
  • Fix: Added a _table_exists(cr, name) helper using to_regclass(). Both list_workflows and list_requests now return {items: [], total: 0} gracefully when the table is absent, matching the frontend's empty-state expectation.

Bug #2: DELETE /api/courses/<id> returned opaque 500 for courses with enrollments

  • Cause: op.course has a RESTRICT FK from op_student_course. Postgres raised a raw violation with no user-friendly handling.
  • Fix: delete_course in encoach_lms_api/controllers/lms_core.py now (a) detects enrollments first and returns a 409 with a clear message, (b) accepts ?force=true to cascade-delete enrollments and detach linked batches before removing the course.

16.3 Admin Workflows — All Pages Pass

Page Route Result
Dashboard /admin/dashboard Stats cards, AI tips, platform insights render
Users /admin/users 7 users shown with roles, filter tabs work
Entities /admin/entities Demo Academy entity, 6 users / 5 roles
Classrooms /admin/classrooms IELTS 2026 Spring (3 students), student dialog works
User Roles /admin/user-roles 7 users, 5 available roles, assignments rendered
Roles & Permissions /admin/roles-permissions 5 roles × 115 permissions × 22 categories
Authority Matrix /admin/authority-matrix 257 granted cells, 45 % coverage, CSV export present
Courses /admin/courses 3 courses, enroll-students dialog with BOTH "Individual" and "By Classroom" tabs verified
Taxonomy /admin/taxonomy 3 subjects (English, Math, IT) with full tree
Resources /admin/resources 15 items (6 library + 9 course materials), filters & cascade pickers work
Exams List /admin/examsList 16 custom exams, search filters correctly
Generation /admin/generation Loads; Official/Practice tabs work
Rubrics /admin/rubrics 3 rubrics + groups tab
Assignments /admin/assignments 2 assignments, filter tabs work
Approval Workflows /admin/approval-workflows Empty-state OK, Create dialog opens

16.4 Teacher Workflows — All Pages Pass

Logged in as khalid@encoach.test (Dr. Khalid). Tested 11 pages; all work:

  • /teacher/dashboard — 3 Active Courses, 55 Total Students, 0 Pending Grading, 82 % Avg Pass Rate
  • /teacher/courses — 3 course cards with Edit / Chapters / AI Workbench buttons
  • /teacher/students — 3 students with batch info, attendance, AI risk indicators
  • /teacher/library — 15 resources, upload dialog's taxonomy cascade confirmed working
  • /teacher/assignments — empty state handled gracefully
  • /teacher/courses/3/chapters — empty state + "Add Chapter" dialog opens with Name/Description/Start Date/Unlock Mode
  • /teacher/attendance — batch selector + trend chart + AI warning banners
  • /teacher/timetable — weekly grid 08:0017:00
  • /teacher/discussions / /teacher/announcements / /teacher/profile — all load cleanly

16.5 Student Workflows — All Pages Pass

Logged in as sarah@encoach.test (password student123). Tested 16 pages + exam session; all work:

  • /student/dashboard — 67 % overall progress, 3 courses, 4 upcoming exams popup
  • /student/courses — 3 cards with progress (100 %, 100 %, 0 %)
  • /student/courses/1 — course detail with "Chapters (1) / Grades (0) / About" tabs + "Take Post-Test"
  • /student/courses/1/chapters/1 — 3/3 materials completed (IELTS Listening video, Reading article, Grammar docs)
  • /student/assignments / /student/grades / /student/attendance / /student/timetable — all load with graceful empty or populated states
  • /student/profile — editable name, email, password change
  • /student/discussions / /student/announcements / /student/messages / /student/journey — empty states clean
  • /student/subjects — 3 subjects with "Start Diagnostic" CTAs
  • /student/subject-registration — registration table renders

Exam flow/student/exam/2/session loaded "IELTS Academic Mock - Spring 2026 Full Test" with 170-minute timer, Listening section, 7 questions, MCQ with 4 radio options, Previous / Flag / Next navigation. Clean and functional.

16.6 Demo Data State (Post-Cleanup)

Entity Count Notes
Users 7 2 admins, 2 teachers, 3 students
Courses 3 IELTS Preparation B2, Python Programming Fundamentals, IELTS Listening & Speaking Intensive (junk "tttttt" removed, "test" renamed)
Batches 1 IELTS 2026 Spring (3 students)
Exams (custom) 16 published + draft mix
Resources 6 library resources; +9 course materials
Taxonomy subjects 3 English (6d/37t), Mathematics (6d/15t), IT (6d/17t) — junk "math" subject removed

16.7 Route Naming Gotchas

Two frontend routes differ from the pattern a developer might guess:

Expected Actual
/admin/exams /admin/examsList
/admin/roles /admin/user-roles

The Admin Pages table in §14 has been updated with the full correct route list.

16.8 Minor Observations (Non-Blocking)

  • React Router v7 future flag warnings in console on every page (v7_startTransition, v7_relativeSplatPath) — cosmetic only, ignore until v7 migration.
  • forwardRef warning in Badge component — dev-only, not a runtime error.
  • /api/scores by itself returns 404, but its sub-endpoints (/api/scores/release etc.) are valid.
  • Session occasionally expired during long admin test runs, requiring re-login. Token TTL may warrant increasing for QA flows.

17. Institutional Admin Pages — Write-Flow QA — 2026-04-18

Comprehensive API-level CREATE / EDIT / DELETE / WORKFLOW sweep across all 14 institutional admin pages (see §14 "Admin — Institutional Pages"). Driven by test_write_flows.py against real Odoo data seeded by seed_institutional.py.

Final result: SUMMARY PASS=44 FAIL=0 TOTAL=44

17.1 Scenarios covered

Page Actions verified
Academic Years CREATE, generate-terms (3), EDIT, DELETE (cascades terms)
Academic Terms EDIT, DELETE
Departments CREATE, EDIT, DELETE
Admission Registers CREATE, confirm, DELETE
Admissions CREATE (date-constrained to register window), DELETE
Inst Exam Sessions CREATE, schedule, EDIT, DELETE
Marksheets CREATE template, generate-marksheets, validate marksheet, DELETE template (cascades registers)
Facilities CREATE, EDIT, DELETE
Assets CREATE, DELETE (backed by new encoach.asset model)
Activities CREATE type, CREATE activity, DELETE
Library CREATE media + authors, VERIFY author link, DELETE
Lessons CREATE, EDIT (preserves course_id + batch_id), DELETE
Gradebook CREATE grading-assignment, DELETE, drill lines
Student Progress DRILL detail
Student Leaves CREATE, approve, DELETE
Fees LIST seeded, create-invoice

17.2 Backend fixes applied

1. encoach.asset — new lightweight model op.asset does not exist in the installed OpenEduCat set. Added encoach_lms_api/models/asset.py with name, code, facility_id, description, active; registered in __init__.py; CRUD access granted via security/ir.model.access.csv. facilities.py now uses _asset_model()env['encoach.asset'].

2. Missing DELETE routes added

  • DELETE /api/admission-registers/<rid>
  • DELETE /api/admissions/<aid>
  • DELETE /api/result-templates/<tid> (cascades op.marksheet.register rows created by generate_result)
  • DELETE /api/student-leaves/<lid>
  • DELETE /api/academic-years/<yid> now pre-unlinks academic_term_ids to avoid the op_academic_term.academic_year_id FK RESTRICT violation.

3. Missing detail (GET-one) endpoints added

  • GET /api/lessons/<lid> — exposes full lesson incl. course_id, batch_id so EDIT can verify those are preserved.
  • GET /api/library/media/<mid> — returns author_ids + author_names (the list endpoint only returns a comma-separated author string).

4. Admissions CREATE — defaults + validation create_admission now defaults application_date = fields.Date.today(), builds name from first_name + last_name, and accepts nationality_id or nationality. The Odoo constraint @api.constrains(...) requires application_date ∈ [register.start_date, register.end_date] — the test payload uses 2028-06-15 to sit inside the seeded register window. Added companion PATCH/PUT /api/admissions/<aid> for edits.

5. Grading assignments — required fields resolved create_grading_assignment no longer sends user_id (invalid on grading.assignment in this OpenEduCat version). Auto-resolves the required fields:

  • assignment_type: find-or-create grading.assignment.type with code DEFAULT
  • faculty_id: first available op.faculty
  • issued_date: defaults to fields.Datetime.now()

6. Marksheets generate — Odoo 19 compatibility Route aliased as /api/result-templates/<tid>/generate and /generate-marksheets. Controller tries rec.generate_result() (OpenEduCat 19) then falls back to rec.action_generate_marksheet() (older versions).

7. Fees create-invoice — Odoo 19 JSONB properties OpenEduCat's op.student.fees.details.get_invoice() raised UserError: There is no income account defined for this product. Root cause: Odoo 19 removed ir_property and stores company-dependent fields (like property_account_income_id) as JSONB columns on the model itself — e.g. product_template.property_account_income_id is now jsonb keyed by company_id.

Fix: create_invoice auto-wires a default income account before invoking get_invoice():

            if not (fp or {}).get('income'):
                AA = request.env['account.account'].sudo()
                # Odoo 19: account.account has a M2M `company_ids`.
                try:
                    income_acc = AA.search([
                        ('account_type', '=', 'income'),
                        ('company_ids', 'in', request.env.company.id),
                    ], limit=1)
                except Exception:
                    income_acc = AA.browse()
                if not income_acc:
                    income_acc = AA.search([('account_type', '=', 'income')], limit=1)
                if income_acc:
                    try:
                        prod_tmpl.with_company(request.env.company).write({
                            'property_account_income_id': income_acc.id,
                        })
                    except Exception:
                        _logger.exception('auto-wire income account')

The seed_institutional.py fee-product block now performs the same wiring at seed time, so fresh databases don't hit this error.

17.3 Seeding

seed_institutional.py populates the full institutional fixture set:

  • 1 academic year + auto-generated terms
  • 1 department
  • 1 admission register + 1 admission
  • 1 exam session + 1 result template → marksheets
  • Facilities, activities, library media, lessons, gradebooks, student leaves
  • Fees: 3 op.fees.terms (Full, 2-inst 50/50, 3-inst 34/33/33 summing to 100%), 1 product.product (IELTS Preparation B2 Fees) with income account wired, 3 op.student.fees.details records

Run:

cd /Users/yamenahmad/projects2026/odoo/odoo19
./run.sh --shell <<< "exec(open('seed_institutional.py').read())"

17.4 Running the write-flow test

cd /Users/yamenahmad/projects2026/odoo/odoo19
python3 test_write_flows.py

The script is idempotent — it uses timestamped names for academic-year/register payloads so repeated runs don't collide on Odoo's unique constraints. Expected output: SUMMARY PASS=44 FAIL=0 TOTAL=44.

17.5 Operational notes for Odoo 19 institutional data

  • product_template.property_account_income_id is JSONB keyed by company_id — write to it with .with_company(company).write({...}), not via the removed ir_property table.
  • op.admission has a constraint that application_date must fall inside the parent op.admission.register's [start_date, end_date] — front-end forms should default to register.start_date to avoid validation errors.
  • op.result.template.generate_result() creates op.marksheet.register rows; deleting a template without first unlinking those rows will fail. The DELETE endpoint handles this internally.
  • op.academic.yearop.academic.term FK is RESTRICT; always unlink terms before the year.
  • op.fees.terms.line_ids must sum to 100% (percentages). Seeded terms conform.

§18 Support Admin Pages — Backend + Write-Flow QA — 2026-04-18

18.1 Scope

The Support group in the admin sidebar is 3 pages:

Admin URL Component Before After (this session)
/admin/tickets TicketsPage Called /api/tickets which did not exist (404 HTML). Full backend — encoach.ticket model + /api/tickets CRUD + assignedToUser.
/admin/payment-record PaymentRecordPage Hard-coded mock arrays. Live data from op.student.fees.details + account.move + stubbed /api/paymob-orders.
/admin/settings-platform SettingsPage Hard-coded mock arrays. Codes tab → encoach.code, Packages tab → ir.config_parameter, Grading tab → ir.config_parameter.

18.2 New backend artefacts

Modelbackend/custom_addons/encoach_lms_api/models/ticket.py:

class EncoachTicket(models.Model):
    _name = 'encoach.ticket'
    _inherit = ['mail.thread']
    subject, description, type, status, source, page_context,
    reporter_id, assignee_id, entity_id, resolved_at

Registered in models/__init__.py and security/ir.model.access.csv (full CRUD for base.group_user).

Controllers — added three new files, all registered in controllers/__init__.py:

File Routes
controllers/tickets.py GET/POST /api/tickets, GET/PATCH/DELETE /api/tickets/<id>, GET /api/tickets/assignedToUser
controllers/payments.py GET /api/payment-records (derived from op.student.fees.details), GET /api/payment-records/invoices (from account.move out_invoice), GET /api/paymob-orders (stub, returns {data:[], message:"Paymob integration not configured."})
controllers/platform_settings.py GET/POST/DELETE /api/codes, POST /api/codes/generate, GET/POST/PATCH/DELETE /api/packages, GET/PATCH /api/grading-config

Packages and grading config are persisted in ir.config_parameter under the keys encoach.platform.packages and encoach.platform.grading — no new model table needed.

18.3 Frontend changes

File Change
frontend/src/services/payments.service.ts NEW — typed client for /api/payment-records + /api/paymob-orders.
frontend/src/services/platformSettings.service.ts NEW — typed client for codes, packages, grading-config.
frontend/src/pages/PaymentRecordPage.tsx Rewritten: 4 KPI cards + live Payments table + Paymob empty-state + CSV export. Removed all mock arrays.
frontend/src/pages/SettingsPage.tsx Rewritten: Codes tab with real Generate Single / Generate Batch and delete; Packages tab with Add Package dialog + delete; Grading tab with Save + client/server validation.
frontend/src/pages/TicketsPage.tsx Small fix: accept both data.items and data.data from the pagination envelope.

18.4 Seeding

seed_institutional.py now also seeds:

  • 4 tickets covering all 4 statuses (open, in_progress, resolved) and all main types (bug, feature, support, feedback), with realistic subjects / descriptions / page_context.
  • 4 registration codes (2 individual student, 2 corporate batch) — one already marked as used to exercise the badge variant.

18.5 Write-flow test — 29/29 PASS

test_support_flows.py exercises:

Page Actions verified
Tickets LIST seeded, filter by status + type, text search, CREATE, GET detail, EDIT → in_progress, EDIT → resolved (auto-stamps resolved_at), ASSIGN, assignedToUser listing, DELETE
Codes (Settings) LIST seeded, generate individual ×3, filter used=false, DELETE, generate corporate ×2
Packages (Settings) LIST defaults, CREATE, EDIT price+discount, DELETE, persistence roundtrip
Grading (Settings) GET, set new scale, server-side validation (rejects min >= max), restore IELTS default
Payment Record LIST + totals, filter paid=false, invoices list (account.move), Paymob stub returns empty

Run with:

cd /Users/yamenahmad/projects2026/odoo/odoo19
python3 test_support_flows.py

Latest output: SUMMARY PASS=29 FAIL=0 TOTAL=29.

18.6 Browser verification

All 3 pages were opened in a real browser (localhost:8080) after login as admin / admin and visually verified:

  • Tickets — 4 seeded rows visible, reporter = "Administrator", Create Ticket dialog submits and a new row appears immediately.
  • Payment Record — 4 KPI cards (Total records, Paid, Unpaid, Total amount), 3 payment rows for the seeded students, Paymob tab shows the empty-state message.
  • Settings — Codes tab lists all seeded + generated codes with Generate Single working and a success toast; Packages tab shows the 3 default cards; Grading tab shows 0..9 step 0.5 and Save returns a success toast.

No critical console errors — only React Router v7 future-flag warnings.

18.7 Operational notes

  • The encoach.ticket model inherits mail.thread so OCA-style change tracking works out of the box. Adding followers / comments will work without further wiring.
  • The /api/payment-records endpoint is derived (not stored) — it reflects whatever is in op.student.fees.details. Creating an invoice from the Fees page will flip the paid badge automatically on the Payment Record page once the invoice is paid.
  • /api/paymob-orders is a deliberate empty stub. When real Paymob integration is wired, replace the stub body in controllers/payments.py::list_paymob_orders — no UI change required; the Payment Record page will light up automatically.
  • Packages and grading config live in ir.config_parameter. To reset to defaults, delete the two parameters encoach.platform.packages and encoach.platform.grading.

§19 Training Admin Pages — Backend + Write-Flow QA — 2026-04-18

19.1 Scope

The Training group in the admin sidebar is 2 pages:

Admin URL Component Before After (this session)
/admin/training/vocabulary VocabularyPage Hard-coded vocabItems array (no API). Full backend: CRUD + search + level filter + per-user progress tracking.
/admin/training/grammar GrammarPage Hard-coded grammarItems array (no API). Full backend: CRUD + search + level filter + per-user progress tracking.

19.2 New backend artefacts

Modelsbackend/custom_addons/encoach_lms_api/models/training.py:

Model Key fields
encoach.vocab.item word (unique), meaning, example_sentence, level (CEFR A1C2), part_of_speech, category, active, computed learners_count + completion_count
encoach.vocab.progress user_id, vocab_id, completed, mastery (learning/familiar/mastered), last_reviewed, review_count — unique per (user, vocab)
encoach.grammar.rule name (unique), description, example, level, category, active, computed stats
encoach.grammar.progress user_id, rule_id, completed, last_reviewed, review_count — unique per (user, rule)

All four models are registered in models/__init__.py and security/ir.model.access.csv (full CRUD for base.group_user). Uniqueness is enforced via @api.constrains instead of _sql_constraints — Odoo 19 deprecated the latter (see operational notes below).

Controllerbackend/custom_addons/encoach_lms_api/controllers/training.py:

Method / Path Verb Purpose
/api/training/vocabulary GET List with filters: level, category, active, search, pagination. Returns items, total, and a summary block (total, completed, remaining, completion_rate) relative to the authenticated user.
/api/training/vocabulary POST Create (word + meaning required, unique word).
/api/training/vocabulary/<id> GET Fetch a single item with the caller's progress merged in.
/api/training/vocabulary/<id> PATCH Update any mutable field.
/api/training/vocabulary/<id> DELETE Delete (cascades progress).
/api/training/vocabulary/<id>/progress POST Mark completed / set mastery for the authenticated user (creates the progress row if absent, increments review_count).
/api/training/grammar (+ same suffixes) Analogous five endpoints for grammar rules plus /progress.

ValidationError and UserError raised from the model layer now surface as HTTP 400 (not 500), so the frontend can show user-friendly toasts.

19.3 Frontend changes

File Change
frontend/src/services/training.service.ts NEW — typed client for both vocabulary and grammar (list, CRUD, progress).
frontend/src/pages/VocabularyPage.tsx Rewritten. Now: progress summary card with live % complete, search + level filter, checkable list rows, Add Word dialog with CEFR + part-of-speech, delete button per row. AI panel text is dynamic based on summary.remaining.
frontend/src/pages/GrammarPage.tsx Rewritten. Same pattern as vocabulary but tailored to grammar (rule name, description, example, category).

19.4 Seeding

seed_institutional.py now seeds:

  • 10 vocabulary items spanning B1C1, with realistic meanings and example sentences (Ubiquitous, Pragmatic, Eloquent, Meticulous, Ambiguous, Coherent, Versatile, Concise, Collaborate, Rationale).
  • 8 grammar rules spanning A2C1 (Conditionals Type 2/3, Passive Voice, Relative Clauses, Subject-Verb Agreement, Modal Speculation, Reported Speech, Articles, Present Perfect vs Past Simple).
  • 6 progress rows for the admin user (3 vocab mastered, 3 grammar completed) so the UI always shows non-zero state on a freshly seeded DB.

Run with:

.conda-envs/odoo19/bin/python odoo/odoo-bin shell -c odoo.conf --no-http <<'EOF'
exec(open('seed_institutional.py').read())
EOF

The seeding block is idempotent — re-running it never creates duplicates (each row checks by unique key first).

19.5 Write-flow test — 26/26 PASS

test_training_flows.py exercises:

Page Actions verified
Vocabulary LIST seeded, filter by level=B2, search eloqu, CREATE, duplicate-word rejection, missing-meaning rejection, GET detail, EDIT level+meaning, PROGRESS mark completed, summary recomputation after completion, PROGRESS un-mark, DELETE, GET-after-DELETE → 404
Grammar LIST seeded, filter by level=B1, search passive, CREATE, missing-description rejection, GET detail, EDIT level+category, PROGRESS mark / un-mark, DELETE, GET-after-DELETE → 404
Pagination envelope size=3 page=1 (vocab), size=3 page=2 (grammar) — confirms page + size are echoed in the response

Run with:

cd /Users/yamenahmad/projects2026/odoo/odoo19
python3 test_training_flows.py

Latest output: SUMMARY PASS=26 FAIL=0 TOTAL=26.

19.6 Browser verification

Both admin pages were opened in a real browser (localhost:8080) after login as admin / admin:

  • Vocabulary — progress card shows live X/N completed (Y%) with a progress bar; clicking the circle next to a row marks it complete and the summary instantly recomputes; search "elo" narrows the list to just Eloquent; the Add Word dialog creates a new row + shows a toast.
  • Grammar — all 8 seeded rules visible (A2C1), progress tracking works identically; Add Rule dialog creates a new row.

No functional errors in the console — only React Router v7 future-flag warnings.

19.7 Operational notes for Odoo 19 training data

  • _sql_constraints is deprecated in Odoo 19 (the registry emits Model attribute '_sql_constraints' is no longer supported). Uniqueness and range checks in these models are therefore implemented with @api.constrains, which continues to work on both Odoo 18 and 19.
  • Because both progress models have ondelete='cascade' back to the library item, deleting a vocabulary/grammar row automatically cleans up every learner's progress row — no orphan rows.
  • Progress rows are keyed to res.users.id, not op.student. Any authenticated API user (admin, teacher, student) sees only their own completions in the summary block, while learners_count / completion_count on the library item itself aggregate across all users.
  • The /api/training/<kind>/<id>/progress endpoint is upsert-like: the first call creates the progress row, subsequent calls update it and bump review_count. This lets the frontend treat it as "toggle" without worrying about a missing row.

20. Reports Section (2026-04-19)

Before this pass, the three pages under the sidebar "Reports" group were pure mock UIs — hardcoded JavaScript arrays in each component, non-functional filters, no network requests. They now aggregate real data from the attempt pipeline.

20.1 Scope

Page URL Before After
Student Performance /admin/student-performance 6 hardcoded students ("Sarah Johnson", "Ahmed Hassan"…) Per-student band averages from encoach.student.attempt, CEFR level derived from the latest reportable attempt, entity + level + search filters, CSV export, three KPI cards
Stats Corporate /admin/stats-corporate Hardcoded bar/line/pie data Four tabs (Overview, Trends, Distribution, Comparison) driven by a single /api/reports/stats-corporate rollup — bar chart of module averages (× 10), six-month trend, CEFR distribution pie, and entity comparison bar. Threshold + entity + months filters all hit the backend.
Record /admin/record Four hardcoded rows Paginated per-user attempt history with search / entity / user / period filters, real exam codes (EX-###), duration derived from started_atcompleted_at, CSV export

All three pages share a single /api/reports/filters endpoint that returns the entities and students that actually have attempts, so dropdowns never offer filters that would produce empty results.

20.2 Backend artifacts

New controllerbackend/custom_addons/encoach_lms_api/controllers/reports.py:

Route Method Purpose
/api/reports/student-performance GET One row per student. Aggregates per-skill bands across all their reportable attempts, takes the CEFR level from the most recent attempt (or derives it from overall band). Filters: entity_id, level, search, since.
/api/reports/stats-corporate GET Corporate rollups: by_module (4 module averages × 10 for the bar chart), trend (last N months, default 6), distribution (CEFR bins with palette colors), comparison (per-entity average × 10). Filters: entity_id, threshold (0-100), months, since.
/api/reports/record GET Paginated attempt history. Same shape as {items, total, page, size}. Filters: user_id, entity_id, period=day|week|month, status, page, size. Emits a normalized status_label and an EX-### exam code.
/api/reports/filters GET Lightweight picker payload: {entities: [...], students: [...]} — only students that have at least one attempt, only entities that exist.

An attempt is considered reportable if status ∈ {completed, scoring, scored, released}. In-progress attempts are skipped from aggregates so a student with only a half-finished exam doesn't show up with zeros.

CEFR labels are normalized to canonical display casing (Pre-A1 / A1 / … / C2) via _normalize_cefr, and falls back to band-based bucketing via _cefr_for_band when cefr_level is null on the attempt.

No new models were needed — the Reports section only reads from:

  • encoach.student.attempt (overall / per-skill bands, cefr_level, entity_id, started/completed timestamps)
  • encoach.exam.custom (exam title — note: this model uses title, not name, which tripped the first version of the controller)
  • encoach.exam.assignment (optional, to link an attempt back to its assignment when one exists)
  • encoach.entity, res.users

20.3 Frontend artifacts

New servicefrontend/src/services/reports.service.ts:

reportsService.studentPerformance({ entity_id, level, search, since })
reportsService.statsCorporate({ entity_id, threshold, months, since })
reportsService.record({ user_id, entity_id, period, status, page, size })
reportsService.filters()

Typed interfaces: StudentPerformanceRow, StatsCorporateResponse (with nested StatsModuleRow / StatsTrendRow / StatsDistributionRow / StatsComparisonRow), RecordRow, ReportsFiltersResponse.

Rewritten pages:

  • frontend/src/pages/StudentPerformancePage.tsx — dropped all hardcoded arrays; added three KPI cards (students tracked, avg overall, top performer level), a search input, Entity select populated from /api/reports/filters, Level select (all / A1 / A2 / B1 / B2 / C1 / C2), CSV export, loading + empty states. Per-row AI Grade Explainer retained.
  • frontend/src/pages/StatsCorporatePage.tsx — dropped all hardcoded chart data; wired every tab to the stats-corporate payload. Added loading + empty states. Threshold buttons (0/50/70/90%) now hit the backend. Comparison tab now renders a real entity bar chart instead of a placeholder panel. AI narrative bubbles receive live data.
  • frontend/src/pages/RecordPage.tsx — dropped all hardcoded records; wired filters (entity, user, period) + CSV export. Status badge variant adapts to completed / released / scored / in_progress / scoring.

20.4 Seeding — seed_reports.py (idempotent)

The live database had only 12 attempts, most of them in-progress with no band scores, so the Reports pages would have shown empty charts. The new script:

  1. Ensures three entities exist: Acme Corp (ACME), Global Ltd (GLOBAL), Tech Co (TECHCO) — using code (required NOT NULL on encoach.entity) as the idempotency key.
  2. Walks every existing in_progress attempt and assigns it a plausible score matrix from BAND_MATRIX, marks it completed, stamps completed_at = started_at + 120 min, and parks it in one of the three entities if it had none.
  3. Creates up to 6 months × 4 students = 24 additional historical attempts (with a random 30% skip to keep the trend line jagged), using a small monthly delta so the trend chart rises over time. Each new row is guarded by a 6-day proximity lookup so re-running only fills gaps.
  4. Commits once at the end and prints the final reportable-attempt count.

Result after the first run: 28 reportable attempts spread across 3 entities, 4 students, and 6 months — enough to populate every chart.

Run with:

cd /Users/yamenahmad/projects2026/odoo/odoo19/odoo
../.conda-envs/odoo19/bin/python odoo-bin shell -c ../odoo.conf --no-http --stop-after-init < ../seed_reports.py

20.5 Smoke test — 25/25 PASS

test_reports_flows.py exercises all four endpoints:

Endpoint Checks
/api/reports/filters GET returns {entities: [...], students: [...]} with at least one of each
/api/reports/student-performance LIST returns rows, row shape complete, overall numeric, CEFR level is a known label, search filter narrows to the matching student, level filter returns only that level, entity_id filter narrows rows
/api/reports/stats-corporate Envelope has by_module + trend + distribution + comparison + meta; by_module has the 4 IELTS modules; trend length == months (default 6, also tested with 3); distribution has per-level color; threshold=70 drops attempts considered (28 → 14); entity filter narrows comparison; months=3 shortens trend
/api/reports/record Paginated list, row shape complete, exam_code is EX-###, status_label is titlecased, user_id filter scopes to that student only, entity_id filter scopes to that entity, period=month returns the in-window subset, pagination size=5 page=1 + page=2 both work

Latest output: Summary: 25 passed, 0 failed, 25 total.

Regressions also re-run on this pass: Configuration 24/24, Support 29/29, Training 26/26 — all still green.

20.6 Browser verification

Logged in as admin / admin on localhost:8080 and walked all three pages:

  • Student Performance — KPI cards populated (5 students tracked, 6.3 average band), table shows real students (Sarah Ahmed, Omar Khan, Layla Nasser, TestUser WriteFlow, Admin User), Level A2 filter narrowed to 2 rows, Entity dropdown showed real entities, CSV export triggered a download. No console errors.
  • Stats Corporate — Heading confirmed (28 scored attempts). Overview bar chart shows 4 modules with values ~6472. Trends line chart has 6 monthly points OctApr peaking at ~85. Distribution pie: C1 11 / B2 6 / B1 5 / A2 4. Comparison bar: Global Ltd ~81, Acme Corp ~63, Tech Co ~62. AI Summary bubbles quote the real numbers. All filters responsive.
  • Record — Heading (28 attempts), table shows real student names / exam codes (EX-001, EX-005, EX-017…) / dates / scores / statuses. User filter scoped the list to that student; CSV export worked.

Network tab showed /api/reports/filters 200, /api/reports/record?size=100 200, /api/reports/student-performance 200, /api/reports/stats-corporate 200 — zero 4xx/5xx.

20.7 Gotchas fixed during this pass

  • encoach.exam.custom uses title, not name. The first version of the Record endpoint threw AttributeError: 'encoach.exam.custom' object has no attribute 'name'. The controller now uses getattr(exam, 'title', None) or getattr(exam, 'display_name', '') so it survives an upstream rename.
  • encoach.entity has a NOT NULL code column. seed_reports.py originally tried to create entities with just {'name': ...} and hit a NotNullViolation. The seeder now passes (name, code) and also looks up by code when probing for existing rows.
  • Both encoach_exam_template and encoach_scoring declare encoach.student.attempt. The Reports controller only reads fields that exist on both definitions (listening/reading/writing/speaking/overall_band, cefr_level, status, started_at, and whichever of completed_at / finished_at is present — _attempt_completed_at() probes both). No field access assumes a specific addon version.
  • In-progress attempts are excluded from the aggregates. If you ever notice a student you expect to see missing from Student Performance or Stats Corporate, check their attempt status — in_progress attempts are deliberately skipped. They DO still appear in the Record page (which shows everything) unless you also pass status or period filters.

§21 — Hardening Release (Phase 2/3 completion)

This section records the final wave of work that closed out all remaining items in the Phase 2 (performance & reliability) and Phase 3 (core AI features, accessibility, onboarding) roadmaps. After this release the platform is considered deployment-ready.

21.1 What shipped

The release is organised by capability, not file path.

Human-in-the-loop AI quality loop (P3.3 → P3.5)

Stage Where What it does
1. Quality gate encoach_exam_template.services.quality_checker + ielts_validator Runs after every AI exam submit; if scores fall below thresholds the exam is parked in pending_review instead of published.
2. Review queue encoach_exam_template/controllers/review_workflow.py + frontend/src/pages/admin/ExamReview{Queue,Detail}.tsx Admin-only pages list pending exams, surface aggregate + per-question quality reports, and let a reviewer approve (→ published) or reject (→ draft) with audit notes. Writes to new reviewed_by_id, reviewed_at, review_notes fields.
3. Prompt library encoach_ai.models.ai_prompt + controllers/prompt_controller.py + frontend/src/pages/admin/AIPromptEditor.tsx encoach.ai.prompt with a unique (key, version) key, one active row per key. Authors can add a new version, activate it (auto-deactivates prior), and dry-run render with sample variables. Renderer uses str.format_map against a safe dict.
4. Student feedback encoach_ai.models.ai_feedback + controllers/feedback_controller.py + frontend/src/components/AIFeedbackButtons.tsx + frontend/src/pages/admin/AIFeedbackTriage.tsx Thumbs up/down on any AI artefact (question, coach, explanation, translation, narrative, other). Unique (user, subject_type, subject_id) constraint gives upsert semantics; thumbs-down requires a comment. Admin triage page lists/filters/resolves feedback (`open → acknowledged

Together these four stages mean every AI output can be gated, reviewed, iterated, and measured — closing the loop from prompt → generation → delivery → feedback → next prompt revision.

Compliance (P3.2)

New encoach_api/controllers/gdpr.py + models/gdpr_erasure.py:

  • GET /api/gdpr/export — JSON dump of the calling user's profile, entity memberships, exam attempts, answers, AI feedback, AI calls, tickets, coaching sessions.
  • POST /api/gdpr/delete — requires {"confirm": true}; anonymises the partner PII, deletes personal feedback + coaching transcripts, strips free-text PII from retained exam answers, nulls user_id on AI logs, deactivates the res.users row, and writes a tombstone to encoach.gdpr.erasure.request for audit. Admin accounts are blocked from self-erasure.
  • frontend/src/pages/PrivacyCenter.tsx — routed at /student/privacy, /teacher/privacy, /admin/privacy; download-my-data button produces a timestamped JSON file; erase flow is gated behind a type "DELETE" to confirm alert dialog.

Paymob real checkout (P2.6)

New encoach_lms_api/controllers/paymob.py + models/paymob_order.py:

  • POST /api/payments/paymob/checkout runs the three-step Paymob Accept flow (auth → order → payment key) and returns the hosted iframe URL. Our encoach.paymob.order row is created before the external call so partial failures don't lose orders.
  • POST /api/payments/paymob/webhook verifies the HMAC-SHA512 signature over Paymob's canonical field sequence (amount_cents|created_at|currency|…|success). Mismatches return 401; duplicates are idempotent; unknown orders return 200 to stop Paymob retrying.
  • Credentials read at request time from ir.config_parameter (encoach.paymob.api_key, hmac_secret, integration_id, iframe_id) so operators can rotate without restarting Odoo.
  • GET /api/paymob-orders now reads real rows instead of returning [].

Internationalisation (P3.1)

  • frontend/src/i18n/ bootstraps i18next + i18next-browser-languagedetector with nested feature-scoped namespaces.
  • Initial locales: en (source of truth, typed with Translations interface) and ar (full translation of the same keys).
  • RTL handling: when the active language is in RTL_LANGS, document.documentElement.dir is flipped to rtl so Tailwind utilities and Radix primitives adapt.
  • frontend/src/components/LanguageToggle.tsx added next to the theme toggle in both AdminLmsLayout and RoleLayout headers.
  • Loaded via main.tsx side-effect import so every route picks it up.

CI scaffolding (P3.8)

  • .github/workflows/ci.yml with two jobs:
    • frontend: tsc --noEmiteslintvite build → Playwright smoke tests against the Vite preview server; uploads a Playwright HTML report on failure.
    • backend: spins up Postgres 16 as a service and runs the Odoo 19 container with --test-enable --test-tags encoach_api against a throwaway encoach_ci database.
  • backend/custom_addons/encoach_api/tests/test_health.py — first HttpCase: asserts /api/health returns 200 and /api/health/ready returns 200 or 503 (never 5xx).
  • frontend/playwright.config.ts + frontend/e2e/login.spec.ts — the login page renders the sign-in form; / redirects unauthenticated traffic to /login.
  • New npm scripts: test:e2e, test:e2e:install.

21.2 New DB tables (summary)

Table Purpose Added in
encoach.ai.prompt Versioned prompt templates (key, version, is_active, content). P3.4
encoach.ai.feedback Student thumbs up/down on AI output, with triage status. P3.5
encoach.gdpr.erasure.request Tombstone row per right-to-erasure request. P3.2
encoach.paymob.order Full lifecycle of every Paymob checkout, including the verified HMAC. P2.6

All have per-group ir.model.access.csv entries: admin read/write, authenticated-user read for metadata, create-only on feedback.

21.3 New REST endpoints

Everything requires a valid JWT unless noted; write routes additionally check base.group_system where relevant.

GET  /api/ai/prompts                          list keys (latest per key)
GET  /api/ai/prompts/:key/versions            full history for a key
GET  /api/ai/prompts/:id                      one version (full content)
POST /api/ai/prompts                          create new version (admin)
POST /api/ai/prompts/:id/activate             flip active pointer (admin)
POST /api/ai/prompts/:id/render               dry-run render with sample vars

POST /api/ai/feedback                         upsert (user)
GET  /api/ai/feedback/summary                 up/down counts + my rating
GET  /api/ai/feedback                         admin triage list
POST /api/ai/feedback/:id/resolve             admin triage resolve

GET  /api/gdpr/export                         full data-subject export (self)
POST /api/gdpr/delete                         right-to-erasure (self, confirm=true)

POST /api/payments/paymob/checkout            start a checkout
POST /api/payments/paymob/webhook             HMAC-verified callback (public)
GET  /api/payments/paymob/orders              list my orders
GET  /api/paymob-orders                       admin + self order list (rewired)

21.4 Frontend surface (new routes)

/admin/exam/review-queue      → ExamReviewQueue.tsx
/admin/exam/review/:examId    → ExamReviewDetail.tsx
/admin/ai/prompts             → AIPromptEditor.tsx
/admin/ai/feedback            → AIFeedbackTriage.tsx
/student|teacher|admin/privacy → PrivacyCenter.tsx

Plus the AIFeedbackButtons primitive (drop-in <ThumbsUp/>/<ThumbsDown/> that handles the upsert, dialog, and comment prompt) and LanguageToggle in both main layouts.

21.5 Verification

  • python3 -m compileall -q backend/custom_addons/encoach_ai backend/custom_addons/encoach_api backend/custom_addons/encoach_lms_api — clean.
  • npx tsc --noEmit -p tsconfig.app.json — clean.
  • npm run build — succeeds; bundle analyser shows the same split profile as the §20 release (initial JS ≈ 250 KB gzipped on the login path); new features are lazy-loaded chunks.
  • New modules do not depend on optional addons: all cross-model reads use if model in request.env guards so missing addons degrade gracefully instead of 500-ing.

21.6 Configuration operators need to set

Before the Paymob flow will actually charge anything, set in Settings → Technical → System Parameters:

encoach.paymob.api_key        = <merchant API key>
encoach.paymob.hmac_secret    = <from Paymob dashboard>
encoach.paymob.integration_id = <default integration (card)>
encoach.paymob.iframe_id      = <hosted iframe id>

Until those are set, POST /api/payments/paymob/checkout returns 503 with a descriptive message instead of silently crashing.

21.7 Deferred (known)

  • P1.2 — blanket-sudo audit + entity-isolation ir.rules. Needs a coordinated frontend + data migration pass; deferred to a follow-up release. Current security model relies on JWT identity + controller-level has_group checks.
  • Broader i18n coverage. Only the keys listed in src/i18n/locales/en.ts are translated today — extending to every page is a rolling job that can happen in small PRs now that the plumbing is in place.
  • Playwright coverage is intentionally minimal (login + redirect); it exists as a safety net, not as full end-to-end coverage.