EduTrack Online - SiteOwner API

Version: 1.3 Date: July 16, 2026 Target Audience: SiteOwner Frontend Backend: Django REST Framework


Table of Contents

  1. Teacher Management
  2. Student Management
  3. Course Management
  4. View Assistants (Read-Only)
  5. Recharge Codes (Phase 2+)
  6. Revenue Analytics (Phase 2+)
  7. SiteOwner Dashboard (Phase 2+)

1. Teacher Management

GET /accounts/teachers/

Description: List all teachers with full details.

Authentication: SiteOwner

Query Parameters:

Parameter Type Description
is_active Boolean Filter by active status
subject Integer Filter by subject ID
grades Integer Filter by grade ID
search String Search by name
all String Set to true to bypass pagination

Success Response — 200 OK:

{ "count": "Integer", "next": "String (URL) | null", "previous": "String (URL) | null", "results": [ { "id": "Integer", "name": "String", "profile_picture": "String (URL) | null", "subject_name": "String — Primary subject name", "subject_names": "Array[String] — All subject names", "grade_names": "Array[String]", "is_active": "Boolean", "can_access_course": "Boolean", "courses_count": "Integer", "students_count": "Integer" } ] }

List view is lightweight. To get full teacher details (phone, gmail, gender, profile_picture, biography, facebook, subjects_detail, grade_ids, video_security, bunny_collection_id), use GET /accounts/teachers/<id>/.


GET /accounts/teachers/<id>/

Authentication: SiteOwner

Success Response — 200 OK:

{ "id": "Integer", "user_id": "Integer", "username": "String", "name": "String", "phone": "String", "secondary_phone": "String | null", "gmail": "String", "gender": "String (male|female)", "profile_picture": "String (URL) | null", "biography": "String | null", "facebook": "String | null", "subject_id": "Integer | null — Primary subject ID", "subject_ids": "Array[Integer] — All subject IDs", "subject_detail": { "id": "Integer", "name": "String" }, "subjects_detail": [ {"id": "Integer", "name": "String"} ], "grade_ids": "Array[Integer]", "grades_detail": [{"id": "Integer", "name": "String"}], "video_security_id": "Integer | null", "video_security_detail": { "id": "Integer", "name": "String", "price_per_student": "String (Decimal)" }, "bunny_collection_id": "String | null — Bunny Stream collection GUID assigned to this teacher", "is_active": "Boolean", "can_access_course": "Boolean", "courses_count": "Integer", "students_count": "Integer", "created_at": "DateTime (ISO 8601)" }

POST /accounts/teachers/

Description: Create a new teacher user and profile atomically. Supports profile picture upload via multipart/form-data.

Authentication: SiteOwner

Content-Type: multipart/form-data

Request Body:

{ "username": "String (Required)", "password": "String (Required) — Minimum 8 characters", "name": "String (Required)", "phone": "String (Required) — Egyptian format: 010/011/012/015 + 8 digits", "secondary_phone": "String (Optional) — Egyptian format", "gmail": "String (Required) — Must be globally unique", "gender": "String (Required) — male|female", "profile_picture": "File (Optional) — image/jpeg|image/png|image/webp", "biography": "String (Optional)", "facebook": "String (Optional)", "is_active": "Boolean (Optional) — Default: true", "subjects": "Array[Integer] (Required) — Subject IDs (a teacher can teach multiple subjects)", "grades": "Array[Integer] (Required) — Grade IDs", "video_security": "Integer (Optional) — VideoSecurity ID" }

Success Response — 201 Created: Returns the full teacher profile (same structure as GET detail above, including bunny_collection_id which will be null until assigned).

Error Responses:

Status Condition Response Body
400 Username exists {"username": ["Username already exists."]}
400 Email exists {"gmail": ["This email is already associated with an account."]}
400 Invalid phone {"phone": ["Phone number must be exactly 11 digits..."]}
400 Grade not covered by any subject {"grades": ["No selected subject is available for grade ..."]}
400 Weak password {"password": ["Password validation error"]} — Django validators: too common, entirely numeric, too similar to username
403 Not authorized {"detail": "You do not have permission..."}

Business Rules:


PATCH /accounts/teachers/<id>/

Authentication: SiteOwner

Content-Type: multipart/form-data

Request Body: Same as POST body (all fields optional for PATCH). Additionally:

Success Response — 200 OK: Returns the full teacher profile (same structure as GET detail above, including bunny_collection_id).

Error Responses:

Status Condition Response Body
400 Email exists (other user) {"gmail": ["This email is already associated with an account."]}
403 Not authorized {"detail": "You do not have permission..."}
404 Teacher not found {"detail": "Not found."}

Business Rules:


Teacher Collection Management

Teachers must have a Bunny Stream collection assigned before they can upload videos. Create a collection manually in your Bunny Stream dashboard → Collections → Create Collection, then assign it via the API.

The response for teacher list/detail views includes bunny_collection_id.

PATCH /accounts/teachers/<id>/ (set bunny_collection_id directly)

Note: Collection assignment is done directly via PATCH /accounts/teachers/<id>/ — set bunny_collection_id in the update body.

Authentication: SiteOwner, Teacher, Assistant

Content-Type: application/json

Request Body:

{ "bunny_collection_id": "String (Required) — Bunny Stream Collection GUID" }

Success Response — 200 OK:

{ "bunny_collection_id": "String — The assigned collection GUID", "message": "Collection assigned to teacher." }

Error Responses:

Status Condition Response Body
400 Missing bunny_collection_id {"error": "bunny_collection_id is required."}
404 Teacher not found {"error": "Teacher not found"}

POST /accounts/teachers/<id>/create-collection/

Description: Automatically create a new Bunny Stream collection and assign it to a teacher. The collection name auto-generates from the teacher's name and subject.

Authentication: SiteOwner

Content-Type: application/json

Request Body:

{ "name": "String (Optional) — Override auto-generated collection name" }

Success Response — 201 Created:

{ "bunny_collection_id": "String — The newly created collection GUID", "name": "String — The collection name", "message": "Collection created and assigned to teacher." }

2. Student Management

GET /accounts/students/

Description: List all students.

Authentication: SiteOwner

Query Parameters:

Parameter Type Description
status String Filter by status
grade Integer Filter by grade ID
school_type Integer Filter by school type ID
division Integer Filter by division ID
governorate Integer Filter by governorate ID
is_active Boolean Filter by active status
search String Search by name, student code, or national ID
all String Set to true to bypass pagination

Success Response — 200 OK:

{ "count": "Integer", "next": "String (URL) | null", "previous": "String (URL) | null", "results": [ { "id": "Integer", "student_code": "String", "name_en": "String", "name_ar": "String", "phone_number": "String", "father_number": "String | null", "mother_number": "String | null", "grade": "Integer | null", "grade_name": "String | null", "status": "String", "created_at": "DateTime (ISO 8601)" } ] }

List view is lightweight. To get full student details (username, gmail, birth_date, gender, school_type, division, school_name, governorate, area, is_active, updated_at, status_history), use GET /accounts/students/<id>/.


GET /accounts/students/<id>/

Authentication: SiteOwner

Success Response — 200 OK: Full student profile with enrollments and per-course balances.

{ "id": "Integer", "student_code": "String", "user_id": "Integer", "username": "String", "email": "String", "name_ar": "String", "name_en": "String", "full_name": "String", "phone_number": "String", "father_number": "String | null", "mother_number": "String | null", "school_type": "Integer | null", "school_type_name": "String | null", "grade": "Integer | null", "grade_name": "String | null", "division": "Integer | null", "division_name": "String | null", "school_name": "String", "birth_date": "Date (YYYY-MM-DD)", "gender": "String", "gmail": "String", "governorate": "Integer | null", "governorate_name": "String | null", "area": "Integer | null", "area_name": "String | null", "status": "String (pending|verified|declined|suspended_temporary|suspended_permanent)", "is_active": "Boolean", "can_access_course": "Boolean", "status_history": "Array[Object]", "created_at": "DateTime (ISO 8601)", "updated_at": "DateTime (ISO 8601)", "enrollments": [ { "id": "Integer", "course": "Integer", "course_name": "String", "teacher_name": "String", "grade_name": "String | null", "status": "String (pending|approved|rejected)", "balance": "String (Decimal)", "enrolled_at": "DateTime (ISO 8601)", "responded_by_name": "String | null", "responded_at": "DateTime (ISO 8601) | null" } ] }

PATCH /accounts/students/<id>/

Description: Update a student's profile fields and/or status. Status changes are logged with the acting user and reason.

Authentication: SiteOwner

Content-Type: application/json

Request Body:

{ "name_ar": "String (Optional)", "name_en": "String (Optional)", "phone_number": "String (Optional)", "father_number": "String (Optional)", "mother_number": "String (Optional)", "school_type": "Integer (Optional)", "grade": "Integer (Optional)", "division": "Integer (Optional)", "school_name": "String (Optional)", "birth_date": "Date (YYYY-MM-DD, Optional)", "gender": "String (Optional)", "gmail": "String (Optional)", "governorate": "Integer (Optional)", "area": "Integer (Optional)", "status": "String (Optional) — verified|declined|suspended_temporary|suspended_permanent", "is_active": "Boolean (Optional)", "reason": "String (Optional) — Required for decline and suspension" }

Allowed Status Transitions:

Current Status Allowed New Status
pending verified, declined
verified suspended_temporary, suspended_permanent
declined verified
suspended_temporary verified
suspended_permanent verified

Business Rules:

Error Responses:

Status Condition Response Body
400 Transition not allowed {"status": ["Cannot change status from 'pending' to 'suspended_temporary'."]}
400 Missing reason for decline/suspend {"reason": ["A reason is required when declining or suspending a student."]}

3. Course Management

POST /courses/

Description: Create a new course.

Authentication: SiteOwner

Content-Type: multipart/form-data

Request Body:

{ "name": "String (Required)", "teacher": "Integer (Required) — Teacher ID", "grade": "Integer (Required) — Grade ID", "subject": "Integer (Required) — Subject ID", "description": "String (Optional)", "cover_picture": "File (Optional) — image/jpeg|image/png|image/webp", "is_active": "Boolean (Optional) — Default: true" }

Success Response — 201 Created:

{ "id": "Integer", "name": "String", "teacher": "Integer", "teacher_name": "String", "grade": "Integer", "grade_name": "String", "subject": "Integer", "subject_name": "String", "description": "String | null", "cover_picture": "String (URL) | null", "is_active": "Boolean", "can_access_course": "Boolean", "topics": "Array[Object]", "topic_count": "Integer", "total_lectures": "Integer", "total_videos": "Integer", "enrolled_count": "Integer | null - Number of approved students (teachers only)", "pending_count": "Integer | null - Number of pending requests (teachers only)", "rejected_count": "Integer | null - Number of rejected requests (teachers only)", "created_at": "DateTime (ISO 8601)", "updated_at": "DateTime (ISO 8601)" }

Error Responses:

Status Condition Response Body
400 Teacher doesn't teach grade {"grade": ["Teacher X does not teach grade Y."]}
400 Teacher doesn't teach subject {"subject": ["Teacher X does not teach subject Y."]}
400 Subject not available for grade {"subject": ["Subject X is not available for grade Y."]}
400 Duplicate course {"non_field_errors": ["A course with this Teacher, Grade, and Name already exists."]}
400 Duplicate active course for teacher+grade+subject {"non_field_errors": ["..." ]}
403 Not site owner {"detail": "You do not have permission..."}

Business Rules:


PATCH /courses/<id>/

Authentication: SiteOwner

Content-Type: multipart/form-data

Request Body: Same as POST (all fields optional for PATCH, including subject).

Error Responses:

Status Condition Response Body
400 Changing teacher {"teacher": ["Cannot change the teacher of an existing course."]}
400 Changing grade {"grade": ["Cannot change the grade of an existing course."]}

Business Rules:


DELETE /courses/<id>/

Authentication: SiteOwner

Error Responses:

Status Condition Response Body
400 Students enrolled {"detail": "Cannot delete a course that has students enrolled in it."}

Business Rules:


4. View Assistants (Read-Only)

GET /accounts/teacher/assistants/

Description: SiteOwner can view all assistants across all teachers.

Authentication: SiteOwner, Teacher, Assistant

Query Parameters:

Parameter Type Description
teacher Integer Filter by teacher ID
search String Search by name, phone, or gmail

Success Response — 200 OK:

{ "count": "Integer", "next": "String (URL) | null", "previous": "String (URL) | null", "results": [ { "id": "Integer", "user_id": "Integer", "username": "String", "name": "String", "phone": "String", "gmail": "String", "gender": "String (male|female)", "is_active": "Boolean", "can_access_course": "Boolean", "created_at": "DateTime (ISO 8601)", "teacher": "Integer — Teacher ID", "teacher_name": "String - Teacher's name" } ] }

5. Recharge Codes

POST /payments/codes/

Description: Generate new recharge codes. Codes are course-specific and can only be redeemed for the course they are generated for.

Authentication: SiteOwner, Teacher

Content-Type: application/json

Request Body:

{ "course": "Integer (Required) — Course ID", "value": "Decimal (Required) — Amount in EGP (1.00 - 1000.00)", "count": "Integer (Optional, default: 1) — Number of codes to generate (1-1000)", "notes": "String (Required) — Description for this batch (e.g. 'Batch for Center X')" }

Success Response — 201 Created:

{ "id": "Integer", "course": "Integer", "course_name": "String", "teacher_name": "String", "code": "String — Auto-generated (e.g. X7K9-M2P4-QR1W-L5D8)", "code_masked": "String — Masked (e.g. X7K9 XXXX XXXX XXXX)", "value": "Decimal", "status": "String (valid)", "status_display": "String (Valid)", "used_by": null, "used_by_name": null, "used_at": null, "created_at": "DateTime (ISO 8601)", "expires_at": "DateTime (ISO 8601) | null", "created_by": "Integer", "created_by_username": "String" }

Error Responses:

Status Condition Response Body
400 Invalid course {"course": ["Invalid pk \"X\" - object does not exist."]}
400 Invalid value {"value": ["Ensure this value is greater than or equal to 1."]}
403 Not site owner {"detail": "You do not have permission..."}

Business Rules:


POST /payments/codes//request-blacklist-otp/

Description: Request an OTP to authorize blacklisting a single recharge code. Sends a 6-digit OTP to the SiteOwner's registered gmail. The OTP expires in 30 minutes.

Authentication: SiteOwner

Success Response — 200 OK:

{ "detail": "OTP sent to your email." }

Error Responses:

Status Condition
400 Code is already used or already blacklisted
500 Failed to send OTP email

POST /payments/codes/request-blacklist-otp/

Description: Request an OTP to authorize batch blacklisting (same OTP powers a single or batch blacklist call). Sends a 6-digit OTP to the SiteOwner's registered gmail.

Authentication: SiteOwner

Request Body: None

Success Response — 200 OK:

{ "detail": "OTP sent to your email." }

POST /payments/codes/batch-blacklist/

Description: Permanently blacklist multiple recharge codes in one request. Uses the same OTP flow as single blacklist. Only codes with status='valid' are blacklisted. Already used or blacklisted codes are skipped with a warning.

Authentication: SiteOwner

Request Body:

{ "code_ids": [1, 2, 3, 4, 5], "otp": "123456" }

Success Response — 200 OK:

{ "detail": "3 code(s) blacklisted, 1 skipped.", "blacklisted": [ { "id": 1, "code": "X7K9-M2P4-QR1W-L5D8" }, { "id": 2, "code": "A3F8-G7H2-J4K9-M1N6" }, { "id": 3, "code": "B4X8-N7M2-Q9W5-K3R6" } ], "skipped": [ { "id": 4, "code": "C5Y9-P8M3-R2W6-L4T7", "reason": "Status is Used" } ], "not_found": [99] }

Error Responses:

Status Condition
400 Missing or invalid code_ids
400 Missing otp
400 Invalid or expired OTP

POST /payments/codes//blacklist/

Description: Permanently blacklist a recharge code. This is a one-way irreversible action — once blacklisted, the code can never be used or un-blacklisted. Requires a valid OTP.

Authentication: SiteOwner, Teacher, Assistant

Request Body:

{ "otp": "String — 6-digit OTP sent to your email (required)" }

Success Response — 200 OK:

{ "detail": "Code X7K9-M2P4-QR1W-L5D8 has been blacklisted.", "code": "X7K9-M2P4-QR1W-L5D8", "status": "blacklisted" }

Error Responses:

Status Condition
400 Code is already used or already blacklisted
400 Missing OTP
400 Invalid or expired OTP

GET /payments/codes/

Description: List all recharge codes. Supports filtering and ordering.

Authentication: SiteOwner, Teacher, Assistant

Query Parameters:

Parameter Type Description
course Integer Filter by course ID
status String Filter: valid, used, or blacklisted
batch_id UUID Filter by batch UUID
search String Search by code (partial match, e.g. ?search=KTMD)

Success Response — 200 OK:

{ "count": "Integer", "next": "String (URL) | null", "previous": "String (URL) | null", "results": [ { "id": "Integer", "course": "Integer", "course_name": "String", "teacher_name": "String", "code": "String", "value": "Decimal", "status": "String (valid|used|blacklisted)", "status_display": "String (Valid|Used|Blacklisted)", "used_by": "Integer | null", "used_by_name": "String | null", "used_at": "DateTime (ISO 8601) | null", "blacklisted_by": "Integer | null", "blacklisted_by_name": "String | null", "blacklisted_at": "DateTime (ISO 8601) | null", "created_at": "DateTime (ISO 8601)", "expires_at": "DateTime (ISO 8601) | null", "created_by": "Integer", "created_by_username": "String", "created_by_role": "String" } ] } **Filter by batch:** `?batch_id=<uuid>` filters the codes list. Use `?batch=<uuid>` on `GET /codes/export/` to download one batch as an Excel sheet. Use `GET /codes/export/` (no params) to export ALL visible codes as Excel. Use `GET /codes/summary/?course=X` to get aggregate stats for a course. Use `GET /codes/history/` for a unified code audit log (redemptions + blacklists).

GET /payments/codes/analytics/

Description: Aggregate analytics for the codes dashboard.

Authentication: SiteOwner, Teacher, Assistant

Success Response — 200 OK:

{ "total_batches": 45, "total_codes": 5000, "valid_count": 3000, "valid_value": "150000.00", "used_count": 1800, "used_value": "90000.00", "blacklisted_count": 200, "blacklisted_value": "10000.00" }

Note: Revenue splits (Teacher's Cut / SiteOwner's Cut) were removed from this endpoint. It returns the 8 fields above only.


GET /payments/codes/batches/

Description: List code batches grouped by batch_id. Paginated. Each row shows batch name, course, teacher, code counts per status, and code value.

Authentication: SiteOwner, Teacher, Assistant

Query Parameters:

Parameter Type Description
teacher Integer Filter by teacher ID
course Integer Filter by course ID
page Integer Page number (default: 1)
page_size Integer Items per page (default: 20, max: 100)

Success Response — 200 OK:

{ "count": 45, "results": [ { "batch_id": "UUID", "batch_name": "Dr Hany Hassanin Batch #1", "course": 28, "course_name": "Chemistry 3rd Secondary", "teacher_name": "Dr Hany Hassanin", "notes": "Batch for Center X (1/100)", "total_codes": 100, "code_value": "50.00", "status_counts": { "valid": 60, "used": 35, "blacklisted": 5 }, "created_at": "DateTime (ISO 8601)" } ] }

GET /payments/codes/batches/uuid:batch_id/

Description: View individual codes within a batch. Paginated. Shows each code's value, status, who used it and when.

Authentication: SiteOwner, Teacher, Assistant

Success Response — 200 OK:

{ "batch_id": "UUID", "batch_name": "Dr Hany Hassanin Batch #1", "course": 28, "course_name": "Chemistry 3rd Secondary", "teacher_name": "Dr Hany Hassanin", "notes": "Batch for Center X (1/100)", "total_codes": 100, "redeemed_value": "1750.00", "codes": [ { "code": "X7K9-M2P4-QR1W-L5D8", "code_masked": "X7K9 XXXX XXXX XXXX", "value": "50.00", "status": "used", "status_display": "Used", "used_by_name": "Ahmed / أحمد", "used_by_student_code": "S-001", "used_at": "DateTime (ISO 8601) | null" } ] }

code_masked: By default, the full code is returned alongside code_masked ("ABDC XXXX XXXX XXXX"). The frontend should display the masked version and provide a "Reveal" button to show the full code on demand. This prevents shoulder surfing / photo-taking of the screen.


GET /payments/codes/history/

Description: Unified code audit log — redemption events + blacklist events combined into one paginated feed, newest first.

Authentication: SiteOwner, Teacher, Assistant

Query Parameters:

Parameter Type Description
start_date String Start date (YYYY-MM-DD)
end_date String End date (YYYY-MM-DD)
type String Filter: redeemed or blacklisted
search String Search by code text
teacher Integer Filter by teacher ID
course Integer Filter by course ID
page Integer Page number (default: 1)
page_size Integer Items per page (default: 20, max: 100)

Success Response — 200 OK:

{ "count": 65, "analytics": { "total": 32, "redeemed": 24, "blacklisted": 8, "revenue": "1739.52" }, "results": [ { "id": 123, "code": "X7K9-M2P4-QR1W-L5D8", "code_masked": "X7K9 XXXX XXXX XXXX", "type": "redeemed", "course_name": "Chemistry 3rd Secondary", "teacher_name": "Dr Hany Hassanin", "student_name": "Ahmed / أحمد", "student_code": "S-001", "amount": "50.00", "balance_before": "200.00", "balance_after": "250.00", "performed_by": "Student", "performed_by_role": "student", "date": "2026-07-15T10:30:00Z", "reason": null, "batch_id": null }, { "id": 456, "code": "A3F8-G7H2-J4K9-M1N6", "code_masked": "A3F8 XXXX XXXX XXXX", "type": "blacklisted", "course_name": "Math 1st Secondary", "teacher_name": "Sara Ali", "student_name": null, "student_code": null, "amount": "75.00", "balance_before": null, "balance_after": null, "performed_by": "Admin", "performed_by_role": "siteowner", "date": "2026-07-14T14:22:00Z", "reason": "Suspicious activity — reported by teacher", "batch_id": "c4375c67-..." } ] }

Notes:


6. Transactions

GET /payments/transactions/

Description: Full balance transaction ledger. SiteOwner must select both a student and a course to view transactions (prevents mixing up students across courses).

Authentication: SiteOwner, Teacher, Assistant

Query Parameters:

Parameter Type Required Description
student Integer Student ID
course Integer Course ID
transaction_type String Filter: code_redeemed or lecture_purchase
code String Search by recharge code
search String Search in description text
start_date String Start date (YYYY-MM-DD)
end_date String End date (YYYY-MM-DD)

Success Response — 200 OK:

{ "count": "Integer", "results": [ { "id": "Integer", "student": "Integer", "student_name": "String", "course": "Integer", "course_name": "String", "teacher_name": "String", "teacher_picture": "String (URL) | null", "transaction_type": "String", "transaction_type_display": "String", "amount": "String (Decimal)", "balance_before": "String (Decimal)", "balance_after": "String (Decimal)", "description": "String", "metadata": { "code": "String | null", "recharged_by_name": "String | null" }, "performed_by": "Integer | null", "performed_by_name": "String | null", "created_at": "DateTime (ISO 8601)" } ] }

"purchase_timeline": [ { "student_name": "String", "student_code": "String", "amount_paid": "String — Decimal as string", "purchased_at": "DateTime (ISO 8601)" } ] }

--- ## 7. Cut Invoices Manual, invoice-based platform-cut system. The siteowner decides the cut per lecture on each invoice; the system computes what the teacher owes for lecture purchases in the chosen period. **Rules:** - `lectures_count` is a **snapshot** at creation — editing the cut or discount later recomputes `total_owed` from the snapshot, it never re-counts. - **Math:** `total_owed = max(0, (cut_per_lecture × lectures_count) − discount)`; `gross_total = cut_per_lecture × lectures_count` (read-only, always exposed). - **Discount** cannot be negative and cannot exceed the gross total (400 otherwise). - **Overlap prevention:** an unpaid/paid invoice blocks a new invoice for the same teacher whose period intersects it. Cancelled invoices don't block (a period can be re-invoiced after cancellation). Adjacent periods are allowed. - **Paid lock:** `cut_per_lecture` and `discount` cannot be edited once `status='paid'` (revert to `unpaid` first). - Teacher and period are immutable after creation — cancel and recreate instead. - `DELETE` soft-cancels (status → `cancelled`, clears `paid_at` and `paid_note`). ### GET /payments/cuts/overview/ **Description:** Per-teacher summary: total lectures, lectures purchased, outstanding/paid invoice totals, plus a per-course breakdown (course, grade, lectures, purchases, purchase revenue). **Authentication:** SiteOwner, Teacher, Assistant — teachers/assistants are **always scoped to their own teacher's data** (`?teacher=` and `?search=` are ignored for them; the response contains exactly their teacher's row). **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `teacher` | Integer | Optional — filter to one teacher | | `search` | String | Optional — teacher name, case-insensitive partial match | | `start` | String | Optional — period start (YYYY-MM-DD); must be paired with `end` | | `end` | String | Optional — period end (YYYY-MM-DD); must be paired with `start` | **Success Response — 200 OK:** ```json [ { "teacher_id": 5, "teacher_name": "Dr Hany Hassanin", "profile_picture": "https://.../media/profile_pictures/drhany.jpg | null", "total_lectures": 42, "purchased_count": 320, "outstanding_total": "6400.00", "paid_total": "1200.00", "courses": [ { "course_id": 28, "course_name": "Chemistry 3rd Secondary", "grade_name": "3rd Secondary", "total_lectures": 24, "purchased_count": 210, "purchased_revenue": "8400.00" } ] } ]

Detail mode: calling GET /payments/cuts/overview/?teacher=<id> returns a single row that additionally embeds the teacher's per-lecture breakdown:

{ "teacher_id": 5, "teacher_name": "Dr Hany Hassanin", "profile_picture": "...", "total_lectures": 42, "purchased_count": 320, "outstanding_total": "6400.00", "paid_total": "1200.00", "courses": [], "lectures_count": 42, "lectures": [ { "lecture_id": 10, "lecture_name": "Intro to Reactions", "topic_id": 1, "topic_name": "Unit 1: Chemical Reactions", "course_id": 28, "course_name": "Chemistry 3rd Secondary", "grade_name": "3rd Secondary", "price": "50.00", "final_price": "40.00", "purchased_count": 25, "purchased_revenue": "1000.00" } ] }

purchased_count/purchased_revenue respect the start/end range when provided.

Error Responses:

Status Condition
400 Only one of start/end provided, or start after end, or invalid date format
404 ?teacher= ID not found

GET /payments/cuts/lectures/

Description: Itemized purchased-lecture records for a teacher (optional range / course) — used to verify an invoice before creation.

Authentication: SiteOwner

Query Parameters:

Parameter Type Description
teacher Integer Required — Teacher ID
search String Optional — student name (en/ar), student code, lecture name, or course name (case-insensitive partial)
start String Optional — period start (YYYY-MM-DD)
end String Optional — period end (YYYY-MM-DD)
course Integer Optional — filter by course ID
page / page_size Integer Pagination (50/page, max 200)

Success Response — 200 OK (paginated):

{ "count": 2, "results": [ { "id": 101, "student_name": "Ahmed Ali", "student_code": "1234567", "lecture_name": "Intro to Reactions", "course_name": "Chemistry 3rd Secondary", "amount_paid": "40.00", "purchased_at": "2026-08-05T09:00:00Z" } ] }

GET /payments/cuts/invoices/

Description: List cut invoices.

Authentication: SiteOwner

Query Parameters:

Parameter Type Description
teacher Integer Filter by teacher ID
status String Filter: unpaid, paid, cancelled
search String Teacher name or invoice note (case-insensitive partial)
start String Invoices starting on/after this date (YYYY-MM-DD)
end String Invoices ending on/before this date (YYYY-MM-DD)
page / page_size Integer Pagination (50/page)

Success Response — 200 OK (paginated): CutInvoice objects (same shape as POST response below).

POST /payments/cuts/invoices/

Description: Create an invoice. The backend counts the teacher's lecture purchases in the period and computes the total.

Authentication: SiteOwner

Content-Type: application/json

Request Body:

{ "teacher": 5, "start_date": "2026-08-01", "end_date": "2026-08-31", "cut_per_lecture": "20.00", "discount": "10.00", "note": "August cut" }

Success Response — 201 Created:

{ "id": 12, "teacher": 5, "teacher_name": "Dr Hany Hassanin", "start_date": "2026-08-01", "end_date": "2026-08-31", "lectures_count": 320, "cut_per_lecture": "20.00", "discount": "10.00", "gross_total": "6400.00", "total_owed": "6390.00", "status": "unpaid", "note": "August cut", "paid_note": "", "paid_at": null, "created_by": 1, "created_at": "2026-08-08T10:00:00Z", "updated_at": "2026-08-08T10:00:00Z" }

Error Responses:

Status Condition
400 start_date after end_date
400 Negative cut_per_lecture or negative discount
400 discount exceeds the gross total (cut_per_lecture × lectures_count)
400 Unknown teacher
400 Overlapping period{"detail": "An invoice already exists for this teacher in an overlapping period. Adjust the dates or cancel the existing invoice first."}

GET /payments/cuts/invoices//

Authentication: SiteOwner

Success Response — 200 OK: Single CutInvoice object (same shape as POST response).

PATCH /payments/cuts/invoices//

Description: Edit the invoice. cut_per_lecture, discount, note, paid_note, and status are editable.

Authentication: SiteOwner

Request Body (any subset):

{ "cut_per_lecture": "25.00", "discount": "5.00", "status": "paid", "paid_note": "Cash received on site" }

Rules:

Success Response — 200 OK: Updated CutInvoice.

DELETE /payments/cuts/invoices//

Description: Soft-cancel the invoice (status → cancelled, paid_at cleared). The row is kept for history.

Authentication: SiteOwner

Success Response — 200 OK:

{ "detail": "Invoice 12 for Dr Hany Hassanin cancelled." }

8. SiteOwner Dashboard

GET /dashboards/site-owner/

Description: Returns comprehensive dashboard statistics for site owners. Aggregates system-wide metrics including user counts, time-based metrics, and detailed breakdowns.

Authentication: SiteOwner, Teacher, Assistant

Success Response — 200 OK:

{ "total_students": "integer - Total number of student accounts", "total_teachers": "integer - Total number of teacher accounts", "total_assistants": "integer - Total number of assistant accounts", "new_users_this_month": "integer - New users registered this month", "new_students_this_month": "integer - New students registered this month", "new_teachers_this_month": "integer - New teachers created this month", "students_by_status": { "verified": "integer - Count of verified students", "pending": "integer - Count of pending students", "declined": "integer - Count of declined students", "suspended_temporary": "integer - Count of temporarily suspended students", "suspended_permanent": "integer - Count of permanently suspended students" }, "teachers_active_count": "integer - Number of active teachers", "teachers_inactive_count": "integer - Number of inactive teachers", "teachers_by_grade_subject": [ { "grade": "string - Grade name", "subject": "string - Subject name", "count": "integer - Number of teachers for this grade/subject" } ], "total_subjects": "integer - Total number of subjects", "total_grades": "integer - Total number of grades", "total_school_types": "integer - Total number of school types", "total_divisions": "integer - Total number of divisions", "total_governorates": "integer - Total number of governorates", "total_areas": "integer - Total number of areas", "total_video_security_options": "integer - Total number of video security options" }

Error Responses:

Status Condition Response Body
401 Not authenticated {"detail": "Authentication credentials were not provided."}
403 Not site owner {"detail": "You do not have permission to perform this action."}

Generated from main API documentation. Covers only SiteOwner-accessible endpoints.