172 lines
5.7 KiB
PHP
172 lines
5.7 KiB
PHP
<?php
|
|
|
|
namespace App\Commands;
|
|
|
|
use App\Libraries\InvoiceLedgerService;
|
|
use App\Models\InvoiceModel;
|
|
use CodeIgniter\CLI\BaseCommand;
|
|
use CodeIgniter\CLI\CLI;
|
|
|
|
class RecalculateInvoices extends BaseCommand
|
|
{
|
|
protected $group = 'Financial';
|
|
protected $name = 'financial:recalculate-invoices';
|
|
protected $description = 'Audits invoice totals against the centralized ledger and optionally writes the corrected values.';
|
|
protected $usage = 'financial:recalculate-invoices [--commit] [--invoice-id=123] [--parent-id=456] [--school-year=2025-2026] [--semester=Fall]';
|
|
protected $options = [
|
|
'--commit' => '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;
|
|
}
|
|
}
|