addInstallmentSequenceColumn(); $this->ensureIndexes(); $this->ensureConfigurationDefaults(); $this->archivePaypalTables(); $this->refreshFinancialNavItems(); } public function down() { $this->restorePaypalTables(); if ($this->db->tableExists('payments') && $this->db->fieldExists('installment_seq', 'payments')) { $this->forge->dropColumn('payments', 'installment_seq'); } $this->dropIndexIfExists('payments', 'idx_payments_invoice_id'); $this->dropIndexIfExists('payments', 'idx_payments_parent_year_semester'); $this->dropIndexIfExists('payments', 'uniq_payments_transaction_id'); $this->dropIndexIfExists('invoices', 'idx_invoices_parent_year_semester'); $this->dropIndexIfExists('discount_usages', 'idx_discount_usages_invoice_id'); $this->dropIndexIfExists('discount_usages', 'idx_discount_usages_parent_year_semester'); $this->dropIndexIfExists('refunds', 'idx_refunds_invoice_id'); $this->dropIndexIfExists('refunds', 'idx_refunds_parent_year_semester_status'); $this->dropIndexIfExists('additional_charges', 'idx_additional_charges_invoice_id'); $this->dropIndexIfExists('additional_charges', 'idx_additional_charges_parent_year_semester_status'); $this->dropIndexIfExists('invoice_event', 'idx_invoice_event_invoice_id'); if ($this->db->tableExists('nav_items')) { $forecastRow = $this->db->table('nav_items') ->select('id') ->where('url', 'administrator/tuition-forecast') ->get() ->getRowArray(); if ($forecastRow && $this->db->tableExists('role_nav_items')) { $this->db->table('role_nav_items') ->where('nav_item_id', (int) $forecastRow['id']) ->delete(); } $this->db->table('nav_items') ->where('url', 'administrator/tuition-forecast') ->delete(); } } protected function addInstallmentSequenceColumn(): void { if ($this->db->tableExists('payments') && !$this->db->fieldExists('installment_seq', 'payments')) { $this->forge->addColumn('payments', [ 'installment_seq' => [ 'type' => 'INT', 'constraint' => 11, 'null' => true, 'after' => 'number_of_installments', ], ]); } } protected function ensureIndexes(): void { $this->addIndexIfMissing('payments', 'idx_payments_invoice_id', ['invoice_id']); $this->addIndexIfMissing('payments', 'idx_payments_parent_year_semester', ['parent_id', 'school_year', 'semester']); $this->addIndexIfMissing('invoices', 'idx_invoices_parent_year_semester', ['parent_id', 'school_year', 'semester']); $this->addIndexIfMissing('discount_usages', 'idx_discount_usages_invoice_id', ['invoice_id']); $this->addIndexIfMissing('discount_usages', 'idx_discount_usages_parent_year_semester', ['parent_id', 'school_year', 'semester']); $this->addIndexIfMissing('refunds', 'idx_refunds_invoice_id', ['invoice_id']); $this->addIndexIfMissing('refunds', 'idx_refunds_parent_year_semester_status', ['parent_id', 'school_year', 'semester', 'status']); $this->addIndexIfMissing('additional_charges', 'idx_additional_charges_invoice_id', ['invoice_id']); $this->addIndexIfMissing('additional_charges', 'idx_additional_charges_parent_year_semester_status', ['parent_id', 'school_year', 'semester', 'status']); $this->addIndexIfMissing('invoice_event', 'idx_invoice_event_invoice_id', ['invoice_id']); if ($this->db->tableExists('payments') && $this->db->fieldExists('transaction_id', 'payments') && !$this->hasDuplicateTransactionIds()) { $this->addIndexIfMissing('payments', 'uniq_payments_transaction_id', ['transaction_id'], true); } } protected function ensureConfigurationDefaults(): void { if (!$this->db->tableExists('configuration')) { return; } $defaults = [ 'tuition_calculator_version' => 'old', 'youth_fee' => '200.00', 'new_tuition_full_amount' => '370.00', 'new_tuition_youth_amount' => '200.00', 'new_tuition_second_student_discount' => '50.00', 'new_tuition_third_student_discount' => '50.00', 'new_tuition_fourth_plus_discount' => '100.00', ]; foreach ($defaults as $key => $value) { $row = $this->db->table('configuration') ->select('id, config_value') ->where('config_key', $key) ->orderBy('id', 'ASC') ->get() ->getRowArray(); if (!$row) { $this->db->table('configuration')->insert([ 'config_key' => $key, 'config_value' => $value, ]); continue; } if (trim((string) ($row['config_value'] ?? '')) === '') { $this->db->table('configuration') ->where('config_key', $key) ->update(['config_value' => $value]); } } } protected function archivePaypalTables(): void { $this->renameTableIfPresent('paypal_payments', 'archived_paypal_payments'); $this->renameTableIfPresent('paypal_transactions', 'archived_paypal_transactions'); } protected function restorePaypalTables(): void { $this->renameTableIfPresent('archived_paypal_payments', 'paypal_payments'); $this->renameTableIfPresent('archived_paypal_transactions', 'paypal_transactions'); } protected function refreshFinancialNavItems(): void { if (!$this->db->tableExists('nav_items')) { return; } $parentColumn = null; if ($this->db->fieldExists('menu_parent_id', 'nav_items')) { $parentColumn = 'menu_parent_id'; } elseif ($this->db->fieldExists('parent_id', 'nav_items')) { $parentColumn = 'parent_id'; } if ($parentColumn === null) { return; } $this->db->table('nav_items') ->groupStart() ->where('url', 'administrator/paypal_transactions') ->orWhere('url', 'admin/paypal-transactions') ->orWhere('label', 'PaypalTransactions') ->orWhere('label', 'PayPal Transactions') ->groupEnd() ->delete(); $financialRow = $this->db->table('nav_items') ->select('id') ->where('label', 'Financial') ->where($parentColumn, null) ->get() ->getRowArray(); if (!$financialRow) { return; } $forecastUrl = 'administrator/tuition-forecast'; $existing = $this->db->table('nav_items') ->select('id') ->where('url', $forecastUrl) ->get() ->getRowArray(); if ($existing) { $forecastId = (int) $existing['id']; } else { $this->db->table('nav_items')->insert([ $parentColumn => (int) $financialRow['id'], 'label' => 'Tuition Forecast', 'url' => $forecastUrl, 'sort_order' => 4, 'is_enabled' => 1, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]); $forecastId = (int) $this->db->insertID(); } if ($forecastId <= 0 || !$this->db->tableExists('role_nav_items') || !$this->db->tableExists('roles')) { return; } $roles = $this->db->table('roles') ->select('id') ->whereIn('name', ['administrator', 'administrative staff', 'principal', 'vice_principal', 'head of department (finance)']) ->get() ->getResultArray(); foreach ($roles as $role) { $roleId = (int) ($role['id'] ?? 0); if ($roleId <= 0) { continue; } $exists = $this->db->table('role_nav_items') ->where('role_id', $roleId) ->where('nav_item_id', $forecastId) ->get() ->getRowArray(); if ($exists) { continue; } $this->db->table('role_nav_items')->insert([ 'role_id' => $roleId, 'nav_item_id' => $forecastId, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]); } } protected function renameTableIfPresent(string $from, string $to): void { if (!$this->db->tableExists($from) || $this->db->tableExists($to)) { return; } $this->db->query(sprintf('RENAME TABLE `%s` TO `%s`', $from, $to)); } protected function addIndexIfMissing(string $table, string $indexName, array $columns, bool $unique = false): void { if (!$this->db->tableExists($table) || $this->indexExists($table, $indexName)) { return; } $quotedColumns = implode(', ', array_map(static fn (string $column): string => '`' . $column . '`', $columns)); $type = $unique ? 'UNIQUE INDEX' : 'INDEX'; $sql = sprintf('ALTER TABLE `%s` ADD %s `%s` (%s)', $table, $type, $indexName, $quotedColumns); $this->db->query($sql); } protected function dropIndexIfExists(string $table, string $indexName): void { if (!$this->db->tableExists($table) || !$this->indexExists($table, $indexName)) { return; } $this->db->query(sprintf('ALTER TABLE `%s` DROP INDEX `%s`', $table, $indexName)); } protected function indexExists(string $table, string $indexName): bool { if (!$this->db->tableExists($table)) { return false; } $rows = $this->db->query(sprintf('SHOW INDEX FROM `%s`', $table))->getResultArray(); foreach ($rows as $row) { if (($row['Key_name'] ?? null) === $indexName) { return true; } } return false; } protected function hasDuplicateTransactionIds(): bool { $row = $this->db->table('payments') ->select('transaction_id') ->where('transaction_id IS NOT NULL', null, false) ->where('transaction_id !=', '') ->groupBy('transaction_id') ->having('COUNT(*) >', 1, false) ->get(1) ->getRowArray(); return $row !== null; } }