db->tableExists('school_years')) { $this->forge->addField([ 'id' => [ 'type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true, ], 'name' => [ 'type' => 'VARCHAR', 'constraint' => 9, 'null' => false, ], 'status' => [ 'type' => 'VARCHAR', 'constraint' => 20, 'null' => false, 'default' => 'draft', ], 'starts_on' => [ 'type' => 'DATE', 'null' => true, ], 'ends_on' => [ 'type' => 'DATE', 'null' => true, ], 'description' => [ 'type' => 'TEXT', 'null' => true, ], 'registration_starts_on' => [ 'type' => 'DATE', 'null' => true, ], 'registration_ends_on' => [ 'type' => 'DATE', 'null' => true, ], 'previous_school_year_id' => [ 'type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true, ], 'next_school_year_id' => [ 'type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true, ], 'activated_at' => [ 'type' => 'DATETIME', 'null' => true, ], 'closing_started_at' => [ 'type' => 'DATETIME', 'null' => true, ], 'closed_at' => [ 'type' => 'DATETIME', 'null' => true, ], 'archived_at' => [ 'type' => 'DATETIME', 'null' => true, ], 'created_by' => [ 'type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true, ], 'updated_by' => [ 'type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true, ], 'created_at' => [ 'type' => 'DATETIME', 'null' => true, ], 'updated_at' => [ 'type' => 'DATETIME', 'null' => true, ], ]); $this->forge->addKey('id', true); $this->forge->addKey('name', false, true); $this->forge->addKey('status'); $this->forge->createTable('school_years'); } else { $this->ensureSchoolYearColumns(); } $this->createClosingTables(); $configuredYear = $this->configuredSchoolYear(); if ($configuredYear !== null && $this->isValidYearName($configuredYear)) { $existing = $this->db->table('school_years') ->where('name', $configuredYear) ->get() ->getRowArray(); if ($existing === null) { $this->db->table('school_years')->insert([ 'name' => $configuredYear, 'status' => 'active', 'activated_at' => date('Y-m-d H:i:s'), 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]); } } } public function down(): void { $this->forge->dropTable('school_year_transition_logs', true); $this->forge->dropTable('school_year_closing_items', true); $this->forge->dropTable('school_year_closing_batches', true); $this->forge->dropTable('school_years', true); } private function ensureSchoolYearColumns(): void { $fields = $this->db->getFieldNames('school_years'); $add = []; $definitions = [ 'description' => ['type' => 'TEXT', 'null' => true], 'registration_starts_on' => ['type' => 'DATE', 'null' => true], 'registration_ends_on' => ['type' => 'DATE', 'null' => true], 'previous_school_year_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true], 'next_school_year_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true], 'activated_at' => ['type' => 'DATETIME', 'null' => true], 'closing_started_at' => ['type' => 'DATETIME', 'null' => true], 'closed_at' => ['type' => 'DATETIME', 'null' => true], 'archived_at' => ['type' => 'DATETIME', 'null' => true], 'created_by' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true], 'updated_by' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true], ]; foreach ($definitions as $field => $definition) { if (! in_array($field, $fields, true)) { $add[$field] = $definition; } } if ($add !== []) { $this->forge->addColumn('school_years', $add); } } private function createClosingTables(): void { if (! $this->db->tableExists('school_year_closing_batches')) { $this->forge->addField([ 'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true], 'source_school_year_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true], 'target_school_year_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true], 'status' => ['type' => 'VARCHAR', 'constraint' => 30, 'null' => false, 'default' => 'preview'], 'preview_hash' => ['type' => 'CHAR', 'constraint' => 64, 'null' => true], 'total_families' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => false, 'default' => 0], 'total_positive_balance' => ['type' => 'DECIMAL', 'constraint' => '12,2', 'null' => false, 'default' => '0.00'], 'total_credit_balance' => ['type' => 'DECIMAL', 'constraint' => '12,2', 'null' => false, 'default' => '0.00'], 'started_by' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true], 'completed_by' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true], 'started_at' => ['type' => 'DATETIME', 'null' => true], 'completed_at' => ['type' => 'DATETIME', 'null' => true], 'failed_at' => ['type' => 'DATETIME', 'null' => true], 'failure_message' => ['type' => 'TEXT', 'null' => true], 'created_at' => ['type' => 'DATETIME', 'null' => true], 'updated_at' => ['type' => 'DATETIME', 'null' => true], ]); $this->forge->addKey('id', true); $this->forge->addKey(['source_school_year_id', 'status']); $this->forge->createTable('school_year_closing_batches'); } if (! $this->db->tableExists('school_year_closing_items')) { $this->forge->addField([ 'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true], 'closing_batch_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true], 'family_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true], 'source_balance' => ['type' => 'DECIMAL', 'constraint' => '12,2', 'null' => false, 'default' => '0.00'], 'credit_amount' => ['type' => 'DECIMAL', 'constraint' => '12,2', 'null' => false, 'default' => '0.00'], 'adjustment_amount' => ['type' => 'DECIMAL', 'constraint' => '12,2', 'null' => false, 'default' => '0.00'], 'carry_forward_amount' => ['type' => 'DECIMAL', 'constraint' => '12,2', 'null' => false, 'default' => '0.00'], 'target_invoice_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true], 'target_adjustment_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true], 'status' => ['type' => 'VARCHAR', 'constraint' => 30, 'null' => false, 'default' => 'pending'], 'error_message' => ['type' => 'TEXT', 'null' => true], 'created_at' => ['type' => 'DATETIME', 'null' => true], 'updated_at' => ['type' => 'DATETIME', 'null' => true], ]); $this->forge->addKey('id', true); $this->forge->addKey(['closing_batch_id', 'family_id'], false, true); $this->forge->createTable('school_year_closing_items'); } if (! $this->db->tableExists('school_year_transition_logs')) { $this->forge->addField([ 'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true], 'school_year_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true], 'from_status' => ['type' => 'VARCHAR', 'constraint' => 20, 'null' => true], 'to_status' => ['type' => 'VARCHAR', 'constraint' => 20, 'null' => true], 'action' => ['type' => 'VARCHAR', 'constraint' => 80, 'null' => false], 'performed_by' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true], 'metadata_json' => ['type' => 'TEXT', 'null' => true], 'created_at' => ['type' => 'DATETIME', 'null' => false], ]); $this->forge->addKey('id', true); $this->forge->addKey(['school_year_id', 'created_at']); $this->forge->createTable('school_year_transition_logs'); } } private function configuredSchoolYear(): ?string { if (! $this->db->tableExists('configuration')) { return null; } $row = $this->db->table('configuration') ->select('config_value') ->where('config_key', 'school_year') ->orderBy('id', 'DESC') ->get(1) ->getRowArray(); $value = trim((string) ($row['config_value'] ?? '')); return $value !== '' ? $value : null; } private function isValidYearName(string $value): bool { if (! preg_match('/^(\d{4})-(\d{4})$/', $value, $matches)) { return false; } return (int) $matches[2] === (int) $matches[1] + 1; } }