Version: 1.3 Date: July 16, 2026 Target Audience: SiteOwner Frontend Backend: Django REST Framework
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)"
}
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:
subjects is an array of Subject IDs; a teacher can teach many subjects.grades must be a subset of the grades covered by the selected subjects. For example, if a subject is only assigned to "3rd Secondary", the teacher cannot be assigned "1st Secondary" unless another selected subject covers it.PATCH /accounts/teachers/<id>/Authentication: SiteOwner
Content-Type: multipart/form-data
Request Body: Same as POST body (all fields optional for PATCH). Additionally:
bunny_collection_id can be set directly here, or use the dedicated create-collection endpoint below.password can be set to reset the teacher's password directly (optional). The teacher can also use the forgot-password flow themselves.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:
is_active = False. The courses become hidden from both the teacher and enrolled students.is_active = False. The selected grades must also be covered by the new subjects — adjust them in the same request if needed.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."
}
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:
declined or suspended automatically sets is_active = false.verified automatically sets is_active = true.pending once they've been processed (only declined students can edit their own profile to reset to pending).status_history array on the student detail response. The history is stored in the StudentStatusLog model — an immutable audit trail that records who changed the status, from what, to what, why, and when.enrollments[] array with course name, teacher, status, and balance for each enrollment.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."]} |
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:
subject is required — it is NOT auto-derived from the teacher.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:
name, subject, description, cover_picture, and is_active are editable.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:
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"
}
]
}
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:
XXXX-XXXX-XXXX-XXXX (16 random characters, cryptographically secure).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 |
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."
}
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 |
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 |
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).
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.
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)"
}
]
}
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.
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:
analytics contains aggregate counts computed from the full filtered dataset (respects all query parameters). Values change when you filter by date, type, or code search.type: "redeemed") include student_name, student_code, balance_before, balance_after, and amount.type: "blacklisted") include a reason (from code notes) and batch_id.date descending (newest first).?teacher=X and ?course=X to filter; otherwise sees everything.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 |
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"
}
]
}
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).
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."} |
Authentication: SiteOwner
Success Response — 200 OK: Single CutInvoice object (same shape as POST response).
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:
cut_per_lecture / discount → total_owed recomputed from the existing lectures_count snapshot: max(0, (cut × count) − discount). Blocked with 400 when status='paid' ("Mark the invoice as unpaid before editing the cut or discount."). discount may not exceed the gross total (400).status: "paid" → sets paid_at and saves paid_note (from the same request). status: "unpaid" / "cancelled" → clears paid_at and paid_note.teacher, start_date, end_date → immutable; sending them returns 400.gross_total is read-only — always computed, never accepted.Success Response — 200 OK: Updated CutInvoice.
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."
}
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.