fix test run issue
Tests / PHPUnit (push) Successful in 1m21s

This commit is contained in:
root
2026-07-18 23:18:49 -04:00
parent a30c1398a1
commit 2be16553df
15 changed files with 368 additions and 17 deletions
@@ -661,6 +661,7 @@ class RefundController extends BaseController
$payoutId = $this->refundPayoutModel->insert([
'refund_id' => $refundId,
'school_year' => (string)($lockedRefund['school_year'] ?? ''),
'amount_cents' => $newPaidCents,
'currency' => (string)($lockedRefund['currency'] ?? 'USD') ?: 'USD',
'payout_type' => 'cash_out',
@@ -830,6 +831,7 @@ class RefundController extends BaseController
$reversalId = $this->refundPayoutModel->insert([
'refund_id' => $refundId,
'school_year' => (string)($refund['school_year'] ?? ''),
'amount_cents' => $reverseCents,
'currency' => (string)($payout['currency'] ?? $refund['currency'] ?? 'USD') ?: 'USD',
'payout_type' => 'reversal',
@@ -22,6 +22,11 @@ class CreateInvoiceLines extends Migration
'unsigned' => true,
'null' => false,
],
'school_year' => [
'type' => 'VARCHAR',
'constraint' => 9,
'null' => false,
],
'line_type' => [
'type' => 'VARCHAR',
'constraint' => 50,
@@ -101,6 +106,7 @@ class CreateInvoiceLines extends Migration
$this->forge->createTable('invoice_lines', true);
}
$this->ensureSchoolYearColumn();
$this->backfillLegacyInvoiceLines();
}
@@ -125,7 +131,7 @@ class CreateInvoiceLines extends Migration
$existing = array_fill_keys(array_map(static fn ($row) => (int) ($row['invoice_id'] ?? 0), $existingRows), true);
$invoices = $this->db->table('invoices')
->select('id, total_amount, invoice_number, created_at, updated_at')
->select('id, total_amount, invoice_number, school_year, created_at, updated_at')
->orderBy('id', 'ASC')
->get()
->getResultArray();
@@ -143,6 +149,7 @@ class CreateInvoiceLines extends Migration
$this->db->table('invoice_lines')->insert([
'invoice_id' => $invoiceId,
'school_year' => (string)($invoice['school_year'] ?? ''),
'line_type' => 'legacy_invoice_total',
'source_type' => 'legacy_invoice',
'source_id' => $invoiceId,
@@ -164,4 +171,33 @@ class CreateInvoiceLines extends Migration
]);
}
}
private function ensureSchoolYearColumn(): void
{
if (!$this->db->tableExists('invoice_lines')) {
return;
}
if (!$this->db->fieldExists('school_year', 'invoice_lines')) {
$this->forge->addColumn('invoice_lines', [
'school_year' => [
'type' => 'VARCHAR',
'constraint' => 9,
'null' => true,
'after' => 'invoice_id',
],
]);
}
if ($this->db->tableExists('invoices')) {
$this->db->query(
"UPDATE invoice_lines il
INNER JOIN invoices i ON i.id = il.invoice_id
SET il.school_year = i.school_year
WHERE il.school_year IS NULL OR TRIM(il.school_year) = ''"
);
}
$this->db->query("ALTER TABLE invoice_lines MODIFY school_year VARCHAR(9) NOT NULL");
}
}
@@ -10,6 +10,7 @@ class AddRefundSourcesAndPayouts extends Migration
{
$this->ensureRefundSourceColumns();
$this->ensureRefundPayoutsTable();
$this->ensureRefundPayoutSchoolYearColumn();
$this->ensureRefundPayoutFingerprintColumns();
$this->backfillRefundSources();
$this->backfillLegacyPayouts();
@@ -67,6 +68,7 @@ class AddRefundSourcesAndPayouts extends Migration
$this->forge->addField([
'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
'refund_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => false],
'school_year' => ['type' => 'VARCHAR', 'constraint' => 9, 'null' => false],
'amount_cents' => ['type' => 'INT', 'constraint' => 11, 'null' => false],
'currency' => ['type' => 'CHAR', 'constraint' => 3, 'null' => false, 'default' => 'USD'],
'payout_type' => ['type' => 'VARCHAR', 'constraint' => 30, 'null' => false, 'default' => 'cash_out'],
@@ -94,6 +96,35 @@ class AddRefundSourcesAndPayouts extends Migration
$this->forge->createTable('refund_payouts', true);
}
private function ensureRefundPayoutSchoolYearColumn(): void
{
if (!$this->db->tableExists('refund_payouts')) {
return;
}
if (!$this->db->fieldExists('school_year', 'refund_payouts')) {
$this->forge->addColumn('refund_payouts', [
'school_year' => [
'type' => 'VARCHAR',
'constraint' => 9,
'null' => true,
'after' => 'refund_id',
],
]);
}
if ($this->db->tableExists('refunds')) {
$this->db->query(
"UPDATE refund_payouts rp
INNER JOIN refunds r ON r.id = rp.refund_id
SET rp.school_year = r.school_year
WHERE rp.school_year IS NULL OR TRIM(rp.school_year) = ''"
);
}
$this->db->query("ALTER TABLE refund_payouts MODIFY school_year VARCHAR(9) NOT NULL");
}
private function ensureRefundPayoutFingerprintColumns(): void
{
if (!$this->db->tableExists('refund_payouts')) {
@@ -161,7 +192,7 @@ class AddRefundSourcesAndPayouts extends Migration
}
$refunds = $this->db->table('refunds')
->select('id, refund_paid_amount, currency, refund_method, check_nbr, check_file, refunded_at, updated_by, created_at, updated_at')
->select('id, refund_paid_amount, currency, refund_method, check_nbr, check_file, school_year, refunded_at, updated_by, created_at, updated_at')
->where('refund_paid_amount >', 0)
->get()
->getResultArray();
@@ -182,6 +213,7 @@ class AddRefundSourcesAndPayouts extends Migration
$this->db->table('refund_payouts')->insert([
'refund_id' => $refundId,
'school_year' => (string)($refund['school_year'] ?? ''),
'amount_cents' => (int) round(((float) ($refund['refund_paid_amount'] ?? 0)) * 100),
'currency' => (string) ($refund['currency'] ?? 'USD') ?: 'USD',
'payout_type' => 'cash_out',
@@ -69,6 +69,8 @@ class FinancialReimbursementPoHardening extends Migration
private function ensurePurchaseOrderInvariants(): void
{
$this->ensurePurchaseOrderSchoolYearColumns();
if (!$this->db->tableExists('purchase_order_items')) {
return;
}
@@ -108,6 +110,63 @@ class FinancialReimbursementPoHardening extends Migration
}
}
private function ensurePurchaseOrderSchoolYearColumns(): void
{
foreach (['purchase_orders' => 'status', 'purchase_order_items' => 'purchase_order_id'] as $table => $after) {
if (!$this->db->tableExists($table)) {
continue;
}
if (!$this->db->fieldExists('school_year', $table)) {
$this->forge->addColumn($table, [
'school_year' => [
'type' => 'VARCHAR',
'constraint' => 9,
'null' => true,
'after' => $after,
],
]);
}
}
if ($this->db->tableExists('purchase_orders')) {
$currentYear = $this->currentSchoolYear();
$this->db->query(
'UPDATE purchase_orders SET school_year = ? WHERE school_year IS NULL OR TRIM(school_year) = ?',
[$currentYear, '']
);
$this->db->query('ALTER TABLE purchase_orders MODIFY school_year VARCHAR(9) NOT NULL');
}
if ($this->db->tableExists('purchase_orders') && $this->db->tableExists('purchase_order_items')) {
$this->db->query(
"UPDATE purchase_order_items poi
INNER JOIN purchase_orders po ON po.id = poi.purchase_order_id
SET poi.school_year = po.school_year
WHERE poi.school_year IS NULL OR TRIM(poi.school_year) = ''"
);
$this->db->query('ALTER TABLE purchase_order_items MODIFY school_year VARCHAR(9) NOT NULL');
}
}
private function currentSchoolYear(): string
{
if ($this->db->tableExists('school_years')) {
$row = $this->db->table('school_years')
->select('name')
->where('status', 'active')
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
$name = trim((string)($row['name'] ?? ''));
if (preg_match('/^\d{4}-\d{4}$/', $name) === 1) {
return $name;
}
}
$year = (int)date('Y');
return $year . '-' . ($year + 1);
}
private function createIndexIfMissing(string $table, string $indexName, string $sql): void
{
if (!$this->db->tableExists($table)) {
@@ -9,6 +9,7 @@ class CreatePaymentCorrections extends Migration
public function up()
{
if ($this->db->tableExists('payment_corrections')) {
$this->ensureSchoolYearColumn();
return;
}
@@ -17,6 +18,7 @@ class CreatePaymentCorrections extends Migration
'payment_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => false],
'invoice_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => false],
'parent_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => false],
'school_year' => ['type' => 'VARCHAR', 'constraint' => 9, 'null' => false],
'correction_type' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => false],
'approved_refundable_cents' => ['type' => 'INT', 'constraint' => 11, 'null' => false],
'status' => ['type' => 'VARCHAR', 'constraint' => 30, 'null' => false],
@@ -31,7 +33,9 @@ class CreatePaymentCorrections extends Migration
$this->db->query('CREATE INDEX idx_payment_corrections_payment_status ON payment_corrections (payment_id, status)');
$this->db->query('CREATE INDEX idx_payment_corrections_invoice_status ON payment_corrections (invoice_id, status)');
$this->db->query('CREATE INDEX idx_payment_corrections_year_status ON payment_corrections (school_year, status)');
$this->assertIndexExists('payment_corrections', 'idx_payment_corrections_payment_status');
$this->assertIndexExists('payment_corrections', 'idx_payment_corrections_year_status');
}
public function down()
@@ -52,4 +56,45 @@ class CreatePaymentCorrections extends Migration
throw new \RuntimeException("Required index {$indexName} was not created on {$table}.");
}
private function ensureSchoolYearColumn(): void
{
if (!$this->db->fieldExists('school_year', 'payment_corrections')) {
$this->forge->addColumn('payment_corrections', [
'school_year' => [
'type' => 'VARCHAR',
'constraint' => 9,
'null' => true,
'after' => 'parent_id',
],
]);
}
if ($this->db->tableExists('invoices')) {
$this->db->query(
"UPDATE payment_corrections pc
INNER JOIN invoices i ON i.id = pc.invoice_id
SET pc.school_year = i.school_year
WHERE pc.school_year IS NULL OR TRIM(pc.school_year) = ''"
);
}
$this->db->query("ALTER TABLE payment_corrections MODIFY school_year VARCHAR(9) NOT NULL");
if (!$this->indexExists('payment_corrections', 'idx_payment_corrections_year_status')) {
$this->db->query('CREATE INDEX idx_payment_corrections_year_status ON payment_corrections (school_year, status)');
}
$this->assertIndexExists('payment_corrections', 'idx_payment_corrections_year_status');
}
private function indexExists(string $table, string $indexName): bool
{
$indexes = $this->db->query('SHOW INDEX FROM ' . $this->db->escapeIdentifiers($table))->getResultArray();
foreach ($indexes as $index) {
if (($index['Key_name'] ?? '') === $indexName) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,141 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class EnsureFinancialModelSchoolYears extends Migration
{
public function up()
{
$this->ensureColumn('invoice_lines', 'invoice_id');
$this->backfillFromParent('invoice_lines', 'invoices', 'invoice_id');
$this->ensureColumn('refund_payouts', 'refund_id');
$this->backfillFromParent('refund_payouts', 'refunds', 'refund_id');
$this->ensureColumn('payment_corrections', 'parent_id');
$this->backfillFromParent('payment_corrections', 'invoices', 'invoice_id');
$this->ensureColumn('reimbursement_batch_admin_files', 'batch_id');
$this->backfillFromParent('reimbursement_batch_admin_files', 'reimbursement_batches', 'batch_id');
$this->ensureColumn('purchase_orders', 'status');
$this->backfillCurrentYear('purchase_orders');
$this->ensureColumn('purchase_order_items', 'purchase_order_id');
$this->backfillFromParent('purchase_order_items', 'purchase_orders', 'purchase_order_id');
}
public function down()
{
// school_year is now part of the financial model contract; do not drop it.
}
private function ensureColumn(string $table, string $after): void
{
if (!$this->db->tableExists($table)) {
return;
}
if (!$this->db->fieldExists('school_year', $table)) {
$this->forge->addColumn($table, [
'school_year' => [
'type' => 'VARCHAR',
'constraint' => 9,
'null' => true,
'after' => $after,
],
]);
}
}
private function backfillFromParent(string $childTable, string $parentTable, string $foreignKey): void
{
if (
!$this->db->tableExists($childTable)
|| !$this->db->tableExists($parentTable)
|| !$this->db->fieldExists('school_year', $childTable)
|| !$this->db->fieldExists('school_year', $parentTable)
|| !$this->db->fieldExists($foreignKey, $childTable)
) {
return;
}
$this->db->query(sprintf(
"UPDATE %s child
INNER JOIN %s parent ON parent.id = child.%s
SET child.school_year = parent.school_year
WHERE child.school_year IS NULL OR TRIM(child.school_year) = ''",
$this->db->escapeIdentifiers($childTable),
$this->db->escapeIdentifiers($parentTable),
$this->db->escapeIdentifiers($foreignKey)
));
$this->backfillCurrentYear($childTable);
}
private function backfillCurrentYear(string $table): void
{
if (!$this->db->tableExists($table) || !$this->db->fieldExists('school_year', $table)) {
return;
}
$this->db->query(
sprintf(
'UPDATE %s SET school_year = ? WHERE school_year IS NULL OR TRIM(school_year) = ?',
$this->db->escapeIdentifiers($table)
),
[$this->currentSchoolYear(), '']
);
$this->db->query(sprintf(
'ALTER TABLE %s MODIFY school_year VARCHAR(9) NOT NULL',
$this->db->escapeIdentifiers($table)
));
$this->ensureIndex($table);
}
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);
}
$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(sprintf(
'CREATE INDEX %s ON %s (school_year)',
$this->db->escapeIdentifiers($indexName),
$this->db->escapeIdentifiers($table)
));
}
private function currentSchoolYear(): string
{
if ($this->db->tableExists('school_years')) {
$row = $this->db->table('school_years')
->select('name')
->where('status', 'active')
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
$name = trim((string)($row['name'] ?? ''));
if (preg_match('/^\d{4}-\d{4}$/', $name) === 1) {
return $name;
}
}
$year = (int)date('Y');
return $year . '-' . ($year + 1);
}
}
@@ -56,6 +56,7 @@ class InvoiceAdjustmentService
$now = utc_now();
$lineId = $this->invoiceLineModel->insert([
'invoice_id' => $invoiceId,
'school_year' => (string)$invoice['school_year'],
'line_type' => $amountCents > 0 ? 'additional_charge' : 'additional_deduction',
'source_type' => 'additional_charge',
'source_id' => $chargeId,
@@ -139,6 +140,7 @@ class InvoiceAdjustmentService
$now = utc_now();
$lineId = $this->invoiceLineModel->insert([
'invoice_id' => $invoiceId,
'school_year' => (string)$invoice['school_year'],
'line_type' => $reverseAmountCents > 0 ? 'additional_charge_reversal' : 'additional_deduction_reversal',
'source_type' => 'additional_charge_reversal',
'source_id' => $chargeId,
+1
View File
@@ -388,6 +388,7 @@ class InvoiceLedgerService
): array {
return [
'invoice_id' => $invoiceId,
'school_year' => (string)($metadata['school_year'] ?? ''),
'line_type' => $lineType,
'source_type' => $sourceType,
'source_id' => $sourceId,
+5
View File
@@ -3,9 +3,12 @@
namespace App\Models;
use CodeIgniter\Model;
use App\Models\Concerns\SchoolYearAutoFillTrait;
class InvoiceLineModel extends Model
{
use SchoolYearAutoFillTrait;
protected $table = 'invoice_lines';
protected $primaryKey = 'id';
protected $returnType = 'array';
@@ -13,6 +16,7 @@ class InvoiceLineModel extends Model
protected $allowedFields = [
'invoice_id',
'school_year',
'line_type',
'source_type',
'source_id',
@@ -31,6 +35,7 @@ class InvoiceLineModel extends Model
protected $validationRules = [
'invoice_id' => 'required|integer',
'school_year' => 'required|string|max_length[9]',
'line_type' => 'required|max_length[50]',
'description' => 'required|max_length[255]',
'quantity' => 'required|decimal',
+14
View File
@@ -3,9 +3,12 @@
namespace App\Models;
use CodeIgniter\Model;
use App\Models\Concerns\SchoolYearAutoFillTrait;
class PaymentCorrectionModel extends Model
{
use SchoolYearAutoFillTrait;
protected $table = 'payment_corrections';
protected $primaryKey = 'id';
protected $returnType = 'array';
@@ -15,6 +18,7 @@ class PaymentCorrectionModel extends Model
'payment_id',
'invoice_id',
'parent_id',
'school_year',
'correction_type',
'approved_refundable_cents',
'status',
@@ -24,4 +28,14 @@ class PaymentCorrectionModel extends Model
'created_at',
'updated_at',
];
protected $validationRules = [
'payment_id' => 'required|integer',
'invoice_id' => 'required|integer',
'parent_id' => 'required|integer',
'school_year' => 'required|string|max_length[9]',
'correction_type' => 'required|max_length[50]',
'approved_refundable_cents' => 'required|integer|greater_than[0]',
'status' => 'required|max_length[30]',
];
}
+5 -1
View File
@@ -3,17 +3,21 @@
namespace App\Models;
use CodeIgniter\Model;
use App\Models\Concerns\SchoolYearAutoFillTrait;
class PurchaseOrderItemModel extends Model
{
use SchoolYearAutoFillTrait;
protected $table = 'purchase_order_items';
protected $primaryKey = 'id';
protected $allowedFields = ['purchase_order_id','supply_id','description','quantity','received_qty','unit_cost'];
protected $allowedFields = ['purchase_order_id','supply_id','school_year','description','quantity','received_qty','unit_cost'];
protected $useTimestamps = true;
protected $validationRules = [
'purchase_order_id' => 'required|is_natural_no_zero',
'supply_id' => 'required|is_natural_no_zero',
'school_year' => 'required|string|max_length[9]',
'quantity' => 'required|is_natural_no_zero',
'received_qty' => 'if_exist|integer|greater_than_equal_to[0]',
'unit_cost' => 'required|decimal|greater_than_equal_to[0]',
+5 -1
View File
@@ -3,18 +3,22 @@
namespace App\Models;
use CodeIgniter\Model;
use App\Models\Concerns\SchoolYearAutoFillTrait;
class PurchaseOrderModel extends Model
{
use SchoolYearAutoFillTrait;
protected $table = 'purchase_orders';
protected $primaryKey = 'id';
protected $allowedFields = ['po_number','supplier_id','status','order_date','expected_date','subtotal','tax','total','notes'];
protected $allowedFields = ['po_number','supplier_id','status','school_year','order_date','expected_date','subtotal','tax','total','notes'];
protected $useTimestamps = true;
protected $useSoftDeletes = true;
protected $validationRules = [
'po_number' => 'required|max_length[60]|is_unique[purchase_orders.po_number,id,{id}]',
'supplier_id' => 'required|is_natural_no_zero',
'school_year' => 'required|string|max_length[9]',
'status' => 'in_list[draft,ordered,received,canceled]',
];
}
+5
View File
@@ -3,9 +3,12 @@
namespace App\Models;
use CodeIgniter\Model;
use App\Models\Concerns\SchoolYearAutoFillTrait;
class RefundPayoutModel extends Model
{
use SchoolYearAutoFillTrait;
protected $table = 'refund_payouts';
protected $primaryKey = 'id';
protected $returnType = 'array';
@@ -13,6 +16,7 @@ class RefundPayoutModel extends Model
protected $allowedFields = [
'refund_id',
'school_year',
'amount_cents',
'currency',
'payout_type',
@@ -36,6 +40,7 @@ class RefundPayoutModel extends Model
protected $validationRules = [
'refund_id' => 'required|integer',
'school_year' => 'required|string|max_length[9]',
'amount_cents' => 'required|integer|greater_than[0]',
'currency' => 'required|exact_length[3]',
'payout_type' => 'required|in_list[cash_out,account_credit,reversal]',
+13 -13
View File
@@ -301,12 +301,12 @@
/app/Controllers/View/PaymentNotificationController.php:333 matches /Paid/i
/app/Controllers/View/PaymentNotificationController.php:333 matches /Unpaid/i
/app/Controllers/View/PaymentNotificationController.php:93 matches /balance\s*=/i
/app/Controllers/View/RefundController.php:1007 matches /Paid/i
/app/Controllers/View/RefundController.php:1014 matches /Paid/i
/app/Controllers/View/RefundController.php:1015 matches /Paid/i
/app/Controllers/View/RefundController.php:1028 matches /Paid/i
/app/Controllers/View/RefundController.php:1028 matches /refund_paid_amount/i
/app/Controllers/View/RefundController.php:1214 matches /Paid/i
/app/Controllers/View/RefundController.php:1009 matches /Paid/i
/app/Controllers/View/RefundController.php:1016 matches /Paid/i
/app/Controllers/View/RefundController.php:1017 matches /Paid/i
/app/Controllers/View/RefundController.php:1030 matches /Paid/i
/app/Controllers/View/RefundController.php:1030 matches /refund_paid_amount/i
/app/Controllers/View/RefundController.php:1216 matches /Paid/i
/app/Controllers/View/RefundController.php:164 matches /Paid/i
/app/Controllers/View/RefundController.php:168 matches /Paid/i
/app/Controllers/View/RefundController.php:178 matches /Paid/i
@@ -333,14 +333,14 @@
/app/Controllers/View/RefundController.php:631 matches /Paid/i
/app/Controllers/View/RefundController.php:636 matches /Paid/i
/app/Controllers/View/RefundController.php:660 matches /Paid/i
/app/Controllers/View/RefundController.php:664 matches /Paid/i
/app/Controllers/View/RefundController.php:698 matches /Paid/i
/app/Controllers/View/RefundController.php:698 matches /refund_paid_amount/i
/app/Controllers/View/RefundController.php:858 matches /Paid/i
/app/Controllers/View/RefundController.php:862 matches /Paid/i
/app/Controllers/View/RefundController.php:865 matches /Paid/i
/app/Controllers/View/RefundController.php:865 matches /refund_paid_amount/i
/app/Controllers/View/RefundController.php:665 matches /Paid/i
/app/Controllers/View/RefundController.php:699 matches /Paid/i
/app/Controllers/View/RefundController.php:699 matches /refund_paid_amount/i
/app/Controllers/View/RefundController.php:860 matches /Paid/i
/app/Controllers/View/RefundController.php:864 matches /Paid/i
/app/Controllers/View/RefundController.php:867 matches /Paid/i
/app/Controllers/View/RefundController.php:867 matches /refund_paid_amount/i
/app/Controllers/View/RefundController.php:869 matches /Paid/i
/app/Controllers/View/ReimbursementController.php:1204 matches /Paid/i
/app/Controllers/View/ReimbursementController.php:1288 matches /Paid/i
/app/Controllers/View/ReimbursementController.php:2038 matches /Paid/i
+1
View File
@@ -14,6 +14,7 @@ class PaymentModelTest extends ModelCrudTestCase
if ($model instanceof PaymentModel) {
$overrides['transaction_id'] = uniqid('PAY-TEST-', true);
$overrides['request_fingerprint_hash'] = hash('sha256', uniqid('payment-model-test-', true));
$overrides['payment_method'] = 'cash';
$overrides['status'] = 'recorded';
}