use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_json; use uuid::Uuid; #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] #[sqlx(default)] pub struct Course { pub id: Uuid, pub organization_id: Uuid, pub title: String, pub description: Option, pub instructor_id: Uuid, pub pacing_mode: String, // "self_paced" or "instructor_led" pub start_date: Option>, pub end_date: Option>, pub passing_percentage: i32, pub certificate_template: Option, pub price: f64, pub currency: String, pub marketing_metadata: Option, pub course_image_url: Option, pub level: Option, pub course_type: Option, pub created_at: DateTime, pub updated_at: DateTime, } impl Default for Course { fn default() -> Self { Self { id: Uuid::new_v4(), organization_id: Uuid::new_v4(), title: String::new(), description: None, instructor_id: Uuid::new_v4(), pacing_mode: "self_paced".to_string(), start_date: None, end_date: None, passing_percentage: 0, certificate_template: None, price: 0.0, currency: "USD".to_string(), marketing_metadata: None, course_image_url: None, level: None, course_type: None, created_at: Utc::now(), updated_at: Utc::now(), } } } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct Module { pub id: Uuid, pub organization_id: Uuid, pub course_id: Uuid, pub title: String, pub position: i32, pub created_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] #[sqlx(default)] pub struct Lesson { pub id: Uuid, pub organization_id: Uuid, pub module_id: Uuid, pub title: String, pub content_type: String, pub content_url: Option, pub summary: Option, pub transcription: Option, pub metadata: Option, pub grading_category_id: Option, pub is_graded: bool, pub max_attempts: Option, pub allow_retry: bool, pub position: i32, pub due_date: Option>, pub important_date_type: Option, // "exam", "assignment", "milestone", etc. pub transcription_status: Option, pub is_previewable: bool, pub content_blocks: Option, pub created_at: DateTime, } impl Default for Lesson { fn default() -> Self { Self { id: Uuid::new_v4(), organization_id: Uuid::new_v4(), module_id: Uuid::new_v4(), title: String::new(), content_type: String::new(), content_url: None, summary: None, transcription: None, metadata: None, content_blocks: None, grading_category_id: None, is_graded: false, max_attempts: None, allow_retry: false, position: 0, due_date: None, important_date_type: None, transcription_status: None, is_previewable: false, created_at: Utc::now(), } } } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct GradingCategory { pub id: Uuid, pub organization_id: Uuid, pub course_id: Uuid, pub name: String, pub weight: i32, // 0-100 pub drop_count: i32, pub tipo_nota_id: Option, // Maps to idTipoNota in external MySQL system pub created_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] pub struct UserGrade { pub id: Uuid, pub user_id: Uuid, pub course_id: Uuid, pub lesson_id: Uuid, pub score: f32, // 0.0 to 1.0 pub attempts_count: i32, pub metadata: Option, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct LessonInteraction { pub id: Uuid, pub organization_id: Uuid, pub user_id: Uuid, pub lesson_id: Uuid, pub video_timestamp: Option, pub event_type: String, pub metadata: Option, pub created_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct HeatmapPoint { pub second: i32, pub count: i64, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct Notification { pub id: Uuid, pub organization_id: Uuid, pub user_id: Uuid, pub title: String, pub message: String, pub notification_type: String, pub is_read: bool, pub link_url: Option, pub created_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] pub struct AuditLogResponse { pub id: Uuid, pub user_id: Uuid, pub user_full_name: Option, pub action: String, pub entity_type: String, pub entity_id: Uuid, pub changes: serde_json::Value, pub created_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] pub struct Enrollment { pub id: Uuid, pub user_id: Uuid, pub organization_id: Uuid, pub course_id: Uuid, pub external_id: Option, // idDetalleContrato from the external system pub enrolled_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct UserBookmark { pub id: Uuid, pub organization_id: Uuid, pub user_id: Uuid, pub course_id: Uuid, pub lesson_id: Uuid, pub created_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct CourseInstructor { pub id: Uuid, pub organization_id: Uuid, pub course_id: Uuid, pub user_id: Uuid, pub role: String, // "primary", "instructor", "assistant" pub created_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct LtiRegistration { pub id: Uuid, pub organization_id: Uuid, pub issuer: String, pub client_id: String, pub deployment_id: String, pub auth_token_url: String, pub auth_login_url: String, pub jwks_url: String, pub platform_name: Option, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct LtiResourceLink { pub id: Uuid, pub organization_id: Uuid, pub resource_link_id: String, pub course_id: Uuid, pub created_at: DateTime, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct LtiLaunchClaims { #[serde(rename = "iss")] pub issuer: String, #[serde(rename = "sub")] pub subject: String, #[serde(rename = "aud")] pub audience: serde_json::Value, // Can be string or array #[serde(rename = "exp")] pub expires_at: i64, #[serde(rename = "iat")] pub issued_at: i64, pub nonce: String, #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/message_type")] pub message_type: String, #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/version")] pub version: String, #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/deployment_id")] pub deployment_id: String, #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/resource_link")] pub resource_link: Option, #[serde(rename = "https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings")] pub deep_linking_settings: Option, #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/context")] pub context: Option, #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/roles")] pub roles: Vec, pub name: Option, pub email: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct LtiResourceLinkClaim { pub id: String, pub title: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct LtiContextClaim { pub id: String, pub label: Option, pub title: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct LtiDeepLinkingSettings { pub deep_link_return_url: String, pub accept_types: Vec, pub accept_presentation_document_targets: Vec, pub accept_media_types: Option, pub accept_multiple: Option, pub accept_copy_advice: Option, pub auto_create: Option, pub title: Option, pub text: Option, pub data: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct LtiDeepLinkingResponseClaims { #[serde(rename = "iss")] pub issuer: String, #[serde(rename = "sub")] pub subject: String, #[serde(rename = "aud")] pub audience: String, #[serde(rename = "exp")] pub expires_at: i64, #[serde(rename = "iat")] pub issued_at: i64, pub nonce: String, #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/message_type")] pub message_type: String, // "LtiDeepLinkingResponse" #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/version")] pub version: String, // "1.3.0" #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/deployment_id")] pub deployment_id: String, #[serde(rename = "https://purl.imsglobal.org/spec/lti-dl/claim/content_items")] pub content_items: Vec, #[serde(rename = "https://purl.imsglobal.org/spec/lti-dl/claim/data")] pub data: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct LtiDeepLinkingContentItem { #[serde(rename = "type")] pub item_type: String, // "ltiResourceLink" pub title: Option, pub text: Option, pub url: Option, pub icon: Option, pub thumbnail: Option, #[serde(flatten)] pub extra: serde_json::Map, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct LtiImage { pub url: String, pub width: Option, pub height: Option, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct Asset { pub id: Uuid, pub organization_id: Uuid, pub uploaded_by: Option, pub course_id: Option, pub filename: String, pub storage_path: String, pub mimetype: String, pub size_bytes: i64, pub created_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct Transaction { pub id: Uuid, pub organization_id: Uuid, pub user_id: Uuid, pub course_id: Uuid, pub amount: f64, pub currency: String, pub status: String, // "pending", "success", "failure" pub provider_reference: Option, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] #[sqlx(default)] pub struct User { pub id: Uuid, pub organization_id: Uuid, pub email: String, pub password_hash: String, pub full_name: String, pub role: String, // admin, instructor, student pub xp: i32, pub level: i32, pub avatar_url: Option, pub bio: Option, pub language: Option, pub is_public_profile: Option, pub linkedin_url: Option, pub github_url: Option, pub created_at: DateTime, pub updated_at: DateTime, } impl Default for User { fn default() -> Self { Self { id: Uuid::new_v4(), organization_id: Uuid::new_v4(), email: String::new(), password_hash: String::new(), full_name: String::new(), role: "student".to_string(), xp: 0, level: 1, avatar_url: None, bio: None, language: None, is_public_profile: Some(true), linkedin_url: None, github_url: None, created_at: Utc::now(), updated_at: Utc::now(), } } } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] pub struct UserResponse { pub id: Uuid, pub email: String, pub full_name: String, pub role: String, pub organization_id: Uuid, pub xp: i32, pub level: i32, pub avatar_url: Option, pub bio: Option, pub language: Option, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] #[sqlx(default)] pub struct Organization { pub id: Uuid, pub name: String, pub domain: Option, pub logo_url: Option, pub primary_color: Option, pub secondary_color: Option, pub certificate_template: Option, pub platform_name: Option, pub favicon_url: Option, pub logo_variant: Option, pub created_at: DateTime, pub updated_at: DateTime, } impl Default for Organization { fn default() -> Self { Self { id: Uuid::new_v4(), name: String::new(), domain: None, logo_url: None, primary_color: None, secondary_color: None, certificate_template: None, platform_name: None, favicon_url: None, logo_variant: None, created_at: Utc::now(), updated_at: Utc::now(), } } } #[derive(Debug, Serialize, Deserialize)] pub struct AuthResponse { pub user: UserResponse, pub token: String, } #[derive(Debug, Serialize, Deserialize)] pub struct PublishedCourse { pub course: Course, pub organization: Organization, pub grading_categories: Vec, pub modules: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub instructors: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub dependencies: Option>, } #[derive(Debug, Serialize, Deserialize)] pub struct PublishedModule { pub module: Module, pub lessons: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CourseAnalytics { pub course_id: Uuid, pub total_enrollments: i64, pub average_score: f32, // 0.0-1.0 pub lessons: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LessonAnalytics { pub lesson_id: Uuid, pub lesson_title: String, pub average_score: f32, // 0.0-1.0 pub submission_count: i64, } #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct CohortData { pub period: String, pub count: i64, pub completion_rate: f32, } #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct RetentionData { pub lesson_id: Uuid, pub lesson_title: String, pub student_count: i64, pub completion_rate: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AdvancedAnalytics { pub cohorts: Vec, pub retention: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct DailyProgress { pub date: String, pub count: i64, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct AnalyticsFilter { pub cohort_id: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Recommendation { pub title: String, pub description: String, pub lesson_id: Option, pub priority: String, // "high", "medium", "low" pub reason: String, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct RecommendationResponse { pub recommendations: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProgressStats { pub total_lessons: i64, pub completed_lessons: i64, pub progress_percentage: f32, pub daily_completions: Vec, pub estimated_completion_date: Option>, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct Webhook { pub id: Uuid, pub organization_id: Uuid, pub url: String, pub events: Vec, pub secret: Option, pub is_active: bool, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct OrganizationSSOConfig { pub organization_id: Uuid, pub issuer_url: String, pub client_id: String, pub client_secret: String, pub enabled: bool, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct AuditLog { pub id: Uuid, pub organization_id: Option, pub user_id: Option, pub action: String, pub entity_type: String, pub entity_id: Uuid, pub event_type: String, pub old_data: Option, pub new_data: Option, pub ip_address: Option, pub user_agent: Option, pub changes: Option, pub created_at: DateTime, } // Discussion Forums Models #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct DiscussionThread { pub id: Uuid, pub organization_id: Uuid, pub course_id: Uuid, pub lesson_id: Option, pub author_id: Uuid, pub title: String, pub content: String, pub is_pinned: bool, pub is_locked: bool, pub view_count: i32, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct DiscussionPost { pub id: Uuid, pub organization_id: Uuid, pub thread_id: Uuid, pub parent_post_id: Option, pub author_id: Uuid, pub content: String, pub upvotes: i32, pub is_endorsed: bool, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct DiscussionVote { pub id: Uuid, pub organization_id: Uuid, pub post_id: Uuid, pub user_id: Uuid, pub vote_type: String, // 'upvote' or 'downvote' pub created_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct DiscussionSubscription { pub id: Uuid, pub organization_id: Uuid, pub thread_id: Uuid, pub user_id: Uuid, pub created_at: DateTime, } // Response DTOs for Discussion APIs #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct ThreadWithAuthor { // Thread fields pub id: Uuid, pub organization_id: Uuid, pub course_id: Uuid, pub lesson_id: Option, pub author_id: Uuid, pub title: String, pub content: String, pub is_pinned: bool, pub is_locked: bool, pub view_count: i32, pub created_at: DateTime, pub updated_at: DateTime, // Author info pub author_name: String, pub author_avatar: Option, // Aggregated data pub post_count: i64, pub has_endorsed_answer: bool, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct PostWithAuthor { // Post fields pub id: Uuid, pub organization_id: Uuid, pub thread_id: Uuid, pub parent_post_id: Option, pub author_id: Uuid, pub content: String, pub upvotes: i32, pub is_endorsed: bool, pub created_at: DateTime, pub updated_at: DateTime, // Author info pub author_name: String, pub author_avatar: Option, // User interaction pub user_vote: Option, // 'upvote', 'downvote', or null // Nested replies (not from DB, populated manually) #[sqlx(skip)] pub replies: Vec, } // Course Announcements #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct CourseAnnouncement { pub id: Uuid, pub organization_id: Uuid, pub course_id: Uuid, pub author_id: Uuid, pub title: String, pub content: String, pub is_pinned: bool, pub created_at: DateTime, pub updated_at: DateTime, #[sqlx(skip)] pub cohort_ids: Option>, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct AnnouncementWithAuthor { // Announcement fields pub id: Uuid, pub organization_id: Uuid, pub course_id: Uuid, pub author_id: Uuid, pub title: String, pub content: String, pub is_pinned: bool, pub created_at: DateTime, pub updated_at: DateTime, // Author info pub author_name: String, pub author_avatar: Option, #[sqlx(skip)] pub cohort_ids: Option>, } // Student Notes #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct StudentNote { pub id: Uuid, pub user_id: Uuid, pub lesson_id: Uuid, pub content: String, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Deserialize)] pub struct SaveNotePayload { pub content: String, } // Cohorts & Groups #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct Cohort { pub id: Uuid, pub organization_id: Uuid, pub name: String, pub description: Option, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct UserCohort { pub id: Uuid, pub cohort_id: Uuid, pub user_id: Uuid, pub assigned_at: DateTime, } #[derive(Debug, Deserialize)] pub struct CreateCohortPayload { pub name: String, pub description: Option, } #[derive(Debug, Deserialize)] pub struct AddMemberPayload { pub user_id: Uuid, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct StudentGradeReport { pub user_id: Uuid, pub full_name: String, pub email: String, pub progress: f32, pub average_score: Option, pub last_active_at: Option>, } // Peer Assessment #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct CourseSubmission { pub id: Uuid, pub user_id: Uuid, pub course_id: Uuid, pub lesson_id: Uuid, pub content: String, pub submitted_at: DateTime, pub updated_at: DateTime, pub organization_id: Uuid, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct PeerReview { pub id: Uuid, pub submission_id: Uuid, pub reviewer_id: Uuid, pub score: i32, pub feedback: String, pub created_at: DateTime, pub updated_at: DateTime, pub organization_id: Uuid, } #[derive(Debug, Deserialize)] pub struct SubmitAssignmentPayload { pub content: String, } #[derive(Debug, Deserialize)] pub struct SubmitPeerReviewPayload { pub submission_id: Uuid, pub score: i32, pub feedback: String, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct SubmissionWithReviews { pub id: Uuid, pub user_id: Uuid, pub full_name: String, pub email: String, pub submitted_at: DateTime, pub review_count: i64, pub average_score: Option, } // Content Libraries #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct LibraryBlock { pub id: Uuid, pub organization_id: Uuid, pub created_by: Uuid, pub name: String, pub description: Option, pub block_type: String, pub block_data: serde_json::Value, pub tags: Option>, pub usage_count: i32, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Serialize, Deserialize)] pub struct CreateLibraryBlockPayload { pub name: String, pub description: Option, pub block_type: String, pub block_data: serde_json::Value, pub tags: Option>, } #[derive(Debug, Serialize, Deserialize)] pub struct UpdateLibraryBlockPayload { pub name: Option, pub description: Option, pub tags: Option>, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct LibraryTemplate { pub id: Uuid, pub organization_id: Uuid, pub created_by: Uuid, pub name: String, pub description: Option, pub lesson_data: serde_json::Value, pub tags: Option>, pub usage_count: i32, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::Type, Clone, Copy, PartialEq)] #[sqlx(type_name = "dropout_risk_level", rename_all = "lowercase")] pub enum DropoutRiskLevel { Low, Medium, High, Critical, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct DropoutRisk { pub id: Uuid, pub organization_id: Uuid, pub course_id: Uuid, pub user_id: Uuid, pub risk_level: DropoutRiskLevel, pub score: f32, // 0.0 to 1.0 (Higher means higher risk) pub reasons: Option, // e.g., ["low_grades", "inactivity"] pub last_calculated_at: DateTime, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct DropoutRiskReason { pub metric: String, pub value: f32, pub description: String, } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn test_published_course_serialization() { let lesson_id = Uuid::new_v4(); let module_id = Uuid::new_v4(); let course_id = Uuid::new_v4(); let lesson = Lesson { id: lesson_id, organization_id: course_id, // Use course_id as proxy for org_id in test module_id, title: "Test Lesson".to_string(), content_type: "activity".to_string(), content_url: None, summary: None, transcription: None, metadata: Some(json!({ "blocks": [ { "id": "b1", "type": "fill-in-the-blanks", "content": "The capital of France is [[Paris]]." }, { "id": "b2", "type": "matching", "pairs": [{"left": "Term", "right": "Definition"}] } ] })), grading_category_id: None, is_graded: false, max_attempts: None, allow_retry: true, position: 1, due_date: None, important_date_type: None, transcription_status: None, is_previewable: true, content_blocks: None, created_at: Utc::now(), }; let pub_module = PublishedModule { module: Module { id: module_id, organization_id: course_id, course_id, title: "Test Module".to_string(), position: 1, created_at: Utc::now(), }, lessons: vec![lesson], }; let pub_course = PublishedCourse { course: Course { id: course_id, organization_id: Uuid::new_v4(), title: "Test Course".to_string(), description: None, instructor_id: Uuid::new_v4(), pacing_mode: "self_paced".to_string(), start_date: None, end_date: None, passing_percentage: 70, certificate_template: None, price: 0.0, currency: "USD".to_string(), marketing_metadata: None, course_image_url: None, level: None, course_type: None, created_at: Utc::now(), updated_at: Utc::now(), }, organization: Organization { id: Uuid::new_v4(), name: "Test Org".to_string(), domain: None, logo_url: None, primary_color: None, secondary_color: None, certificate_template: None, platform_name: None, favicon_url: None, logo_variant: None, created_at: Utc::now(), updated_at: Utc::now(), }, grading_categories: vec![], modules: vec![pub_module], instructors: None, dependencies: None, }; let course_with_price = Course { id: course_id, organization_id: Uuid::new_v4(), title: "Test Course".to_string(), description: None, instructor_id: Uuid::new_v4(), pacing_mode: "self_paced".to_string(), start_date: None, end_date: None, passing_percentage: 70, certificate_template: None, price: 29.99, currency: "USD".to_string(), marketing_metadata: None, course_image_url: None, level: None, course_type: None, created_at: Utc::now(), updated_at: Utc::now(), }; assert_eq!(course_with_price.price, 29.99); let serialized = serde_json::to_string(&pub_course).unwrap(); let deserialized: PublishedCourse = serde_json::from_str(&serialized).unwrap(); assert_eq!(pub_course.course.title, deserialized.course.title); assert_eq!(pub_course.modules.len(), deserialized.modules.len()); assert_eq!(deserialized.modules[0].lessons[0].title, "Test Lesson"); } } // ==================== Advanced Grading / Rubrics ==================== #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct Rubric { pub id: Uuid, pub organization_id: Uuid, pub course_id: Option, pub created_by: Uuid, pub name: String, pub description: Option, pub total_points: i32, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct RubricCriterion { pub id: Uuid, pub rubric_id: Uuid, pub name: String, pub description: Option, pub max_points: i32, pub position: i32, pub created_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct RubricLevel { pub id: Uuid, pub criterion_id: Uuid, pub name: String, pub description: Option, pub points: i32, pub position: i32, pub created_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct LessonRubric { pub id: Uuid, pub lesson_id: Uuid, pub rubric_id: Uuid, pub is_active: bool, pub assigned_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] pub struct RubricAssessment { pub id: Uuid, pub lesson_id: Uuid, pub rubric_id: Uuid, pub user_id: Uuid, pub graded_by: Option, pub submission_id: Option, pub total_score: f32, pub max_score: i32, pub feedback: Option, pub status: String, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] pub struct AssessmentScore { pub id: Uuid, pub assessment_id: Uuid, pub criterion_id: Uuid, pub level_id: Option, pub points: f32, pub feedback: Option, pub created_at: DateTime, } // ==================== Learning Sequences / Dependencies ==================== #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct LessonDependency { pub id: Uuid, pub organization_id: Uuid, pub lesson_id: Uuid, pub prerequisite_lesson_id: Uuid, pub min_score_percentage: Option, pub created_at: DateTime, } // ==================== Live Learning (Meetings) ==================== #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct Meeting { pub id: Uuid, pub organization_id: Uuid, pub course_id: Uuid, pub title: String, pub description: Option, pub provider: String, // "jitsi" | "bbb" pub meeting_id: String, // Room name or external ID pub start_at: DateTime, pub duration_minutes: i32, pub join_url: Option, pub is_active: bool, pub created_at: DateTime, pub updated_at: DateTime, } // ==================== Student portfolio & Badges ==================== #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct Badge { pub id: Uuid, pub organization_id: Uuid, pub name: String, pub description: String, pub icon_url: String, pub criteria: serde_json::Value, pub created_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct UserBadge { pub id: Uuid, pub user_id: Uuid, pub badge_id: Uuid, pub awarded_at: DateTime, pub evidence_url: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct PublicProfile { pub user_id: Uuid, pub full_name: String, pub avatar_url: Option, pub bio: Option, pub badges: Vec, pub level: i32, pub xp: i32, pub completed_courses_count: i64, } // ==================== Test Templates ==================== #[derive(Debug, Serialize, Deserialize, sqlx::Type, Clone, PartialEq)] #[sqlx(type_name = "course_level", rename_all = "lowercase")] #[serde(rename_all = "lowercase")] pub enum CourseLevel { Beginner, Beginner_1, Beginner_2, Intermediate, Intermediate_1, Intermediate_2, Advanced, Advanced_1, Advanced_2, } #[derive(Debug, Serialize, Deserialize, sqlx::Type, Clone, PartialEq)] #[sqlx(type_name = "course_type", rename_all = "lowercase")] #[serde(rename_all = "snake_case")] pub enum CourseType { Intensive, Regular, } #[derive(Debug, Serialize, Deserialize, sqlx::Type, Clone, PartialEq)] #[sqlx(type_name = "test_type")] pub enum TestType { #[sqlx(rename = "CA")] CA, // Continuous Assessment #[sqlx(rename = "MWT")] MWT, // Midterm Written Test #[sqlx(rename = "MOT")] MOT, // Midterm Oral Test #[sqlx(rename = "FOT")] FOT, // Final Oral Test #[sqlx(rename = "FWT")] FWT, // Final Written Test } impl std::fmt::Display for TestType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { TestType::CA => write!(f, "CA"), TestType::MWT => write!(f, "MWT"), TestType::MOT => write!(f, "MOT"), TestType::FOT => write!(f, "FOT"), TestType::FWT => write!(f, "FWT"), } } } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct TestTemplate { pub id: Uuid, pub organization_id: Uuid, pub mysql_course_id: Option, // Reference to imported MySQL course pub name: String, pub description: Option, pub level: Option, // Deprecated: use mysql_course_id instead pub course_type: Option, // Deprecated: use mysql_course_id instead pub test_type: TestType, pub duration_minutes: i32, pub passing_score: i32, // 0-100 percentage pub total_points: i32, pub instructions: Option, pub template_data: serde_json::Value, // Complete test structure with sections and questions pub tags: Option>, pub is_active: bool, pub usage_count: i32, pub created_by: Uuid, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct TestTemplateSection { pub id: Uuid, pub template_id: Uuid, pub title: String, pub description: Option, pub section_order: i32, pub points: i32, pub instructions: Option, pub section_data: Option, // Section-specific configuration pub created_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct TestTemplateQuestion { pub id: Uuid, pub template_id: Uuid, pub section_id: Option, pub question_order: i32, pub question_type: String, // "multiple-choice", "true-false", "short-answer", "essay", "matching", "ordering" pub question_text: String, pub options: Option, // Array of options for multiple choice pub correct_answer: Option, // Can be index, array of indices, or text pub explanation: Option, pub points: i32, pub metadata: Option, // Additional question metadata pub created_at: DateTime, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct CreateTestTemplatePayload { pub name: String, pub description: Option, pub mysql_course_id: Option, // Reference to imported MySQL course (preferred) pub level: Option, // Fallback if mysql_course_id not provided pub course_type: Option, // Fallback if mysql_course_id not provided pub test_type: TestType, pub duration_minutes: i32, pub passing_score: i32, pub total_points: i32, pub instructions: Option, pub template_data: serde_json::Value, pub tags: Option>, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct UpdateTestTemplatePayload { pub name: Option, pub description: Option, pub mysql_course_id: Option, pub level: Option, pub course_type: Option, pub test_type: Option, pub duration_minutes: Option, pub passing_score: Option, pub total_points: Option, pub instructions: Option, pub template_data: Option, pub tags: Option>, pub is_active: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct TestTemplateWithQuestions { pub template: TestTemplate, pub sections: Vec, pub questions: Vec, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ApplyTemplatePayload { pub lesson_id: Uuid, pub grading_category_id: Option, } // ==================== Question Bank ==================== #[derive(Debug, Serialize, Deserialize, Clone, Copy, sqlx::Type, PartialEq)] #[sqlx(type_name = "question_bank_type")] pub enum QuestionBankType { #[sqlx(rename = "multiple-choice")] MultipleChoice, #[sqlx(rename = "true-false")] TrueFalse, #[sqlx(rename = "short-answer")] ShortAnswer, #[sqlx(rename = "essay")] Essay, #[sqlx(rename = "matching")] Matching, #[sqlx(rename = "ordering")] Ordering, #[sqlx(rename = "fill-in-the-blanks")] FillInTheBlanks, #[sqlx(rename = "audio-response")] AudioResponse, #[sqlx(rename = "hotspot")] Hotspot, #[sqlx(rename = "code-lab")] CodeLab, } impl std::fmt::Display for QuestionBankType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { QuestionBankType::MultipleChoice => write!(f, "multiple-choice"), QuestionBankType::TrueFalse => write!(f, "true-false"), QuestionBankType::ShortAnswer => write!(f, "short-answer"), QuestionBankType::Essay => write!(f, "essay"), QuestionBankType::Matching => write!(f, "matching"), QuestionBankType::Ordering => write!(f, "ordering"), QuestionBankType::FillInTheBlanks => write!(f, "fill-in-the-blanks"), QuestionBankType::AudioResponse => write!(f, "audio-response"), QuestionBankType::Hotspot => write!(f, "hotspot"), QuestionBankType::CodeLab => write!(f, "code-lab"), } } } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct QuestionBank { pub id: Uuid, pub organization_id: Uuid, pub question_text: String, pub question_type: QuestionBankType, pub options: Option, pub correct_answer: Option, pub explanation: Option, pub audio_url: Option, pub audio_text: Option, pub audio_status: Option, pub audio_metadata: Option, pub media_url: Option, pub media_type: Option, pub points: i32, pub difficulty: Option, pub tags: Option>, pub skill_assessed: Option, // reading, listening, speaking, writing pub source: Option, pub source_metadata: Option, pub imported_mysql_id: Option, pub imported_mysql_course_id: Option, pub usage_count: Option, pub last_used_at: Option>, pub is_active: bool, pub is_archived: bool, pub created_by: Option, pub created_at: chrono::DateTime, pub updated_at: chrono::DateTime, pub embedding: Option, // PGVector embedding for semantic search pub embedding_updated_at: Option>, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct CreateQuestionBankPayload { pub question_text: String, pub question_type: QuestionBankType, pub options: Option, pub correct_answer: Option, pub explanation: Option, pub points: Option, pub difficulty: Option, pub tags: Option>, pub media_url: Option, pub media_type: Option, pub skill_assessed: Option, // reading, listening, speaking, writing } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct UpdateQuestionBankPayload { pub question_text: Option, pub question_type: Option, pub options: Option, pub correct_answer: Option, pub explanation: Option, pub points: Option, pub difficulty: Option, pub tags: Option>, pub is_active: Option, pub is_archived: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ImportQuestionFromMySQLPayload { pub mysql_course_id: Option, pub question_ids: Option>, // MySQL question IDs pub import_all: Option, // Import all questions from MySQL } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct QuestionBankFilters { pub question_type: Option, pub difficulty: Option, pub tags: Option, // Comma-separated pub source: Option, pub search: Option, pub has_audio: Option, } // ==================== AUDIO RESPONSE MODELS ==================== // For speaking practice exercises with AI + Teacher evaluation #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] pub enum AudioResponseStatus { Pending, AiEvaluated, TeacherEvaluated, BothEvaluated, } impl std::fmt::Display for AudioResponseStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { AudioResponseStatus::Pending => write!(f, "pending"), AudioResponseStatus::AiEvaluated => write!(f, "ai_evaluated"), AudioResponseStatus::TeacherEvaluated => write!(f, "teacher_evaluated"), AudioResponseStatus::BothEvaluated => write!(f, "both_evaluated"), } } } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct AudioResponse { pub id: Uuid, pub organization_id: Uuid, pub user_id: Uuid, pub course_id: Uuid, pub lesson_id: Uuid, pub block_id: Uuid, pub prompt: String, pub transcript: Option, pub audio_url: Option, pub audio_data: Option>, // AI Evaluation pub ai_score: Option, pub ai_found_keywords: Option>, pub ai_feedback: Option, pub ai_evaluated_at: Option>, // Teacher Evaluation pub teacher_score: Option, pub teacher_feedback: Option, pub teacher_evaluated_at: Option>, pub teacher_evaluated_by: Option, // Status and metadata pub status: AudioResponseStatus, pub attempt_number: i32, pub duration_seconds: Option, pub metadata: Option, // Timestamps pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct AudioResponseWithUser { pub id: Uuid, pub user_id: Uuid, pub student_name: String, pub student_email: String, pub course_id: Uuid, pub course_title: String, pub lesson_id: Uuid, pub lesson_title: String, pub block_id: Uuid, pub prompt: String, pub transcript: Option, pub ai_score: Option, pub ai_feedback: Option, pub teacher_score: Option, pub teacher_feedback: Option, pub status: AudioResponseStatus, pub created_at: DateTime, pub attempt_number: i32, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] pub struct AudioResponseStats { pub organization_id: Uuid, pub course_id: Uuid, pub lesson_id: Uuid, pub total_responses: i64, pub ai_evaluated: i64, pub teacher_evaluated: i64, pub fully_evaluated: i64, pub pending: i64, pub avg_ai_score: Option, pub avg_teacher_score: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct CreateAudioResponsePayload { pub lesson_id: Uuid, pub block_id: Uuid, pub prompt: String, pub transcript: Option, pub duration_seconds: Option, pub metadata: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct UpdateAudioResponsePayload { pub teacher_score: i32, pub teacher_feedback: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct AudioResponseEvaluation { pub score: i32, pub found_keywords: Vec, pub feedback: String, }