feature/cierres (#25)

Varios cambios

Co-authored-by: Juan Pablo Vial <jpvialb@incoviba.cl>
Reviewed-on: #25
This commit is contained in:
2025-07-22 13:18:00 +00:00
parent ba57cad514
commit 307f2ac7d7
418 changed files with 20045 additions and 984 deletions

View File

@ -0,0 +1,85 @@
<?php
namespace Incoviba\Command;
use DateTimeZone;
use Throwable;
use DateTimeInterface;
use DateTimeImmutable;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console;
use Incoviba\Service\Schedule;
#[Console\Attribute\AsCommand(
name: 'loop',
description: 'Run base loop',
)]
class BaseLoop extends Console\Command\Command
{
public function __construct(protected LoggerInterface $logger, protected Schedule $scheduleService,
protected DateTimeZone $timezone,
protected int $timeout = 5 * 60,
?string $name = null)
{
parent::__construct($name);
}
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
$this->write($output, 'Running loop...');
$this->waitNextTimeout($output);
$this->logger->info("Starting loop...");
$this->write($output, 'Starting loop...');
while (true) {
$commands = $this->scheduleService->getPending();
$start = new DateTimeImmutable('now', $this->timezone);
foreach ($commands as $command) {
$this->runCommand($input, $output, $command);
}
unset($commands);
memory_reset_peak_usage();
$this->waitNextTimeout($output, $start);
}
return self::SUCCESS;
}
protected function waitNextTimeout(Console\Output\OutputInterface $output, ?DateTimeInterface $start = null): void
{
// wait for next minute
if ($start === null) {
$start = new DateTimeImmutable('now', $this->timezone);
}
$nextTimeout = new DateTimeImmutable($start->format('Y-m-d H:i:00'), $this->timezone);
$nextTimeout = $nextTimeout->add(new \DateInterval("PT{$this->timeout}S"));
$diff = $nextTimeout->getTimestamp() - $start->getTimestamp();
if ($diff > 0) {
$this->logger->debug("Waiting {$diff} seconds...");
sleep($diff);
}
}
protected function runCommand(Console\Input\InputInterface $input, Console\Output\OutputInterface $output, string $commandName): int
{
try {
$command = $this->getApplication()->find($commandName);
} catch (Console\Exception\CommandNotFoundException $exception) {
$this->logger->warning($exception);
return self::FAILURE;
}
$cmd = new Console\Input\ArrayInput([
'command' => $commandName
]);
try {
$this->logger->notice("Running commnand: {$commandName}");
return $this->getApplication()->doRun($cmd, $output);
} catch (Throwable $exception) {
$this->logger->warning($exception);
return self::FAILURE;
}
}
protected function write(Console\Output\OutputInterface $output, string $message): void
{
$now = new DateTimeImmutable('now', $this->timezone);
$output->writeln("[{$now->format('Y-m-d H:i:s e')}] {$message}");
}
}

View File

@ -10,8 +10,8 @@ use Incoviba\Common\Alias\Command;
)]
class Comunas extends Command
{
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output)
{
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
$this->logger->debug("Running {$this->getName()}");
$uri = '/api/direcciones/region/13/comunas';
$output->writeln("GET {$uri}");

View File

@ -10,7 +10,7 @@ use Incoviba\Common\Alias\Command;
)]
class Update extends Command
{
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output)
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
$this->logger->debug("Running {$this->getName()}");
$uri = '/api/contabilidad/cartolas/update';

View File

@ -0,0 +1,34 @@
<?php
namespace Incoviba\Command;
use Symfony\Component\Console;
use Incoviba\Common\Alias;
#[Console\Attribute\AsCommand(
name: 'external:services',
description: 'Check external services',
)]
class ExternalServices extends Alias\Command
{
protected function configure(): void
{
$this->addOption('update', 'u', Console\Input\InputOption::VALUE_NONE, 'Update');
}
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
$update = $input->getOption('update');
$url = '/api/external/services/check';
if ($update) {
$url = '/api/external/services/update';
}
$output->writeln("GET {$url}");
$response = $this->client->get($url);
$output->writeln("Response Code: {$response->getStatusCode()}");
return Console\Command\Command::SUCCESS;
}
}

View File

@ -12,12 +12,12 @@ use Incoviba\Common\Alias\Command;
)]
class Full extends Command
{
public function __construct(ClientInterface $client, LoggerInterface $logger, protected array $commandsList, string $name = null)
public function __construct(ClientInterface $client, LoggerInterface $logger, protected array $commandsList, ?string $name = null)
{
parent::__construct($client, $logger, $name);
}
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output)
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
foreach ($this->commandsList as $command) {
$cmd = new Console\Input\ArrayInput([

View File

@ -0,0 +1,67 @@
<?php
namespace Incoviba\Command\Job;
use DateMalformedStringException;
use DateTimeImmutable;
use DateTimeZone;
use Incoviba\Exception\Client\FastCGI as FastCGIException;
use Incoviba\Service;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console;
#[Console\Attribute\AsCommand(name: 'jobs:run', description: 'Run job')]
class Run extends Console\Command\Command
{
public function __construct(protected Service\FastCGI $fastcgi, protected LoggerInterface $logger,
protected Service\Job $jobService,
protected DateTimeZone $timeZone, ?string $name = null)
{
parent::__construct($name);
}
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
try {
$now = new DateTimeImmutable('now', $this->timeZone);
} catch (DateMalformedStringException) {
$now = new DateTimeImmutable();
}
if ($this->jobService->getPending() === 0) {
$output->writeln("[{$now->format('Y-m-d H:i:s e')}] No pending jobs to run.");
return self::SUCCESS;
}
$output->writeln("[{$now->format('Y-m-d H:i:s e')}] Running Ready Job...");
$this->runJob();
return $this->getResponses();
}
protected function runJob(): bool
{
$uri = "/api/queue/run";
try {
$this->fastcgi->get($uri);
return true;
} catch (FastCGIException $exception) {
$this->logger->error($exception->getMessage(), ['uri' => $uri, 'exception' =>$exception]);
return false;
}
}
protected function getResponses(): int
{
$result = self::SUCCESS;
$responses = $this->fastcgi->awaitResponses();
foreach ($responses as $response) {
if ($response->getError() !== '') {
$this->logger->error("Error running job", [
'error' => $response->getError(),
'body' => $response->getBody(),
'headers' => $response->getHeaders(),
]);
$result = self::FAILURE;
}
}
return $result;
}
}

View File

@ -3,6 +3,7 @@ namespace Incoviba\Command\Money;
use DateTimeImmutable;
use DateInterval;
use DateTimeZone;
use Symfony\Component\Console;
use Incoviba\Common\Alias\Command;
@ -12,13 +13,15 @@ use Incoviba\Common\Alias\Command;
)]
class IPC extends Command
{
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output)
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
$this->logger->debug("Running {$this->getName()}");
$lastMonth = (new DateTimeImmutable())->sub(new DateInterval('P1M'));
$endLastYear = (new DateTimeImmutable())->sub(new DateInterval('P1Y'));
$timezone = new DateTimeZone($_ENV['TZ'] ?? 'America/Santiago');
$now = new DateTimeImmutable('now', $timezone);
$lastMonth = $now->sub(new DateInterval('P1M'));
$endLastYear = $now->sub(new DateInterval('P1Y'));
$uri = '/api/money/ipc';
$current = new DateTimeImmutable($lastMonth->format('Y-m-d'));
$current = new DateTimeImmutable($lastMonth->format('Y-m-d'), $timezone);
while ($current->diff($endLastYear)->days > 30) {
$data = [
'start' => $endLastYear->format('Y-12-1'),

View File

@ -2,6 +2,7 @@
namespace Incoviba\Command\Money;
use DateTimeImmutable;
use DateTimeZone;
use Symfony\Component\Console;
use Incoviba\Common\Alias\Command;
@ -11,10 +12,10 @@ use Incoviba\Common\Alias\Command;
)]
class UF extends Command
{
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output)
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
$this->logger->debug("Running {$this->getName()}");
$now = new DateTimeImmutable();
$now = new DateTimeImmutable('now', new DateTimeZone($_ENV['TZ'] ?? 'America/Santiago'));
$uri = '/api/money/uf';
$data = [
'fecha' => $now->format('Y-m-d')

View File

@ -11,7 +11,7 @@ use Incoviba\Common\Alias\Command;
)]
class Update extends Command
{
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output)
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
$this->logger->debug("Running {$this->getName()}");
$url = '/api/money/ufs';

View File

@ -10,7 +10,7 @@ use Incoviba\Common\Alias\Command;
)]
class Activos extends Command
{
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output)
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
$this->logger->debug("Running {$this->getName()}");
$uri = '/api/proyectos';

81
cli/src/Command/Queue.php Normal file
View File

@ -0,0 +1,81 @@
<?php
namespace Incoviba\Command;
use DateTimeImmutable;
use DateTimeZone;
use Psr\Http\Client\ClientInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console;
use Incoviba\Service\Job;
use Incoviba\Common\Alias\Command;
use Throwable;
#[Console\Attribute\AsCommand(
name: 'queue',
description: 'Run queue'
)]
class Queue extends Command
{
public function __construct(ClientInterface $client, LoggerInterface $logger,
protected Job $jobService,
protected DateTimeZone $timezone,
protected string $baseCommand = '/code/bin/incoviba',
protected int $batchSize = 10,
?string $name = null)
{
parent::__construct($client, $logger, $name);
}
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
$this->logger->debug("Running {$this->getName()}");
$this->sections = [
'top' => $output->section(),
'bottom' => $output->section(),
];
$io = new Console\Style\SymfonyStyle($input, $this->sections['top']);
$now = new DateTimeImmutable('now', $this->timezone);
if ($this->jobService->getPending() === 0) {
$io->success("[{$now->format('Y-m-d H:i:s e')}] Queue is empty");
return self::SUCCESS;
}
$io->title("[{$now->format('Y-m-d H:i:s e')}] Running Queue...");
$results = [];
for ($i = 0; $i < $this->batchSize; $i++) {
if ($this->jobService->getPending() === 0) {
break;
}
$results []= $this->runJob();
}
return count(array_filter($results, fn ($result) => $result === self::FAILURE)) === 0 ? self::SUCCESS : self::FAILURE;
}
protected array $sections;
protected array $outputs = [];
protected function runJob(): int
{
$baseCommand = "{$this->baseCommand} jobs:run";
$command = "{$baseCommand}";
try {
exec($command, $output, $resultCode);
$this->outputs []= $output;
} catch (Throwable $exception) {
$this->logger->error("Failed to run command", [
'command' => $command,
'exception' => $exception
]);
return self::FAILURE;
}
if ($resultCode !== 0) {
$this->logger->error("Failed to run command", [
'command' => $command,
'result_code' => $resultCode
]);
return self::FAILURE;
} else {
return self::SUCCESS;
}
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Incoviba\Command\Queue;
use Incoviba\Service;
use Symfony\Component\Console;
#[Console\Attribute\AsCommand(name: 'queue:pending', description: 'List pending jobs in queue')]
class Pending extends Console\Command\Command
{
public function __construct(protected Service\Job $jobService, ?string $name = null)
{
parent::__construct($name);
}
protected function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
$jobCount = $this->jobService->getPending();
$output->writeln("Found {$jobCount} pending jobs");
return self::SUCCESS;
}
}

View File

@ -0,0 +1,121 @@
<?php
namespace Incoviba\Command\Queue;
use Throwable;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console;
use Incoviba\Service;
#[Console\Attribute\AsCommand(name: 'queue:push', description: 'Push a job to the queue')]
class Push extends Console\Command\Command
{
public function __construct(protected LoggerInterface $logger, protected Service\Job $jobService, ?string $name = null)
{
parent::__construct($name);
}
protected function configure(): void
{
$this->addOption('configurations', 'c', Console\Input\InputOption::VALUE_REQUIRED | Console\Input\InputOption::VALUE_IS_ARRAY, 'Job configuration options array, each job configuration must be in valid JSON format');
$this->addOption('files', 'f', Console\Input\InputOption::VALUE_REQUIRED | Console\Input\InputOption::VALUE_IS_ARRAY, 'Paths to jobs configurations files with JSON array content');
}
protected function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
$io = new Console\Style\SymfonyStyle($input, $output);
$io->title("Pushing job");
$configurations = $this->getConfigurations($input);
if (count($configurations) === 0) {
$io->error('Missing configurations');
return self::FAILURE;
}
$result = self::SUCCESS;
foreach ($configurations as $configuration) {
if (!json_validate($configuration)) {
$io->error("Invalid JSON: {$configuration}");
continue;
}
$configuration = json_decode($configuration, true);
try {
$job = $this->jobService->push($configuration);
$io->success("Job pushed with ID {$job['id']}");
} catch (Throwable $exception) {
$io->error($exception->getMessage());
$result = self::FAILURE;
}
}
return $result;
}
protected function getConfigurations(Console\Input\InputInterface $input): array
{
return [
...$this->getFilesConfigurations($input),
...$this->getOptionConfigurations($input),
];
}
protected function getFilesConfigurations(Console\Input\InputInterface $input): array
{
$configurations = [];
$files = $input->getOption('files');
if ($files === null) {
return $configurations;
}
foreach ($files as $filePath) {
if (!file_exists($filePath)) {
continue;
}
$configurations = array_merge($configurations, $this->getFileConfigurations($filePath));
}
return $configurations;
}
protected function getFileConfigurations(string $filePath): array
{
$configurations = [];
if (!file_exists($filePath)) {
return $configurations;
}
$json = file_get_contents($filePath);
if (!json_validate($json)) {
return $configurations;
}
$tmp = json_decode($json, true);
foreach ($tmp as $config) {
try {
$configurations []= $this->processConfiguration(json_encode($config));
} catch (Throwable $exception) {
$this->logger->warning($exception->getMessage(), ['exception' => $exception, 'config' => $config]);
}
}
return $configurations;
}
protected function getOptionConfigurations(Console\Input\InputInterface $input): array
{
$configurations = [];
$configOptions = $input->getOption('configurations');
if ($configOptions === null) {
return $configurations;
}
foreach ($configOptions as $config) {
try {
$configurations []= $this->processConfiguration($config);
} catch (Throwable $exception) {
$this->logger->warning($exception->getMessage(), ['exception' => $exception, 'config' => $config]);
}
}
return $configurations;
}
protected function processConfiguration(string $configuration): string
{
$json = json_decode($configuration, true);
if (!array_key_exists('type', $json) and !array_key_exists('configuration', $json)) {
throw new Console\Exception\InvalidArgumentException('Missing type or configuration key in JSON');
}
if (array_key_exists('type', $json)) {
return json_encode($json);
}
return json_encode($json['configuration']);
}
}

View File

@ -10,7 +10,7 @@ use Incoviba\Common\Alias\Command;
)]
class Vigentes extends Command
{
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output)
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
$this->logger->debug("Running {$this->getName()}");
$uri = '/api/ventas/cierres/vigentes';

View File

@ -10,7 +10,7 @@ use Incoviba\Common\Alias\Command;
)]
class Hoy extends Command
{
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output)
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
$this->logger->debug("Running {$this->getName()}");
$uri = '/api/ventas/cuotas/hoy';

View File

@ -10,7 +10,7 @@ use Incoviba\Common\Alias\Command;
)]
class Pendientes extends Command
{
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output)
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
$this->logger->debug("Running {$this->getName()}");
$uri = '/api/ventas/cuotas/pendiente';

View File

@ -10,7 +10,7 @@ use Incoviba\Common\Alias\Command;
)]
class PorVencer extends Command
{
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output)
public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
$this->logger->debug("Running {$this->getName()}");
$uri = '/api/ventas/cuotas/vencer';

View File

@ -0,0 +1,26 @@
<?php
namespace Incoviba\Command\Ventas\MedioPagos\Toku;
use Symfony\Component\Console;
use Incoviba\Common\Alias\Command;
#[Console\Attribute\AsCommand(name: 'external:toku:enqueue')]
class Enqueue extends Command
{
protected function configure(): void
{
$this->addArgument('venta_ids', Console\Input\InputArgument::REQUIRED, 'Venta IDs separated by |');
}
protected function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
$this->logger->debug("Running {$this->getName()}");
$uri = '/api/external/toku/enqueue';
$output->writeln("POST {$uri}");
$body = ['venta_ids' => explode('|', $input->getArgument('venta_ids'))];
$output->writeln(json_encode($body));
$response = $this->client->post($uri, ['json' => $body, 'headers' => ['Content-Type' => 'application/json']]);
$output->writeln("Response Code: {$response->getStatusCode()}");
$output->writeln($response->getBody()->getContents());
return Console\Command\Command::SUCCESS;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Incoviba\Command\Ventas\MedioPagos\Toku;
use Symfony\Component\Console;
use Incoviba\Common\Alias\Command;
#[Console\Attribute\AsCommand(name: 'external:toku:reset')]
class Reset extends Command
{
protected function configure(): void
{
$this->addOption('venta_ids', 'vid', Console\Input\InputOption::VALUE_REQUIRED, 'Venta IDs separated by |', '');
}
protected function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
$this->logger->debug("Running {$this->getName()}");
$uri = '/api/external/toku/reset';
$output->writeln("DELETE {$uri}");
$venta_ids = $input->getOption('venta_ids');
$body = [];
if (!empty($venta_ids)) {
$body = ['skips' => ['subscription' => explode('|', $venta_ids)]];
}
$options = count($body) > 0 ? ['json' => $body] : [];
$response = $this->client->delete($uri, $options);
$output->writeln("Response Code: {$response->getStatusCode()}");
$this->logger->debug("Response: {$response->getBody()->getContents()}");
return self::SUCCESS;
}
}