'integer', 'subject_id' => 'integer', 'is_member' => 'boolean', 'verified_by' => 'integer', 'verified_at' => 'datetime', 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; /* ============================================================ * Constants * ============================================================ */ public const TYPE_PRIMARY = 'primary'; public const TYPE_SECOND = 'second'; public static function normType(string $type): string { return strtolower(trim($type)) === self::TYPE_SECOND ? self::TYPE_SECOND : self::TYPE_PRIMARY; } /* ============================================================ * Scopes * ============================================================ */ public function scopeForSections(Builder $q, array $sectionIds): Builder { $ids = array_values(array_unique(array_filter(array_map('intval', $sectionIds)))); return empty($ids) ? $q->whereRaw('1=0') : $q->whereIn('class_section_id', $ids); } public function scopeForYear(Builder $q, string $year): Builder { return $q->where('school_year', trim($year)); } public function scopeForSemester(Builder $q, string $sem): Builder { $sem = trim($sem); return $sem === '' ? $q : $q->where('semester', $sem); } public function scopeForSubject(Builder $q, string $type, int $subjectId): Builder { return $q->where('subject_type', static::normType($type)) ->where('subject_id', $subjectId); } /* ============================================================ * Methods (legacy-compatible behavior) * ============================================================ */ /** * Fetch all membership rows for the given sections and term. * Returns an associative index ["::" => row(array)]. */ public static function getBySectionsAndTerm(array $sectionIds, string $year, string $sem): array { $ids = array_values(array_unique(array_filter(array_map('intval', $sectionIds)))); if (empty($ids)) { return []; } $year = trim($year); $sem = trim($sem); $q = static::query() ->forSections($ids) ->forYear($year); if ($sem !== '') { $q->where('semester', $sem); } // Prefer most recently updated membership when duplicates exist. $rows = $q->orderByDesc('updated_at') ->orderByDesc('id') ->get(); $out = []; foreach ($rows as $m) { $k = sprintf('%d:%s:%d', (int) $m->class_section_id, (string) $m->subject_type, (int) $m->subject_id); if (! isset($out[$k])) { $out[$k] = $m->toArray(); } } return $out; } /** * Upsert membership row for a specific subject and class/term. * Returns the row ID. * * Matches legacy logic: * - Key is (class_section_id, school_year, subject_type, subject_id) plus semester if provided. * - If semester is blank, we update most recent row for that key and keep its semester. * - If verifiedBy provided, set verified_by + verified_at. */ public static function upsertMembership( int $classSectionId, string $year, string $sem, string $type, int $subjectId, bool $isMember, ?int $verifiedBy = null ): int { $type = static::normType($type); $year = trim($year); $sem = trim($sem); $payload = [ 'class_section_id' => $classSectionId, 'school_year' => $year, 'semester' => $sem, 'subject_type' => $type, 'subject_id' => $subjectId, 'is_member' => $isMember ? 1 : 0, ]; if ($verifiedBy) { $payload['verified_by'] = $verifiedBy; $payload['verified_at'] = now(); } // If semester is provided, we can do a clean updateOrCreate. if ($sem !== '') { $row = static::query()->updateOrCreate( [ 'class_section_id' => $classSectionId, 'school_year' => $year, 'semester' => $sem, 'subject_type' => $type, 'subject_id' => $subjectId, ], $payload ); return (int) $row->id; } // Semester blank => match legacy behavior: find most recent row for the key (without semester filter). $existing = static::query() ->where('class_section_id', $classSectionId) ->where('school_year', $year) ->where('subject_type', $type) ->where('subject_id', $subjectId) ->orderByDesc('updated_at') ->orderByDesc('id') ->first(); if ($existing) { // Keep semester from the existing row if present (legacy behavior). if (! empty($existing->semester)) { $payload['semester'] = (string) $existing->semester; } $existing->fill($payload); $existing->save(); return (int) $existing->id; } // No existing row => create a new one (semester remains blank) $created = static::query()->create($payload); return (int) $created->id; } /* ============================================================ * Optional validation helper * ============================================================ */ public static function rules(bool $updating = false): array { $req = $updating ? 'sometimes' : 'required'; return [ 'class_section_id' => [$req, 'integer', 'min:1'], 'school_year' => [$req, 'string', 'max:20'], 'semester' => ['nullable', 'string', 'max:20'], 'subject_type' => [$req, 'string', 'in:'.implode(',', [self::TYPE_PRIMARY, self::TYPE_SECOND])], 'subject_id' => [$req, 'integer', 'min:1'], 'is_member' => ['nullable', 'boolean'], 'verified_by' => ['nullable', 'integer'], 'verified_at' => ['nullable', 'date'], ]; } }