154 lines
6.0 KiB
PHP
154 lines
6.0 KiB
PHP
<?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());
|
|
}
|
|
}
|
|
}
|