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