@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class FinancialReimbursementPoHardening extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->ensureReimbursementInvariants();
|
||||
$this->ensurePurchaseOrderInvariants();
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
if ($this->db->tableExists('reimbursements')) {
|
||||
$this->dropIndexIfExists('reimbursements', 'uniq_reimbursement_active_expense');
|
||||
if ($this->db->fieldExists('active_expense_id', 'reimbursements')) {
|
||||
$this->forge->dropColumn('reimbursements', 'active_expense_id');
|
||||
}
|
||||
}
|
||||
$this->dropIndexIfExists('reimbursement_batches', 'uniq_reimbursement_batch_year_sequence');
|
||||
$this->dropIndexIfExists('reimbursement_batch_items', 'uniq_reimbursement_batch_item_active_expense');
|
||||
}
|
||||
|
||||
private function ensureReimbursementInvariants(): void
|
||||
{
|
||||
if (!$this->db->tableExists('reimbursements')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->forge->modifyColumn('reimbursements', [
|
||||
'status' => [
|
||||
'type' => 'ENUM',
|
||||
'constraint' => ['pending', 'approved', 'paid', 'rejected', 'reversed', 'Pending', 'Approved', 'Paid', 'Rejected', 'Reversed'],
|
||||
'default' => 'pending',
|
||||
'null' => false,
|
||||
],
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
throw new \RuntimeException('Could not normalize reimbursement statuses: ' . $e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
if (!$this->db->fieldExists('active_expense_id', 'reimbursements')) {
|
||||
try {
|
||||
$this->db->query(
|
||||
"ALTER TABLE reimbursements
|
||||
ADD active_expense_id INT
|
||||
GENERATED ALWAYS AS (
|
||||
CASE
|
||||
WHEN expense_id IS NOT NULL
|
||||
AND LOWER(status) NOT IN ('reversed','rejected','cancelled','canceled','voided')
|
||||
THEN expense_id
|
||||
ELSE NULL
|
||||
END
|
||||
) STORED"
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
throw new \RuntimeException('Could not add reimbursement active_expense_id generated column: ' . $e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
$this->createIndexIfMissing('reimbursements', 'uniq_reimbursement_active_expense', 'CREATE UNIQUE INDEX uniq_reimbursement_active_expense ON reimbursements (active_expense_id)');
|
||||
$this->createIndexIfMissing('reimbursement_batches', 'uniq_reimbursement_batch_year_sequence', 'CREATE UNIQUE INDEX uniq_reimbursement_batch_year_sequence ON reimbursement_batches (school_year, yearly_batch_number)');
|
||||
$this->createIndexIfMissing('reimbursement_batch_items', 'uniq_reimbursement_batch_item_active_expense', 'CREATE UNIQUE INDEX uniq_reimbursement_batch_item_active_expense ON reimbursement_batch_items (batch_id, expense_id)');
|
||||
}
|
||||
|
||||
private function ensurePurchaseOrderInvariants(): void
|
||||
{
|
||||
if (!$this->db->tableExists('purchase_order_items')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->forge->modifyColumn('purchase_order_items', [
|
||||
'quantity' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'null' => false,
|
||||
],
|
||||
'received_qty' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'default' => 0,
|
||||
'null' => false,
|
||||
],
|
||||
'unit_cost' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '10,2',
|
||||
'default' => 0,
|
||||
'null' => false,
|
||||
],
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
throw new \RuntimeException('Could not tighten purchase order item columns: ' . $e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->db->query(
|
||||
'ALTER TABLE purchase_order_items
|
||||
ADD CONSTRAINT chk_po_item_quantities
|
||||
CHECK (quantity > 0 AND received_qty >= 0 AND received_qty <= quantity AND unit_cost >= 0)'
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
throw new \RuntimeException('Could not add purchase order quantity check: ' . $e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function createIndexIfMissing(string $table, string $indexName, string $sql): void
|
||||
{
|
||||
if (!$this->db->tableExists($table)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$indexes = $this->db->query('SHOW INDEX FROM ' . $this->db->escapeIdentifiers($table))->getResultArray();
|
||||
foreach ($indexes as $index) {
|
||||
if (($index['Key_name'] ?? '') === $indexName) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->query($sql);
|
||||
$indexes = $this->db->query('SHOW INDEX FROM ' . $this->db->escapeIdentifiers($table))->getResultArray();
|
||||
foreach ($indexes as $index) {
|
||||
if (($index['Key_name'] ?? '') === $indexName) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Required index ' . $indexName . ' was not created.');
|
||||
}
|
||||
|
||||
private function dropIndexIfExists(string $table, string $indexName): void
|
||||
{
|
||||
if (!$this->db->tableExists($table)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$indexes = $this->db->query('SHOW INDEX FROM ' . $this->db->escapeIdentifiers($table))->getResultArray();
|
||||
foreach ($indexes as $index) {
|
||||
if (($index['Key_name'] ?? '') === $indexName) {
|
||||
$this->db->query('DROP INDEX ' . $this->db->escapeIdentifiers($indexName) . ' ON ' . $this->db->escapeIdentifiers($table));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('warning', 'Could not drop index ' . $indexName . ': ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user