From fcc84ac09ca5bc099086e12ba3b92b8a60e5a21b Mon Sep 17 00:00:00 2001 From: Aldarien Date: Fri, 25 Mar 2022 10:10:43 -0300 Subject: [PATCH 1/5] Console application --- console/Dockerfile | 12 ++++++++ console/bin/console | 7 +++++ console/common/Define/Application.php | 21 +++++++++++++ console/composer.json | 25 ++++++++++++++++ console/php.ini | 4 +++ console/setup/app.php | 39 ++++++++++++++++++++++++ console/setup/composer.php | 6 ++++ console/setup/settings/02_api.php | 5 ++++ console/setup/setups/02_client.php | 13 ++++++++ docker-compose.yml | 43 +++++++++++++++++++-------- 10 files changed, 163 insertions(+), 12 deletions(-) create mode 100644 console/Dockerfile create mode 100644 console/bin/console create mode 100644 console/common/Define/Application.php create mode 100644 console/composer.json create mode 100644 console/php.ini create mode 100644 console/setup/app.php create mode 100644 console/setup/composer.php create mode 100644 console/setup/settings/02_api.php create mode 100644 console/setup/setups/02_client.php diff --git a/console/Dockerfile b/console/Dockerfile new file mode 100644 index 0000000..181486b --- /dev/null +++ b/console/Dockerfile @@ -0,0 +1,12 @@ +FROM php:8-cli + +RUN apt-get update -y && apt-get install -y cron git libzip-dev zip + +RUN docker-php-ext-install zip + +COPY --from=composer /usr/bin/composer /usr/bin/composer + +WORKDIR /app + +CMD ["cron", "-f", "-l", "2"] +ENTRYPOINT ["cron", "-f", "-l", "2"] diff --git a/console/bin/console b/console/bin/console new file mode 100644 index 0000000..0606ece --- /dev/null +++ b/console/bin/console @@ -0,0 +1,7 @@ +run(); diff --git a/console/common/Define/Application.php b/console/common/Define/Application.php new file mode 100644 index 0000000..9f69131 --- /dev/null +++ b/console/common/Define/Application.php @@ -0,0 +1,21 @@ +setContainer($container); + } + protected $container; + public function setContainer(Container $container) { + $this->container = $container; + return $this; + } + public function getContainer() { + return $this->container; + } +} diff --git a/console/composer.json b/console/composer.json new file mode 100644 index 0000000..928333f --- /dev/null +++ b/console/composer.json @@ -0,0 +1,25 @@ +{ + "name": "provm/contabilidad-console", + "type": "project", + "require": { + "symfony/console": "^6.0", + "php-di/php-di": "^6.3", + "nesbot/carbon": "^2.57", + "guzzlehttp/guzzle": "^7.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "kint-php/kint": "^4.1" + }, + "authors": [ + { + "name": "Aldarien", + "email": "aldarien85@gmail.com" + } + ], + "autoload": { + "psr-4": { + "Contabilidad\\Common\\": "common/" + } + } +} diff --git a/console/php.ini b/console/php.ini new file mode 100644 index 0000000..2f3a539 --- /dev/null +++ b/console/php.ini @@ -0,0 +1,4 @@ +[PHP] +display_errors = E_ALL +log_errors = true +error_log = /var/log/php/error.log diff --git a/console/setup/app.php b/console/setup/app.php new file mode 100644 index 0000000..e64445f --- /dev/null +++ b/console/setup/app.php @@ -0,0 +1,39 @@ +isDir()) { + continue; + } + $builder->addDefinitions($file->getRealPath()); + } +} +$container = $builder->build(); +$app = new Application($container); + +$folder = implode(DIRECTORY_SEPARATOR, [__DIR__, 'commands']); +if (file_exists($folder)) { + $files = new DirectoryIterator($folder); + foreach ($files as $file) { + if ($file->isDir() or $file->getExtension() != 'php') { + continue; + } + include_once $file->getRealPath(); + } +} + +return $app; diff --git a/console/setup/composer.php b/console/setup/composer.php new file mode 100644 index 0000000..c8fb135 --- /dev/null +++ b/console/setup/composer.php @@ -0,0 +1,6 @@ + $_ENV['API_URL'], + 'api_key' => $_ENV['API_KEY'] +]; diff --git a/console/setup/setups/02_client.php b/console/setup/setups/02_client.php new file mode 100644 index 0000000..f924a65 --- /dev/null +++ b/console/setup/setups/02_client.php @@ -0,0 +1,13 @@ + function(Container $container) { + return new \GuzzleHttp\Client([ + 'base_uri' => $container->get('api_url'), + 'headers' => [ + 'Authorization' => "Bearer {$container->get('api_key')}" + ] + ]); + } +]; diff --git a/docker-compose.yml b/docker-compose.yml index e59f2b4..7c33a42 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,18 @@ version: '3' +x-restart: &restart + restart: unless-stopped + services: api: profiles: - api - restart: unless-stopped + <<: *restart image: php build: context: api - env_file: - - .env + env_file: + - .db.env - .api.env - .python.env volumes: @@ -19,26 +22,26 @@ services: api-proxy: profiles: - api - restart: unless-stopped + <<: *restart image: nginx ports: - "9001:80" volumes: - ./api/nginx.conf:/etc/nginx/conf.d/default.conf - - ./logs/api/:/var/log/nginx/ + - ./logs/api/proxy/:/var/log/nginx/ - ./api/:/app/ db: profiles: - api - restart: unless-stopped + <<: *restart image: mariadb - env_file: .env + env_file: .db.env volumes: - contabilidad_data:/var/lib/mysql adminer: profiles: - api - restart: unless-stopped + <<: *restart image: adminer ports: - "9002:8080" @@ -46,7 +49,7 @@ services: ui: profiles: - ui - restart: unless-stopped + <<: *restart image: php-ui env_file: - .api.env @@ -60,19 +63,19 @@ services: ui-proxy: profiles: - ui - restart: unless-stopped + <<: *restart image: nginx ports: - "9000:80" volumes: - ./ui/nginx.conf:/etc/nginx/conf.d/default.conf - - ./logs/ui/:/var/log/nginx/ + - ./logs/ui/proxy/:/var/log/nginx/ - ./ui/:/app/ python: profiles: - python - restart: unless-stopped + <<: *restart build: context: ./python env_file: @@ -85,5 +88,21 @@ services: - ./api/public/uploads/pdfs/:/app/data/ - ./logs/python/:/var/log/python/ + console: + profiles: + - console + <<: *restart + build: + context: ./console + env_file: + - .api.env + - .console.env + - .db.env + volumes: + - ./console/:/app/ + - ./console/php.ini:/usr/local/etc/php/conf.d/php.ini + - ./logs/console/:/var/log/php/ + - ./console/crontab:/var/spool/cron/crontabs/root + volumes: contabilidad_data: From e3737aba27b7b5caf3133fec371d997e1908ff0c Mon Sep 17 00:00:00 2001 From: Aldarien Date: Fri, 25 Mar 2022 10:11:02 -0300 Subject: [PATCH 2/5] Consolidar --- api/common/Controller/Consolidados.php | 53 ++++++++ api/common/Middleware/Consolidar.php | 20 +++ api/common/Service/Consolidar.php | 148 +++++++++++++++++++++++ api/resources/routes/consolidar.php | 4 + api/setup/setups/03_consolidar.php | 8 ++ api/src/Consolidado.php | 51 ++++++++ api/src/Cuenta.php | 61 ++++++---- api/src/Moneda.php | 13 +- api/src/Transaccion.php | 16 +++ console/common/Command/Consolidar.php | 37 ++++++ console/crontab | 24 ++++ console/setup/commands/consolidar.php | 4 + ui/public/assets/scripts/cuentas.show.js | 2 +- 13 files changed, 411 insertions(+), 30 deletions(-) create mode 100644 api/common/Controller/Consolidados.php create mode 100644 api/common/Middleware/Consolidar.php create mode 100644 api/common/Service/Consolidar.php create mode 100644 api/resources/routes/consolidar.php create mode 100644 api/setup/setups/03_consolidar.php create mode 100644 api/src/Consolidado.php create mode 100644 console/common/Command/Consolidar.php create mode 100644 console/crontab create mode 100644 console/setup/commands/consolidar.php diff --git a/api/common/Controller/Consolidados.php b/api/common/Controller/Consolidados.php new file mode 100644 index 0000000..dfff5c8 --- /dev/null +++ b/api/common/Controller/Consolidados.php @@ -0,0 +1,53 @@ +find(Consolidados::class)->where([['cuenta_id' => $cuenta_id]])->many(); + $output = [ + 'consolidados' => array_map(function($item) { + return $item->toArray(); + }, $consolidados) + ]; + return $this->withJson($response, $output); + } + public function add(Request $request, Response $response, Factory $factory): Response { + $input = json_decode($request->getBody()->getContents()); + $output = [ + 'input' => $input, + 'consolidados' => [] + ]; + if (!is_array($input)) { + $input = [$input]; + } + foreach ($input as $data) { + $consolidado = Consolidado::add($factory, $data); + $status = $consolidado->save(); + $output['consolidados'] []= [ + 'consolidado' => $consolidado->toArray(), + 'added' => $status + ]; + } + return $this->withJson($response, $output); + } + public function cli(Request $request, Response $response, Service $service): Response { + try { + if (!$service->isConsolidado()) { + $service->consolidar(); + } + } catch (\Error | \Exception $e) { + error_log($e); + throw $e; + } + return $this->withJson($response); + } +} diff --git a/api/common/Middleware/Consolidar.php b/api/common/Middleware/Consolidar.php new file mode 100644 index 0000000..d3e82b3 --- /dev/null +++ b/api/common/Middleware/Consolidar.php @@ -0,0 +1,20 @@ +service = $service; + } + public function __invoke(Request $request, Handler $handler): Response { + if (!$this->service->isConsolidado()) { + $this->service->consolidar(); + } + return $handler->handle($request); + } +} diff --git a/api/common/Service/Consolidar.php b/api/common/Service/Consolidar.php new file mode 100644 index 0000000..5be69f1 --- /dev/null +++ b/api/common/Service/Consolidar.php @@ -0,0 +1,148 @@ +factory = $factory; + } + protected $cuentas; + public function getCuentas() { + if ($this->cuentas === null) { + $this->cuentas = $this->factory->find(Cuenta::class)->many(); + } + return $this->cuentas; + } + + public function isConsolidado() { + $consolidado = true; + $cuentas = $this->getCuentas(); + if ($cuentas === null) { + return false; + } + foreach ($cuentas as $cuenta) { + $transacciones = $cuenta->hasTransacciones(); + if (!$transacciones) { + continue; + } + $consolidados = $cuenta->hasConsolidados(); + if (!$consolidados and $transacciones) { + $consolidado = false; + break; + } + } + return $consolidado; + } + public function consolidar() { + ini_set('max_execution_time', 60*5); + + foreach ($this->getCuentas() as $cuenta) { + if (!$cuenta->hasTransacciones()) { + continue; + } + $first = $this->getFirst($cuenta); + $last = $this->getLast($cuenta); + for ($current = $first->copy()->startOfMonth(); $current < $last->copy()->addMonthWithoutOverflow()->endOfMonth(); $current = $current->copy()->addMonthWithoutOverflow()) { + $transacciones = $this->getTransacciones($cuenta, $current); + if (count($transacciones) == 0) { + continue; + } + $f = $this->factory; + array_walk($transacciones, function(&$item) use ($cuenta, $f) { + $item->setFactory($f); + $item->valor = $item->transformar($cuenta->moneda()); + }); + $saldo = array_reduce($transacciones, function($sum, $item) { + return $sum + $item->valor; + }); + $data = [ + 'cuenta_id' => $cuenta->id, + 'fecha' => $current->format('Y-m-1'), + 'periodo' => 'P1M', + 'saldo' => $saldo + ]; + $consolidado = $this->factory->create(Consolidado::class, $data); + $consolidado->save(); + } + } + } + public function getFirst(Cuenta $cuenta): ?Carbon { + $first = [ + Model::factory(Transaccion::class) + ->select('fecha') + ->whereEqual('debito_id', $cuenta->id) + ->orderByAsc('fecha') + ->findOne(), + Model::factory(Transaccion::class) + ->select('fecha') + ->whereEqual('credito_id', $cuenta->id) + ->orderByAsc('fecha') + ->findOne() + ]; + if (!$first[0]) { + if (!$first[1]) { + return null; + } + return $first[1]->fecha(); + } + if (!$first[1]) { + return $first[0]->fecha(); + } + if ($first[0]->fecha() < $first[1]->fecha()) { + return $first[0]->fecha(); + } + return $first[1]->fecha(); + } + public function getLast(Cuenta $cuenta): ?Carbon { + $fechas = [ + Model::factory(Transaccion::class) + ->select('fecha') + ->whereEqual('debito_id', $cuenta->id) + ->orderByDesc('fecha') + ->findOne(), + Model::factory(Transaccion::class) + ->select('fecha') + ->whereEqual('credito_id', $cuenta->id) + ->orderByDesc('fecha') + ->findOne() + ]; + if (!$fechas[0]) { + if (!$fechas[1]) { + return null; + } + return $fechas[1]->fecha(); + } + if (!$fechas[1]) { + return $fechas[0]->fecha(); + } + if ($fechas[0]->fecha() > $fechas[1]->fecha()) { + return $fechas[0]->fecha(); + } + return $fechas[1]->fecha(); + } + public function getTransacciones(Cuenta $cuenta, Carbon $fecha) { + $start = $fecha->copy()->startOfMonth(); + $end = $fecha->copy()->endOfMonth(); + $debitos = Model::factory(Transaccion::class) + ->whereEqual('debito_id', $cuenta->id) + ->whereRaw("fecha BETWEEN '{$start->format('Y-m-d')}' AND '{$end->format('Y-m-d')}'") + ->findMany(); + if ($debitos) { + array_walk($debitos, function(&$item) { + $item->valor *= -1; + }); + } + $creditos = Model::factory(Transaccion::class) + ->whereEqual('credito_id', $cuenta->id) + ->whereRaw("fecha BETWEEN '{$start->format('Y-m-d')}' AND '{$end->format('Y-m-d')}'") + ->findMany(); + return array_merge($debitos, $creditos); + } +} diff --git a/api/resources/routes/consolidar.php b/api/resources/routes/consolidar.php new file mode 100644 index 0000000..e49c0ba --- /dev/null +++ b/api/resources/routes/consolidar.php @@ -0,0 +1,4 @@ +get('/consolidar', [Consolidados::class, 'cli']); diff --git a/api/setup/setups/03_consolidar.php b/api/setup/setups/03_consolidar.php new file mode 100644 index 0000000..b7c7bb8 --- /dev/null +++ b/api/setup/setups/03_consolidar.php @@ -0,0 +1,8 @@ + function(Container $container) { + return new \Contabilidad\Common\Service\Consolidar($container->get(\ProVM\Common\Factory\Model::class)); + } +]; diff --git a/api/src/Consolidado.php b/api/src/Consolidado.php new file mode 100644 index 0000000..2c20a72 --- /dev/null +++ b/api/src/Consolidado.php @@ -0,0 +1,51 @@ +cuenta === null) { + $this->cuenta = $this->childOf(Cuenta::class, [Model::SELF_KEY => 'cuenta_id']); + } + return $this->cuenta; + } + public function fecha(DateTimeInterface $fecha = null) { + if ($fecha === null) { + return Carbon::parse($this->fecha); + } + if ($this->periodo()->days > 31) { + $this->fecha = $fecha->format('Y-1-1'); + } else { + $this->fecha = $fecha->format('Y-m-1'); + } + return $this; + } + public function periodo(DateInterval $periodo = null) { + if ($periodo === null) { + return new CarbonInterval($this->periodo); + } + $this->periodo = CarbonInterval::getDateIntervalSpec($periodo); + return $this; + } + public function saldo(DateTimeInterface $fecha = null) { + if ($fecha === null) { + $fecha = $this->fecha(); + } + return $this->cuenta()->moneda()->cambiar($fecha, $this->saldo); + } +} diff --git a/api/src/Cuenta.php b/api/src/Cuenta.php index 46f6d18..7fa3538 100644 --- a/api/src/Cuenta.php +++ b/api/src/Cuenta.php @@ -1,9 +1,7 @@ abonos; } + protected $consolidados; + public function consolidados() { + if ($this->consolidados === null) { + $this->consolidados = $this->parentOf(Consolidado::class, [Model::CHILD_KEY => 'cuenta_id']); + } + return $this->consolidados; + } + public function hasConsolidados(): bool { + return (bool) Model::factory(Consolidado::class) + ->whereEqual('cuenta_id', $this->id) + ->count('id'); + } + public function hasTransacciones(): bool { + return (bool) Model::factory(Transaccion::class) + ->select('transacciones.*') + ->join('cuentas', 'cuentas.id = transacciones.debito_id OR cuentas.id = transacciones.credito_id') + ->whereEqual('cuentas.id', $this->id) + ->count('transacciones.id'); + } protected $transacciones; + protected function parseTransaccion(Transaccion $transaccion) { + $transaccion->setFactory($this->factory); + if ($transaccion->debito_id === $this->id) { + $transaccion->valor = - $transaccion->valor; + } + $transaccion->valor = $transaccion->transformar($this->moneda()); + return $transaccion; + } public function transacciones($limit = null, $start = 0) { $transacciones = Model::factory(Transaccion::class) ->select('transacciones.*') @@ -67,10 +92,7 @@ class Cuenta extends Model { } $transacciones = $transacciones->findMany(); foreach ($transacciones as &$transaccion) { - $transaccion->setFactory($this->factory); - if ($transaccion->debito_id === $this->id) { - $transaccion->valor = - $transaccion->valor; - } + $transaccion = $this->parseTransaccion($transaccion); } return $transacciones; } @@ -87,30 +109,21 @@ class Cuenta extends Model { $transacciones = $transacciones->findMany(); foreach ($transacciones as &$transaccion) { - $transaccion->setFactory($this->factory); - if ($transaccion->desde_id === $this->id) { - $transaccion->valor = - $transaccion->valor; - } + $transaccion = $this->parseTransaccion($transaccion); } return $transacciones; } public function acumulacion(Carbon $date) { - $abonos = Model::factory(Transaccion::class) - ->whereEqual('credito_id', $this->id) - ->whereLt('fecha', $date->format('Y-m-d')) - ->groupBy('credito_id') - ->sum('valor'); - $cargos = Model::factory(Transaccion::class) - ->whereEqual('debito_id', $this->id) - ->whereLt('fecha', $date->format('Y-m-d')) - ->groupBy('debito_id') - ->sum('valor'); - - if (in_array($this->tipo()->descripcion, ['activo', 'banco', 'perdida'])) { - return $abonos - $cargos; + $consolidados = $this->consolidados(); + $saldo = 0; + foreach ($consolidados as $consolidado) { + if ($consolidado->fecha() >= $date) { + continue; + } + $saldo += $consolidado->saldo($date); } - return $cargos - $abonos; + return $saldo; } protected $saldo; public function saldo(Service $service = null, $in_clp = false) { diff --git a/api/src/Moneda.php b/api/src/Moneda.php index 9405f41..f643019 100644 --- a/api/src/Moneda.php +++ b/api/src/Moneda.php @@ -22,19 +22,22 @@ class Moneda extends Model { $this->sufijo ])); } - public function cambio(\DateTime $fecha) { + public function cambio(\DateTime $fecha, Moneda $moneda = null) { + if ($moneda === null) { + $moneda = $this->factory->find(Moneda::class)->one(1); + } $cambio = $this->factory->find(TipoCambio::class) - ->where([['desde_id', $this->id], ['hasta_id', 1], ['fecha', $fecha->format('Y-m-d H:i:s')]]) + ->where([['desde_id', $this->id], ['hasta_id', $moneda->id], ['fecha', $fecha->format('Y-m-d H:i:s')]]) ->one(); if ($cambio === null) { $cambio = $this->factory->find(TipoCambio::class) - ->where([['hasta_id', $this->id], ['desde_id', 1], ['fecha', $fecha->format('Y-m-d H:i:s')]]) + ->where([['hasta_id', $this->id], ['desde_id', $moneda->id], ['fecha', $fecha->format('Y-m-d H:i:s')]]) ->one(); } return $cambio; } - public function cambiar(\DateTime $fecha, float $valor) { - $cambio = $this->cambio($fecha); + public function cambiar(\DateTime $fecha, float $valor, Moneda $moneda = null) { + $cambio = $this->cambio($fecha, $moneda); if (!$cambio) { return $valor; } diff --git a/api/src/Transaccion.php b/api/src/Transaccion.php index 5fca6e8..fb7f71c 100644 --- a/api/src/Transaccion.php +++ b/api/src/Transaccion.php @@ -13,6 +13,7 @@ use ProVM\Common\Alias\Model; * @property string $glosa * @property string $detalle * @property double $valor + * @property Moneda $moneda_id */ class Transaccion extends Model { public static $_table = 'transacciones'; @@ -37,6 +38,21 @@ class Transaccion extends Model { return Carbon::parse($this->fecha); } $this->fecha = $fecha->format('Y-m-d'); + return $this; + } + protected $moneda; + public function moneda() { + if ($this->moneda === null) { + $this->moneda = $this->childOf(Moneda::class, [Model::SELF_KEY => 'moneda_id']); + } + return $this->moneda; + } + + public function transformar(Moneda $moneda = null) { + if (($moneda !== null and $this->moneda()->id === $moneda->id) or ($moneda === null and $this->moneda()->id === 1)) { + return $this->valor; + } + return $this->moneda()->cambiar($this->fecha(), $this->valor, $moneda); } public function toArray(): array { diff --git a/console/common/Command/Consolidar.php b/console/common/Command/Consolidar.php new file mode 100644 index 0000000..d9cedb4 --- /dev/null +++ b/console/common/Command/Consolidar.php @@ -0,0 +1,37 @@ +setClient($client); + } + + public function setClient(ClientInterface $client) { + $this->client = $client; + return $this; + } + public function getClient(): ClientInterface { + return $this->client; + } + + public function execute(InputInterface $input, OutputInterface $output) { + error_log('Starting Consolidar'); + try { + $response = $this->getClient()->get('/consolidar'); + if ($response->getStatusCode() === 200) { + return 0; + } + return $response->getStatusCode(); + } catch (\Exception $e) { + error_log($e); + return $e->getCode(); + } + } +} diff --git a/console/crontab b/console/crontab new file mode 100644 index 0000000..24803bf --- /dev/null +++ b/console/crontab @@ -0,0 +1,24 @@ +# Edit this file to introduce tasks to be run by cron. +# +# Each task to run has to be defined through a single line +# indicating with different fields when the task will be run +# and what command to run for the task +# +# To define the time you can provide concrete values for +# minute (m), hour (h), day of month (dom), month (mon), +# and day of week (dow) or use '*' in these fields (for 'any'). +# +# Notice that tasks will be started based on the cron's system +# daemon's notion of time and timezones. +# +# Output of the crontab jobs (including errors) is sent through +# email to the user the crontab file belongs to (unless redirected). +# +# For example, you can run a backup of all your user accounts +# at 5 a.m every week with: +# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/ +# +# For more information see the manual pages of crontab(5) and cron(8) +# +# m h dom mon dow command +0 2 1 * * /usr/local/bin/php /app/bin/console consolidar diff --git a/console/setup/commands/consolidar.php b/console/setup/commands/consolidar.php new file mode 100644 index 0000000..87da64b --- /dev/null +++ b/console/setup/commands/consolidar.php @@ -0,0 +1,4 @@ +add(new Consolidar($app->getContainer()->get(\Psr\Http\Client\ClientInterface::class), 'consolidar')); diff --git a/ui/public/assets/scripts/cuentas.show.js b/ui/public/assets/scripts/cuentas.show.js index 5f863ce..adaa54d 100644 --- a/ui/public/assets/scripts/cuentas.show.js +++ b/ui/public/assets/scripts/cuentas.show.js @@ -238,7 +238,7 @@ const transacciones = { const parent = $(this.id) parent.html('') $.each(this.transacciones, (i, el) => { - this.saldo = this.saldo + parseInt(el.valor.valor) * ((el.isIncrement()) ? 1 : -1) + this.saldo = this.saldo + parseInt(el.valor.valor)// * ((el.isIncrement()) ? 1 : -1) parent.append(el.draw({saldo: this.saldo, format: this.intl_format, format_array: this.cuenta.moneda.format, format_call: this.format})) }) }, From dbad283e14114994329c94c2fe110390102f6324 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Fri, 25 Mar 2022 15:03:49 -0300 Subject: [PATCH 3/5] Command queue --- api/common/Controller/Queues.php | 47 ++++++++++++++++ api/resources/routes/queues.php | 8 +++ api/src/Queue.php | 83 +++++++++++++++++++++++++++++ api/src/QueueArgument.php | 26 +++++++++ console/common/Command/Queue.php | 53 ++++++++++++++++++ console/crontab | 2 +- console/setup/commands/01_queue.php | 4 ++ 7 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 api/common/Controller/Queues.php create mode 100644 api/resources/routes/queues.php create mode 100644 api/src/Queue.php create mode 100644 api/src/QueueArgument.php create mode 100644 console/common/Command/Queue.php create mode 100644 console/setup/commands/01_queue.php diff --git a/api/common/Controller/Queues.php b/api/common/Controller/Queues.php new file mode 100644 index 0000000..1031d36 --- /dev/null +++ b/api/common/Controller/Queues.php @@ -0,0 +1,47 @@ +find(Queue::class)->many(); + $output = [ + 'queues' => array_map(function($item) {return $item->toArray();}, $queues) + ]; + return $this->withJson($response, $output); + } + public function pending(Request $request, Response $response, Factory $factory): Response { + $pending = $factory->find(Queue::class)->where([['processed', 0]])->many(); + $output = [ + 'pending' => array_map(function($item) {return $item->toArray();}, $pending) + ]; + return $this->withJson($response, $output); + } + public function processed(Request $request, Response $response, Factory $factory): Response { + $input = json_decode($request->getBody()->getContents()); + $output = [ + 'input' => $input, + 'queues' => [] + ]; + if (!is_array($input->processed)) { + $input->processed = [$input->processed]; + } + foreach ($input->processed as $id) { + $queue = $factory->find(Queue::class)->one($id); + $queue->setProcessed(true); + $status = $queue->save(); + $output['queues'] []= [ + 'queue' => $queue->toArray(), + 'processed' => $status + ]; + } + return $this->withJson($response, $output); + } +} diff --git a/api/resources/routes/queues.php b/api/resources/routes/queues.php new file mode 100644 index 0000000..d4a802f --- /dev/null +++ b/api/resources/routes/queues.php @@ -0,0 +1,8 @@ +group('/queues', function($app) { + $app->get('/pending', [Queues::class, 'pending']); + $app->post('/processed', [Queues::class, 'processed']); + $app->get('[/]', Queues::class); +}); diff --git a/api/src/Queue.php b/api/src/Queue.php new file mode 100644 index 0000000..36d018b --- /dev/null +++ b/api/src/Queue.php @@ -0,0 +1,83 @@ +created); + } + $this->created = $fecha->format('Y-m-d H:i:s'); + return $this; + } + public function hasArguments() { + return Model::factory(QueueArgument::class) + ->whereEqual('queue_id', $this->id) + ->groupBy('queue_id') + ->count('id') > 0; + } + protected $arguments; + public function arguments() { + if ($this->arguments === null) { + $this->arguments = $this->parentOf(QueueArgument::class, [Model::CHILD_KEY => 'queue_id']); + } + return $this->arguments; + } + public function isProcessed() { + return $this->processed > 0; + } + public function setProcessed(bool $processed) { + $this->processed = $processed ? 1 : 0; + return $this; + } + + public static function find(Factory $factory, $data) { + $where = [ + 'command' => $data['command'], + 'processed' => $data['processed'] ?? 0 + ]; + array_walk($where, function(&$item, $key) { + $item = [$key, $item]; + }); + $where = array_values($where); + return $factory->find(Queue::class)->where($where)->one(); + } + + public function toArray(): array + { + $arr = parent::toArray(); + $cmd = [(string) $this]; + $arr['arguments'] = []; + if ($this->hasArguments()) { + $arr['arguments'] = array_map(function($item) use (&$cmd) { + $cmd []= (string) $item; + return $item->toArray(); + }, $this->arguments()); + } + $arr['cmd'] = implode(' ', $cmd); + return $arr; + } + + public function __toString(): string { + $str = "{$this->command}"; + $arguments = $this->arguments(); + if ($arguments !== null and count($arguments) > 0) { + $arguments = implode(' ', array_map(function($item) {return (string) $item;}, $arguments)); + $str .= " {$arguments}"; + } + return $str; + } +} diff --git a/api/src/QueueArgument.php b/api/src/QueueArgument.php new file mode 100644 index 0000000..e090c26 --- /dev/null +++ b/api/src/QueueArgument.php @@ -0,0 +1,26 @@ +queue === null) { + $this->queue = $this->childOf(Queue::class, [Model::SELF_KEY => 'queue_id']); + } + return $this->queue; + } + + public function __toString(): string { + return "{$this->argument}='{$this->value}'"; + } +} diff --git a/console/common/Command/Queue.php b/console/common/Command/Queue.php new file mode 100644 index 0000000..847502c --- /dev/null +++ b/console/common/Command/Queue.php @@ -0,0 +1,53 @@ +setClient($client); + } + + public function setClient(ClientInterface $client) { + $this->client = $client; + return $this; + } + public function getClient(): ClientInterface { + return $this->client; + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $response = $this->getClient()->get('/queues/pending'); + if ($response->getStatusCode() !== 200) { + return Command::FAILURE; + } + $input = json_decode($response->getBody()->getContents()); + $output = [ + 'input' => $input, + 'processed' => [] + ]; + foreach ($input->pending as $queue) { + $log = "Running {$queue->command} from queue. Created in {$queue->created}."; + error_log($log); + $cmd = '/usr/local/bin/php /app/bin/console ' . $queue->cmd; + exec($cmd, $result, $code); + if ($code != Command::SUCCESS) { + error_log(var_export($queue, true)); + error_log(var_export($result, true)); + continue; + } + $output['processed'] []= $queue->id; + } + $response = $this->getClient()->post('/queues/processed', ['json' => $output]); + if ($response->getStatusCode() !== 200) { + return Command::FAILURE; + } + return Command::SUCCESS; + } +} diff --git a/console/crontab b/console/crontab index 24803bf..3934f8f 100644 --- a/console/crontab +++ b/console/crontab @@ -21,4 +21,4 @@ # For more information see the manual pages of crontab(5) and cron(8) # # m h dom mon dow command -0 2 1 * * /usr/local/bin/php /app/bin/console consolidar +0 2 * * * /usr/local/bin/php /app/bin/console queue diff --git a/console/setup/commands/01_queue.php b/console/setup/commands/01_queue.php new file mode 100644 index 0000000..3bf65be --- /dev/null +++ b/console/setup/commands/01_queue.php @@ -0,0 +1,4 @@ +add(new Queue($app->getContainer()->get(\Psr\Http\Client\ClientInterface::class), 'queue')); From ee3133da728478c434cc3e45be60ff6946b5d27f Mon Sep 17 00:00:00 2001 From: Aldarien Date: Fri, 25 Mar 2022 15:04:10 -0300 Subject: [PATCH 4/5] Consolidar command --- api/common/Middleware/Consolidar.php | 2 +- api/common/Service/Consolidar.php | 23 +++++++++++++++-------- api/setup/middlewares/03_consolidar.php | 4 ++++ api/src/Cuenta.php | 9 +++++++++ console/common/Command/Consolidar.php | 8 ++++---- 5 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 api/setup/middlewares/03_consolidar.php diff --git a/api/common/Middleware/Consolidar.php b/api/common/Middleware/Consolidar.php index d3e82b3..74cb8f6 100644 --- a/api/common/Middleware/Consolidar.php +++ b/api/common/Middleware/Consolidar.php @@ -13,7 +13,7 @@ class Consolidar { } public function __invoke(Request $request, Handler $handler): Response { if (!$this->service->isConsolidado()) { - $this->service->consolidar(); + $this->service->queue(); } return $handler->handle($request); } diff --git a/api/common/Service/Consolidar.php b/api/common/Service/Consolidar.php index 5be69f1..427eae4 100644 --- a/api/common/Service/Consolidar.php +++ b/api/common/Service/Consolidar.php @@ -2,6 +2,7 @@ namespace Contabilidad\Common\Service; use Carbon\Carbon; +use Contabilidad\Queue; use \Model; use ProVM\Common\Factory\Model as ModelFactory; use Contabilidad\Consolidado; @@ -21,24 +22,30 @@ class Consolidar { return $this->cuentas; } - public function isConsolidado() { - $consolidado = true; + public function isConsolidado(): bool { $cuentas = $this->getCuentas(); if ($cuentas === null) { - return false; + return true; } foreach ($cuentas as $cuenta) { $transacciones = $cuenta->hasTransacciones(); if (!$transacciones) { continue; } - $consolidados = $cuenta->hasConsolidados(); - if (!$consolidados and $transacciones) { - $consolidado = false; - break; + $pendientes = $cuenta->hasConsolidadosPending(); + if ($pendientes) { + return false; } } - return $consolidado; + return true; + } + public function queue() { + $data = [ + 'command' => 'consolidar', + 'created' => Carbon::now()->format('Y-m-d H:i:s') + ]; + $queue = Queue::add($this->factory, $data); + $queue->save(); } public function consolidar() { ini_set('max_execution_time', 60*5); diff --git a/api/setup/middlewares/03_consolidar.php b/api/setup/middlewares/03_consolidar.php new file mode 100644 index 0000000..9bb8d83 --- /dev/null +++ b/api/setup/middlewares/03_consolidar.php @@ -0,0 +1,4 @@ +add(new Consolidar($app->getContainer()->get(\Contabilidad\Common\Service\Consolidar::class))); diff --git a/api/src/Cuenta.php b/api/src/Cuenta.php index 7fa3538..67f32f2 100644 --- a/api/src/Cuenta.php +++ b/api/src/Cuenta.php @@ -60,10 +60,19 @@ class Cuenta extends Model { return $this->consolidados; } public function hasConsolidados(): bool { + $t = Carbon::now(); return (bool) Model::factory(Consolidado::class) ->whereEqual('cuenta_id', $this->id) ->count('id'); } + public function hasConsolidadosPending(): bool { + $t = Carbon::now(); + return !(bool) Model::factory(Consolidado::class) + ->whereEqual('cuenta_id', $this->id) + ->whereGte('fecha', $t->copy()->subMonthNoOverflow()->startOfMonth()->format('Y-m-d')) + ->orderByDesc('fecha') + ->count('id'); + } public function hasTransacciones(): bool { return (bool) Model::factory(Transaccion::class) ->select('transacciones.*') diff --git a/console/common/Command/Consolidar.php b/console/common/Command/Consolidar.php index d9cedb4..3a2e43f 100644 --- a/console/common/Command/Consolidar.php +++ b/console/common/Command/Consolidar.php @@ -22,16 +22,16 @@ class Consolidar extends Command { } public function execute(InputInterface $input, OutputInterface $output) { - error_log('Starting Consolidar'); try { $response = $this->getClient()->get('/consolidar'); if ($response->getStatusCode() === 200) { - return 0; + return Command::SUCCESS; } - return $response->getStatusCode(); + error_log($response->getReasonPhrase()); + return Command::FAILURE; } catch (\Exception $e) { error_log($e); - return $e->getCode(); + return Command::FAILURE; } } } From f9076d3bac10c538828da56864637b2af0343e24 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Fri, 25 Mar 2022 15:04:42 -0300 Subject: [PATCH 5/5] Error catching in API --- api/public/index.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/api/public/index.php b/api/public/index.php index e6d2557..4ccf8a4 100644 --- a/api/public/index.php +++ b/api/public/index.php @@ -1,7 +1,12 @@ run(); +try { + require_once implode(DIRECTORY_SEPARATOR, [ + dirname(__DIR__), + 'setup', + 'app.php' + ]); + $app->run(); +} catch (Error | Exception $e) { + error_log($e); + throw $e; +}