'integer', 'school_id' => 'integer', 'class_section_id' => 'integer', 'updated_by' => 'integer', // averages/scores: keep decimal to avoid float drift 'homework_avg' => 'decimal:2', 'quiz_avg' => 'decimal:2', 'project_avg' => 'decimal:2', 'midterm_exam_score' => 'decimal:2', 'final_exam_score' => 'decimal:2', 'attendance_score' => 'decimal:2', 'participation_score' => 'decimal:2', 'ptap_score' => 'decimal:2', 'test_avg' => 'decimal:2', 'semester_score' => 'decimal:2', 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; /* ============================================================ * Relationships (optional) * ============================================================ */ public function student(): BelongsTo { return $this->belongsTo(Student::class, 'student_id'); } public function classSection(): BelongsTo { return $this->belongsTo(ClassSection::class, 'class_section_id'); } public function school(): BelongsTo { return $this->belongsTo(School::class, 'school_id'); } public function updater(): BelongsTo { // Change Admin/User model to match your app return $this->belongsTo(Admin::class, 'updated_by'); } /* ============================================================ * Scopes * ============================================================ */ public function scopeForStudent(Builder $q, int $studentId): Builder { return $q->where('student_id', $studentId); } public function scopeForTerm(Builder $q, string $semester, string $schoolYear): Builder { return $q->where('semester', $semester) ->where('school_year', $schoolYear); } public function scopeForClassSection(Builder $q, ?int $classSectionId): Builder { return $classSectionId !== null ? $q->where('class_section_id', $classSectionId) : $q; } /* ============================================================ * Upsert (Laravel style) * ============================================================ */ /** * True upsert (update or create) by (student_id, semester, school_year). * Returns the saved model or null if required keys missing. */ public static function upsertScore(array $data): ?self { if (empty($data['student_id']) || empty($data['semester']) || empty($data['school_year'])) { return null; } $keys = [ 'student_id' => (int) $data['student_id'], 'semester' => (string) $data['semester'], 'school_year' => (string) $data['school_year'], ]; // Update fields: only allow fillable keys $payload = array_intersect_key($data, array_flip((new static)->getFillable())); return static::updateOrCreate($keys, $payload); } /** * legacy-compatible boolean wrapper (matches your legacy upsert(): bool). */ public static function upsertBool(array $data): bool { return (bool) static::upsertScore($data); } /* ============================================================ * Optional validation helper (FormRequest/controller) * ============================================================ */ public static function rules(bool $updating = false): array { $required = $updating ? 'sometimes' : 'required'; return [ 'student_id' => [$required, 'integer', 'min:1'], 'semester' => [$required, 'string', 'max:20'], 'school_year' => [$required, 'string', 'max:20'], 'school_id' => ['nullable', 'integer', 'min:1'], 'class_section_id' => ['nullable', 'integer', 'min:1'], 'updated_by' => ['nullable', 'integer', 'min:1'], 'homework_avg' => ['nullable', 'numeric', 'min:0'], 'quiz_avg' => ['nullable', 'numeric', 'min:0'], 'project_avg' => ['nullable', 'numeric', 'min:0'], 'midterm_exam_score' => ['nullable', 'numeric', 'min:0'], 'final_exam_score' => ['nullable', 'numeric', 'min:0'], 'attendance_score' => ['nullable', 'numeric', 'min:0'], 'participation_score' => ['nullable', 'numeric', 'min:0'], 'ptap_score' => ['nullable', 'numeric', 'min:0'], 'test_avg' => ['nullable', 'numeric', 'min:0'], 'semester_score' => ['nullable', 'numeric', 'min:0'], ]; } }