142 lines
4.5 KiB
PHP
142 lines
4.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Database\Migrations;
|
|
|
|
use CodeIgniter\Database\Migration;
|
|
use RuntimeException;
|
|
|
|
final class EnsureSchoolYearOnFinancialTables extends Migration
|
|
{
|
|
/** @var array<string, bool> table => nullable */
|
|
private array $tables = [
|
|
'discount_usages' => false,
|
|
'event_charges' => true,
|
|
'invoice_event' => true,
|
|
'payment_error' => true,
|
|
'payment_notification_logs' => true,
|
|
'payment_transactions' => true,
|
|
'reimbursement_batch_items' => true,
|
|
];
|
|
|
|
public function up(): void
|
|
{
|
|
if ($this->db->DBDriver !== 'MySQLi') {
|
|
throw new RuntimeException('This migration requires MySQL/MariaDB through the MySQLi driver.');
|
|
}
|
|
|
|
foreach ($this->tables as $table => $nullable) {
|
|
if (! $this->db->tableExists($table)) {
|
|
continue;
|
|
}
|
|
|
|
if (! $this->db->fieldExists('school_year', $table)) {
|
|
$this->forge->addColumn($table, [
|
|
'school_year' => [
|
|
'type' => 'VARCHAR',
|
|
'constraint' => 9,
|
|
'null' => $nullable,
|
|
'after' => $this->afterColumn($table),
|
|
],
|
|
]);
|
|
}
|
|
|
|
$this->backfillSchoolYear($table);
|
|
$this->assertValidYears($table);
|
|
|
|
$nullSql = $nullable ? 'NULL' : 'NOT NULL';
|
|
$this->db->query(
|
|
sprintf(
|
|
'ALTER TABLE `%s` MODIFY `school_year` VARCHAR(9) %s',
|
|
str_replace('`', '``', $table),
|
|
$nullSql
|
|
)
|
|
);
|
|
|
|
$this->ensureIndex($table);
|
|
}
|
|
}
|
|
|
|
public function down(): void
|
|
{
|
|
// These columns are part of the business model and must not be dropped on rollback.
|
|
}
|
|
|
|
private function assertValidYears(string $table): void
|
|
{
|
|
$sql = sprintf(
|
|
"SELECT COUNT(*) AS invalid_count
|
|
FROM `%s`
|
|
WHERE `school_year` IS NOT NULL
|
|
AND (
|
|
`school_year` NOT REGEXP '^[0-9]{4}-[0-9]{4}$'
|
|
OR CAST(RIGHT(`school_year`, 4) AS UNSIGNED)
|
|
<> CAST(LEFT(`school_year`, 4) AS UNSIGNED) + 1
|
|
)",
|
|
str_replace('`', '``', $table)
|
|
);
|
|
|
|
$row = $this->db->query($sql)->getRowArray();
|
|
$invalid = (int) ($row['invalid_count'] ?? 0);
|
|
|
|
if ($invalid > 0) {
|
|
throw new RuntimeException(
|
|
"Cannot normalize {$table}.school_year: {$invalid} invalid value(s). Expected YYYY-YYYY, for example 2025-2026."
|
|
);
|
|
}
|
|
}
|
|
|
|
private function backfillSchoolYear(string $table): void
|
|
{
|
|
if ($table === 'discount_usages' && $this->db->tableExists('invoices')) {
|
|
$this->db->query(
|
|
"UPDATE `discount_usages` du
|
|
INNER JOIN `invoices` i ON i.`id` = du.`invoice_id`
|
|
SET du.`school_year` = i.`school_year`
|
|
WHERE (du.`school_year` IS NULL OR TRIM(du.`school_year`) = '')
|
|
AND i.`school_year` REGEXP '^[0-9]{4}-[0-9]{4}$'
|
|
AND CAST(RIGHT(i.`school_year`, 4) AS UNSIGNED)
|
|
= CAST(LEFT(i.`school_year`, 4) AS UNSIGNED) + 1"
|
|
);
|
|
}
|
|
}
|
|
|
|
private function ensureIndex(string $table): void
|
|
{
|
|
$indexName = 'idx_' . $table . '_school_year';
|
|
if (strlen($indexName) > 64) {
|
|
$indexName = 'idx_' . substr(hash('sha256', $table . '_school_year'), 0, 24);
|
|
}
|
|
|
|
$row = $this->db->query(
|
|
'SELECT COUNT(*) AS index_count
|
|
FROM information_schema.STATISTICS
|
|
WHERE TABLE_SCHEMA = DATABASE()
|
|
AND TABLE_NAME = ?
|
|
AND COLUMN_NAME = ?',
|
|
[$table, 'school_year']
|
|
)->getRowArray();
|
|
|
|
if ((int) ($row['index_count'] ?? 0) === 0) {
|
|
$this->db->query(sprintf(
|
|
'CREATE INDEX `%s` ON `%s` (`school_year`)',
|
|
str_replace('`', '``', $indexName),
|
|
str_replace('`', '``', $table)
|
|
));
|
|
}
|
|
}
|
|
|
|
private function afterColumn(string $table): string
|
|
{
|
|
$preferred = ['semester', 'invoice_id', 'payment_id', 'batch_id'];
|
|
foreach ($preferred as $column) {
|
|
if ($this->db->fieldExists($column, $table)) {
|
|
return $column;
|
|
}
|
|
}
|
|
|
|
return 'id';
|
|
}
|
|
}
|