fix school year for all tables
Tests / PHPUnit (push) Failing after 1m20s

This commit is contained in:
root
2026-07-18 14:45:38 -04:00
parent 4db8334a3c
commit 716cc8b8d3
125 changed files with 2315 additions and 81 deletions
+167 -10
View File
@@ -6,6 +6,7 @@ use App\Models\SchoolYearClosingBatchModel;
use App\Models\SchoolYearClosingItemModel;
use App\Models\SchoolYearModel;
use App\Models\ConfigurationModel;
use App\Models\InvoiceModel;
use App\Support\SchoolYear\SchoolYearStatus;
use CodeIgniter\Database\BaseConnection;
use InvalidArgumentException;
@@ -123,8 +124,8 @@ final class SchoolYearClosingService
'status' => 'started',
'preview_hash' => $preview['hash'],
'total_families' => count($preview['carry_forward']),
'total_positive_balance' => $preview['finance']['positive_balance'],
'total_credit_balance' => $preview['finance']['credit_balance'],
'total_positive_balance' => $this->sumCarryForwardBalances($preview['carry_forward'], true),
'total_credit_balance' => $this->sumCarryForwardBalances($preview['carry_forward'], false),
'started_by' => $userId,
'started_at' => $now,
], true);
@@ -168,22 +169,44 @@ final class SchoolYearClosingService
$preview = $this->preview($sourceYearId, (int) $batch['target_school_year_id']);
if ($preview['hash'] !== (string) $batch['preview_hash']) {
throw new InvalidArgumentException('Closing preview has changed. Refresh and restart closing before executing carry-forward.');
if (($batch['status'] ?? '') === 'started') {
$this->refreshBatchFromPreview($batch, $preview);
$batch = $this->latestOpenBatch($sourceYearId) ?? $batch;
} elseif (in_array((string) ($batch['status'] ?? ''), ['executed', 'completed'], true) && $this->countExistingTargetInvoices($batch) === 0) {
$this->refreshBatchFromPreview($batch, $preview);
$batch = $this->latestOpenBatch($sourceYearId) ?? $batch;
} elseif (! in_array((string) ($batch['status'] ?? ''), ['executed', 'completed'], true)) {
throw new InvalidArgumentException('Closing preview has changed. Refresh and restart closing before executing carry-forward.');
}
}
$source = $this->requireYear($sourceYearId);
$target = $this->requireYear((int) $batch['target_school_year_id']);
$sourceName = (string) ($source['name'] ?? '');
$targetName = (string) ($target['name'] ?? '');
$this->db->transStart();
$items = $this->itemModel->where('closing_batch_id', (int) $batch['id'])->findAll();
foreach ($items as $item) {
if (($item['status'] ?? '') === 'completed') {
if (($item['status'] ?? '') === 'completed' && (int) ($item['target_invoice_id'] ?? 0) > 0) {
continue;
}
$this->itemModel->update((int) $item['id'], ['status' => 'completed']);
$targetInvoiceId = $this->createCarryForwardInvoice($item, $sourceName, $targetName, $userId);
$this->itemModel->update((int) $item['id'], [
'target_invoice_id' => $targetInvoiceId,
'status' => 'completed',
'error_message' => null,
]);
}
$this->batchModel->update((int) $batch['id'], ['status' => 'executed']);
$this->managementService->log($sourceYearId, SchoolYearStatus::CLOSING, SchoolYearStatus::CLOSING, 'carry_forward_execute', $userId, [
if (($batch['status'] ?? '') !== 'completed') {
$this->batchModel->update((int) $batch['id'], ['status' => 'executed']);
}
$currentStatus = (string) ($source['status'] ?? SchoolYearStatus::CLOSING);
$this->managementService->log($sourceYearId, $currentStatus, $currentStatus, 'carry_forward_execute', $userId, [
'closing_batch_id' => (int) $batch['id'],
'note' => 'Marked previewed carry-forward items complete. Target accounting records require the dedicated opening-balance schema.',
'target_school_year_id' => (int) $batch['target_school_year_id'],
'note' => 'Created target-year opening balance invoices for previewed carry-forward items.',
]);
$this->db->transComplete();
@@ -358,6 +381,7 @@ final class SchoolYearClosingService
$count = $this->db->table('invoices')
->where('school_year', $schoolYear)
->where('balance >', 0)
->where("LOWER(status) IN ('unpaid', 'partially paid')", null, false)
->countAllResults();
return $count > 0
@@ -374,7 +398,9 @@ final class SchoolYearClosingService
$builder = $this->db->table('invoices i')
->select('i.parent_id AS family_id')
->select('COALESCE(SUM(i.balance), 0) AS source_balance')
->where('i.school_year', $schoolYear);
->where('i.school_year', $schoolYear)
->where('i.balance !=', 0)
->where("LOWER(i.status) IN ('unpaid', 'partially paid')", null, false);
if ($this->db->tableExists('users')) {
$builder
@@ -407,6 +433,137 @@ final class SchoolYearClosingService
}, $rows);
}
private function refreshBatchFromPreview(array $batch, array $preview): void
{
$batchId = (int) ($batch['id'] ?? 0);
if ($batchId <= 0) {
throw new InvalidArgumentException('Closing batch was not found.');
}
$this->itemModel->where('closing_batch_id', $batchId)->delete();
foreach ($preview['carry_forward'] as $row) {
$this->itemModel->insert([
'closing_batch_id' => $batchId,
'family_id' => (int) $row['family_id'],
'source_balance' => $row['source_balance'],
'credit_amount' => $row['credit_amount'],
'carry_forward_amount' => $row['carry_forward_amount'],
'status' => 'pending',
]);
}
$this->batchModel->update($batchId, [
'preview_hash' => $preview['hash'],
'total_families' => count($preview['carry_forward']),
'total_positive_balance' => $this->sumCarryForwardBalances($preview['carry_forward'], true),
'total_credit_balance' => $this->sumCarryForwardBalances($preview['carry_forward'], false),
]);
}
private function countExistingTargetInvoices(array $batch): int
{
$batchId = (int) ($batch['id'] ?? 0);
if ($batchId <= 0 || ! $this->db->tableExists('invoices')) {
return 0;
}
$items = $this->itemModel
->select('target_invoice_id')
->where('closing_batch_id', $batchId)
->where('target_invoice_id IS NOT NULL', null, false)
->where('target_invoice_id >', 0)
->findAll();
$invoiceIds = array_values(array_unique(array_map(static fn (array $row): int => (int) ($row['target_invoice_id'] ?? 0), $items)));
if ($invoiceIds === []) {
return 0;
}
return $this->db->table('invoices')
->whereIn('id', $invoiceIds)
->countAllResults();
}
private function sumCarryForwardBalances(array $rows, bool $positive): float
{
$total = 0.0;
foreach ($rows as $row) {
$amount = (float) ($row['carry_forward_amount'] ?? 0);
if ($positive && $amount > 0) {
$total += $amount;
} elseif (! $positive && $amount < 0) {
$total += abs($amount);
}
}
return round($total, 2);
}
private function createCarryForwardInvoice(array $item, string $sourceYear, string $targetYear, ?int $userId): int
{
$existingId = (int) ($item['target_invoice_id'] ?? 0);
if ($existingId > 0) {
$existing = $this->db->table('invoices')->where('id', $existingId)->get()->getRowArray();
if ($existing !== null) {
return $existingId;
}
}
$amount = round((float) ($item['carry_forward_amount'] ?? 0), 2);
if (abs($amount) < 0.005) {
return 0;
}
$parentId = (int) ($item['family_id'] ?? 0);
$invoiceNumber = $this->carryForwardInvoiceNumber((int) ($item['id'] ?? 0), $parentId, $sourceYear, $targetYear);
$existing = $this->db->table('invoices')
->select('id')
->where('invoice_number', $invoiceNumber)
->get()
->getRowArray();
if ($existing !== null) {
return (int) $existing['id'];
}
$now = function_exists('utc_now') ? utc_now() : gmdate('Y-m-d H:i:s');
$payload = [
'parent_id' => $parentId,
'invoice_number' => $invoiceNumber,
'total_amount' => $amount,
'paid_amount' => 0,
'balance' => $amount,
'school_year' => $targetYear,
'semester' => 'Opening Balance',
'issue_date' => $now,
'due_date' => null,
'status' => $amount > 0 ? 'Unpaid' : 'Credit',
'created_at' => $now,
'updated_at' => $now,
'updated_by' => $userId,
];
if ($this->db->fieldExists('description', 'invoices')) {
$label = $amount > 0 ? 'Balance carried over' : 'Credit carried over';
$payload['description'] = "{$label} from previous school year {$sourceYear}.";
}
$invoiceModel = new InvoiceModel();
$invoiceId = $invoiceModel->insert($payload, true);
if (! $invoiceId) {
throw new RuntimeException('Unable to create carry-forward invoice: ' . json_encode($invoiceModel->errors()));
}
return (int) $invoiceId;
}
private function carryForwardInvoiceNumber(int $itemId, int $parentId, string $sourceYear, string $targetYear): string
{
$source = preg_replace('/[^0-9A-Za-z]/', '', $sourceYear);
$target = preg_replace('/[^0-9A-Za-z]/', '', $targetYear);
return sprintf('CF-%s-%s-P%d-I%d', $source, $target, $parentId, $itemId);
}
private function promotionPreview(string $schoolYear, ?string $targetSchoolYear): array
{
$summary = [
@@ -764,7 +921,7 @@ final class SchoolYearClosingService
{
return $this->batchModel
->where('source_school_year_id', $sourceYearId)
->whereIn('status', ['started', 'executed'])
->whereIn('status', ['started', 'executed', 'completed'])
->orderBy('id', 'DESC')
->first();
}