fix payment issues
Tests / PHPUnit (push) Successful in 1m17s

This commit is contained in:
root
2026-07-18 19:12:13 -04:00
parent 4aac9472af
commit 068e739408
8 changed files with 1204 additions and 40 deletions
+37 -16
View File
@@ -15,6 +15,7 @@ use App\Models\ClassSectionModel;
use App\Models\ParentModel;
use App\Models\PaymentModel;
use App\Models\CalendarModel;
use App\Libraries\FinancialStatus;
use App\Services\EmailService;
use Config\Database;
use App\Controllers\View\InvoiceController;
@@ -1175,7 +1176,7 @@ class EventController extends ResourceController
(float)($invoice['balance'] ?? 0.0)
) ?? 0);
} elseif (!$isPaid && $paymentId > 0) {
$this->paymentModel->delete($paymentId);
$this->voidPayment($paymentId, 'Event charge marked unpaid.');
$paymentId = 0;
}
@@ -1258,20 +1259,13 @@ class EventController extends ResourceController
return null;
}
$monthSemester = $semester ?: $this->semester;
$newPaid = (float)$invoicePaid + $amount;
$newBalance = (float)$invoiceBalance - $amount;
$paymentStatus = ($newBalance <= 0.00001)
? 'Paid'
: (($newPaid > 0) ? 'Partially Paid' : 'Unpaid');
$exclude = ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled'];
$priorCount = $this->paymentModel
$newBalance = max(0.0, round((float)$invoiceBalance - $amount, 2));
$row = $this->paymentModel->db->table('payments')
->select('COALESCE(MAX(installment_seq), 0) + 1 AS next_seq', false)
->where('invoice_id', $invoiceId)
->where('paid_amount >', 0)
->whereNotIn('status', $exclude)
->countAllResults();
$installmentSeq = $priorCount + 1;
->get()
->getRowArray();
$installmentSeq = (int)($row['next_seq'] ?? 1);
$data = [
'parent_id' => $parentId,
@@ -1280,11 +1274,11 @@ class EventController extends ResourceController
'paid_amount' => $amount,
'balance' => $newBalance,
'number_of_installments' => $installmentSeq,
'installment_seq' => $installmentSeq,
'payment_method' => 'cash',
'payment_date' => utc_now(),
'school_year' => $schoolYear,
'semester' => $monthSemester,
'status' => $paymentStatus,
'status' => FinancialStatus::PAYMENT_RECORDED,
'transaction_id' => $this->paymentModel->generateNewTransactionId(),
'updated_by' => session()->get('user_id'),
];
@@ -1297,6 +1291,33 @@ class EventController extends ResourceController
return (int)$this->paymentModel->getInsertID();
}
private function voidPayment(int $paymentId, string $reason): bool
{
if ($paymentId <= 0) {
return false;
}
$data = [
'status' => FinancialStatus::PAYMENT_VOIDED,
'updated_by' => session()->get('user_id'),
];
if ($this->paymentModel->db->fieldExists('is_void', 'payments')) {
$data['is_void'] = 1;
}
if ($this->paymentModel->db->fieldExists('voided_at', 'payments')) {
$data['voided_at'] = utc_now();
}
if ($this->paymentModel->db->fieldExists('voided_by', 'payments')) {
$data['voided_by'] = session()->get('user_id');
}
if ($this->paymentModel->db->fieldExists('void_reason', 'payments')) {
$data['void_reason'] = $reason;
}
return (bool) $this->paymentModel->update($paymentId, $data);
}
private function normalizeExternalName(?string $value): string
{
$raw = trim((string)$value);
+47 -21
View File
@@ -995,15 +995,11 @@ class PaymentController extends ResourceController
);
}
// Installment sequence
$paidCount = $this->getSuccessfulPaymentCount($invoiceId);
$installmentSeq = $paidCount + 1;
// Generate a stable transaction id
$transactionId = 'INV-' . $invoiceId . '-' . str_replace('.', '', (string)microtime(true));
// Record payment
$ok = $this->processPayment(
$paymentResult = $this->processPayment(
$invoiceId,
$amount,
$paymentMethod,
@@ -1012,15 +1008,16 @@ class PaymentController extends ResourceController
$paymentDate,
$invYear,
$checkNumber,
$installmentSeq,
(array) $this->invoiceModel->find($invoiceId),
null,
(array) $row,
$currentBalance
);
if (!$ok) {
if ($paymentResult === false) {
$this->db->transRollback();
return redirect()->back()->with('error', 'Failed to record payment.');
}
$installmentSeq = (int) ($paymentResult['installment_seq'] ?? 1);
if (!$this->recordManualPayment(
(string)($row['invoice_number'] ?? $invoiceId),
@@ -1383,34 +1380,57 @@ class PaymentController extends ResourceController
$checkNumber = null,
?int $installmentSeq = null,
?array $invoice = null,
?float $currentBalance = null
?float $currentBalance = null,
?string $idempotencyKey = null
) {
$invoice = $invoice ?? $this->invoiceModel->find($invoiceId);
if (!$invoice) {
return false;
}
$invoiceId = (int) ($invoice['id'] ?? $invoiceId);
$amount = round((float) $amount, 2);
if ($amount <= 0) {
return false;
}
$transactionId = $transactionId ?? 'INV-' . $invoiceId . '-' . time();
$paymentDate = $paymentDate ?? utc_now();
// If sequence wasn't provided (legacy callers), compute it here.
// NOTE: If you call this from inside a transaction (recommended), this will be consistent.
if ($this->paymentModel->where('transaction_id', $transactionId)->first()) {
return false;
}
if ($idempotencyKey !== null && $idempotencyKey !== '') {
$existing = $this->paymentModel->where('idempotency_key', $idempotencyKey)->first();
if ($existing) {
return [
'payment_id' => (int) ($existing['id'] ?? 0),
'installment_seq' => (int) ($existing['installment_seq'] ?? $existing['number_of_installments'] ?? 1),
'duplicate' => true,
];
}
}
// If sequence wasn't provided, compute it inside the caller's invoice lock.
if ($installmentSeq === null || $installmentSeq < 1) {
$exclude = ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled'];
$priorCount = $this->paymentModel
$row = $this->db->table('payments')
->select('COALESCE(MAX(installment_seq), 0) + 1 AS next_seq', false)
->where('invoice_id', $invoiceId)
->where('paid_amount >', 0)
->whereNotIn('status', $exclude)
->countAllResults();
$installmentSeq = $priorCount + 1;
->get()
->getRowArray();
$installmentSeq = (int) ($row['next_seq'] ?? 1);
}
$preBalance = $currentBalance ?? $this->getCurrentInvoiceBalance((int) $invoiceId);
$newBalance = max(0.0, round($preBalance - (float) $amount, 2));
if ($amount > $preBalance + 0.00001) {
return false;
}
$newBalance = max(0.0, round($preBalance - $amount, 2));
$paymentData = [
'parent_id' => $invoice['parent_id'],
'parent_id' => (int) $invoice['parent_id'],
'invoice_id' => $invoiceId,
'total_amount' => $invoice['total_amount'],
'paid_amount' => $amount,
@@ -1418,6 +1438,7 @@ class PaymentController extends ResourceController
'number_of_installments' => $installmentSeq, // <-- installment sequence (1,2,3,...)
'installment_seq' => $installmentSeq,
'transaction_id' => $transactionId,
'idempotency_key' => $idempotencyKey,
'payment_method' => strtolower($paymentMethod),
'payment_date' => $paymentDate,
'status' => FinancialStatus::PAYMENT_RECORDED,
@@ -1427,11 +1448,16 @@ class PaymentController extends ResourceController
'school_year' => $schoolYear ?? ($invoice['school_year'] ?? $this->schoolYear),
];
if (!$this->paymentModel->insert($paymentData)) {
$paymentId = $this->paymentModel->insert($paymentData);
if (!$paymentId) {
return false;
}
return true;
return [
'payment_id' => (int) $paymentId,
'installment_seq' => $installmentSeq,
'duplicate' => false,
];
}
private function getSuccessfulPaymentCount(int $invoiceId): int
@@ -0,0 +1,456 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class PaymentsLogicAndDataRepairSupport extends Migration
{
public function up()
{
if (!$this->db->tableExists('payments')) {
return;
}
$this->createPaymentBackupTable();
$this->ensurePaymentAuditColumns();
$this->repairPaymentSequences();
$this->normalizePaymentsFromInvoices();
$this->ensureRepairTables();
$this->ensurePaymentIndexes();
}
public function down()
{
$this->dropIndexIfExists('payments', 'uniq_payments_invoice_sequence');
$this->dropIndexIfExists('payments', 'uniq_payments_idempotency_key');
$this->dropIndexIfExists('payments', 'idx_payments_reversal');
foreach ([
'payment_repair_decisions',
'payment_repair_transition_review',
'payment_repair_invoice_review',
'payment_repair_analysis',
'invoice_opening_paid_balances',
'invoice_adjustments',
'payments_backup_20260718',
] as $table) {
if ($this->db->tableExists($table)) {
$this->forge->dropTable($table, true);
}
}
foreach ([
'idempotency_key',
'is_void',
'reversal_of_payment_id',
'voided_at',
'voided_by',
'void_reason',
] as $column) {
if ($this->db->fieldExists($column, 'payments')) {
$this->forge->dropColumn('payments', $column);
}
}
}
private function createPaymentBackupTable(): void
{
if ($this->db->tableExists('payments_backup_20260718')) {
return;
}
$this->db->query('CREATE TABLE `payments_backup_20260718` AS SELECT * FROM `payments`');
}
private function ensurePaymentAuditColumns(): void
{
$columns = [];
if (!$this->db->fieldExists('idempotency_key', 'payments')) {
$columns['idempotency_key'] = [
'type' => 'CHAR',
'constraint' => 36,
'null' => true,
'after' => 'transaction_id',
];
}
if (!$this->db->fieldExists('is_void', 'payments')) {
$columns['is_void'] = [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
'null' => false,
'after' => 'status',
];
}
if (!$this->db->fieldExists('reversal_of_payment_id', 'payments')) {
$columns['reversal_of_payment_id'] = [
'type' => 'INT',
'constraint' => 10,
'unsigned' => true,
'null' => true,
'after' => 'is_void',
];
}
if (!$this->db->fieldExists('voided_at', 'payments')) {
$columns['voided_at'] = [
'type' => 'DATETIME',
'null' => true,
'after' => 'reversal_of_payment_id',
];
}
if (!$this->db->fieldExists('voided_by', 'payments')) {
$columns['voided_by'] = [
'type' => 'INT',
'constraint' => 10,
'unsigned' => true,
'null' => true,
'after' => 'voided_at',
];
}
if (!$this->db->fieldExists('void_reason', 'payments')) {
$columns['void_reason'] = [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
'after' => 'voided_by',
];
}
if ($columns !== []) {
$this->forge->addColumn('payments', $columns);
}
}
private function repairPaymentSequences(): void
{
if (!$this->db->fieldExists('installment_seq', 'payments')) {
$this->forge->addColumn('payments', [
'installment_seq' => [
'type' => 'INT',
'constraint' => 11,
'null' => true,
'after' => 'number_of_installments',
],
]);
}
$rows = $this->db->table('payments')
->select('id, invoice_id, installment_seq')
->orderBy('invoice_id', 'ASC')
->orderBy('payment_date', 'ASC')
->orderBy('id', 'ASC')
->get()
->getResultArray();
$sequenceByInvoice = [];
foreach ($rows as $row) {
$invoiceId = (int) ($row['invoice_id'] ?? 0);
$sequenceByInvoice[$invoiceId] = ($sequenceByInvoice[$invoiceId] ?? 0) + 1;
$expectedSeq = $sequenceByInvoice[$invoiceId];
if ((int) ($row['installment_seq'] ?? 0) === $expectedSeq) {
continue;
}
$this->db->table('payments')
->where('id', (int) $row['id'])
->update(['installment_seq' => $expectedSeq]);
}
if ($this->db->fieldExists('number_of_installments', 'payments')) {
$this->db->query(
"UPDATE `payments` p
JOIN (
SELECT invoice_id, COUNT(*) AS installment_count
FROM `payments`
WHERE LOWER(COALESCE(status, '')) NOT IN (
'void', 'voided', 'refunded', 'failed', 'chargeback',
'declined', 'reversed', 'canceled', 'cancelled'
)
GROUP BY invoice_id
) c ON c.invoice_id = p.invoice_id
SET p.`number_of_installments` = c.installment_count"
);
}
}
private function normalizePaymentsFromInvoices(): void
{
if (!$this->db->tableExists('invoices')) {
return;
}
$this->db->query(
'UPDATE `payments` p
JOIN `invoices` i ON i.`id` = p.`invoice_id`
SET p.`parent_id` = i.`parent_id`,
p.`school_year` = i.`school_year`
WHERE p.`parent_id` <> i.`parent_id`
OR NOT (p.`school_year` <=> i.`school_year`)'
);
}
private function ensureRepairTables(): void
{
foreach ([
'payment_repair_transition_review',
'payment_repair_invoice_review',
'payment_repair_analysis',
] as $view) {
$this->db->query(sprintf('DROP VIEW IF EXISTS `%s`', $view));
}
$this->db->query(
"CREATE TABLE IF NOT EXISTS `invoice_adjustments` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`invoice_id` INT UNSIGNED NOT NULL,
`adjustment_type` ENUM('charge','discount','waiver','credit','reversal','correction') NOT NULL,
`amount` DECIMAL(10,2) NOT NULL,
`description` VARCHAR(255) NOT NULL,
`source_reference` VARCHAR(100) DEFAULT NULL,
`school_year` VARCHAR(9) NOT NULL,
`created_by` INT UNSIGNED DEFAULT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_invoice_adjustments_invoice` (`invoice_id`),
UNIQUE KEY `uniq_invoice_adjustment_source` (`invoice_id`, `source_reference`)
) ENGINE=InnoDB"
);
$this->db->query(
"CREATE TABLE IF NOT EXISTS `invoice_opening_paid_balances` (
`invoice_id` INT UNSIGNED NOT NULL,
`amount` DECIMAL(10,2) NOT NULL,
`effective_before_payment_id` INT UNSIGNED DEFAULT NULL,
`school_year` VARCHAR(9) NOT NULL,
`evidence_reference` VARCHAR(255) NOT NULL,
`approved_by` INT UNSIGNED NOT NULL,
`approved_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`notes` TEXT,
PRIMARY KEY (`invoice_id`)
) ENGINE=InnoDB"
);
$this->db->query(
"CREATE TABLE IF NOT EXISTS `payment_repair_decisions` (
`payment_id` INT UNSIGNED NOT NULL,
`invoice_id` INT UNSIGNED NOT NULL,
`decision` ENUM(
'balance_is_wrong',
'missing_payment',
'missing_credit',
'missing_charge',
'missing_reversal',
'leave_unchanged'
) NOT NULL,
`correction_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
`evidence_reference` VARCHAR(255) NOT NULL,
`approved_by` INT UNSIGNED NOT NULL,
`approved_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`notes` TEXT,
PRIMARY KEY (`payment_id`)
) ENGINE=InnoDB"
);
$this->db->query(
"CREATE TABLE IF NOT EXISTS `payment_repair_analysis` (
`payment_id` INT UNSIGNED NOT NULL,
`invoice_id` INT UNSIGNED NOT NULL,
`paid_amount` DECIMAL(10,2) NOT NULL,
`balance` DECIMAL(10,2) DEFAULT NULL,
`payment_date` DATETIME NOT NULL,
`installment_seq` INT DEFAULT NULL,
`expected_installment_seq` INT NOT NULL,
`running_paid_total` DECIMAL(10,2) NOT NULL,
PRIMARY KEY (`payment_id`),
KEY `idx_payment_repair_analysis_invoice` (`invoice_id`)
) ENGINE=InnoDB"
);
$this->db->query(
"CREATE TABLE IF NOT EXISTS `payment_repair_invoice_review` (
`invoice_id` INT UNSIGNED NOT NULL,
`payment_count` INT NOT NULL,
`ledger_paid_total` DECIMAL(10,2) NOT NULL,
`sequence_matches` TINYINT(1) NOT NULL,
PRIMARY KEY (`invoice_id`)
) ENGINE=InnoDB"
);
$this->db->query(
"CREATE TABLE IF NOT EXISTS `payment_repair_transition_review` (
`payment_id` INT UNSIGNED NOT NULL,
`invoice_id` INT UNSIGNED NOT NULL,
`balance` DECIMAL(10,2) DEFAULT NULL,
`previous_balance` DECIMAL(10,2) DEFAULT NULL,
`paid_amount` DECIMAL(10,2) NOT NULL,
`expected_balance` DECIMAL(10,2) DEFAULT NULL,
`unexplained_change` DECIMAL(10,2) DEFAULT NULL,
PRIMARY KEY (`payment_id`),
KEY `idx_payment_repair_transition_invoice` (`invoice_id`)
) ENGINE=InnoDB"
);
$this->refreshRepairTables();
}
private function refreshRepairTables(): void
{
$this->db->table('payment_repair_transition_review')->truncate();
$this->db->table('payment_repair_invoice_review')->truncate();
$this->db->table('payment_repair_analysis')->truncate();
$payments = $this->db->table('payments')
->select('id, invoice_id, paid_amount, balance, payment_date, installment_seq')
->orderBy('invoice_id', 'ASC')
->orderBy('payment_date', 'ASC')
->orderBy('id', 'ASC')
->get()
->getResultArray();
$sequenceByInvoice = [];
$runningByInvoice = [];
$invoiceReview = [];
$previousByInvoice = [];
foreach ($payments as $payment) {
$invoiceId = (int) ($payment['invoice_id'] ?? 0);
$paidAmount = round((float) ($payment['paid_amount'] ?? 0), 2);
$balance = array_key_exists('balance', $payment) && $payment['balance'] !== null
? round((float) $payment['balance'], 2)
: null;
$sequenceByInvoice[$invoiceId] = ($sequenceByInvoice[$invoiceId] ?? 0) + 1;
$runningByInvoice[$invoiceId] = round(($runningByInvoice[$invoiceId] ?? 0.0) + $paidAmount, 2);
$expectedSeq = $sequenceByInvoice[$invoiceId];
$actualSeq = $payment['installment_seq'] !== null ? (int) $payment['installment_seq'] : null;
$this->db->table('payment_repair_analysis')->insert([
'payment_id' => (int) $payment['id'],
'invoice_id' => $invoiceId,
'paid_amount' => $paidAmount,
'balance' => $balance,
'payment_date' => $payment['payment_date'],
'installment_seq' => $actualSeq,
'expected_installment_seq' => $expectedSeq,
'running_paid_total' => $runningByInvoice[$invoiceId],
]);
if (!isset($invoiceReview[$invoiceId])) {
$invoiceReview[$invoiceId] = [
'payment_count' => 0,
'ledger_paid_total' => 0.0,
'sequence_matches' => 1,
];
}
$invoiceReview[$invoiceId]['payment_count']++;
$invoiceReview[$invoiceId]['ledger_paid_total'] = round($invoiceReview[$invoiceId]['ledger_paid_total'] + $paidAmount, 2);
if ($actualSeq !== $expectedSeq) {
$invoiceReview[$invoiceId]['sequence_matches'] = 0;
}
if (isset($previousByInvoice[$invoiceId])) {
$previous = $previousByInvoice[$invoiceId];
$expectedBalance = $previous['balance'] !== null
? round($previous['balance'] - $paidAmount, 2)
: null;
$unexplainedChange = ($balance !== null && $expectedBalance !== null)
? round($balance - $expectedBalance, 2)
: null;
if ($unexplainedChange !== null && abs($unexplainedChange) > 0.01) {
$this->db->table('payment_repair_transition_review')->insert([
'payment_id' => (int) $payment['id'],
'invoice_id' => $invoiceId,
'balance' => $balance,
'previous_balance' => $previous['balance'],
'paid_amount' => $paidAmount,
'expected_balance' => $expectedBalance,
'unexplained_change' => $unexplainedChange,
]);
}
}
$previousByInvoice[$invoiceId] = [
'balance' => $balance,
];
}
foreach ($invoiceReview as $invoiceId => $review) {
$this->db->table('payment_repair_invoice_review')->insert([
'invoice_id' => $invoiceId,
'payment_count' => $review['payment_count'],
'ledger_paid_total' => $review['ledger_paid_total'],
'sequence_matches' => $review['sequence_matches'],
]);
}
}
private function ensurePaymentIndexes(): void
{
$this->addIndexIfMissing('payments', 'idx_payments_reversal', ['reversal_of_payment_id']);
$this->addIndexIfMissing('payments', 'uniq_payments_idempotency_key', ['idempotency_key'], true);
if (!$this->hasDuplicatePaymentSequences()) {
$this->addIndexIfMissing('payments', 'uniq_payments_invoice_sequence', ['invoice_id', 'installment_seq'], true);
}
}
private function hasDuplicatePaymentSequences(): bool
{
$row = $this->db->table('payments')
->select('invoice_id, installment_seq')
->where('installment_seq IS NOT NULL', null, false)
->groupBy('invoice_id, installment_seq')
->having('COUNT(*) >', 1, false)
->get(1)
->getRowArray();
return $row !== null;
}
private function addIndexIfMissing(string $table, string $indexName, array $columns, bool $unique = false): void
{
if (!$this->db->tableExists($table) || $this->indexExists($table, $indexName)) {
return;
}
$quotedColumns = implode(', ', array_map(static fn (string $column): string => '`' . $column . '`', $columns));
$type = $unique ? 'UNIQUE INDEX' : 'INDEX';
$this->db->query(sprintf('ALTER TABLE `%s` ADD %s `%s` (%s)', $table, $type, $indexName, $quotedColumns));
}
private function dropIndexIfExists(string $table, string $indexName): void
{
if (!$this->db->tableExists($table) || !$this->indexExists($table, $indexName)) {
return;
}
$this->db->query(sprintf('ALTER TABLE `%s` DROP INDEX `%s`', $table, $indexName));
}
private function indexExists(string $table, string $indexName): bool
{
if (!$this->db->tableExists($table)) {
return false;
}
$rows = $this->db->query(sprintf('SHOW INDEX FROM `%s`', $table))->getResultArray();
foreach ($rows as $row) {
if (($row['Key_name'] ?? null) === $indexName) {
return true;
}
}
return false;
}
}
+20 -1
View File
@@ -34,12 +34,18 @@ class PaymentModel extends Model
'number_of_installments',
'installment_seq',
'transaction_id',
'idempotency_key',
'check_file',
'check_number',
'payment_method',
'payment_date',
'school_year',
'status',
'is_void',
'reversal_of_payment_id',
'voided_at',
'voided_by',
'void_reason',
'updated_by',
];
@@ -69,11 +75,17 @@ class PaymentModel extends Model
'number_of_installments' => 'permit_empty|integer|greater_than[0]',
'installment_seq' => 'permit_empty|integer|greater_than[0]',
'transaction_id' => 'permit_empty|max_length[100]',
'transaction_id' => 'required|max_length[100]',
'idempotency_key' => 'permit_empty|max_length[36]',
'payment_method' => 'required|in_list[cash,check,card]',
'payment_date' => 'required|valid_date',
'school_year' => 'required|string|max_length[9]',
'status' => 'required|max_length[50]',
'is_void' => 'permit_empty|in_list[0,1]',
'reversal_of_payment_id' => 'permit_empty|integer',
'voided_at' => 'permit_empty|valid_date',
'voided_by' => 'permit_empty|integer',
'void_reason' => 'permit_empty|max_length[255]',
'check_file' => 'permit_empty|max_length[255]',
'check_number' => 'permit_empty|max_length[100]',
'updated_by' => 'permit_empty|integer',
@@ -124,10 +136,17 @@ class PaymentModel extends Model
'integer' => 'Number of installments must be an integer.',
'greater_than' => 'Number of installments must be greater than zero.',
],
'transaction_id' => [
'required' => 'Transaction ID is required.',
'max_length' => 'Transaction ID must not exceed 100 characters.',
],
'installment_seq' => [
'integer' => 'Installment sequence must be an integer.',
'greater_than' => 'Installment sequence must be greater than zero.',
],
'idempotency_key' => [
'max_length' => 'Idempotency key must not exceed 36 characters.',
],
'check_number' => [
'max_length' => 'Check number must not exceed 100 characters.',
],
+1 -1
View File
@@ -25,7 +25,7 @@
</tr>
<tr>
<th>Number of Installments</th>
<td><?= esc($payment['number_of_installments']) ?></td>
<td><?= esc($payment['installment_seq'] ?? $payment['number_of_installments'] ?? '') ?></td>
</tr>
<tr>
<th>Semester</th>
+1 -1
View File
@@ -87,7 +87,7 @@
?>
</td>
<td><?= !empty($payment['check_number']) ? esc($payment['check_number']) : '-' ?></td>
<td><?= esc($payment['number_of_installments']) ?></td>
<td><?= esc($payment['installment_seq'] ?? $payment['number_of_installments'] ?? '') ?></td>
<td><?= esc($payment['status']) ?></td>
<td>
<?php if (!empty($payment['check_file'])): ?>