Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e8da3cc2c | |||
| 384ae8b719 |
@@ -74,7 +74,7 @@ class InvoiceController extends ResourceController
|
|||||||
$this->dueDate = $this->configModel->getConfig('due_date');
|
$this->dueDate = $this->configModel->getConfig('due_date');
|
||||||
$this->firstStudentFee = (float) ($this->configModel->getConfig('first_student_fee') ?? 350);
|
$this->firstStudentFee = (float) ($this->configModel->getConfig('first_student_fee') ?? 350);
|
||||||
$this->secondStudentFee = (float) ($this->configModel->getConfig('second_student_fee') ?? 200);
|
$this->secondStudentFee = (float) ($this->configModel->getConfig('second_student_fee') ?? 200);
|
||||||
$this->youthFee = (float) ($this->configModel->getConfig('youth_fee') ?? 180);
|
$this->youthFee = (float) ($this->configModel->getConfig('youth_fee') ?? 200);
|
||||||
$this->refundDeadline = date('Y-m-d', strtotime($this->configModel->getConfig('refund_deadline')));
|
$this->refundDeadline = date('Y-m-d', strtotime($this->configModel->getConfig('refund_deadline')));
|
||||||
$this->db = \Config\Database::connect();
|
$this->db = \Config\Database::connect();
|
||||||
$this->request = \Config\Services::request();
|
$this->request = \Config\Services::request();
|
||||||
|
|||||||
@@ -611,14 +611,18 @@ class ReimbursementController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$expenseId = (int) $this->request->getPost('expense_id');
|
$expenseId = (int) $this->request->getPost('expense_id');
|
||||||
|
|
||||||
$batchIdRaw = $this->request->getPost('batch_id');
|
$batchIdRaw = $this->request->getPost('batch_id');
|
||||||
$batchId = (int) $batchIdRaw;
|
$batchId = (int) $batchIdRaw;
|
||||||
|
|
||||||
$fallbackBatchNumber = $this->request->getPost('batch_number');
|
$fallbackBatchNumber = $this->request->getPost('batch_number');
|
||||||
if ($batchId <= 0 && $fallbackBatchNumber !== null && trim((string) $fallbackBatchNumber) !== '') {
|
if ($batchId <= 0 && $fallbackBatchNumber !== null && trim((string) $fallbackBatchNumber) !== '') {
|
||||||
$batchId = (int) $fallbackBatchNumber;
|
$batchId = (int) $fallbackBatchNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
$adminIdRaw = $this->request->getPost('admin_id');
|
$adminIdRaw = $this->request->getPost('admin_id');
|
||||||
$adminId = ($adminIdRaw === '' || $adminIdRaw === null) ? null : (int) $adminIdRaw;
|
$adminId = ($adminIdRaw === '' || $adminIdRaw === null) ? null : (int) $adminIdRaw;
|
||||||
|
|
||||||
$reimbursementId = (int) $this->request->getPost('reimbursement_id') ?: null;
|
$reimbursementId = (int) $this->request->getPost('reimbursement_id') ?: null;
|
||||||
|
|
||||||
if ($expenseId <= 0) {
|
if ($expenseId <= 0) {
|
||||||
@@ -630,123 +634,185 @@ class ReimbursementController extends BaseController
|
|||||||
|
|
||||||
$now = date('Y-m-d H:i:s');
|
$now = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$newHashResponse = function (array $payload = []) {
|
||||||
|
$newHash = function_exists('csrf_hash') ? csrf_hash() : null;
|
||||||
|
|
||||||
|
return $this->response
|
||||||
|
->setHeader('X-CSRF-HASH', (string) $newHash)
|
||||||
|
->setJSON(array_merge($payload, [
|
||||||
|
'csrf_hash' => $newHash,
|
||||||
|
]));
|
||||||
|
};
|
||||||
|
|
||||||
|
$this->db->transBegin();
|
||||||
|
|
||||||
|
try {
|
||||||
$activeItem = $this->batchItemModel
|
$activeItem = $this->batchItemModel
|
||||||
->where('expense_id', $expenseId)
|
->where('expense_id', $expenseId)
|
||||||
->where('unassigned_at IS NULL', null, false)
|
->where('unassigned_at IS NULL', null, false)
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If no batch is provided, remove the item from active batch processing.
|
||||||
|
* This means "unassigned from batch", not "unassigned admin".
|
||||||
|
*/
|
||||||
if ($batchId <= 0) {
|
if ($batchId <= 0) {
|
||||||
if ($activeItem) {
|
if ($activeItem) {
|
||||||
$this->batchItemModel->update((int) $activeItem['id'], ['unassigned_at' => $now]);
|
$this->batchItemModel->update((int) $activeItem['id'], [
|
||||||
|
'unassigned_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
if (!$reimbursementId && !empty($activeItem['reimbursement_id'])) {
|
if (!$reimbursementId && !empty($activeItem['reimbursement_id'])) {
|
||||||
$reimbursementId = (int) $activeItem['reimbursement_id'];
|
$reimbursementId = (int) $activeItem['reimbursement_id'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$newHash = function_exists('csrf_hash') ? csrf_hash() : null;
|
if (!$this->db->transStatus()) {
|
||||||
return $this->response
|
throw new \RuntimeException('Transaction failed while unassigning batch item.');
|
||||||
->setHeader('X-CSRF-HASH', (string) $newHash)
|
}
|
||||||
->setJSON([
|
|
||||||
|
$this->db->transCommit();
|
||||||
|
|
||||||
|
return $newHashResponse([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'batch_id' => 0,
|
'batch_id' => 0,
|
||||||
'admin_id' => null,
|
'admin_id' => null,
|
||||||
'reimbursement_id' => $reimbursementId,
|
'reimbursement_id' => $reimbursementId,
|
||||||
'csrf_hash' => $newHash,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$batch = $this->batchModel->find($batchId);
|
$batch = $this->batchModel->find($batchId);
|
||||||
if (!$batch || strtolower((string) ($batch['status'] ?? 'open')) !== 'open') {
|
if (!$batch || strtolower((string) ($batch['status'] ?? 'open')) !== 'open') {
|
||||||
|
$this->db->transRollback();
|
||||||
|
|
||||||
return $this->response->setStatusCode(404)->setJSON([
|
return $this->response->setStatusCode(404)->setJSON([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'error' => 'Batch not found or already closed.',
|
'error' => 'Batch not found or already closed.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$currentAdmin = $activeItem ? (int) ($activeItem['admin_id'] ?? 0) : null;
|
if (!$reimbursementId) {
|
||||||
|
if ($activeItem && !empty($activeItem['reimbursement_id'])) {
|
||||||
|
$reimbursementId = (int) $activeItem['reimbursement_id'];
|
||||||
|
} else {
|
||||||
|
$reimbursementId = $this->lookupReimbursementId($expenseId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$activeSameBatch = $activeItem && (int) ($activeItem['batch_id'] ?? 0) === $batchId;
|
$activeSameBatch = $activeItem && (int) ($activeItem['batch_id'] ?? 0) === $batchId;
|
||||||
if ($activeSameBatch && ($currentAdmin === ($adminId ?? 0))) {
|
$currentAdmin = $activeItem ? (int) ($activeItem['admin_id'] ?? 0) : null;
|
||||||
$newHash = function_exists('csrf_hash') ? csrf_hash() : null;
|
$requestedAdmin = $adminId ?? 0;
|
||||||
return $this->response
|
|
||||||
->setHeader('X-CSRF-HASH', (string) $newHash)
|
/**
|
||||||
->setJSON([
|
* If the item is already active in the same batch/admin slot, just return success.
|
||||||
|
*/
|
||||||
|
if ($activeSameBatch && $currentAdmin === $requestedAdmin) {
|
||||||
|
if (!$this->db->transStatus()) {
|
||||||
|
throw new \RuntimeException('Transaction failed while checking existing assignment.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->transCommit();
|
||||||
|
|
||||||
|
return $newHashResponse([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'batch_id' => $batchId,
|
'batch_id' => $batchId,
|
||||||
'admin_id' => $adminId,
|
'admin_id' => $adminId,
|
||||||
'reimbursement_id' => $activeItem['reimbursement_id'] ?? null,
|
'reimbursement_id' => $activeItem['reimbursement_id'] ?? $reimbursementId,
|
||||||
'csrf_hash' => $newHash,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If same batch but different admin, update the existing active row.
|
||||||
|
*
|
||||||
|
* Important:
|
||||||
|
* admin_id = null means active in the batch but not assigned to an admin.
|
||||||
|
* unassigned_at != null means removed from active batch processing.
|
||||||
|
*/
|
||||||
if ($activeSameBatch) {
|
if ($activeSameBatch) {
|
||||||
if (!$reimbursementId) {
|
$this->batchItemModel->update((int) $activeItem['id'], [
|
||||||
$reimbursementId = $activeItem['reimbursement_id'] ? (int) $activeItem['reimbursement_id'] : $this->lookupReimbursementId($expenseId);
|
|
||||||
}
|
|
||||||
$updatePayload = [
|
|
||||||
'admin_id' => $adminId,
|
'admin_id' => $adminId,
|
||||||
'assigned_at' => $now,
|
'assigned_at' => $now,
|
||||||
'reimbursement_id' => $reimbursementId,
|
'reimbursement_id' => $reimbursementId,
|
||||||
'unassigned_at' => $adminId === null ? $now : null,
|
'unassigned_at' => null,
|
||||||
];
|
]);
|
||||||
$this->batchItemModel->update((int) $activeItem['id'], $updatePayload);
|
|
||||||
$newHash = function_exists('csrf_hash') ? csrf_hash() : null;
|
if (!$this->db->transStatus()) {
|
||||||
return $this->response
|
throw new \RuntimeException('Transaction failed while updating existing assignment.');
|
||||||
->setHeader('X-CSRF-HASH', (string) $newHash)
|
}
|
||||||
->setJSON([
|
|
||||||
|
$this->db->transCommit();
|
||||||
|
|
||||||
|
return $newHashResponse([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'batch_id' => $batchId,
|
'batch_id' => $batchId,
|
||||||
'admin_id' => $adminId,
|
'admin_id' => $adminId,
|
||||||
'reimbursement_id' => $reimbursementId,
|
'reimbursement_id' => $reimbursementId,
|
||||||
'csrf_hash' => $newHash,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the item is active in another batch, soft-unassign that row first.
|
||||||
|
*/
|
||||||
if ($activeItem) {
|
if ($activeItem) {
|
||||||
$this->batchItemModel->update((int) $activeItem['id'], ['unassigned_at' => $now]);
|
$this->batchItemModel->update((int) $activeItem['id'], [
|
||||||
if (!$reimbursementId && !empty($activeItem['reimbursement_id'])) {
|
'unassigned_at' => $now,
|
||||||
$reimbursementId = (int) $activeItem['reimbursement_id'];
|
]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$reimbursementId) {
|
/**
|
||||||
$reimbursementId = $this->lookupReimbursementId($expenseId);
|
* Critical fix:
|
||||||
}
|
* Check whether this expense was previously assigned to this batch.
|
||||||
|
* Because uq_batch_expense(batch_id, expense_id) blocks duplicate rows,
|
||||||
|
* we must reactivate/update the old row instead of inserting again.
|
||||||
|
*/
|
||||||
|
$existingBatchItem = $this->batchItemModel
|
||||||
|
->where('batch_id', $batchId)
|
||||||
|
->where('expense_id', $expenseId)
|
||||||
|
->first();
|
||||||
|
|
||||||
$insertData = [
|
$assignmentData = [
|
||||||
'batch_id' => $batchId,
|
'batch_id' => $batchId,
|
||||||
'expense_id' => $expenseId,
|
'expense_id' => $expenseId,
|
||||||
'reimbursement_id' => $reimbursementId,
|
'reimbursement_id' => $reimbursementId,
|
||||||
'admin_id' => $adminId,
|
'admin_id' => $adminId,
|
||||||
'assigned_at' => $now,
|
'assigned_at' => $now,
|
||||||
|
'unassigned_at' => null,
|
||||||
'school_year' => $this->schoolYear,
|
'school_year' => $this->schoolYear,
|
||||||
'semester' => $this->semester,
|
'semester' => $this->semester,
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
if ($existingBatchItem) {
|
||||||
$this->batchItemModel->insert($insertData);
|
$this->batchItemModel->update((int) $existingBatchItem['id'], $assignmentData);
|
||||||
|
} else {
|
||||||
|
$this->batchItemModel->insert($assignmentData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->db->transStatus()) {
|
||||||
|
throw new \RuntimeException('Transaction failed while saving assignment.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->transCommit();
|
||||||
|
|
||||||
|
return $newHashResponse([
|
||||||
|
'success' => true,
|
||||||
|
'batch_id' => $batchId,
|
||||||
|
'admin_id' => $adminId,
|
||||||
|
'reimbursement_id' => $reimbursementId,
|
||||||
|
]);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
log_message('error', 'Failed to assign expense #{expense} to batch #{batch}: {msg}', [
|
$this->db->transRollback();
|
||||||
|
|
||||||
|
log_message('error', 'Failed to update batch assignment for expense #{expense}, batch #{batch}: {msg}', [
|
||||||
'expense' => $expenseId,
|
'expense' => $expenseId,
|
||||||
'batch' => $batchId,
|
'batch' => $batchId,
|
||||||
'msg' => $e->getMessage(),
|
'msg' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $this->response->setStatusCode(500)->setJSON([
|
return $this->response->setStatusCode(500)->setJSON([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'error' => 'Unable to update batch assignment right now.',
|
'error' => 'Unable to update batch assignment right now.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$newHash = function_exists('csrf_hash') ? csrf_hash() : null;
|
|
||||||
|
|
||||||
return $this->response
|
|
||||||
->setHeader('X-CSRF-HASH', (string) $newHash)
|
|
||||||
->setJSON([
|
|
||||||
'success' => true,
|
|
||||||
'batch_id' => $batchId,
|
|
||||||
'admin_id' => $adminId,
|
|
||||||
'reimbursement_id' => $reimbursementId,
|
|
||||||
'csrf_hash' => $newHash,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function lockBatch()
|
public function lockBatch()
|
||||||
|
|||||||
@@ -71,12 +71,13 @@ class TuitionForecastController extends BaseController
|
|||||||
fputcsv($handle, ['Calculator Mode', $result['calculator_mode']]);
|
fputcsv($handle, ['Calculator Mode', $result['calculator_mode']]);
|
||||||
fputcsv($handle, []);
|
fputcsv($handle, []);
|
||||||
fputcsv($handle, ['Summary']);
|
fputcsv($handle, ['Summary']);
|
||||||
fputcsv($handle, ['Families', 'Students', 'Billable Students', 'Unit Price', 'Old Projected Income', 'New Projected Income', 'Difference']);
|
fputcsv($handle, ['Families', 'Students', 'Billable Students', 'Grade Unit Price', 'Youth Unit Price', 'Old Projected Income', 'New Projected Income', 'Difference']);
|
||||||
fputcsv($handle, [
|
fputcsv($handle, [
|
||||||
$result['summary']['family_count'],
|
$result['summary']['family_count'],
|
||||||
$result['summary']['student_count'],
|
$result['summary']['student_count'],
|
||||||
$result['summary']['billable_student_count'],
|
$result['summary']['billable_student_count'],
|
||||||
$result['summary']['unit_price'],
|
$result['summary']['unit_price'],
|
||||||
|
$result['summary']['youth_unit_price'],
|
||||||
$result['summary']['old_projected_income'] ?? $result['summary']['old_projected_tuition'],
|
$result['summary']['old_projected_income'] ?? $result['summary']['old_projected_tuition'],
|
||||||
$result['summary']['new_projected_income'] ?? $result['summary']['new_projected_tuition'],
|
$result['summary']['new_projected_income'] ?? $result['summary']['new_projected_tuition'],
|
||||||
$result['summary']['difference'],
|
$result['summary']['difference'],
|
||||||
@@ -132,6 +133,7 @@ class TuitionForecastController extends BaseController
|
|||||||
'include_event_only' => $this->request->{$source}('include_event_only') ?? '0',
|
'include_event_only' => $this->request->{$source}('include_event_only') ?? '0',
|
||||||
'include_paid_invoices' => $this->request->{$source}('include_paid_invoices') ?? '0',
|
'include_paid_invoices' => $this->request->{$source}('include_paid_invoices') ?? '0',
|
||||||
'unit_price' => $this->request->{$source}('unit_price') ?? '',
|
'unit_price' => $this->request->{$source}('unit_price') ?? '',
|
||||||
|
'youth_unit_price' => $this->request->{$source}('youth_unit_price') ?? '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,9 @@ class FinancialSystemLedgerCleanup extends Migration
|
|||||||
|
|
||||||
$defaults = [
|
$defaults = [
|
||||||
'tuition_calculator_version' => 'old',
|
'tuition_calculator_version' => 'old',
|
||||||
|
'youth_fee' => '200.00',
|
||||||
'new_tuition_full_amount' => '370.00',
|
'new_tuition_full_amount' => '370.00',
|
||||||
|
'new_tuition_youth_amount' => '200.00',
|
||||||
'new_tuition_second_student_discount' => '50.00',
|
'new_tuition_second_student_discount' => '50.00',
|
||||||
'new_tuition_third_student_discount' => '50.00',
|
'new_tuition_third_student_discount' => '50.00',
|
||||||
'new_tuition_fourth_plus_discount' => '100.00',
|
'new_tuition_fourth_plus_discount' => '100.00',
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
|
class UpdateYouthTuitionDefaults extends Migration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
if (!$this->db->tableExists('configuration')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->upsertConfig('youth_fee', '200.00', ['180', '180.00']);
|
||||||
|
$this->upsertConfig('new_tuition_youth_amount', '200.00', ['180', '180.00']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
if (!$this->db->tableExists('configuration')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->rollbackConfig('youth_fee', '180.00');
|
||||||
|
$this->rollbackConfig('new_tuition_youth_amount', '180.00');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function upsertConfig(string $key, string $value, array $legacyValues = []): void
|
||||||
|
{
|
||||||
|
$row = $this->db->table('configuration')
|
||||||
|
->select('id, config_value')
|
||||||
|
->where('config_key', $key)
|
||||||
|
->orderBy('id', 'ASC')
|
||||||
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
if (!$row) {
|
||||||
|
$this->db->table('configuration')->insert([
|
||||||
|
'config_key' => $key,
|
||||||
|
'config_value' => $value,
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current = trim((string) ($row['config_value'] ?? ''));
|
||||||
|
if ($current === '' || in_array($current, $legacyValues, true)) {
|
||||||
|
$this->db->table('configuration')
|
||||||
|
->where('config_key', $key)
|
||||||
|
->update(['config_value' => $value]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function rollbackConfig(string $key, string $value): void
|
||||||
|
{
|
||||||
|
$this->db->table('configuration')
|
||||||
|
->where('config_key', $key)
|
||||||
|
->where('config_value', '200.00')
|
||||||
|
->update(['config_value' => $value]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -307,6 +307,7 @@ class InvoiceLedgerService
|
|||||||
'second_student_fee' => $this->configurationModel->getConfig('second_student_fee'),
|
'second_student_fee' => $this->configurationModel->getConfig('second_student_fee'),
|
||||||
'youth_fee' => $this->configurationModel->getConfig('youth_fee'),
|
'youth_fee' => $this->configurationModel->getConfig('youth_fee'),
|
||||||
'new_tuition_full_amount' => $this->configurationModel->getConfig('new_tuition_full_amount'),
|
'new_tuition_full_amount' => $this->configurationModel->getConfig('new_tuition_full_amount'),
|
||||||
|
'new_tuition_youth_amount' => $this->configurationModel->getConfig('new_tuition_youth_amount'),
|
||||||
'new_tuition_second_student_discount' => $this->configurationModel->getConfig('new_tuition_second_student_discount'),
|
'new_tuition_second_student_discount' => $this->configurationModel->getConfig('new_tuition_second_student_discount'),
|
||||||
'new_tuition_third_student_discount' => $this->configurationModel->getConfig('new_tuition_third_student_discount'),
|
'new_tuition_third_student_discount' => $this->configurationModel->getConfig('new_tuition_third_student_discount'),
|
||||||
'new_tuition_fourth_plus_discount' => $this->configurationModel->getConfig('new_tuition_fourth_plus_discount'),
|
'new_tuition_fourth_plus_discount' => $this->configurationModel->getConfig('new_tuition_fourth_plus_discount'),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ final class NewTuitionCalculatorService implements TuitionCalculatorInterface
|
|||||||
{
|
{
|
||||||
$gradeFee = (int) ($config['grade_fee'] ?? 9);
|
$gradeFee = (int) ($config['grade_fee'] ?? 9);
|
||||||
$fullAmountCents = $this->toCents($config['new_tuition_full_amount'] ?? 370);
|
$fullAmountCents = $this->toCents($config['new_tuition_full_amount'] ?? 370);
|
||||||
|
$youthAmountCents = $this->toCents($config['new_tuition_youth_amount'] ?? $config['youth_fee'] ?? 200);
|
||||||
$secondDiscountCents = $this->toCents($config['new_tuition_second_student_discount'] ?? 50);
|
$secondDiscountCents = $this->toCents($config['new_tuition_second_student_discount'] ?? 50);
|
||||||
$thirdDiscountCents = $this->toCents($config['new_tuition_third_student_discount'] ?? 50);
|
$thirdDiscountCents = $this->toCents($config['new_tuition_third_student_discount'] ?? 50);
|
||||||
$fourthPlusDiscountCents = $this->toCents($config['new_tuition_fourth_plus_discount'] ?? 100);
|
$fourthPlusDiscountCents = $this->toCents($config['new_tuition_fourth_plus_discount'] ?? 100);
|
||||||
@@ -22,9 +23,19 @@ final class NewTuitionCalculatorService implements TuitionCalculatorInterface
|
|||||||
});
|
});
|
||||||
|
|
||||||
$details = [];
|
$details = [];
|
||||||
|
$regularPosition = 0;
|
||||||
|
|
||||||
foreach (array_values($students) as $index => $student) {
|
foreach (array_values($students) as $student) {
|
||||||
$position = $index + 1;
|
$level = GradeLevelParser::parse($student['grade_level'] ?? null, $gradeFee);
|
||||||
|
|
||||||
|
if ($level > $gradeFee) {
|
||||||
|
$position = null;
|
||||||
|
$discountCents = 0;
|
||||||
|
$rule = 'new_youth_unit_price';
|
||||||
|
$baseAmountCents = $youthAmountCents;
|
||||||
|
} else {
|
||||||
|
$regularPosition++;
|
||||||
|
$position = $regularPosition;
|
||||||
|
|
||||||
if ($position === 1) {
|
if ($position === 1) {
|
||||||
$discountCents = 0;
|
$discountCents = 0;
|
||||||
@@ -40,14 +51,17 @@ final class NewTuitionCalculatorService implements TuitionCalculatorInterface
|
|||||||
$rule = 'new_fourth_plus_student_discount';
|
$rule = 'new_fourth_plus_student_discount';
|
||||||
}
|
}
|
||||||
|
|
||||||
$amountCents = max(0, $fullAmountCents - $discountCents);
|
$baseAmountCents = $fullAmountCents;
|
||||||
|
}
|
||||||
|
|
||||||
|
$amountCents = max(0, $baseAmountCents - $discountCents);
|
||||||
|
|
||||||
$details[] = [
|
$details[] = [
|
||||||
'student_id' => (int) ($student['student_id'] ?? 0),
|
'student_id' => (int) ($student['student_id'] ?? 0),
|
||||||
'student_name' => (string) ($student['student_name'] ?? ''),
|
'student_name' => (string) ($student['student_name'] ?? ''),
|
||||||
'grade_level' => $student['grade_level'] ?? null,
|
'grade_level' => $student['grade_level'] ?? null,
|
||||||
'family_position' => $position,
|
'family_position' => $position,
|
||||||
'full_amount' => $this->fromCents($fullAmountCents),
|
'full_amount' => $this->fromCents($baseAmountCents),
|
||||||
'discount' => $this->fromCents($discountCents),
|
'discount' => $this->fromCents($discountCents),
|
||||||
'rule' => $rule,
|
'rule' => $rule,
|
||||||
'amount' => $this->fromCents($amountCents),
|
'amount' => $this->fromCents($amountCents),
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ final class OldTuitionCalculatorService implements TuitionCalculatorInterface
|
|||||||
$gradeFee = (int) ($config['grade_fee'] ?? 9);
|
$gradeFee = (int) ($config['grade_fee'] ?? 9);
|
||||||
$firstStudentFee = $this->toCents($config['first_student_fee'] ?? 370);
|
$firstStudentFee = $this->toCents($config['first_student_fee'] ?? 370);
|
||||||
$secondStudentFee = $this->toCents($config['second_student_fee'] ?? 200);
|
$secondStudentFee = $this->toCents($config['second_student_fee'] ?? 200);
|
||||||
$youthFee = $this->toCents($config['youth_fee'] ?? 180);
|
$youthFee = $this->toCents($config['youth_fee'] ?? 200);
|
||||||
|
|
||||||
usort($students, function (array $left, array $right) use ($gradeFee): int {
|
usort($students, function (array $left, array $right) use ($gradeFee): int {
|
||||||
$leftLevel = GradeLevelParser::parse($left['grade_level'] ?? null, $gradeFee);
|
$leftLevel = GradeLevelParser::parse($left['grade_level'] ?? null, $gradeFee);
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class TuitionForecastService
|
|||||||
$mode = $this->normalizeMode($mode);
|
$mode = $this->normalizeMode($mode);
|
||||||
$options = $this->normalizeOptions($options);
|
$options = $this->normalizeOptions($options);
|
||||||
$this->unitPriceOverride = $options['unit_price'];
|
$this->unitPriceOverride = $options['unit_price'];
|
||||||
|
$this->youthUnitPriceOverride = $options['youth_unit_price'];
|
||||||
$tuitionConfig = $this->getTuitionConfig();
|
$tuitionConfig = $this->getTuitionConfig();
|
||||||
$familyRows = [];
|
$familyRows = [];
|
||||||
$summary = [
|
$summary = [
|
||||||
@@ -57,6 +58,7 @@ class TuitionForecastService
|
|||||||
'projected_tuition' => '0.00',
|
'projected_tuition' => '0.00',
|
||||||
'projected_income' => '0.00',
|
'projected_income' => '0.00',
|
||||||
'unit_price' => '0.00',
|
'unit_price' => '0.00',
|
||||||
|
'youth_unit_price' => '0.00',
|
||||||
];
|
];
|
||||||
|
|
||||||
$oldProjectedCents = 0;
|
$oldProjectedCents = 0;
|
||||||
@@ -117,6 +119,7 @@ class TuitionForecastService
|
|||||||
$summary['projected_tuition'] = $mode === 'old' ? $summary['old_projected_tuition'] : $summary['new_projected_tuition'];
|
$summary['projected_tuition'] = $mode === 'old' ? $summary['old_projected_tuition'] : $summary['new_projected_tuition'];
|
||||||
$summary['projected_income'] = $mode === 'old' ? $summary['old_projected_income'] : $summary['new_projected_income'];
|
$summary['projected_income'] = $mode === 'old' ? $summary['old_projected_income'] : $summary['new_projected_income'];
|
||||||
$summary['unit_price'] = number_format((float) ($tuitionConfig['new_tuition_full_amount'] ?? 0), 2, '.', '');
|
$summary['unit_price'] = number_format((float) ($tuitionConfig['new_tuition_full_amount'] ?? 0), 2, '.', '');
|
||||||
|
$summary['youth_unit_price'] = number_format((float) ($tuitionConfig['new_tuition_youth_amount'] ?? 0), 2, '.', '');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'school_year' => $schoolYear,
|
'school_year' => $schoolYear,
|
||||||
@@ -232,6 +235,7 @@ class TuitionForecastService
|
|||||||
'include_event_only' => $this->toBool($options['include_event_only'] ?? false),
|
'include_event_only' => $this->toBool($options['include_event_only'] ?? false),
|
||||||
'include_paid_invoices' => $this->toBool($options['include_paid_invoices'] ?? true),
|
'include_paid_invoices' => $this->toBool($options['include_paid_invoices'] ?? true),
|
||||||
'unit_price' => $this->normalizeMoney($options['unit_price'] ?? null),
|
'unit_price' => $this->normalizeMoney($options['unit_price'] ?? null),
|
||||||
|
'youth_unit_price' => $this->normalizeMoney($options['youth_unit_price'] ?? null),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,8 +545,9 @@ class TuitionForecastService
|
|||||||
'grade_fee' => $this->configurationModel->getConfig('grade_fee'),
|
'grade_fee' => $this->configurationModel->getConfig('grade_fee'),
|
||||||
'first_student_fee' => $this->configurationModel->getConfig('first_student_fee'),
|
'first_student_fee' => $this->configurationModel->getConfig('first_student_fee'),
|
||||||
'second_student_fee' => $this->configurationModel->getConfig('second_student_fee'),
|
'second_student_fee' => $this->configurationModel->getConfig('second_student_fee'),
|
||||||
'youth_fee' => $this->configurationModel->getConfig('youth_fee'),
|
'youth_fee' => $this->configurationModel->getConfig('youth_fee') ?? '200.00',
|
||||||
'new_tuition_full_amount' => $this->normalizedUnitPriceOverride(),
|
'new_tuition_full_amount' => $this->normalizedUnitPriceOverride(),
|
||||||
|
'new_tuition_youth_amount' => $this->normalizedYouthUnitPriceOverride(),
|
||||||
'new_tuition_second_student_discount' => $this->configurationModel->getConfig('new_tuition_second_student_discount'),
|
'new_tuition_second_student_discount' => $this->configurationModel->getConfig('new_tuition_second_student_discount'),
|
||||||
'new_tuition_third_student_discount' => $this->configurationModel->getConfig('new_tuition_third_student_discount'),
|
'new_tuition_third_student_discount' => $this->configurationModel->getConfig('new_tuition_third_student_discount'),
|
||||||
'new_tuition_fourth_plus_discount' => $this->configurationModel->getConfig('new_tuition_fourth_plus_discount'),
|
'new_tuition_fourth_plus_discount' => $this->configurationModel->getConfig('new_tuition_fourth_plus_discount'),
|
||||||
@@ -550,6 +555,7 @@ class TuitionForecastService
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected ?string $unitPriceOverride = null;
|
protected ?string $unitPriceOverride = null;
|
||||||
|
protected ?string $youthUnitPriceOverride = null;
|
||||||
|
|
||||||
protected function normalizedUnitPriceOverride(): string
|
protected function normalizedUnitPriceOverride(): string
|
||||||
{
|
{
|
||||||
@@ -557,6 +563,16 @@ class TuitionForecastService
|
|||||||
?? (string) ($this->configurationModel->getConfig('new_tuition_full_amount') ?? '0.00');
|
?? (string) ($this->configurationModel->getConfig('new_tuition_full_amount') ?? '0.00');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function normalizedYouthUnitPriceOverride(): string
|
||||||
|
{
|
||||||
|
return $this->youthUnitPriceOverride
|
||||||
|
?? (string) (
|
||||||
|
$this->configurationModel->getConfig('new_tuition_youth_amount')
|
||||||
|
?? $this->configurationModel->getConfig('youth_fee')
|
||||||
|
?? '200.00'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
protected function normalizeMoney($value): ?string
|
protected function normalizeMoney($value): ?string
|
||||||
{
|
{
|
||||||
if ($value === null || trim((string) $value) === '') {
|
if ($value === null || trim((string) $value) === '') {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class FeeCalculationService
|
|||||||
// Retrieve fee configs
|
// Retrieve fee configs
|
||||||
$firstStudentFee = (float) ($configModel->getConfig('first_student_fee') ?? 350);
|
$firstStudentFee = (float) ($configModel->getConfig('first_student_fee') ?? 350);
|
||||||
$secondStudentFee = (float) ($configModel->getConfig('second_student_fee') ?? 200);
|
$secondStudentFee = (float) ($configModel->getConfig('second_student_fee') ?? 200);
|
||||||
$youthFee = (float) ($configModel->getConfig('youth_fee') ?? 180);
|
$youthFee = (float) ($configModel->getConfig('youth_fee') ?? 200);
|
||||||
|
|
||||||
// Assign tuition_fee to all students (before filtering refunds)
|
// Assign tuition_fee to all students (before filtering refunds)
|
||||||
$regularCount = 0;
|
$regularCount = 0;
|
||||||
@@ -148,7 +148,7 @@ class FeeCalculationService
|
|||||||
|
|
||||||
$firstStudentFee = (float) ($configModel->getConfig('first_student_fee') ?? 350);
|
$firstStudentFee = (float) ($configModel->getConfig('first_student_fee') ?? 350);
|
||||||
$secondStudentFee = (float) ($configModel->getConfig('second_student_fee') ?? 200);
|
$secondStudentFee = (float) ($configModel->getConfig('second_student_fee') ?? 200);
|
||||||
$youthFee = (float) ($configModel->getConfig('youth_fee') ?? 180);
|
$youthFee = (float) ($configModel->getConfig('youth_fee') ?? 200);
|
||||||
|
|
||||||
// ✅ Pre-fetch and assign grade/class section names before sorting
|
// ✅ Pre-fetch and assign grade/class section names before sorting
|
||||||
foreach ($students as &$student) {
|
foreach ($students as &$student) {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ $warningText = static function (array $warnings): string {
|
|||||||
'include_event_only' => !empty($filters['include_event_only']) ? '1' : '0',
|
'include_event_only' => !empty($filters['include_event_only']) ? '1' : '0',
|
||||||
'include_paid_invoices' => !empty($filters['include_paid_invoices']) ? '1' : '0',
|
'include_paid_invoices' => !empty($filters['include_paid_invoices']) ? '1' : '0',
|
||||||
'unit_price' => $filters['unit_price'] ?? '',
|
'unit_price' => $filters['unit_price'] ?? '',
|
||||||
|
'youth_unit_price' => $filters['youth_unit_price'] ?? '',
|
||||||
]);
|
]);
|
||||||
?>
|
?>
|
||||||
<a href="<?= site_url('administrator/tuition-forecast/export?' . $exportQuery) ?>" class="btn btn-success">
|
<a href="<?= site_url('administrator/tuition-forecast/export?' . $exportQuery) ?>" class="btn btn-success">
|
||||||
@@ -79,7 +80,7 @@ $warningText = static function (array $warnings): string {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label for="unit_price" class="form-label">Unit Price</label>
|
<label for="unit_price" class="form-label">Grades Unit Price</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
@@ -88,7 +89,19 @@ $warningText = static function (array $warnings): string {
|
|||||||
name="unit_price"
|
name="unit_price"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
value="<?= esc((string) ($filters['unit_price'] ?? ($summary['unit_price'] ?? ''))) ?>"
|
value="<?= esc((string) ($filters['unit_price'] ?? ($summary['unit_price'] ?? ''))) ?>"
|
||||||
placeholder="New tuition unit price">
|
placeholder="New tuition unit price for grades">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="youth_unit_price" class="form-label">Youth Unit Price</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
id="youth_unit_price"
|
||||||
|
name="youth_unit_price"
|
||||||
|
class="form-control"
|
||||||
|
value="<?= esc((string) ($filters['youth_unit_price'] ?? ($summary['youth_unit_price'] ?? ''))) ?>"
|
||||||
|
placeholder="New tuition unit price for youth">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
@@ -109,7 +122,7 @@ $warningText = static function (array $warnings): string {
|
|||||||
|
|
||||||
<?php if ($result): ?>
|
<?php if ($result): ?>
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-md-2">
|
<div class="col-xl col-lg-3 col-md-4">
|
||||||
<div class="card border-0 shadow-sm h-100">
|
<div class="card border-0 shadow-sm h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="text-muted small">Total Parents</div>
|
<div class="text-muted small">Total Parents</div>
|
||||||
@@ -117,7 +130,7 @@ $warningText = static function (array $warnings): string {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-xl col-lg-3 col-md-4">
|
||||||
<div class="card border-0 shadow-sm h-100">
|
<div class="card border-0 shadow-sm h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="text-muted small">Total Students</div>
|
<div class="text-muted small">Total Students</div>
|
||||||
@@ -126,7 +139,7 @@ $warningText = static function (array $warnings): string {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-xl col-lg-3 col-md-4">
|
||||||
<div class="card border-0 shadow-sm h-100">
|
<div class="card border-0 shadow-sm h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="text-muted small">Old Projected Income</div>
|
<div class="text-muted small">Old Projected Income</div>
|
||||||
@@ -134,7 +147,7 @@ $warningText = static function (array $warnings): string {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-xl col-lg-3 col-md-4">
|
||||||
<div class="card border-0 shadow-sm h-100">
|
<div class="card border-0 shadow-sm h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="text-muted small">New Projected Income</div>
|
<div class="text-muted small">New Projected Income</div>
|
||||||
@@ -142,15 +155,23 @@ $warningText = static function (array $warnings): string {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-xl col-lg-3 col-md-4">
|
||||||
<div class="card border-0 shadow-sm h-100">
|
<div class="card border-0 shadow-sm h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="text-muted small">Unit Price</div>
|
<div class="text-muted small">Grades Unit Price</div>
|
||||||
<div class="fs-5 fw-semibold"><?= esc($fmtMoney($summary['unit_price'] ?? 0)) ?></div>
|
<div class="fs-5 fw-semibold"><?= esc($fmtMoney($summary['unit_price'] ?? 0)) ?></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-xl col-lg-3 col-md-4">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="text-muted small">Youth Unit Price</div>
|
||||||
|
<div class="fs-5 fw-semibold"><?= esc($fmtMoney($summary['youth_unit_price'] ?? 0)) ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl col-lg-3 col-md-4">
|
||||||
<div class="card border-0 shadow-sm h-100">
|
<div class="card border-0 shadow-sm h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="text-muted small">Difference</div>
|
<div class="text-muted small">Difference</div>
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"advisories": {
|
||||||
|
"phpoffice/math": [
|
||||||
|
{
|
||||||
|
"advisoryId": "PKSA-jw72-bn8m-h7rc",
|
||||||
|
"packageName": "phpoffice/math",
|
||||||
|
"affectedVersions": "<=0.2.0",
|
||||||
|
"title": "PHPOffice Math allows XXE when processing an XML file in the MathML format ",
|
||||||
|
"cve": "CVE-2025-48882",
|
||||||
|
"link": "https://github.com/advisories/GHSA-42hm-pq2f-3r7m",
|
||||||
|
"reportedAt": "2025-05-29T17:27:39+00:00",
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "GitHub",
|
||||||
|
"remoteId": "GHSA-42hm-pq2f-3r7m"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"severity": "high"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"phpunit/phpunit": [
|
||||||
|
{
|
||||||
|
"advisoryId": "PKSA-z3gr-8qht-p93v",
|
||||||
|
"packageName": "phpunit/phpunit",
|
||||||
|
"affectedVersions": ">=0,<8.5.52|>=9.0.0,<9.6.33|>=10.0.0,<10.5.62|>=11.0.0,<11.5.50|>=12.0.0,<12.5.8",
|
||||||
|
"title": "Unsafe Deserialization in PHPT Code Coverage Handling",
|
||||||
|
"cve": "CVE-2026-24765",
|
||||||
|
"link": "https://github.com/sebastianbergmann/phpunit/security/advisories/GHSA-vvj3-c3rp-c85p",
|
||||||
|
"reportedAt": "2026-01-27T05:21:14+00:00",
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "GitHub",
|
||||||
|
"remoteId": "GHSA-vvj3-c3rp-c85p"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FriendsOfPHP/security-advisories",
|
||||||
|
"remoteId": "phpunit/phpunit/CVE-2026-24765.yaml"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"severity": "high"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"abandoned": {
|
||||||
|
"paypal/rest-api-sdk-php": "paypal/paypal-server-sdk"
|
||||||
|
},
|
||||||
|
"filter": []
|
||||||
|
}
|
||||||
@@ -65,6 +65,7 @@ class FakeForecastService extends TuitionForecastService
|
|||||||
'include_event_only' => $options['include_event_only'] ?? '0',
|
'include_event_only' => $options['include_event_only'] ?? '0',
|
||||||
'include_paid_invoices' => $options['include_paid_invoices'] ?? '0',
|
'include_paid_invoices' => $options['include_paid_invoices'] ?? '0',
|
||||||
'unit_price' => $options['unit_price'] ?? '370.00',
|
'unit_price' => $options['unit_price'] ?? '370.00',
|
||||||
|
'youth_unit_price' => $options['youth_unit_price'] ?? '200.00',
|
||||||
],
|
],
|
||||||
'summary' => [
|
'summary' => [
|
||||||
'family_count' => 1,
|
'family_count' => 1,
|
||||||
@@ -75,6 +76,7 @@ class FakeForecastService extends TuitionForecastService
|
|||||||
'old_projected_income' => '550.00',
|
'old_projected_income' => '550.00',
|
||||||
'new_projected_income' => '650.00',
|
'new_projected_income' => '650.00',
|
||||||
'unit_price' => '370.00',
|
'unit_price' => '370.00',
|
||||||
|
'youth_unit_price' => '200.00',
|
||||||
'difference' => '100.00',
|
'difference' => '100.00',
|
||||||
],
|
],
|
||||||
'families' => [[
|
'families' => [[
|
||||||
@@ -160,6 +162,7 @@ class TuitionForecastControllerTest extends CIUnitTestCase
|
|||||||
'include_event_only' => '1',
|
'include_event_only' => '1',
|
||||||
'include_paid_invoices' => '1',
|
'include_paid_invoices' => '1',
|
||||||
'unit_price' => '395.00',
|
'unit_price' => '395.00',
|
||||||
|
'youth_unit_price' => '200.00',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$result = $this->controller->setRequestObject($request)->index();
|
$result = $this->controller->setRequestObject($request)->index();
|
||||||
@@ -170,6 +173,7 @@ class TuitionForecastControllerTest extends CIUnitTestCase
|
|||||||
$this->assertSame('new', $result['data']['filters']['calculator_mode']);
|
$this->assertSame('new', $result['data']['filters']['calculator_mode']);
|
||||||
$this->assertSame('1', $result['data']['filters']['include_payment_pending']);
|
$this->assertSame('1', $result['data']['filters']['include_payment_pending']);
|
||||||
$this->assertSame('395.00', $result['data']['filters']['unit_price']);
|
$this->assertSame('395.00', $result['data']['filters']['unit_price']);
|
||||||
|
$this->assertSame('200.00', $result['data']['filters']['youth_unit_price']);
|
||||||
$this->assertSame('always', $this->service->calls[0]['options']['include_withdrawn_mode']);
|
$this->assertSame('always', $this->service->calls[0]['options']['include_withdrawn_mode']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +188,7 @@ class TuitionForecastControllerTest extends CIUnitTestCase
|
|||||||
'include_event_only' => '0',
|
'include_event_only' => '0',
|
||||||
'include_paid_invoices' => '1',
|
'include_paid_invoices' => '1',
|
||||||
'unit_price' => '390.00',
|
'unit_price' => '390.00',
|
||||||
|
'youth_unit_price' => '210.00',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->controller->setRequestObject($request)->calculate();
|
$response = $this->controller->setRequestObject($request)->calculate();
|
||||||
@@ -193,6 +198,7 @@ class TuitionForecastControllerTest extends CIUnitTestCase
|
|||||||
$this->assertSame('Fall', $payload['semester']);
|
$this->assertSame('Fall', $payload['semester']);
|
||||||
$this->assertSame('compare', $payload['calculator_mode']);
|
$this->assertSame('compare', $payload['calculator_mode']);
|
||||||
$this->assertSame('390.00', $payload['options']['unit_price']);
|
$this->assertSame('390.00', $payload['options']['unit_price']);
|
||||||
|
$this->assertSame('210.00', $payload['options']['youth_unit_price']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testExportCsvBuildsDownloadWithSummaryAndFamilyRows(): void
|
public function testExportCsvBuildsDownloadWithSummaryAndFamilyRows(): void
|
||||||
@@ -210,6 +216,7 @@ class TuitionForecastControllerTest extends CIUnitTestCase
|
|||||||
$this->assertStringContainsString('Summary', $body);
|
$this->assertStringContainsString('Summary', $body);
|
||||||
$this->assertStringContainsString('All Year', $body);
|
$this->assertStringContainsString('All Year', $body);
|
||||||
$this->assertStringContainsString('Old Projected Income', $body);
|
$this->assertStringContainsString('Old Projected Income', $body);
|
||||||
|
$this->assertStringContainsString('Youth Unit Price', $body);
|
||||||
$this->assertStringContainsString('Layla Yusuf', $body);
|
$this->assertStringContainsString('Layla Yusuf', $body);
|
||||||
$this->assertStringContainsString('Student One', $body);
|
$this->assertStringContainsString('Student One', $body);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,4 +51,29 @@ class NewTuitionCalculatorServiceTest extends CIUnitTestCase
|
|||||||
$this->assertSame('0.00', $result['details'][3]['amount']);
|
$this->assertSame('0.00', $result['details'][3]['amount']);
|
||||||
$this->assertSame('0.00', $result['details'][4]['amount']);
|
$this->assertSame('0.00', $result['details'][4]['amount']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testYouthStudentsUseSeparateUnitPriceWithoutAffectingRegularDiscountOrder(): void
|
||||||
|
{
|
||||||
|
$service = new NewTuitionCalculatorService();
|
||||||
|
$result = $service->calculateFamilyTuition([
|
||||||
|
['student_id' => 3, 'student_name' => 'Youth', 'grade_level' => 'Youth 1'],
|
||||||
|
['student_id' => 1, 'student_name' => 'Regular One', 'grade_level' => '2'],
|
||||||
|
['student_id' => 2, 'student_name' => 'Regular Two', 'grade_level' => '5'],
|
||||||
|
], [
|
||||||
|
'grade_fee' => 9,
|
||||||
|
'new_tuition_full_amount' => '370.00',
|
||||||
|
'new_tuition_youth_amount' => '200.00',
|
||||||
|
'new_tuition_second_student_discount' => '150.00',
|
||||||
|
'new_tuition_third_student_discount' => '150.00',
|
||||||
|
'new_tuition_fourth_plus_discount' => '150.00',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame('790.00', $result['total']);
|
||||||
|
$this->assertSame('new_first_student_full_amount', $result['details'][0]['rule']);
|
||||||
|
$this->assertSame('370.00', $result['details'][0]['amount']);
|
||||||
|
$this->assertSame('new_second_student_discount', $result['details'][1]['rule']);
|
||||||
|
$this->assertSame('220.00', $result['details'][1]['amount']);
|
||||||
|
$this->assertSame('new_youth_unit_price', $result['details'][2]['rule']);
|
||||||
|
$this->assertSame('200.00', $result['details'][2]['amount']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,10 +38,12 @@ class TuitionForecastServiceTest extends CIUnitTestCase
|
|||||||
'students' => [
|
'students' => [
|
||||||
['student_id' => 1, 'student_name' => 'Mariam', 'grade_level' => '1'],
|
['student_id' => 1, 'student_name' => 'Mariam', 'grade_level' => '1'],
|
||||||
['student_id' => 2, 'student_name' => 'Yahya', 'grade_level' => '2'],
|
['student_id' => 2, 'student_name' => 'Yahya', 'grade_level' => '2'],
|
||||||
|
['student_id' => 4, 'student_name' => 'Youth Student', 'grade_level' => 'Youth 1'],
|
||||||
],
|
],
|
||||||
'all_students' => [
|
'all_students' => [
|
||||||
['student_id' => 1, 'student_name' => 'Mariam', 'grade_level' => '1', 'billable' => true, 'excluded_reason' => null],
|
['student_id' => 1, 'student_name' => 'Mariam', 'grade_level' => '1', 'billable' => true, 'excluded_reason' => null],
|
||||||
['student_id' => 2, 'student_name' => 'Yahya', 'grade_level' => '2', 'billable' => true, 'excluded_reason' => null],
|
['student_id' => 2, 'student_name' => 'Yahya', 'grade_level' => '2', 'billable' => true, 'excluded_reason' => null],
|
||||||
|
['student_id' => 4, 'student_name' => 'Youth Student', 'grade_level' => 'Youth 1', 'billable' => true, 'excluded_reason' => null],
|
||||||
['student_id' => 3, 'student_name' => 'Event Only', 'grade_level' => '3', 'billable' => false, 'excluded_reason' => 'event_only'],
|
['student_id' => 3, 'student_name' => 'Event Only', 'grade_level' => '3', 'billable' => false, 'excluded_reason' => 'event_only'],
|
||||||
],
|
],
|
||||||
'warnings' => ['1 event-only student(s) excluded from tuition.'],
|
'warnings' => ['1 event-only student(s) excluded from tuition.'],
|
||||||
@@ -54,8 +56,9 @@ class TuitionForecastServiceTest extends CIUnitTestCase
|
|||||||
'grade_fee' => 9,
|
'grade_fee' => 9,
|
||||||
'first_student_fee' => '350.00',
|
'first_student_fee' => '350.00',
|
||||||
'second_student_fee' => '200.00',
|
'second_student_fee' => '200.00',
|
||||||
'youth_fee' => '180.00',
|
'youth_fee' => '200.00',
|
||||||
'new_tuition_full_amount' => $this->unitPriceOverride ?? '350.00',
|
'new_tuition_full_amount' => $this->unitPriceOverride ?? '350.00',
|
||||||
|
'new_tuition_youth_amount' => $this->youthUnitPriceOverride ?? '200.00',
|
||||||
'new_tuition_second_student_discount' => '50.00',
|
'new_tuition_second_student_discount' => '50.00',
|
||||||
'new_tuition_third_student_discount' => '50.00',
|
'new_tuition_third_student_discount' => '50.00',
|
||||||
'new_tuition_fourth_plus_discount' => '100.00',
|
'new_tuition_fourth_plus_discount' => '100.00',
|
||||||
@@ -67,16 +70,18 @@ class TuitionForecastServiceTest extends CIUnitTestCase
|
|||||||
'include_payment_pending' => true,
|
'include_payment_pending' => true,
|
||||||
'include_withdrawn_mode' => 'refund_deadline',
|
'include_withdrawn_mode' => 'refund_deadline',
|
||||||
'unit_price' => '400.00',
|
'unit_price' => '400.00',
|
||||||
|
'youth_unit_price' => '200.00',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertSame(1, $result['summary']['family_count']);
|
$this->assertSame(1, $result['summary']['family_count']);
|
||||||
$this->assertSame(3, $result['summary']['student_count']);
|
$this->assertSame(4, $result['summary']['student_count']);
|
||||||
$this->assertSame(2, $result['summary']['billable_student_count']);
|
$this->assertSame(3, $result['summary']['billable_student_count']);
|
||||||
$this->assertSame('550.00', $result['summary']['old_projected_tuition']);
|
$this->assertSame('750.00', $result['summary']['old_projected_tuition']);
|
||||||
$this->assertSame('750.00', $result['summary']['new_projected_tuition']);
|
$this->assertSame('950.00', $result['summary']['new_projected_tuition']);
|
||||||
$this->assertSame('550.00', $result['summary']['old_projected_income']);
|
$this->assertSame('750.00', $result['summary']['old_projected_income']);
|
||||||
$this->assertSame('750.00', $result['summary']['new_projected_income']);
|
$this->assertSame('950.00', $result['summary']['new_projected_income']);
|
||||||
$this->assertSame('400.00', $result['summary']['unit_price']);
|
$this->assertSame('400.00', $result['summary']['unit_price']);
|
||||||
$this->assertSame('event_only', $result['families'][0]['student_details'][2]['excluded_reason']);
|
$this->assertSame('200.00', $result['summary']['youth_unit_price']);
|
||||||
|
$this->assertSame('event_only', $result['families'][0]['student_details'][3]['excluded_reason']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user