'Persist recalculated totals. Without this flag the command runs in dry-run mode.', '--invoice-id' => 'Only process a single invoice ID.', '--parent-id' => 'Only process invoices for one parent.', '--school-year' => 'Only process invoices for the specified school year.', '--semester' => 'Only process invoices for the specified semester.', ]; public function run(array $params) { $options = $this->parseOptions($params); $invoiceModel = new InvoiceModel(); $ledgerService = new InvoiceLedgerService(); $invoiceIds = $this->loadInvoiceIds($invoiceModel, $options); if ($invoiceIds === []) { CLI::write('No invoices matched the provided filters.', 'yellow'); return; } $commit = !empty($options['commit']); CLI::write(($commit ? 'Applying' : 'Dry-run auditing') . ' ' . count($invoiceIds) . ' invoice(s)...', 'yellow'); $changed = 0; $unchanged = 0; $errors = 0; foreach ($invoiceIds as $invoiceId) { $invoice = $invoiceModel->find($invoiceId); if (!$invoice) { continue; } try { $calculation = $ledgerService->calculateInvoice($invoiceId); $diffs = $this->diffInvoice($invoice, $calculation); if ($diffs === []) { $unchanged++; CLI::write('Invoice #' . $invoiceId . ' unchanged.', 'green'); continue; } $changed++; CLI::write('Invoice #' . $invoiceId . ' requires recalculation:', 'light_yellow'); foreach ($diffs as $label => $values) { CLI::write(sprintf( ' %s: %s -> %s', $label, (string) $values['from'], (string) $values['to'] )); } if ($commit) { $ledgerService->recalculateInvoice($invoiceId); CLI::write(' saved', 'blue'); } } catch (\Throwable $e) { $errors++; CLI::error('Invoice #' . $invoiceId . ' failed: ' . $e->getMessage()); } } CLI::newLine(); CLI::write('Processed: ' . count($invoiceIds), 'white'); CLI::write('Changed: ' . $changed, $changed > 0 ? 'yellow' : 'white'); CLI::write('Unchanged: ' . $unchanged, 'green'); CLI::write('Errors: ' . $errors, $errors > 0 ? 'red' : 'white'); if (!$commit) { CLI::write('Dry-run complete. Re-run with --commit to persist corrections.', 'light_blue'); } } protected function parseOptions(array $params): array { $options = [ 'commit' => false, 'invoice-id' => null, 'parent-id' => null, 'school-year' => null, 'semester' => null, ]; foreach ($params as $param) { $value = trim((string) $param); if ($value === '--commit') { $options['commit'] = true; continue; } if (!str_starts_with($value, '--') || !str_contains($value, '=')) { continue; } [$key, $raw] = explode('=', substr($value, 2), 2); if (array_key_exists($key, $options)) { $options[$key] = $raw; } } return $options; } protected function loadInvoiceIds(InvoiceModel $invoiceModel, array $options): array { $builder = $invoiceModel->select('id')->orderBy('id', 'ASC'); if (!empty($options['invoice-id'])) { $builder->where('id', (int) $options['invoice-id']); } if (!empty($options['parent-id'])) { $builder->where('parent_id', (int) $options['parent-id']); } if (!empty($options['school-year'])) { $builder->where('school_year', (string) $options['school-year']); } if (!empty($options['semester'])) { $builder->where('semester', (string) $options['semester']); } return array_map( static fn (array $row): int => (int) ($row['id'] ?? 0), $builder->findAll() ); } protected function diffInvoice(array $invoice, array $calculation): array { $fields = [ 'total_amount' => 'total_amount', 'paid_amount' => 'paid_amount', 'balance' => 'balance', 'status' => 'status', 'has_discount' => 'has_discount', ]; $diffs = []; foreach ($fields as $invoiceKey => $calcKey) { $current = (string) ($invoice[$invoiceKey] ?? ''); $recalculated = (string) ($calculation[$calcKey] ?? ''); if ($current !== $recalculated) { $diffs[$invoiceKey] = [ 'from' => $current, 'to' => $recalculated, ]; } } return $diffs; } }