'Required source school-year record ID.', '--actor-id' => 'Administrator user ID recorded in the repair audit log.', '--commit' => 'Create missing closing items and target-year opening-balance invoices.', '--json' => 'Print machine-readable output.', ]; public function run(array $params) { $sourceYearId = (int) (CLI::getOption('source-year-id') ?? 0); if ($sourceYearId <= 0) { CLI::error('--source-year-id must be a positive integer.'); return; } try { $closing = service('schoolYearClosing'); $batch = $closing->latestBatch($sourceYearId); if ($batch === null) { throw new \RuntimeException('No closing batch exists for the source school year.'); } $preview = $closing->preview($sourceYearId, (int) ($batch['target_school_year_id'] ?? 0)); $db = \Config\Database::connect(); $existingRows = $db->table('school_year_closing_items') ->select('family_id') ->where('closing_batch_id', (int) $batch['id']) ->get() ->getResultArray(); $existing = array_fill_keys(array_map( static fn (array $row): int => (int) ($row['family_id'] ?? 0), $existingRows ), true); $missing = array_values(array_filter( $preview['carry_forward'] ?? [], static fn (array $row): bool => ! isset($existing[(int) ($row['family_id'] ?? 0)]) )); $result = [ 'mode' => CLI::getOption('commit') !== null ? 'commit' : 'dry-run', 'closing_batch_id' => (int) $batch['id'], 'missing_count' => count($missing), 'missing_amount' => round(array_sum(array_map( static fn (array $row): float => (float) ($row['carry_forward_amount'] ?? 0), $missing )), 2), 'missing_items' => array_map(static fn (array $row): array => [ 'family_id' => (int) ($row['family_id'] ?? 0), 'parent' => (string) ($row['parent'] ?? ''), 'carry_forward_amount' => round((float) ($row['carry_forward_amount'] ?? 0), 2), ], $missing), ]; if (CLI::getOption('commit') !== null) { $actorId = (int) (CLI::getOption('actor-id') ?? 0); if ($actorId <= 0) { throw new \InvalidArgumentException('--actor-id must be a positive administrator user ID when using --commit.'); } $result['repair'] = $closing->repairMissingCarryForward($sourceYearId, $actorId); } if (CLI::getOption('json') !== null) { CLI::write(json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); return; } CLI::write(sprintf( '%s: %d missing family balance(s), $%0.2f total.', strtoupper((string) $result['mode']), (int) $result['missing_count'], (float) $result['missing_amount'] )); foreach ($result['missing_items'] as $item) { CLI::write(sprintf( 'Family #%d (%s): $%0.2f', (int) $item['family_id'], (string) $item['parent'], (float) $item['carry_forward_amount'] )); } if (isset($result['repair'])) { CLI::write('Repair completed and audited.', 'green'); } elseif ($missing !== []) { CLI::write('Run again with --actor-id= --commit to apply the repair.', 'yellow'); } } catch (\Throwable $e) { CLI::error($e->getMessage()); } } }