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
AccrueInterestCommand
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 interest accrual command.
18 *
19 * Usage:
20 *   php bin/console.php interest:accrue
21 *   php bin/console.php interest:accrue --date=2026-04-10
22 */
23final class AccrueInterestCommand extends Command
24{
25    private const string JOB_NAME = 'Daily interest accrual';
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('interest:accrue');
40        $this->setDescription('Accrue daily interest for all active accounts');
41        $this->addOption(
42            'date',
43            'd',
44            InputOption::VALUE_REQUIRED,
45            'The date to accrue interest 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>Accruing interest for %s...</info>', $date));
56
57        try {
58            $count = $this->repository->accrueDailyInterest($date);
59        } catch (Throwable $e) {
60            $this->notifier->notifyJobFailure(self::JOB_NAME, $e);
61
62            throw $e;
63        }
64
65        $message = sprintf('Accrued interest for %d account(s) on %s', $count, $date);
66        $output->writeln(sprintf('<info>%s</info>', $message));
67        $this->logger->info($message);
68
69        // Daily run is silent on a normal success — only email when zero
70        // accounts accrued, which usually means something upstream is wrong
71        // (no active accounts with positive balance, missing rate config).
72        if ($count === 0) {
73            $this->notifier->notifyJobAnomaly(self::JOB_NAME, $message);
74        }
75
76        return Command::SUCCESS;
77    }
78}