Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
SnapshotAccountBalancesCommand
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 3
30
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 configure
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3declare(strict_types=1);
4
5namespace App\Console;
6
7use App\Domain\Notification\Service\CronEmailNotifier;
8use App\Domain\SuperAdmin\Repository\SuperAdminRepository;
9use Psr\Log\LoggerInterface;
10use Symfony\Component\Console\Command\Command;
11use Symfony\Component\Console\Input\InputInterface;
12use Symfony\Component\Console\Input\InputOption;
13use Symfony\Component\Console\Output\OutputInterface;
14use Throwable;
15
16/**
17 * Daily account balance snapshot command.
18 *
19 * Usage:
20 *   php bin/console.php balance:snapshot
21 *   php bin/console.php balance:snapshot --date=2026-04-21
22 */
23final class SnapshotAccountBalancesCommand extends Command
24{
25    private const string JOB_NAME = 'Daily balance snapshot';
26
27    public function __construct(
28        private readonly SuperAdminRepository $repository,
29        private readonly LoggerInterface $logger,
30        private readonly CronEmailNotifier $notifier,
31    ) {
32        parent::__construct();
33    }
34
35    protected function configure(): void
36    {
37        parent::configure();
38
39        $this->setName('balance:snapshot');
40        $this->setDescription('Snapshot end-of-day balance for every account');
41        $this->addOption(
42            'date',
43            'd',
44            InputOption::VALUE_REQUIRED,
45            'The date to snapshot balances for (default: today)',
46            date('Y-m-d'),
47        );
48    }
49
50    protected function execute(InputInterface $input, OutputInterface $output): int
51    {
52        /** @var string $date */
53        $date = $input->getOption('date');
54
55        $output->writeln(sprintf('<info>Snapshotting balances for %s...</info>', $date));
56
57        try {
58            $count = $this->repository->snapshotDailyAccountBalance($date);
59        } catch (Throwable $e) {
60            $this->notifier->notifyJobFailure(self::JOB_NAME, $e);
61
62            throw $e;
63        }
64
65        $message = sprintf('Snapshotted balances for %d account(s) on %s', $count, $date);
66        $output->writeln(sprintf('<info>%s</info>', $message));
67        $this->logger->info($message);
68
69        // Snapshot covers every account regardless of status, so zero is
70        // genuinely anomalous (means there are no accounts at all on this env).
71        if ($count === 0) {
72            $this->notifier->notifyJobAnomaly(self::JOB_NAME, $message);
73        }
74
75        return Command::SUCCESS;
76    }
77}