Files
alrahma_sunday_school/app/Database/Migrations/2026-07-19-000500_FinancialReimbursementPoHardening.php
T
root a30c1398a1
Tests / PHPUnit (push) Failing after 1m21s
fix financials
2026-07-18 22:57:40 -04:00

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());
}
}
}