From 38b7a056b72b7ae4e5ae72fbd9f40b94afe49a33 Mon Sep 17 00:00:00 2001
From: "luciano.cassettai" <luciano.cassettai@unam.edu.ar>
Date: Mon, 2 Jan 2023 10:17:32 -0300
Subject: [PATCH 01/11] ADD api-backend : recursos para realizar crud unidades
 de gestion

---
 .../v1/unidades_gestion/unidades_gestion.php  | 271 ++++++++++++++++++
 .../src/UNAM/Tupa/Backend/API/Factory.php     |  15 +
 .../Tupa/Core/Errors/UnidadGestionError.php   |  12 +
 .../Core/Manager/ManagerUnidadGestion.php     | 169 +++++++++++
 .../Negocio/Organizacion/UnidadGestion.php    |  79 +++++
 5 files changed, 546 insertions(+)
 create mode 100644 api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
 create mode 100644 core/src/UNAM/Tupa/Core/Errors/UnidadGestionError.php
 create mode 100644 core/src/UNAM/Tupa/Core/Manager/ManagerUnidadGestion.php
 create mode 100644 core/src/UNAM/Tupa/Core/Negocio/Organizacion/UnidadGestion.php

diff --git a/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
new file mode 100644
index 00000000..8154c4b2
--- /dev/null
+++ b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
@@ -0,0 +1,271 @@
+<?php
+
+use SIUToba\rest\lib\rest_error;
+use SIUToba\rest\lib\rest_hidratador;
+use SIUToba\rest\lib\rest_validador;
+use SIUToba\rest\rest;
+
+use UNAM\Tupa\Core\Errors\UnidadGestionError;
+use UNAM\Tupa\Backend\API\Factory;
+use UNAM\Tupa\Core\Filtros\Filtro;
+use UNAM\Tupa\Core\Negocio\Organizacion\UnidadGestion;
+
+
+class unidades_gestion implements SIUToba\rest\lib\modelable
+{
+    public static function _get_modelos()
+    {
+        $unidadGestionEdit = array(
+            'sigla' => array(
+                'type' => 'string',
+                'required' => true,
+                '_validar' => array(
+                    rest_validador::OBLIGATORIO,
+                ),
+            ),
+            'nombre' => array(
+                'type' => 'string',
+                'required' => true,
+                '_validar' => array(
+                    rest_validador::OBLIGATORIO,
+                ),
+            ),
+            'id_grupo_arai' => array(
+                'type' => 'string',
+                'required' => true,
+                '_validar' => array(
+                    rest_validador::OBLIGATORIO,
+                ),
+            ),
+            
+        );
+
+        $unidadGestion = array_merge(
+            $unidadGestionEdit,
+            array(
+                'id_unidad' => array(
+                    'type' => 'integer',
+                ),
+            )
+        );
+        
+
+        return $models = array(
+            'UnidadGestion' => $unidadGestion,
+            'UnidadGestionEdit' => $unidadGestionEdit,
+        );
+    }
+
+    protected function get_spec_usuario($tipo = 'UnidadGestion')
+    {
+        $m = $this->_get_modelos();
+
+        return $m[$tipo];
+    }
+
+    /**
+     * Se consume en GET /unidades_gestion/{id}.
+     *
+     * @summary Retorna datos de un unidad de gestion
+     * @responses 200 {"$ref": "UnidadGestion"} UnidadGestion
+     * @responses 404 No existe el recurso
+     * @responses 500 Error interno del servidor
+     */
+    public function get($id)
+    {
+        try {
+            $manager = Factory::getManagerUnidadGestion();
+
+            $unidadGestion = $manager->getUnidadGestion($id);
+
+            $fila = rest_hidratador::hidratar_fila($this->get_spec_usuario('UnidadGestionEdit'), $unidadGestion->toArray());
+
+            rest::response()->get($fila);
+        } catch (UnidadGestionError $e) {
+            Factory::getMainLogger()->error($e->getMessage());
+            rest::response()->not_found();
+        } catch (Exception $e) {
+            Factory::getMainLogger()->error($e->getMessage());
+            rest::response()->error_negocio('Error interno del servidor', 500);
+        }
+    }
+
+     /**
+     * Se consume en GET /unidades-gestion.
+     *
+     * @summary Retorna las unidades de gestion existentes
+     * @param_query $sigla string Se define como 'condicion;valor' donde 'condicion' puede ser contiene|no_contiene|comienza_con|termina_con|es_igual_a|es_distinto_de
+     * @param_query $nombre string Se define como 'condicion;valor' donde 'condicion' puede ser es_menor_que|es_menor_igual_que|es_igual_a|es_distinto_de|es_mayor_igual_que|es_mayor_que|entre
+     * @param_query $id_grupo_arai string Se define como 'condicion;valor' donde 'condicion' puede ser es_menor_que|es_menor_igual_que|es_igual_a|es_distinto_de|es_mayor_igual_que|es_mayor_que|entre
+     *
+     * @param_query $limit integer Limitar a esta cantidad de términos y condiciones
+     * @param_query $page integer Limitar desde esta pagina
+     * @param_query $order string +/-campo,...
+     * @responses 200 array {"$ref":"UnidadesGestion"}
+     * @responses 500 Error en los operadores ingresados para el filtro
+     */
+    public function get_list()
+    {
+        try {
+            $filtro = $this->get_filtro_get_list();
+            $filtro->setlimit(rest::request()->get('limit', null));
+            $filtro->setPage(rest::request()->get('page', null));
+            $filtro->setOrder(rest::request()->get('order', null));
+
+            $manager = Factory::getManagerUnidadGestion();
+
+            $unidadesGestion = $manager->getUnidadesGestion($filtro);
+            $resultados = [];
+            foreach ($unidadesGestion as $ug) {
+                $resultados[] = $ug->toArray();
+            }
+            
+            $registros = rest_hidratador::hidratar($this->get_spec_usuario('UnidadGestion'), $resultados);
+
+            rest::response()->get($registros);
+        } catch (Exception $e) {
+            Factory::getMainLogger()->error($e->getMessage());
+            rest::response()->error_negocio('Error interno del servidor', 500);
+        }
+    }
+
+     /**
+     * Se consume en POST /unidades-gestion
+     *
+     * @summary Crea una nueva unidad de géstion
+     * @param_body $unidadGestion UnidadGestionEdit [required] los datos de la unidad de gestion
+     * @responses 201 {"string"} identificador de unidad de gestion otorgado
+     * @responses 400 unidad de gestion inválidoa
+     * @responses 500 Error interno
+     */
+    public function post_list()
+    {
+        // Valido y traduzco los datos al formato de mi modelo
+        $datos = $this->procesar_input_edicion('UnidadGestion',false);
+
+        try {
+            $manager = Factory::getManagerUnidadGestion();
+
+            $ug = new UnidadGestion();
+
+            $ug->loadFromDatos($datos);
+
+            $identificador = $manager->crear($ug);
+
+            $respuesta = [
+                'id_unidad' => $identificador['id_unidad']
+            ];
+
+            rest::response()->post($respuesta);
+        } catch (rest_error $e) {
+            rest::response()->error_negocio($e->get_datalle(), 400);
+        } catch (Exception $e) {
+            if ($e->getCode() >= 400 || $e->getCode() < 500) {
+                rest::response()->error_negocio([$e->getMessage()], 400);
+            } else {
+                rest::response()->error_negocio([$e->getMessage()], 500);
+            }
+        }
+    }
+
+    /**
+     * Se consume en PUT /unidades_gestion/{identificador}
+     *
+     * @summary Modifica una unidad de gestion
+     * @param_body $UnidadGestionEdit UnidadGestionEdit  [required] los datos a editar de la unidad de gestion
+     * @responses 204  el código es correcto
+     * @responses 400 Código no válido
+     * @responses 404 No existe el recurso
+     * @responses 500 Error interno
+     */
+    public function put($id)
+    {
+        $datos = $this->procesar_input_edicion('UnidadGestionEdit');
+
+        try {
+            $manager = Factory::getManagerUnidadGestion();
+            $ug = $manager->getUnidadGestion($id);
+            $ug->loadFromDatos($datos);
+            $manager->actualizar($ug);
+
+            rest::response()->put([ "respuesta" => true ]);
+        } catch (UnidadGestionError $e) {
+            rest::response()->not_found();
+        } catch (\Exception $e) {
+            if ($e->getCode() >= 400 || $e->getCode() < 500) {
+                rest::response()->error_negocio([$e->getMessage()], 400);
+            } else {
+                rest::response()->error_negocio([$e->getMessage()], 500);
+            }
+        }
+    }
+
+      /**
+     * Se consume en DELETE /unidades_gestion/{identificador}
+     *
+     * @summary Elimina una unidad de gestion
+     * @responses 204 Se elimino correctamente
+     * @responses 400 El id no existe
+     * @responses 404 No existe el recurso
+     * @responses 500 Error interno
+     */
+    public function delete($id)
+    {
+        try {
+            $manager = Factory::getManagerUnidadGestion();
+
+            $manager->eliminar($id);
+
+            rest::response()->put([ "respuesta" => true ]);
+        }catch (\Exception $e) {
+            if ($e->getCode() >= 400 || $e->getCode() < 500) {
+                rest::response()->error_negocio([$e->getMessage()], 400);
+            } else {
+                rest::response()->error_negocio([$e->getMessage()], 500);
+            }
+        }
+    }
+
+     /**
+     * @return Filtro
+     * @throws ErrorTupa
+     */
+    protected function get_filtro_get_list()
+    {
+        /* @var Filtro $filtro */
+        $filtro = new Filtro();
+        $filtro->agregarCampoRest('ug.nombre', rest::request()->get('nombre', null));
+        $filtro->agregarCampoRest('ug.sigla', rest::request()->get('sigla', null));
+        $filtro->agregarCampoRest('ug.id_grupo_arai', rest::request()->get('id_grupo_arai', null));
+
+        $filtro->agregarCampoOrdenable('nombre');
+        $filtro->agregarCampoOrdenable('sigla');
+
+        return $filtro;
+    }
+
+    /**
+     * $relajar_ocultos boolean no checkea campos obligatorios cuando no se especifican.
+     * @param string $modelo
+     * @param bool $relajar_ocultos
+     * @param array $datos
+     * @return array|string
+     * @throws rest_error
+     */
+    protected function procesar_input_edicion($modelo = 'UnidadGestionEdit', $relajar_ocultos = false, $datos = null)
+    {
+        if (! $datos) {
+            $datos = rest::request()->get_body_json();
+        }
+
+        $spec_usuario = $this->get_spec_usuario($modelo);
+
+        rest_validador::validar($datos, $spec_usuario, $relajar_ocultos);
+
+        $resultado = rest_hidratador::deshidratar_fila($datos, $spec_usuario);
+
+        return Factory::getVarios()->arrayToUtf8($resultado);
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/api-backend/src/UNAM/Tupa/Backend/API/Factory.php b/api-backend/src/UNAM/Tupa/Backend/API/Factory.php
index e36d3c35..99b99237 100644
--- a/api-backend/src/UNAM/Tupa/Backend/API/Factory.php
+++ b/api-backend/src/UNAM/Tupa/Backend/API/Factory.php
@@ -26,6 +26,7 @@ use UNAM\Tupa\Core\Manager\ManagerTerminosCondiciones;
 use UNAM\Tupa\Core\Manager\ManagerVisitante;
 use UNAM\Tupa\Core\Manager\ManagerVisita;
 use UNAM\Tupa\Core\Manager\ManagerAutorizante;
+use UNAM\Tupa\Core\Manager\ManagerUnidadGestion;
 use UNAM\Tupa\Core\Manager\ManagerPermiso;
 use UNAM\Tupa\Core\Manager\ManagerSolicitud;
 
@@ -253,6 +254,10 @@ class Factory
         $container['manager-autorizante'] = function ($c) {
             return new ManagerAutorizante($c['db-logger'], $c['db'], $c['codigo']);
         };
+        
+        $container['manager-unidad-gestion'] = function ($c) {
+            return new ManagerUnidadGestion($c['db-logger'], $c['db'], $c['codigo']);
+        };
 
         $container['manager-solicitud'] = function ($c) {
             return new ManagerSolicitud($c['db-logger'], $c['db'], $c['codigo']);
@@ -342,6 +347,16 @@ class Factory
     {
         return self::getContainer()['manager-sede'];
     }
+    
+    /**
+     * Singleton de ManagerSede
+     *
+     * @return ManagerUnidadGestion
+     */
+    public static function getManagerUnidadGestion()
+    {
+        return self::getContainer()['manager-unidad-gestion'];
+    }
 
     /**
      * Singleton de ManagerTerminosCondiciones.
diff --git a/core/src/UNAM/Tupa/Core/Errors/UnidadGestionError.php b/core/src/UNAM/Tupa/Core/Errors/UnidadGestionError.php
new file mode 100644
index 00000000..0ff310c3
--- /dev/null
+++ b/core/src/UNAM/Tupa/Core/Errors/UnidadGestionError.php
@@ -0,0 +1,12 @@
+<?php declare(strict_types=1);
+
+namespace UNAM\Tupa\Core\Errors;
+
+/**
+ * Sede 
+ *
+ * @codeCoverageIgnore
+ */
+class UnidadGestionError extends ErrorTupa
+{
+}
\ No newline at end of file
diff --git a/core/src/UNAM/Tupa/Core/Manager/ManagerUnidadGestion.php b/core/src/UNAM/Tupa/Core/Manager/ManagerUnidadGestion.php
new file mode 100644
index 00000000..bf00c59a
--- /dev/null
+++ b/core/src/UNAM/Tupa/Core/Manager/ManagerUnidadGestion.php
@@ -0,0 +1,169 @@
+<?php declare(strict_types=1);
+
+namespace UNAM\Tupa\Core\Manager;
+
+use UNAM\Tupa\Core\Errors\UnidadGestionError;
+use UNAM\Tupa\Core\Filtros\Filtro;
+use UNAM\Tupa\Core\Negocio\Organizacion\UnidadGestion;
+
+class ManagerUnidadGestion extends Manager
+{
+    /**
+     * @param $id
+     * @return UnidadGestion|null
+     * @throws UnidadGestionError No se pudo recuperar la Unidad de gestion
+     */
+    public function getUnidadGestion($id)
+    {
+        $params = [
+            "id" => $id
+        ];
+
+        $sql = "SELECT id_unidad, sigla, nombre, id_grupo_arai "
+            . "FROM unidades_gestion ug "
+            . "WHERE id_unidad = :id";
+
+        $result = $this->db->sentencia_consultar_fila($sql, $params);
+
+        if (!$result) {
+            throw new UnidadGestionError("No se pudo recuperar la unidad de gestion '$id'");
+        }
+
+        return $this->hidratarUnidadGestion($result);
+    }
+
+     /**
+     * @param Filtro|null $filtro
+     * @param bool $hidratar
+     * @return UnidadesGestion[]
+     */
+    public function getUnidadesGestion(Filtro $filtro = null, $hidratar = true)
+    {
+        $where = $this->getSqlWhere($filtro);
+        $orderBy = $this->getSqlOrderBy($filtro);
+        $limit = $this->getSqlLimit($filtro);
+
+        $sql = sprintf("
+            SELECT	
+                ug.id_unidad, ug.nombre, ug.sigla, ug.id_grupo_arai
+            FROM 
+                 unidades_gestion ug
+            %s
+            %s
+            %s;", $where, $orderBy, $limit);
+
+        $result = $this->db->consultar($sql);
+ 
+        if ($hidratar) {
+            $result = $this->hidratarUnidaesdGestion($result);
+        }
+
+        return $result;
+    }
+
+     /**
+     * @param UnidadGestion $ug
+     * @return bool|string
+     * @throws UnidadGestionError No se pudo crear la unidad de gestion
+     */
+    public function crear(UnidadGestion $ug)
+    {
+        $sql = "INSERT INTO unidades_gestion (nombre, sigla, id_grupo_arai) VALUES (:nombre, :sigla, :id_grupo_arai)";
+
+        $sqlParams = [
+            "nombre" => $ug->getNombre(),
+            "sigla" => $ug->getSigla(),
+            "id_grupo_arai" => $ug->getIdGrupoArai(),
+        ];
+
+        $result = $this->db->sentencia_ejecutar($sql, $sqlParams);
+
+        if (!$result) {
+            throw new UnidadGestionError("No se pudo crear la unidad de gestion");
+        }
+
+        $sql = "SELECT id_unidad FROM unidades_gestion 
+                WHERE nombre = :nombre
+                AND sigla = :sigla 
+                AND id_grupo_arai = :id_grupo_arai";
+   
+        return $this->db->sentencia_consultar_fila($sql, $sqlParams);
+    }
+
+    /**
+     * @param UnidadGestion $unidadGestion
+     * @return mixed
+     */
+    public function actualizar(UnidadGestion $ug)
+    {
+        $params = [
+            "sigla" => $ug->getSigla(),
+            "nombre" => $ug->getNombre(),
+            "id_grupo_arai" => $ug->getIdGrupoArai(),
+            "id" => $ug->getId(),
+        ];
+
+        $sql = " UPDATE unidades_gestion
+                SET    nombre = :nombre,
+                       sigla = :sigla,
+                       id_grupo_arai = :id_grupo_arai
+                WHERE  id_unidad = :id";
+
+        return $this->db->sentencia_ejecutar($sql, $params);
+    }
+
+    /**
+     * @param $id
+     * @return bool|null
+     * @throws UnidadGestionError No se pudo eliminar la Unidad de gestion
+     */
+    public function eliminar($id)
+    {
+        $params = [
+            "id" => $id
+        ];
+
+        $sql = "DELETE FROM unidades_gestion "
+            . "WHERE id_unidad = :id";
+
+        $result = $this->db->sentencia_ejecutar($sql, $params);
+
+        if (!$result) {
+            throw new UnidadGestionError("No se pudo eliminar la unidad de gestion '$id'");
+        }
+
+        return $result;
+    }
+
+    /**
+     * @param $datos
+     * @return UnidadGestion
+     */
+    protected function hidratarUnidadGestion($datos)
+    {
+        $ug = new UnidadGestion();
+        
+        $ug->loadFromDatos($datos);
+
+        return $ug;
+    }
+
+    /**
+     * @param $datos
+     * @return UnidadGestion[] colección de Unidades de gestion
+     */
+    protected function hidratarUnidaesdGestion($datos)
+    {
+        $ug = [];
+
+        if (count($datos) < 1) {
+            return $ug;
+        }
+
+        foreach ($datos as $dato) {
+            $ug[] = $this->hidratarUnidadGestion($dato);
+        }
+        
+        return $ug;
+    }
+}
diff --git a/core/src/UNAM/Tupa/Core/Negocio/Organizacion/UnidadGestion.php b/core/src/UNAM/Tupa/Core/Negocio/Organizacion/UnidadGestion.php
new file mode 100644
index 00000000..d2b2d034
--- /dev/null
+++ b/core/src/UNAM/Tupa/Core/Negocio/Organizacion/UnidadGestion.php
@@ -0,0 +1,79 @@
+<?php declare(strict_types=1);
+
+namespace UNAM\Tupa\Core\Negocio\Organizacion;
+
+
+class UnidadGestion {
+
+    private int $id;
+    private string $sigla;
+    private string $nombre;
+    private string $idGrupoArai;
+
+    public function getId():int{
+        return $this->id;
+    }
+
+    public function getSigla():string{
+        return $this->sigla;
+    }
+
+    public function getNombre():string{
+        return $this->nombre;
+    }
+
+    public function getIdGrupoArai():string{
+        return $this->idGrupoArai;
+    }
+
+    public function setId(int $id){
+        $this->id = $id;
+    }
+
+    public function setSigla(string $sigla):void {
+        $this->sigla = $sigla;
+    }
+
+    public function setNombre(string $nombre):void{
+        $this->nombre = $nombre;
+    }
+
+    public function setIdGrupoArai(string $idGrupo):void{
+        $this->idGrupoArai = $idGrupo;
+    }
+
+    /**
+     * Hidrata los atributos del objeto a partir de $datos
+     *
+     * @param array $datos inicializar directamente con un set de datos
+     */
+    public function loadFromDatos(array $datos)
+    {
+        if (isset($datos['id_unidad'])) {
+            $this->setId($datos['id_unidad']);
+        }
+        if (isset($datos['sigla'])) {
+            $this->setSigla($datos['sigla']);
+        }
+        if (isset($datos['nombre'])) {
+            $this->setNombre($datos['nombre']);
+        }
+        if (isset($datos['id_grupo_arai'])) {
+            $this->setIdGrupoArai($datos['id_grupo_arai']);
+        }
+    }
+    
+    /**
+     * @return array
+     */
+    public function toArray(): array
+    {
+        return [
+            'id_unidad' => $this->getId(),
+            'sigla' => $this->getSigla(),
+            'nombre' => $this->getNombre(),
+            'id_grupo_arai' => $this->getIdGrupoArai()
+        ];
+    }
+}
+
-- 
GitLab


From cb95aab99bfe1d74c2f6307111e235324a391a40 Mon Sep 17 00:00:00 2001
From: "luciano.cassettai" <luciano.cassettai@unam.edu.ar>
Date: Wed, 4 Jan 2023 11:23:45 -0300
Subject: [PATCH 02/11] ADD api-backend : recursos para realizar crud servicios

---
 .../API/Endpoints/v1/servicios/servicios.php  | 312 +++++++++++++++
 .../src/UNAM/Tupa/Backend/API/Factory.php     |  18 +-
 ...end_servicios_test.postman_collection.json | 359 ++++++++++++++++++
 api-backend/www/api.php                       |   2 +-
 .../Tupa/Core/Manager/ManagerServicio.php     | 152 ++++++++
 .../Negocio/SolicitudesServicios/Servicio.php | 100 +++++
 6 files changed, 941 insertions(+), 2 deletions(-)
 create mode 100644 api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/servicios/servicios.php
 create mode 100644 api-backend/tests/Integration/v1/collections/servicios/tupa_api_backend_servicios_test.postman_collection.json
 create mode 100644 core/src/UNAM/Tupa/Core/Manager/ManagerServicio.php
 create mode 100644 core/src/UNAM/Tupa/Core/Negocio/SolicitudesServicios/Servicio.php

diff --git a/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/servicios/servicios.php b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/servicios/servicios.php
new file mode 100644
index 00000000..75effba3
--- /dev/null
+++ b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/servicios/servicios.php
@@ -0,0 +1,312 @@
+<?php
+
+use SIUToba\rest\lib\rest_error;
+use SIUToba\rest\lib\rest_hidratador;
+use SIUToba\rest\lib\rest_validador;
+use SIUToba\rest\rest;
+
+use UNAM\Tupa\Core\Errors\ErrorTupa;
+use UNAM\Tupa\Backend\API\Factory;
+use UNAM\Tupa\Core\Filtros\Filtro;
+use UNAM\Tupa\Core\Negocio\SolicitudesServicios\Servicio;
+
+
+class servicios implements SIUToba\rest\lib\modelable
+{
+    public static function _get_modelos()
+    {
+        $vigenciaFin = [
+            'vigencia_fin' => [
+                'type' => 'string',
+                '_validar' => [
+                    rest_validador::TIPO_DATE => [
+                        'format' => 'Y-m-d'
+                    ],
+                ],
+            ]         
+        ];
+
+        $servicioEdit = array(
+            'nombre' => array(
+                'type' => 'string',
+                'required' => true,
+                '_validar' => array(
+                    rest_validador::OBLIGATORIO,
+                ),
+            ),
+            'descripcion' => array(
+                'type' => 'string',
+                'required' => true,
+                '_validar' => array(
+                    rest_validador::OBLIGATORIO,
+                ),
+            ),
+            'id_sistema_arai' => array(
+                'type' => 'string',
+                'required' => true,
+                '_validar' => array(
+                    rest_validador::OBLIGATORIO,
+                ),
+            ),
+            'vigencia_inicio' => [
+                'type' => 'date',
+                '_validar' => [
+                    rest_validador::OBLIGATORIO,
+                    rest_validador::TIPO_DATE => [
+                        'format' => 'Y-m-d'
+                    ],
+                ],
+            ]   
+        );
+
+        $servicio = array_merge(
+            $servicioEdit,
+            $vigenciaFin,
+            array(
+                'id_servicio' => array(
+                    'type' => 'integer',
+                ),
+            )
+        );
+        
+
+        return $models = array(
+            'Servicio' => $servicio,
+            'ServicioEdit' => $servicioEdit,
+            'VigenciaFin' => $vigenciaFin,
+        );
+    }
+
+    protected function get_spec_usuario($tipo = 'Servicio')
+    {
+        $m = $this->_get_modelos();
+
+        return $m[$tipo];
+    }
+
+    /**
+     * Se consume en GET /servicios/{id}.
+     *
+     * @summary Retorna datos de un servicio
+     * @responses 200 {"$ref": "Servicio"} Servicio
+     * @responses 404 No existe el recurso
+     * @responses 500 Error interno del servidor
+     */
+    public function get($id)
+    {
+        try {
+            $manager = Factory::getManagerServicio();
+
+            $servicio = $manager->getServicio($id);
+
+            $fila = rest_hidratador::hidratar_fila($this->get_spec_usuario('Servicio'), $servicio->toArray());
+
+            rest::response()->get($fila);
+        } catch (ErrorTupa $e) {
+            Factory::getMainLogger()->error($e->getMessage());
+            rest::response()->not_found();
+        } catch (Exception $e) {
+            Factory::getMainLogger()->error($e->getMessage());
+            rest::response()->error_negocio('Error interno del servidor', 500);
+        }
+    }
+
+
+    /**
+     * Se consume en GET /servicios.
+     *
+     * @summary Retorna los servicios existentes
+     * @param_query $nombre string Se define como 'condicion;valor' donde 'condicion' puede ser contiene|no_contiene|comienza_con|termina_con|es_igual_a|es_distinto_de
+     * @param_query $descripcion string Se define como 'condicion;valor' donde 'condicion' puede ser es_menor_que|es_menor_igual_que|es_igual_a|es_distinto_de|es_mayor_igual_que|es_mayor_que|entre
+     * @param_query $id_sistema_arai string Se define como 'condicion;valor' donde 'condicion' puede ser es_menor_que|es_menor_igual_que|es_igual_a|es_distinto_de|es_mayor_igual_que|es_mayor_que|entre
+     * @param_query $vigencia_inicio string Se define como 'condicion;valor' donde 'condicion' puede ser es_menor_que|es_menor_igual_que|es_igual_a|es_distinto_de|es_mayor_igual_que|es_mayor_que|entre
+     * @param_query $vigencia_fin string Se define como 'condicion;valor' donde 'condicion' puede ser es_menor_que|es_menor_igual_que|es_igual_a|es_distinto_de|es_mayor_igual_que|es_mayor_que|entre
+     *
+     * @param_query $limit integer Limitar a esta cantidad de servicios
+     * @param_query $page integer Limitar desde esta pagina
+     * @param_query $order string +/-campo,...
+     * @responses 200 array {"$ref":"Servicio"}
+     * @responses 500 Error en los operadores ingresados para el filtro
+     */
+    public function get_list()
+    {
+        try {
+            $filtro = $this->get_filtro_get_list();
+            $filtro->setlimit(rest::request()->get('limit', null));
+            $filtro->setPage(rest::request()->get('page', null));
+            $filtro->setOrder(rest::request()->get('order', null));
+
+            $manager = Factory::getManagerServicio();
+
+            $servicios = $manager->getServicios($filtro);
+
+            $resultados = [];            
+            foreach ($servicios as $s) {
+                $resultados[] = $s->toArray();
+            }
+            
+            $registros = rest_hidratador::hidratar($this->get_spec_usuario('Servicio'), $resultados);
+
+            rest::response()->get($registros);
+        } catch (Exception $e) {
+            Factory::getMainLogger()->error($e->getMessage());
+            rest::response()->error_negocio('Error interno del servidor', 500);
+        }
+    }
+
+
+    /**
+     * Se consume en POST /servicios
+     *
+     * @summary Crea un nuevo servicio
+     * @param_body $servicios ServicioEdit [required] los datos del servicio
+     * @responses 201 {"string"} identificador de un servicio
+     * @responses 400 Servicio inválido
+     * @responses 500 Error interno
+     */
+    public function post_list()
+    {
+        // Valido y traduzco los datos al formato de mi modelo
+        $datos = $this->procesar_input_edicion('ServicioEdit',false);
+
+        try {
+            $manager = Factory::getManagerServicio();
+
+            $servicio = new Servicio();
+
+            $servicio->loadFromDatos($datos);
+
+            $identificador = $manager->crear($servicio);
+
+            $respuesta = [
+                'id_servicio' => $identificador['id_servicio']
+            ];
+
+            rest::response()->post($respuesta);
+        } catch (rest_error $e) {
+            rest::response()->error_negocio($e->get_datalle(), 400);
+        } catch (Exception $e) {
+            if ($e->getCode() >= 400 || $e->getCode() < 500) {
+                rest::response()->error_negocio([$e->getMessage()], 400);
+            } else {
+                rest::response()->error_negocio([$e->getMessage()], 500);
+            }
+        }
+    }
+
+    /**
+     * Se consume en PUT /servicios/{id}
+     *
+     * @summary Modifica un servicio
+     * @param_body $ServicioEdit ServicioEdit  [required] los datos a editar de un servicio
+     * @responses 204 El id es correcto
+     * @responses 400 El id no válido
+     * @responses 404 No existe el recurso
+     * @responses 500 Error interno
+     */
+    public function put($id)
+    {
+        $datos = $this->procesar_input_edicion('ServicioEdit');
+
+        try {
+            $manager = Factory::getManagerServicio();
+            $servicio = $manager->getServicio($id);
+            $servicio->loadFromDatos($datos);
+            $manager->actualizar($servicio);
+
+            rest::response()->put([ "respuesta" => true ]);
+        } catch (ErrorTupa $e) {
+            rest::response()->not_found();
+        } catch (\Exception $e) {
+            if ($e->getCode() >= 400 || $e->getCode() < 500) {
+                rest::response()->error_negocio([$e->getMessage()], 400);
+            } else {
+                rest::response()->error_negocio([$e->getMessage()], 500);
+            }
+        }
+    }
+
+    /**
+     * Se consume en PUT /servicios/{id}/baja.
+     *
+     * @summary Setea vigencia fin de un servicio
+     * @param_body $vigenciaFin VigenciaFin  [required] la fecha de fin de vigencia del servicio
+     * @responses 200 Se seteo la baja correctamente
+     * @responses 404 No existe el recurso
+     * @responses 500 Error interno
+     */
+    public function put_baja_list($id)
+    {
+        $datos = $this->procesar_input_edicion('VigenciaFin');
+
+        try {
+            $manager = Factory::getManagerServicio();
+            $servicio = $manager->getServicio($id);
+
+            if(!empty($datos['vigencia_fin'])){
+                $servicio->setVigenciaFin($datos['vigencia_fin']);
+            }else{
+                $servicio->setVigenciaFin(date("Y-m-d"));
+            }
+
+            $manager->actualizar($servicio);
+
+            rest::response()->put([ "respuesta" => true ]);
+        } catch (ErrorTupa $e) {
+            Factory::getMainLogger()->error($e->getMessage());
+            rest::response()->not_found();
+        } catch (Exception $e) {
+            Factory::getMainLogger()->error($e->getMessage());
+            if ($e->getCode() >= 400 || $e->getCode() < 500) {
+                rest::response()->error_negocio($e->getMessage(), 400);
+            } else {
+                rest::response()->error_negocio($e->getMessage(), 500);
+            }
+        }
+    }
+
+
+     /**
+     * @return Filtro
+     * @throws ErrorTupa
+     */
+    protected function get_filtro_get_list()
+    {
+        /* @var Filtro $filtro */
+        $filtro = new Filtro();
+        $filtro->agregarCampoRest('s.nombre', rest::request()->get('nombre', null));
+        $filtro->agregarCampoRest('s.descripcion', rest::request()->get('descripcion', null));
+        $filtro->agregarCampoRest('s.id_sistema_arai', rest::request()->get('id_sistema_arai', null));
+        $filtro->agregarCampoRest('s.vigencia_inicio', rest::request()->get('vigencia_inicio', null));
+        $filtro->agregarCampoRest('s.vigencia_fin', rest::request()->get('vigencia_fin', null));
+
+        $filtro->agregarCampoOrdenable('nombre');
+        $filtro->agregarCampoOrdenable('vigencia_inicio');
+
+        return $filtro;
+    }
+
+    /**
+     * $relajar_ocultos boolean no checkea campos obligatorios cuando no se especifican.
+     * @param string $modelo
+     * @param bool $relajar_ocultos
+     * @param array $datos
+     * @return array|string
+     * @throws rest_error
+     */
+    protected function procesar_input_edicion($modelo = 'ServicioEdit', $relajar_ocultos = false, $datos = null)
+    {
+        if (! $datos) {
+            $datos = rest::request()->get_body_json();
+        }
+
+        $spec_usuario = $this->get_spec_usuario($modelo);
+
+        rest_validador::validar($datos, $spec_usuario, $relajar_ocultos);
+
+        $resultado = rest_hidratador::deshidratar_fila($datos, $spec_usuario);
+
+        return Factory::getVarios()->arrayToUtf8($resultado);
+    }
+
+}
\ No newline at end of file
diff --git a/api-backend/src/UNAM/Tupa/Backend/API/Factory.php b/api-backend/src/UNAM/Tupa/Backend/API/Factory.php
index 99b99237..ae2c8cc2 100644
--- a/api-backend/src/UNAM/Tupa/Backend/API/Factory.php
+++ b/api-backend/src/UNAM/Tupa/Backend/API/Factory.php
@@ -29,6 +29,7 @@ use UNAM\Tupa\Core\Manager\ManagerAutorizante;
 use UNAM\Tupa\Core\Manager\ManagerUnidadGestion;
 use UNAM\Tupa\Core\Manager\ManagerPermiso;
 use UNAM\Tupa\Core\Manager\ManagerSolicitud;
+use UNAM\Tupa\Core\Manager\ManagerServicio;
 
 class Factory
 {
@@ -267,6 +268,10 @@ class Factory
             return new ManagerPermiso($c['db-logger'], $c['db'], $c['codigo']);
         };
 
+        $container['manager-servicio'] = function ($c) {
+            return new ManagerServicio($c['db-logger'], $c['db'], $c['codigo']);
+        };
+
         $container['worker-redis'] = function ($c) {
             $config = new \SIU\Queue\Transport\Config\Redis();
 
@@ -349,7 +354,7 @@ class Factory
     }
     
     /**
-     * Singleton de ManagerSede
+     * Singleton de ManagerUnidadGestion
      *
      * @return ManagerUnidadGestion
      */
@@ -358,6 +363,17 @@ class Factory
         return self::getContainer()['manager-unidad-gestion'];
     }
 
+     /**
+     * Singleton de ManagerSede
+     *
+     * @return ManagerServicio
+     */
+    public static function getManagerServicio()
+    {
+        return self::getContainer()['manager-servicio'];
+    }
+
+
     /**
      * Singleton de ManagerTerminosCondiciones.
      *
diff --git a/api-backend/tests/Integration/v1/collections/servicios/tupa_api_backend_servicios_test.postman_collection.json b/api-backend/tests/Integration/v1/collections/servicios/tupa_api_backend_servicios_test.postman_collection.json
new file mode 100644
index 00000000..f543607d
--- /dev/null
+++ b/api-backend/tests/Integration/v1/collections/servicios/tupa_api_backend_servicios_test.postman_collection.json
@@ -0,0 +1,359 @@
+{
+	"info": {
+		"_postman_id": "884ded69-9e28-4c50-b25b-a5035b6d4175",
+		"name": "Tupa API Backend Servicios",
+		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
+		"_exporter_id": "16366966"
+	},
+	"item": [
+		{
+			"name": "V1",
+			"item": [
+				{
+					"name": "Servicios",
+					"item": [
+						{
+							"name": "Crear un servicio",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"exec": [
+											"pm.test(\"Status code is 201\", () => {",
+											"    pm.response.to.have.status(201);",
+											"});",
+											"",
+											"const servicioSchema =  {  ",
+											"  \"nombre\": '',",
+											"  \"descripcion\": '',",
+											"  \"id_sistema_arai\": '',",
+											"  \"vigencia_inicio\": '',",
+											"}",
+											"",
+											"const body = JSON.parse(pm.request.body.raw);",
+											"const sameSchema = JSON.stringify(Object.keys(servicioSchema)) === JSON.stringify(Object.keys(body));",
+											"",
+											"pm.test('Estructura del body correcta', () => {",
+											"    pm.expect(true).to.be.eql(sameSchema);",
+											"}); ",
+											"",
+											"var respuesta = pm.response.json();",
+											"",
+											"pm.test(\"Servicio creado con ID \" + respuesta.id_servicio, function () {",
+											"    pm.globals.set(\"idServicio\", respuesta.id_servicio);",
+											"});"
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "POST",
+								"header": [],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"nombre\": \"{{$randomJobTitle}}\",\n  \"descripcion\": \"{{$randomJobTitle}}\",\n  \"id_sistema_arai\": \"{{$randomUUID}}\",\n  \"vigencia_inicio\": \"2022-01-01\"\n}",
+									"options": {
+										"raw": {
+											"language": "json"
+										}
+									}
+								},
+								"url": {
+									"raw": "{{baseUrl}}/v1/servicios",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"v1",
+										"servicios"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "Obtener un servicio",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"exec": [
+											"pm.test(\"Status code is 200\", () => {",
+											"    pm.response.to.have.status(200);",
+											"});",
+											"",
+											"const response = pm.response.json();",
+											"const hasId = Object.keys(response).includes('id_servicio');",
+											"",
+											"pm.test(\"Servicio con id_servicio \" + response.id_servicio, function () {",
+											"    pm.expect(true).to.be.eql(hasId);",
+											"});"
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/v1/servicios/{{idServicio}}",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"v1",
+										"servicios",
+										"{{idServicio}}"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "Obtener servicios activos",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"exec": [
+											"pm.test(\"Status code is 200\", () => {",
+											"    pm.response.to.have.status(200);",
+											"});",
+											"",
+											"const fechaActual = new Date();",
+											"const year = fechaActual.getFullYear();",
+											"const month = fechaActual.getMonth() + 1; ",
+											"const day = fechaActual.getDate();",
+											"const fechaFormateada = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;",
+											"",
+											"const resp = pm.response.json();",
+											"const filteredResponse = resp.filter((obj) => obj.vigencia_fin > fechaFormateada || obj.vigencia_fin == null);",
+											"",
+											"pm.test('Hay un Servicio activo', () => {",
+											"    pm.expect(filteredResponse.length).to.not.eql(0);",
+											"});"
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/v1/servicios",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"v1",
+										"servicios"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "Modificar un servicio",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"exec": [
+											"pm.test(\"Status code is 200\", () => {",
+											"    pm.response.to.have.status(200);",
+											"});",
+											"",
+											"const servicioSchema =  {  ",
+											"  \"nombre\": '',",
+											"  \"descripcion\": '',",
+											"  \"id_sistema_arai\": '',",
+											"  \"vigencia_inicio\": '',",
+											"}",
+											"",
+											"const body = JSON.parse(pm.request.body.raw);",
+											"const sameSchema = JSON.stringify(Object.keys(servicioSchema)) === JSON.stringify(Object.keys(body));",
+											"",
+											"pm.test('Estructura del body correcta', () => {",
+											"    pm.expect(true).to.be.eql(sameSchema);",
+											"}); ",
+											""
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "PUT",
+								"header": [],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n    \"nombre\": \"Servicio Modificado\",\n    \"descripcion\": \"Esto es un Servicio Modificado\",\n    \"id_sistema_arai\": \"_0e5589b482dc77e8d983db51d7ed2f36a9daaa11b4\",\n    \"vigencia_inicio\": \"2050-12-01\"\n}",
+									"options": {
+										"raw": {
+											"language": "json"
+										}
+									}
+								},
+								"url": {
+									"raw": "{{baseUrl}}/v1/servicios/{{idServicio}}",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"v1",
+										"servicios",
+										"{{idServicio}}"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "Inactivar un servicio",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"exec": [
+											"pm.test(\"Status code is 200\", () => {",
+											"    pm.response.to.have.status(200);",
+											"});",
+											"",
+											"const servicioSchema =  {  ",
+											"  \"vigencia_fin\": '',",
+											"}",
+											"",
+											"const body = JSON.parse(pm.request.body.raw);",
+											"const sameSchema = JSON.stringify(Object.keys(servicioSchema)) === JSON.stringify(Object.keys(body));",
+											"",
+											"pm.test('Estructura del body correcta', () => {",
+											"    pm.expect(true).to.be.eql(sameSchema);",
+											"}); "
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "PUT",
+								"header": [],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"vigencia_fin\": \"2023-01-01\"\n}",
+									"options": {
+										"raw": {
+											"language": "json"
+										}
+									}
+								},
+								"url": {
+									"raw": "{{baseUrl}}/v1/servicios/{{idServicio}}/baja",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"v1",
+										"servicios",
+										"{{idServicio}}",
+										"baja"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "Obtener servicios",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"exec": [
+											"pm.test(\"Status code is 200\", () => {",
+											"    pm.response.to.have.status(200);",
+											"});",
+											"",
+											"const response = pm.response.json();",
+											"",
+											"pm.test(\"Se obtuvieron servicios \", function () {",
+											"    // Si existe al menos 1 servicio",
+											"    pm.expect(response.length).to.not.eql(0);",
+											"",
+											"    if(response.length > 0){",
+											"        const hasId = Object.keys(response[0]).includes('id_servicio');",
+											"",
+											"        // Si el servicio tiene un id_servicio",
+											"        pm.expect(true).to.be.eql(hasId);",
+											"    }",
+											"  ",
+											"});"
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/v1/servicios",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"v1",
+										"servicios"
+									]
+								}
+							},
+							"response": []
+						}
+					]
+				}
+			]
+		}
+	],
+	"auth": {
+		"type": "basic",
+		"basic": [
+			{
+				"key": "password",
+				"value": "admin",
+				"type": "string"
+			},
+			{
+				"key": "username",
+				"value": "admin",
+				"type": "string"
+			}
+		]
+	},
+	"event": [
+		{
+			"listen": "prerequest",
+			"script": {
+				"type": "text/javascript",
+				"exec": [
+					""
+				]
+			}
+		},
+		{
+			"listen": "test",
+			"script": {
+				"type": "text/javascript",
+				"exec": [
+					""
+				]
+			}
+		}
+	],
+	"variable": [
+		{
+			"key": "baseUrl",
+			"value": "http://localhost:9002/api"
+		}
+	]
+}
\ No newline at end of file
diff --git a/api-backend/www/api.php b/api-backend/www/api.php
index 4ffb60d8..a2786a22 100644
--- a/api-backend/www/api.php
+++ b/api-backend/www/api.php
@@ -21,7 +21,7 @@ $settings = array(
     'prefijo_controladores' => '',
     'api_version' => $api_version,
     'api_titulo' => 'Tupa (backend)',
-    'url_protegida' => '/pases|registros|sedes|terminos-condiciones|visitantes|visitas/',
+    'url_protegida' => '/pases|registros|sedes|terminos-condiciones|visitantes|visitas|servicios|unidades-gestion/',
     'debug' => true,
 );
 
diff --git a/core/src/UNAM/Tupa/Core/Manager/ManagerServicio.php b/core/src/UNAM/Tupa/Core/Manager/ManagerServicio.php
new file mode 100644
index 00000000..dc9856b4
--- /dev/null
+++ b/core/src/UNAM/Tupa/Core/Manager/ManagerServicio.php
@@ -0,0 +1,152 @@
+<?php declare(strict_types=1);
+
+namespace UNAM\Tupa\Core\Manager;
+
+use UNAM\Tupa\Core\Errors\ErrorTupa;
+use UNAM\Tupa\Core\Filtros\Filtro;
+use UNAM\Tupa\Core\Negocio\SolicitudesServicios\Servicio;
+
+class ManagerServicio extends Manager
+{
+    /**
+     * @param $id
+     * @return Servcio|null
+     * @throws ErrorTupa No se pudo recuperar el servicio
+     */
+    public function getServicio($id)
+    {
+        $params = [
+            "id" => $id
+        ];
+
+        $sql = "SELECT id_servicio, nombre, descripcion, id_sistema_arai, vigencia_inicio,vigencia_fin "
+            . "FROM servicios s "
+            . "WHERE id_servicio = :id";
+
+        $result = $this->db->sentencia_consultar_fila($sql, $params);
+
+        if (!$result) {
+            throw new ErrorTupa("No se pudo recuperar el servicio '$id'");
+        }
+
+        return $this->hidratarServicio($result);
+    }
+
+     /**
+     * @param Filtro|null $filtro
+     * @param bool $hidratar
+     * @return Servicio[]
+     */
+    public function getServicios(Filtro $filtro = null, $hidratar = true)
+    {
+        $where = $this->getSqlWhere($filtro);
+        $orderBy = $this->getSqlOrderBy($filtro);
+        $limit = $this->getSqlLimit($filtro);
+
+        $sql = sprintf("
+            SELECT	
+                s.id_servicio, s.nombre, s.descripcion, s.id_sistema_arai, s.vigencia_inicio,s.vigencia_fin
+            FROM 
+                 servicios s
+            %s
+            %s
+            %s;", $where, $orderBy, $limit);
+
+        $result = $this->db->consultar($sql);
+        
+        if ($hidratar) {
+            $result = $this->hidratarServicios($result);
+        }
+
+        return $result;
+    }
+
+     /**
+     * @param Servicio $servicio
+     * @return bool|string
+     * @throws ErrorTupa No se pudo crear el servicio
+     */
+    public function crear(Servicio $servicio)
+    {
+        $sql = "INSERT INTO servicios (nombre, descripcion, id_sistema_arai,vigencia_inicio) VALUES (:nombre, :descripcion, :id_sistema_arai,:vigencia_inicio)";
+
+        $sqlParams = [
+            "nombre" => $servicio->getNombre(),
+            "descripcion" => $servicio->getDescripcion(),
+            "id_sistema_arai" => $servicio->getIdSistemaArai(),
+            "vigencia_inicio" => $servicio->getVigenciaInicio()
+        ];
+
+        $result = $this->db->sentencia_ejecutar($sql, $sqlParams);
+        
+        if (!$result) {
+            throw new ErrorTupa("No se pudo crear el servicio");
+        }
+
+        $sql = "SELECT id_servicio FROM servicios 
+                WHERE nombre = :nombre
+                AND descripcion = :descripcion
+                AND vigencia_inicio = :vigencia_inicio
+                AND id_sistema_arai = :id_sistema_arai";
+   
+        return $this->db->sentencia_consultar_fila($sql, $sqlParams);
+    }
+
+    /**
+     * @param Servicio $servicio
+     * @return mixed
+     */
+    public function actualizar($servicio)
+    {
+        $params = [
+            "nombre" => $servicio->getNombre(),
+            "descripcion" => $servicio->getDescripcion(),
+            "id_sistema_arai" => $servicio->getIdSistemaArai(),
+            "vigencia_inicio" => $servicio->getVigenciaInicio(),
+            "vigencia_fin" => $servicio->getVigenciaFin(),
+            "id" => $servicio->getId(),
+        ];
+
+        $sql = "UPDATE servicios
+                SET    nombre = :nombre,
+                       descripcion = :descripcion,
+                       id_sistema_arai = :id_sistema_arai,
+                       vigencia_inicio = :vigencia_inicio,
+                       vigencia_fin = :vigencia_fin
+                WHERE  id_servicio = :id";
+
+        return $this->db->sentencia_ejecutar($sql, $params);
+    }
+
+    /**
+     * @param $datos
+     * @return Servicio
+     */
+    protected function hidratarServicio($datos)
+    {
+        $servicio = new Servicio();
+        
+        $servicio->loadFromDatos($datos);
+
+        return $servicio;
+    }
+
+    /**
+     * @param $datos
+     * @return Servicio[] colección de Servicios
+     */
+    protected function hidratarServicios($datos)
+    {
+        $servicios = [];
+
+        if (count($datos) < 1) {
+            return $servicios;
+        }
+
+        foreach ($datos as $dato) {
+            $servicios[] = $this->hidratarServicio($dato);
+        }
+        
+        return $servicios;
+    }
+}
diff --git a/core/src/UNAM/Tupa/Core/Negocio/SolicitudesServicios/Servicio.php b/core/src/UNAM/Tupa/Core/Negocio/SolicitudesServicios/Servicio.php
new file mode 100644
index 00000000..66ee2d10
--- /dev/null
+++ b/core/src/UNAM/Tupa/Core/Negocio/SolicitudesServicios/Servicio.php
@@ -0,0 +1,100 @@
+<?php  declare(strict_types=1);
+
+namespace UNAM\Tupa\Core\Negocio\SolicitudesServicios;
+
+class Servicio {
+    private int $idServicio;
+    private string $nombre;
+    private ?string $descripcion;
+    private string $idServicioArai;
+    private string $vigenciaInicio;
+    private ?string $vigenciaFin;
+
+    public function getId():int {
+        return $this->idServicio;
+    }
+
+    public function getNombre():string {
+        return $this->nombre;
+    }
+
+    public function getDescripcion():string {
+        return $this->descripcion;
+    }
+
+    public function getIdSistemaArai():string {
+        return $this->idServicioArai;
+    }
+
+    public function getVigenciaInicio():string {
+        return $this->vigenciaInicio;
+    }
+
+    public function getVigenciaFin():?string {
+        return $this->vigenciaFin ?? null;
+    }
+
+    public function setId(int $idServicio):void {
+        $this->idServicio = $idServicio;
+    }
+
+    public function setNombre(string $nombre):void {
+        $this->nombre = $nombre;
+    }
+
+    public function setDescripcion(?string $descripcion):void {
+        $this->descripcion = $descripcion;
+    }
+
+    public function setIdServicioArai(string $idServicioArai):void {
+        $this->idServicioArai = $idServicioArai;
+    }
+
+    public function setVigenciaInicio(string $vigenciaInicio):void {
+        $this->vigenciaInicio = $vigenciaInicio;
+    }
+
+    public function setVigenciaFin(?string $vigenciaFin):void {
+        $this->vigenciaFin = $vigenciaFin;
+    }
+
+    /**
+     * Hidrata los atributos del objeto a partir de $datos
+     *
+     * @param array $datos inicializar directamente con un set de datos
+     */
+    public function loadFromDatos(array $datos)
+    {
+        if (isset($datos['id_servicio'])) {
+            $this->setId($datos['id_servicio']);
+        }
+        if (isset($datos['nombre'])) {
+            $this->setNombre($datos['nombre']);
+        }
+        if (isset($datos['descripcion'])) {
+            $this->setDescripcion($datos['descripcion']);
+        }
+        if (isset($datos['id_sistema_arai'])) {
+            $this->setIdServicioArai($datos['id_sistema_arai']);
+        }
+        if (isset($datos['vigencia_inicio'])) {
+            $this->setVigenciaInicio($datos['vigencia_inicio']);
+        }
+        if (isset($datos['vigencia_fin'])) {
+            $this->setVigenciaFin($datos['vigencia_fin']);
+        }
+    }
+
+    public function toArray(): array
+    {
+        return [
+            'id_servicio' => $this->getId(),
+            'nombre' => $this->getNombre(),
+            'descripcion' => $this->getDescripcion(),
+            'id_sistema_arai' => $this->getIdSistemaArai(),
+            'vigencia_inicio' => $this->getVigenciaInicio(),
+            'vigencia_fin' => $this->getVigenciaFin()
+        ];
+    }
+
+}
\ No newline at end of file
-- 
GitLab


From a2c5d637de11e9c716f9c6c2be0ef0608ebc158c Mon Sep 17 00:00:00 2001
From: "luciano.cassettai" <luciano.cassettai@unam.edu.ar>
Date: Thu, 16 Feb 2023 07:58:06 -0300
Subject: [PATCH 03/11] ADD get de servicios por unidad de gestion

---
 .../v1/unidades_gestion/unidades_gestion.php  | 72 ++++++++++++++++++-
 .../Tupa/Core/Manager/ManagerServicio.php     | 21 ++++++
 .../Negocio/Organizacion/UnidadGestion.php    | 15 +++-
 3 files changed, 105 insertions(+), 3 deletions(-)

diff --git a/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
index 8154c4b2..cc8533c3 100644
--- a/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
+++ b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
@@ -5,6 +5,7 @@ use SIUToba\rest\lib\rest_hidratador;
 use SIUToba\rest\lib\rest_validador;
 use SIUToba\rest\rest;
 
+use UNAM\Tupa\Core\Errors\ErrorTupa;
 use UNAM\Tupa\Core\Errors\UnidadGestionError;
 use UNAM\Tupa\Backend\API\Factory;
 use UNAM\Tupa\Core\Filtros\Filtro;
@@ -40,6 +41,42 @@ class unidades_gestion implements SIUToba\rest\lib\modelable
             
         );
 
+        $servicios = [
+            'id_servicio' => array(
+                'type' => 'string',
+            ),
+            'nombre' => array(
+                'type' => 'string',
+                'required' => true,
+                '_validar' => array(
+                    rest_validador::OBLIGATORIO,
+                ),
+            ),
+            'descripcion' => array(
+                'type' => 'string',
+                'required' => true,
+                '_validar' => array(
+                    rest_validador::OBLIGATORIO,
+                ),
+            ),
+            'id_sistema_arai' => array(
+                'type' => 'string',
+                'required' => true,
+                '_validar' => array(
+                    rest_validador::OBLIGATORIO,
+                ),
+            ),
+            'vigencia_inicio' => [
+                'type' => 'date',
+                '_validar' => [
+                    rest_validador::OBLIGATORIO,
+                    rest_validador::TIPO_DATE => [
+                        'format' => 'Y-m-d'
+                    ],
+                ],
+            ]   
+        ];
+
         $unidadGestion = array_merge(
             $unidadGestionEdit,
             array(
@@ -53,6 +90,7 @@ class unidades_gestion implements SIUToba\rest\lib\modelable
         return $models = array(
             'UnidadGestion' => $unidadGestion,
             'UnidadGestionEdit' => $unidadGestionEdit,
+            'Servicios' => $servicios,
         );
     }
 
@@ -129,6 +167,38 @@ class unidades_gestion implements SIUToba\rest\lib\modelable
         }
     }
 
+     /**
+     * Se consume en GET /unidades-gestion/{id}/servicios.
+     *
+     * @summary Retorna los servicios asociados a una unidad de gestion
+     * @responses 200 {"$ref": "UnidadGestion"} UnidadGestion
+     * @responses 404 No existe el recurso
+     * @responses 500 Error interno del servidor
+     */
+    public function get_servicios_list($id)
+    {
+        try {            
+            $manager = Factory::getManagerServicio();
+
+            $servicios = $manager->getServiciosUnidadGestion($id);
+           
+            $resultados = [];
+            foreach ($servicios as $s) {
+                $resultados[] = $s->toArray();
+            }
+            
+            $registros = rest_hidratador::hidratar($this->get_spec_usuario('Servicios'), $resultados);
+
+            rest::response()->get($registros);
+        }catch (ErrorTupa $e) {
+            Factory::getMainLogger()->error($e->getMessage());
+            rest::response()->not_found($e->getMessage());
+        }catch (Exception $e) {
+            Factory::getMainLogger()->error($e->getMessage());
+            rest::response()->error_negocio('Error interno del servidor', 500);
+        }
+    }
+
      /**
      * Se consume en POST /unidades-gestion
      *
@@ -267,5 +337,3 @@ class unidades_gestion implements SIUToba\rest\lib\modelable
         return Factory::getVarios()->arrayToUtf8($resultado);
     }
 }
-
-?>
\ No newline at end of file
diff --git a/core/src/UNAM/Tupa/Core/Manager/ManagerServicio.php b/core/src/UNAM/Tupa/Core/Manager/ManagerServicio.php
index dc9856b4..e557caa1 100644
--- a/core/src/UNAM/Tupa/Core/Manager/ManagerServicio.php
+++ b/core/src/UNAM/Tupa/Core/Manager/ManagerServicio.php
@@ -117,6 +117,27 @@ class ManagerServicio extends Manager
 
         return $this->db->sentencia_ejecutar($sql, $params);
     }
+    
+    public function getServiciosUnidadGestion($id_unidad){
+        $params = [
+            "id" => $id_unidad
+        ];
+
+        $sql = "SELECT s.id_servicio,s.nombre,descripcion,id_sistema_arai,vigencia_inicio,vigencia_fin
+                FROM UNIDADES_GESTION UG
+                INNER JOIN SERVICIOS_UNIDAD_GESTION SUG ON UG.ID_UNIDAD = SUG.ID_UNIDAD
+                INNER JOIN SERVICIOS S ON S.ID_SERVICIO = SUG.ID_SERVICIO
+                WHERE ug.id_unidad = :id";
+
+
+        $result = $this->db->sentencia_consultar($sql,$params);
+
+        if (!$result) {
+            throw new ErrorTupa("No se pudo recuperar la unidad de gestion '$id_unidad'");
+        }
+
+        return $this->hidratarServicios($result);
+    }
 
     /**
      * @param $datos
diff --git a/core/src/UNAM/Tupa/Core/Negocio/Organizacion/UnidadGestion.php b/core/src/UNAM/Tupa/Core/Negocio/Organizacion/UnidadGestion.php
index d2b2d034..a269a02a 100644
--- a/core/src/UNAM/Tupa/Core/Negocio/Organizacion/UnidadGestion.php
+++ b/core/src/UNAM/Tupa/Core/Negocio/Organizacion/UnidadGestion.php
@@ -9,6 +9,7 @@ class UnidadGestion {
     private string $sigla;
     private string $nombre;
     private string $idGrupoArai;
+    private ?array $servicios;
 
     public function getId():int{
         return $this->id;
@@ -26,6 +27,10 @@ class UnidadGestion {
         return $this->idGrupoArai;
     }
 
+    public function getServicios():?array{
+        return $this->servicios ?? null;
+    }
+
     public function setId(int $id){
         $this->id = $id;
     }
@@ -42,6 +47,10 @@ class UnidadGestion {
         $this->idGrupoArai = $idGrupo;
     }
 
+    public function setServicios(?array $servicios){
+        $this->servicios = $servicios;
+    }
+
     /**
      * Hidrata los atributos del objeto a partir de $datos
      *
@@ -61,6 +70,9 @@ class UnidadGestion {
         if (isset($datos['id_grupo_arai'])) {
             $this->setIdGrupoArai($datos['id_grupo_arai']);
         }
+        if (isset($datos['servicios'])) {
+            $this->setServicios($datos['servicios']);
+        }
     }
     
     /**
@@ -72,7 +84,8 @@ class UnidadGestion {
             'id_unidad' => $this->getId(),
             'sigla' => $this->getSigla(),
             'nombre' => $this->getNombre(),
-            'id_grupo_arai' => $this->getIdGrupoArai()
+            'id_grupo_arai' => $this->getIdGrupoArai(),
+            'servicios' => $this->getServicios(),
         ];
     }
 }
-- 
GitLab


From e957d29c48616480e6f828c8737712dd18cc9f81 Mon Sep 17 00:00:00 2001
From: Fernando Alvez <fernando.alvez@unam.edu.ar>
Date: Wed, 1 Mar 2023 12:03:38 -0300
Subject: [PATCH 04/11] ADD test postman de unidad gestion

---
 ...nidad_gestion_test.postman_collection.json | 306 ++++++++++++++++++
 1 file changed, 306 insertions(+)
 create mode 100644 api-backend/tests/Integration/v1/collections/unidad-gestion/tupa_api_backend_unidad_gestion_test.postman_collection.json

diff --git a/api-backend/tests/Integration/v1/collections/unidad-gestion/tupa_api_backend_unidad_gestion_test.postman_collection.json b/api-backend/tests/Integration/v1/collections/unidad-gestion/tupa_api_backend_unidad_gestion_test.postman_collection.json
new file mode 100644
index 00000000..a6b0a0c2
--- /dev/null
+++ b/api-backend/tests/Integration/v1/collections/unidad-gestion/tupa_api_backend_unidad_gestion_test.postman_collection.json
@@ -0,0 +1,306 @@
+{
+	"info": {
+		"_postman_id": "f03eb109-43a9-4bb8-8164-50d7134ca6bd",
+		"name": "Tupa API Backend Unidades Gestion",
+		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
+		"_exporter_id": "16366966"
+	},
+	"item": [
+		{
+			"name": "V1",
+			"item": [
+				{
+					"name": "Unidad Gestión",
+					"item": [
+						{
+							"name": "Crear unidad de gestión",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"exec": [
+											"pm.test(\"Status code is 201\", () => {",
+											"    pm.response.to.have.status(201);",
+											"});",
+											"",
+											"const servicioSchema =  {  ",
+											"  \"sigla\": '',",
+											"  \"nombre\": '',",
+											"  \"id_grupo_arai\": ''",
+											"}",
+											"",
+											"const body = JSON.parse(pm.request.body.raw);",
+											"const sameSchema = JSON.stringify(Object.keys(servicioSchema)) === JSON.stringify(Object.keys(body));",
+											"",
+											"pm.test('Estructura del body correcta', () => {",
+											"    pm.expect(true).to.be.eql(sameSchema);",
+											"}); ",
+											"",
+											"var respuesta = pm.response.json();",
+											"",
+											"pm.test(\"unidad de gestion creada con ID \" + respuesta.id_unidad, function () {",
+											"    pm.globals.set(\"idUnidadGestion\", respuesta.id_unidad);",
+											"});"
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "POST",
+								"header": [],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"sigla\": \"{{$randomWord}}\",\n  \"nombre\": \"{{$randomWords}}\",\n  \"id_grupo_arai\": \"{{$randomUUID}}\"\n}",
+									"options": {
+										"raw": {
+											"language": "json"
+										}
+									}
+								},
+								"url": {
+									"raw": "{{baseUrl}}/v1/unidades-gestion",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"v1",
+										"unidades-gestion"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "Obtener unidad de gestión",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"exec": [
+											"pm.test(\"Status code is 200\", () => {",
+											"    pm.response.to.have.status(200);",
+											"});",
+											"",
+											"/*",
+											"const response = pm.response.json();",
+											"const hasId = Object.keys(response).includes('id_unidad');",
+											"",
+											"pm.test(\"Servicio con id_unidad \" + response.id_unidad, function () {",
+											"    pm.expect(true).to.be.eql(hasId);",
+											"});",
+											"*/"
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/v1/unidades-gestion/{{idUnidadGestion}}",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"v1",
+										"unidades-gestion",
+										"{{idUnidadGestion}}"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "Modificar unidad de gestión",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"exec": [
+											"pm.test(\"Status code is 200\", () => {",
+											"    pm.response.to.have.status(200);",
+											"});",
+											"",
+											"const unidadGestionSchema =  {",
+											"  \"sigla\": \"\",",
+											"  \"nombre\": \"\",",
+											"  \"id_grupo_arai\": \"\"",
+											"}",
+											"",
+											"const body = JSON.parse(pm.request.body.raw);",
+											"const sameSchema = JSON.stringify(Object.keys(unidadGestionSchema)) === JSON.stringify(Object.keys(body));",
+											"",
+											"pm.test('Estructura del body correcta', () => {",
+											"    pm.expect(true).to.be.eql(sameSchema);",
+											"}); ",
+											""
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "PUT",
+								"header": [],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"sigla\": \"UG-MOD\",\n  \"nombre\": \"UG Modificada\",\n  \"id_grupo_arai\": \"123456789\"\n}",
+									"options": {
+										"raw": {
+											"language": "json"
+										}
+									}
+								},
+								"url": {
+									"raw": "{{baseUrl}}/v1/unidades-gestion/{{idUnidadGestion}}",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"v1",
+										"unidades-gestion",
+										"{{idUnidadGestion}}"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "Obtener servicios asociados a una unidad de gestion",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"exec": [
+											"pm.test(\"Status code is 200\", () => {",
+											"    pm.response.to.have.status(200);",
+											"});",
+											"",
+											"pm.test(\"Se obtuvieron servicios \", function () {",
+											"    var jsonData = pm.response.json();",
+											"    pm.expect(jsonData.length).to.be.above(0);",
+											"",
+											"    const hasId = Object.keys(jsonData[0]).includes('id_servicio');",
+											"    // Si el servicio tiene un id_servicio",
+											"    pm.expect(true).to.be.eql(hasId);",
+											"});"
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/v1/unidades-gestion/{{idUnidadGestion}}/servicios",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"v1",
+										"unidades-gestion",
+										"{{idUnidadGestion}}",
+										"servicios"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "Elimina una unidad de gestion",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"exec": [
+											"pm.test(\"Status code is 200\", () => {",
+											"    pm.response.to.have.status(200);",
+											"});",
+											"",
+											"var respuesta = pm.response.json();",
+											"",
+											"pm.test(\"Eliminado correctamente\", function () {",
+											"    var jsonData = pm.response.json();",
+											"    pm.expect(jsonData.respuesta).to.eql(true);",
+											"});"
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "DELETE",
+								"header": [],
+								"body": {
+									"mode": "raw",
+									"raw": "",
+									"options": {
+										"raw": {
+											"language": "json"
+										}
+									}
+								},
+								"url": {
+									"raw": "{{baseUrl}}/v1/unidades-gestion/{{idUnidadGestion}}",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"v1",
+										"unidades-gestion",
+										"{{idUnidadGestion}}"
+									]
+								}
+							},
+							"response": []
+						}
+					]
+				}
+			]
+		}
+	],
+	"auth": {
+		"type": "basic",
+		"basic": [
+			{
+				"key": "password",
+				"value": "admin",
+				"type": "string"
+			},
+			{
+				"key": "username",
+				"value": "admin",
+				"type": "string"
+			}
+		]
+	},
+	"event": [
+		{
+			"listen": "prerequest",
+			"script": {
+				"type": "text/javascript",
+				"exec": [
+					""
+				]
+			}
+		},
+		{
+			"listen": "test",
+			"script": {
+				"type": "text/javascript",
+				"exec": [
+					""
+				]
+			}
+		}
+	],
+	"variable": [
+		{
+			"key": "baseUrl",
+			"value": "http://localhost:9002/api"
+		}
+	]
+}
\ No newline at end of file
-- 
GitLab


From 193eb10089a73da22dc35285e66aaf439eb6b9aa Mon Sep 17 00:00:00 2001
From: "luciano.cassettai" <luciano.cassettai@unam.edu.ar>
Date: Wed, 22 Feb 2023 10:31:33 -0300
Subject: [PATCH 05/11] Fix : agrega vigencia fin al endpoint "unidades-gestion
 servicios"

---
 .../v1/unidades_gestion/unidades_gestion.php  | 23 ++++---------------
 1 file changed, 4 insertions(+), 19 deletions(-)

diff --git a/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
index cc8533c3..33d1ff9f 100644
--- a/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
+++ b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
@@ -47,34 +47,19 @@ class unidades_gestion implements SIUToba\rest\lib\modelable
             ),
             'nombre' => array(
                 'type' => 'string',
-                'required' => true,
-                '_validar' => array(
-                    rest_validador::OBLIGATORIO,
-                ),
             ),
             'descripcion' => array(
                 'type' => 'string',
-                'required' => true,
-                '_validar' => array(
-                    rest_validador::OBLIGATORIO,
-                ),
             ),
             'id_sistema_arai' => array(
                 'type' => 'string',
-                'required' => true,
-                '_validar' => array(
-                    rest_validador::OBLIGATORIO,
-                ),
             ),
             'vigencia_inicio' => [
                 'type' => 'date',
-                '_validar' => [
-                    rest_validador::OBLIGATORIO,
-                    rest_validador::TIPO_DATE => [
-                        'format' => 'Y-m-d'
-                    ],
-                ],
-            ]   
+            ],
+            'vigencia_fin' => [
+                'type' => 'date',
+            ]     
         ];
 
         $unidadGestion = array_merge(
-- 
GitLab


From ba8ef0cd6dcc4e003861a5cab1722800d3b66b21 Mon Sep 17 00:00:00 2001
From: "luciano.cassettai" <luciano.cassettai@unam.edu.ar>
Date: Thu, 2 Mar 2023 10:27:47 -0300
Subject: [PATCH 06/11] Agrega filtro de servicios por unidad de gestion

---
 .../API/Endpoints/v1/servicios/servicios.php   | 18 ++++++++++--------
 .../UNAM/Tupa/Core/Manager/ManagerServicio.php | 13 +++++++++----
 2 files changed, 19 insertions(+), 12 deletions(-)

diff --git a/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/servicios/servicios.php b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/servicios/servicios.php
index 75effba3..e3b3e8cf 100644
--- a/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/servicios/servicios.php
+++ b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/servicios/servicios.php
@@ -17,7 +17,7 @@ class servicios implements SIUToba\rest\lib\modelable
     {
         $vigenciaFin = [
             'vigencia_fin' => [
-                'type' => 'string',
+                'type' => 'date',
                 '_validar' => [
                     rest_validador::TIPO_DATE => [
                         'format' => 'Y-m-d'
@@ -60,13 +60,13 @@ class servicios implements SIUToba\rest\lib\modelable
         );
 
         $servicio = array_merge(
-            $servicioEdit,
-            $vigenciaFin,
             array(
                 'id_servicio' => array(
                     'type' => 'integer',
                 ),
-            )
+            ),
+            $servicioEdit,
+            $vigenciaFin
         );
         
 
@@ -121,6 +121,8 @@ class servicios implements SIUToba\rest\lib\modelable
      * @param_query $id_sistema_arai string Se define como 'condicion;valor' donde 'condicion' puede ser es_menor_que|es_menor_igual_que|es_igual_a|es_distinto_de|es_mayor_igual_que|es_mayor_que|entre
      * @param_query $vigencia_inicio string Se define como 'condicion;valor' donde 'condicion' puede ser es_menor_que|es_menor_igual_que|es_igual_a|es_distinto_de|es_mayor_igual_que|es_mayor_que|entre
      * @param_query $vigencia_fin string Se define como 'condicion;valor' donde 'condicion' puede ser es_menor_que|es_menor_igual_que|es_igual_a|es_distinto_de|es_mayor_igual_que|es_mayor_que|entre
+     * @param_query $unidad_gestion string Se define como 'condicion;valor' donde 'condicion' puede ser es_menor_que|es_menor_igual_que|es_igual_a|es_distinto_de|es_mayor_igual_que|es_mayor_que|entre
+  
      *
      * @param_query $limit integer Limitar a esta cantidad de servicios
      * @param_query $page integer Limitar desde esta pagina
@@ -137,7 +139,6 @@ class servicios implements SIUToba\rest\lib\modelable
             $filtro->setOrder(rest::request()->get('order', null));
 
             $manager = Factory::getManagerServicio();
-
             $servicios = $manager->getServicios($filtro);
 
             $resultados = [];            
@@ -161,7 +162,7 @@ class servicios implements SIUToba\rest\lib\modelable
      * @summary Crea un nuevo servicio
      * @param_body $servicios ServicioEdit [required] los datos del servicio
      * @responses 201 {"string"} identificador de un servicio
-     * @responses 400 Servicio inválido
+     * @responses 400 Servicio inválido
      * @responses 500 Error interno
      */
     public function post_list()
@@ -200,7 +201,7 @@ class servicios implements SIUToba\rest\lib\modelable
      * @summary Modifica un servicio
      * @param_body $ServicioEdit ServicioEdit  [required] los datos a editar de un servicio
      * @responses 204 El id es correcto
-     * @responses 400 El id no válido
+     * @responses 400 El id no válido
      * @responses 404 No existe el recurso
      * @responses 500 Error interno
      */
@@ -279,7 +280,8 @@ class servicios implements SIUToba\rest\lib\modelable
         $filtro->agregarCampoRest('s.id_sistema_arai', rest::request()->get('id_sistema_arai', null));
         $filtro->agregarCampoRest('s.vigencia_inicio', rest::request()->get('vigencia_inicio', null));
         $filtro->agregarCampoRest('s.vigencia_fin', rest::request()->get('vigencia_fin', null));
-
+        $filtro->agregarCampoRest('ug.id_unidad', rest::request()->get('unidad_gestion', null));
+        
         $filtro->agregarCampoOrdenable('nombre');
         $filtro->agregarCampoOrdenable('vigencia_inicio');
 
diff --git a/core/src/UNAM/Tupa/Core/Manager/ManagerServicio.php b/core/src/UNAM/Tupa/Core/Manager/ManagerServicio.php
index e557caa1..00247292 100644
--- a/core/src/UNAM/Tupa/Core/Manager/ManagerServicio.php
+++ b/core/src/UNAM/Tupa/Core/Manager/ManagerServicio.php
@@ -44,10 +44,15 @@ class ManagerServicio extends Manager
         $limit = $this->getSqlLimit($filtro);
 
         $sql = sprintf("
-            SELECT	
-                s.id_servicio, s.nombre, s.descripcion, s.id_sistema_arai, s.vigencia_inicio,s.vigencia_fin
-            FROM 
-                 servicios s
+        SELECT S.ID_SERVICIO,
+               S.NOMBRE,
+               S.DESCRIPCION,
+               S.ID_SISTEMA_ARAI,
+               S.VIGENCIA_INICIO,
+               S.VIGENCIA_FIN
+        FROM SERVICIOS S
+        LEFT JOIN SERVICIOS_UNIDAD_GESTION SUG ON S.ID_SERVICIO = SUG.ID_SERVICIO
+        LEFT JOIN UNIDADES_GESTION UG ON SUG.ID_UNIDAD = UG.ID_UNIDAD
             %s
             %s
             %s;", $where, $orderBy, $limit);
-- 
GitLab


From a8f74cb3ab9ef4010db48ba80b95c73a962a3843 Mon Sep 17 00:00:00 2001
From: "luciano.cassettai" <luciano.cassettai@unam.edu.ar>
Date: Wed, 8 Mar 2023 08:43:14 -0300
Subject: [PATCH 07/11] Permite agregar servicios para una unidad de gestion

---
 .../v1/unidades_gestion/unidades_gestion.php  | 62 +++++++++++++++++--
 .../Core/Manager/ManagerUnidadGestion.php     | 31 ++++++++++
 2 files changed, 87 insertions(+), 6 deletions(-)

diff --git a/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
index 33d1ff9f..1b09f10a 100644
--- a/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
+++ b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
@@ -41,10 +41,10 @@ class unidades_gestion implements SIUToba\rest\lib\modelable
             
         );
 
-        $servicios = [
-            'id_servicio' => array(
-                'type' => 'string',
-            ),
+        $servicio = [
+            'id_servicio' => [
+                'type' => 'integer'
+            ],
             'nombre' => array(
                 'type' => 'string',
             ),
@@ -62,6 +62,23 @@ class unidades_gestion implements SIUToba\rest\lib\modelable
             ]     
         ];
 
+        $servicios = [
+            'servicios' => [
+                'type' => 'array',
+                'required' => true,
+                '_validar' => [ 
+                    rest_validador::OBLIGATORIO,
+                ],
+                'items' => [
+                    'type' => 'integer',
+                    'required' => true,
+                    '_validar' => [
+                        rest_validador::OBLIGATORIO,
+                    ]]
+            ]
+        ];
+
+
         $unidadGestion = array_merge(
             $unidadGestionEdit,
             array(
@@ -75,7 +92,8 @@ class unidades_gestion implements SIUToba\rest\lib\modelable
         return $models = array(
             'UnidadGestion' => $unidadGestion,
             'UnidadGestionEdit' => $unidadGestionEdit,
-            'Servicios' => $servicios,
+            'Servicio' => $servicio,
+            'Servicios' => $servicios
         );
     }
 
@@ -172,7 +190,7 @@ class unidades_gestion implements SIUToba\rest\lib\modelable
                 $resultados[] = $s->toArray();
             }
             
-            $registros = rest_hidratador::hidratar($this->get_spec_usuario('Servicios'), $resultados);
+            $registros = rest_hidratador::hidratar($this->get_spec_usuario('Servicio'), $resultados);
 
             rest::response()->get($registros);
         }catch (ErrorTupa $e) {
@@ -184,6 +202,38 @@ class unidades_gestion implements SIUToba\rest\lib\modelable
         }
     }
 
+     /**
+     * Se consume en POST /unidades-gestion/{id}/servicios.
+     *
+     * @summary Asocia distintos servicios a una unidad de gestion
+     * @param_body $servicios Servicios [required] array de ids de los servicios
+     * @responses 201 {"boolean"} exito
+     * @responses 400 unidad de gestion inválida
+     * @responses 500 Error interno
+     */
+    public function post_servicios_list($id)
+    {
+        // Valido y traduzco los datos al formato de mi modelo
+        $servicios = $this->procesar_input_edicion('Servicios');
+
+        try {
+            $manager = Factory::getManagerUnidadGestion();
+
+            $resultado = $manager->agregarServicios($id,$servicios);
+
+            rest::response()->post([ "respuesta" => $resultado ]);
+        } 
+        catch (UnidadGestionError $e) {
+            rest::response()->error_negocio([$e->getMessage()], 400);
+        } catch (Exception $e) {
+            if ($e->getCode() >= 400 || $e->getCode() < 500) {
+                rest::response()->error_negocio([$e->getMessage()], 400);
+            } else {
+                rest::response()->error_negocio([$e->getMessage()], 500);
+            }
+        }
+    }
+
      /**
      * Se consume en POST /unidades-gestion
      *
diff --git a/core/src/UNAM/Tupa/Core/Manager/ManagerUnidadGestion.php b/core/src/UNAM/Tupa/Core/Manager/ManagerUnidadGestion.php
index bf00c59a..64dd929e 100644
--- a/core/src/UNAM/Tupa/Core/Manager/ManagerUnidadGestion.php
+++ b/core/src/UNAM/Tupa/Core/Manager/ManagerUnidadGestion.php
@@ -5,6 +5,7 @@ namespace UNAM\Tupa\Core\Manager;
 use UNAM\Tupa\Core\Errors\UnidadGestionError;
 use UNAM\Tupa\Core\Filtros\Filtro;
 use UNAM\Tupa\Core\Negocio\Organizacion\UnidadGestion;
+use UNAM\Tupa\Backend\API\Factory;
 
 class ManagerUnidadGestion extends Manager
 {
@@ -135,6 +136,36 @@ class ManagerUnidadGestion extends Manager
         return $result;
     }
 
+    /**
+     * @param int $idUnidad id de la unidad de gestion
+     * @param array $servicios id de los servicios que se van a asociar a la ug 
+     * @return bool
+     * @throws UnidadGestionError 
+     */
+    public function agregarServicios($idUnidad, $servicios)
+    {
+        try{            
+            $this->db->abrir_transaccion();
+            foreach($servicios['servicios'] as $idServicio){
+                $params = [
+                    'id_unidad_gestion' => $idUnidad,
+                    'id_servicio' => $idServicio
+                ];
+    
+                $sql = "INSERT INTO servicios_unidad_gestion VALUES (:id_unidad_gestion,:id_servicio)";
+
+                $this->db->sentencia_ejecutar($sql, $params);    
+            }
+            $this->db->cerrar_transaccion();
+            return true;            
+        }catch(\Exception $e){
+            $this->db->abortar_transaccion();
+            Factory::getMainLogger()->error($e->getMessage());
+            throw new UnidadGestionError("No se pudo agregar el servicio a la unidad $idUnidad");
+        }  
+        return false;     
+    }
+
     /**
      * @param $datos
      * @return UnidadGestion
-- 
GitLab


From 7db4c951f45ec7c7385f36f53f4d0b93ec7f1698 Mon Sep 17 00:00:00 2001
From: "luciano.cassettai" <luciano.cassettai@unam.edu.ar>
Date: Wed, 8 Mar 2023 09:36:40 -0300
Subject: [PATCH 08/11] Agrega get_list de unidad de gestion en las pruebas de
 postman

---
 ...nidad_gestion_test.postman_collection.json | 80 +++++++++++++++++--
 .../Tupa/Core/Manager/ManagerServicio.php     |  2 +-
 2 files changed, 75 insertions(+), 7 deletions(-)

diff --git a/api-backend/tests/Integration/v1/collections/unidad-gestion/tupa_api_backend_unidad_gestion_test.postman_collection.json b/api-backend/tests/Integration/v1/collections/unidad-gestion/tupa_api_backend_unidad_gestion_test.postman_collection.json
index a6b0a0c2..82f750ab 100644
--- a/api-backend/tests/Integration/v1/collections/unidad-gestion/tupa_api_backend_unidad_gestion_test.postman_collection.json
+++ b/api-backend/tests/Integration/v1/collections/unidad-gestion/tupa_api_backend_unidad_gestion_test.postman_collection.json
@@ -1,9 +1,9 @@
 {
 	"info": {
-		"_postman_id": "f03eb109-43a9-4bb8-8164-50d7134ca6bd",
+		"_postman_id": "414381a0-51a4-429f-a4c1-82e5a8754f14",
 		"name": "Tupa API Backend Unidades Gestion",
 		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
-		"_exporter_id": "16366966"
+		"_exporter_id": "25165965"
 	},
 	"item": [
 		{
@@ -180,11 +180,16 @@
 											"",
 											"pm.test(\"Se obtuvieron servicios \", function () {",
 											"    var jsonData = pm.response.json();",
-											"    pm.expect(jsonData.length).to.be.above(0);",
+											"    ",
+											"    if(jsonData.length > 0){",
+											"        pm.expect(jsonData.length).to.be.above(0);",
 											"",
-											"    const hasId = Object.keys(jsonData[0]).includes('id_servicio');",
-											"    // Si el servicio tiene un id_servicio",
-											"    pm.expect(true).to.be.eql(hasId);",
+											"        const hasId = Object.keys(jsonData[0]).includes('id_servicio');",
+											"        // Si el servicio tiene un id_servicio",
+											"        pm.expect(true).to.be.eql(hasId);",
+											"    }else{",
+											"        pm.expect(jsonData.length).to.be.equal(0);",
+											"    }    ",
 											"});"
 										],
 										"type": "text/javascript"
@@ -209,6 +214,69 @@
 							},
 							"response": []
 						},
+						{
+							"name": "Obtener unidades de gestion",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"exec": [
+											"pm.test(\"Status code is 200\", () => {",
+											"    pm.response.to.have.status(200);",
+											"});",
+											"",
+											"const jsonData = pm.response.json();",
+											"",
+											"pm.test(\"Obtener unidades de gestion \", function () {    ",
+											"    if(jsonData.length > 0){",
+											"        pm.expect(jsonData.length).to.be.above(0);",
+											"",
+											"        const hasId = Object.keys(jsonData[0]).includes('id_unidad');",
+											"        // Si el servicio tiene un id_servicio",
+											"        pm.expect(true).to.be.eql(hasId);",
+											"    }else{",
+											"        pm.expect(jsonData.length).to.be.equal(0);",
+											"    }    ",
+											"});",
+											"",
+											"",
+											"const plantillaUA =  {  ",
+											"  \"sigla\": '',",
+											"  \"nombre\": '',",
+											"  \"id_grupo_arai\": '',",
+											"  \"id_unidad\": ''",
+											"};",
+											"",
+											"pm.test('Estructura de la respuesta correcta', () => {",
+											"    if(jsonData.length > 0){",
+											"        const sameSchema = JSON.stringify(Object.keys(plantillaUA)) === JSON.stringify(Object.keys(jsonData[0]));",
+											"        pm.expect(true).to.be.eql(sameSchema);",
+											"    }else{",
+											"        const sameSchema = JSON.stringify([]) === JSON.stringify(jsonData);",
+											"        pm.expect(true).to.be.eql(sameSchema);",
+											"    }  ",
+											"}); "
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/v1/unidades-gestion",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"v1",
+										"unidades-gestion"
+									]
+								}
+							},
+							"response": []
+						},
 						{
 							"name": "Elimina una unidad de gestion",
 							"event": [
diff --git a/core/src/UNAM/Tupa/Core/Manager/ManagerServicio.php b/core/src/UNAM/Tupa/Core/Manager/ManagerServicio.php
index 00247292..bbf6b897 100644
--- a/core/src/UNAM/Tupa/Core/Manager/ManagerServicio.php
+++ b/core/src/UNAM/Tupa/Core/Manager/ManagerServicio.php
@@ -138,7 +138,7 @@ class ManagerServicio extends Manager
         $result = $this->db->sentencia_consultar($sql,$params);
 
         if (!$result) {
-            throw new ErrorTupa("No se pudo recuperar la unidad de gestion '$id_unidad'");
+            return [];
         }
 
         return $this->hidratarServicios($result);
-- 
GitLab


From 6c197ea6f2888da8c63af554dcbfdf7ea7798f79 Mon Sep 17 00:00:00 2001
From: "luciano.cassettai" <luciano.cassettai@unam.edu.ar>
Date: Wed, 8 Mar 2023 11:01:21 -0300
Subject: [PATCH 09/11] Perimita eliminar servicios asociados a una unidad de
 gestion

---
 .../v1/unidades_gestion/unidades_gestion.php  | 33 +++++++++++++++++
 .../Core/Manager/ManagerUnidadGestion.php     | 37 ++++++++++++++++++-
 2 files changed, 69 insertions(+), 1 deletion(-)

diff --git a/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
index 1b09f10a..22ee16ba 100644
--- a/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
+++ b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
@@ -305,6 +305,39 @@ class unidades_gestion implements SIUToba\rest\lib\modelable
         }
     }
 
+      /**
+     * Se consume en DELETE /unidades_gestion/{id}/servicios
+     *
+     * @summary Elimina los servicios asociados a una unidad de gestion
+     * @param_body $servicios Servicios [required] array de ids de los servicios
+     * @responses 200 Se elimino correctamente
+     * @responses 400 El id no existe
+     * @responses 404 No existe el recurso
+     * @responses 500 Error interno
+     */
+    public function delete_servicios_list($id)
+    {
+        $servicios = $this->procesar_input_edicion('Servicios');
+
+        try {
+            $manager = Factory::getManagerUnidadGestion();
+
+            $manager->eliminarServicios($id,$servicios);
+
+            rest::response()->put([ "respuesta" => true ]);
+        }catch (UnidadGestionError $e) {
+            rest::response()->error_negocio([$e->getMessage()], 400);
+        } 
+        catch (\Exception $e) {
+            Factory::getMainLogger()->error($e->getMessage());
+            if ($e->getCode() >= 400 || $e->getCode() < 500) {
+                rest::response()->error_negocio([$e->getMessage()], 400);
+            } else {
+                rest::response()->error_negocio([$e->getMessage()], 500);
+            }
+        }
+    }
+
       /**
      * Se consume en DELETE /unidades_gestion/{identificador}
      *
diff --git a/core/src/UNAM/Tupa/Core/Manager/ManagerUnidadGestion.php b/core/src/UNAM/Tupa/Core/Manager/ManagerUnidadGestion.php
index 64dd929e..e072b27f 100644
--- a/core/src/UNAM/Tupa/Core/Manager/ManagerUnidadGestion.php
+++ b/core/src/UNAM/Tupa/Core/Manager/ManagerUnidadGestion.php
@@ -145,6 +145,8 @@ class ManagerUnidadGestion extends Manager
     public function agregarServicios($idUnidad, $servicios)
     {
         try{            
+            $sql = "INSERT INTO servicios_unidad_gestion VALUES (:id_unidad_gestion,:id_servicio)";
+            
             $this->db->abrir_transaccion();
             foreach($servicios['servicios'] as $idServicio){
                 $params = [
@@ -152,7 +154,6 @@ class ManagerUnidadGestion extends Manager
                     'id_servicio' => $idServicio
                 ];
     
-                $sql = "INSERT INTO servicios_unidad_gestion VALUES (:id_unidad_gestion,:id_servicio)";
 
                 $this->db->sentencia_ejecutar($sql, $params);    
             }
@@ -166,6 +167,40 @@ class ManagerUnidadGestion extends Manager
         return false;     
     }
 
+    /**
+     * @param int $idUnidad id de la unidad de gestion
+     * @param array $servicios id de los servicios que se van a eliminar de laug 
+     * @return bool
+     * @throws UnidadGestionError 
+     */
+    public function eliminarServicios($idUnidad, $servicios)
+    {
+        try{            
+            $sql = "DELETE FROM servicios_unidad_gestion WHERE id_unidad = :id_unidad_gestion and id_servicio = :id_servicio";
+            
+            $this->db->abrir_transaccion();
+            foreach($servicios['servicios'] as $idServicio){
+                $params = [
+                    'id_unidad_gestion' => $idUnidad,
+                    'id_servicio' => $idServicio
+                ];    
+                
+                $resultado = $this->db->sentencia_ejecutar($sql, $params);    
+                
+                if(!$resultado){
+                    throw new \Exception('No se encontro el servicio para esta UG');
+                }
+            }
+            $this->db->cerrar_transaccion();
+            return true;            
+        }catch(\Exception $e){
+            $this->db->abortar_transaccion();
+            Factory::getMainLogger()->error($e->getMessage());
+            throw new UnidadGestionError("No se pudo eliminar el/los servicio de la unidad $idUnidad");
+        }  
+        return false;     
+    }
+
     /**
      * @param $datos
      * @return UnidadGestion
-- 
GitLab


From 4dd5eed5e471ea62325d3e01fa7b8e45de76458d Mon Sep 17 00:00:00 2001
From: "luciano.cassettai" <luciano.cassettai@unam.edu.ar>
Date: Thu, 9 Mar 2023 12:44:33 -0300
Subject: [PATCH 10/11] Verifica que el servicio y la ua existen antes de
 eliminarlos

---
 .../v1/unidades_gestion/unidades_gestion.php      | 15 ++++++++++++---
 .../Tupa/Core/Manager/ManagerUnidadGestion.php    | 11 +++++------
 2 files changed, 17 insertions(+), 9 deletions(-)

diff --git a/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
index 22ee16ba..202ede3b 100644
--- a/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
+++ b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
@@ -10,6 +10,7 @@ use UNAM\Tupa\Core\Errors\UnidadGestionError;
 use UNAM\Tupa\Backend\API\Factory;
 use UNAM\Tupa\Core\Filtros\Filtro;
 use UNAM\Tupa\Core\Negocio\Organizacion\UnidadGestion;
+use UNAM\Tupa\Core\Negocio\SolicitudesServicios\Servicio;
 
 
 class unidades_gestion implements SIUToba\rest\lib\modelable
@@ -317,12 +318,20 @@ class unidades_gestion implements SIUToba\rest\lib\modelable
      */
     public function delete_servicios_list($id)
     {
-        $servicios = $this->procesar_input_edicion('Servicios');
+        $idServicios = $this->procesar_input_edicion('Servicios');
 
         try {
-            $manager = Factory::getManagerUnidadGestion();
+            $managerUG = Factory::getManagerUnidadGestion();
+            $managerServicio = Factory::getManagerServicio();
+            
+            $unidadGestion = $managerUG->getUnidadGestion($id);
+            
+            $filtro = new Filtro();
+            $filtro->agregarCampo('s.id_servicio',$filtro::DENTRO,$idServicios['servicios']);
 
-            $manager->eliminarServicios($id,$servicios);
+            $servicios = $managerServicio->getServicios($filtro);
+            
+            $managerUG->eliminarServicios($unidadGestion,$servicios);
 
             rest::response()->put([ "respuesta" => true ]);
         }catch (UnidadGestionError $e) {
diff --git a/core/src/UNAM/Tupa/Core/Manager/ManagerUnidadGestion.php b/core/src/UNAM/Tupa/Core/Manager/ManagerUnidadGestion.php
index e072b27f..7f55e30e 100644
--- a/core/src/UNAM/Tupa/Core/Manager/ManagerUnidadGestion.php
+++ b/core/src/UNAM/Tupa/Core/Manager/ManagerUnidadGestion.php
@@ -152,8 +152,7 @@ class ManagerUnidadGestion extends Manager
                 $params = [
                     'id_unidad_gestion' => $idUnidad,
                     'id_servicio' => $idServicio
-                ];
-    
+                ];    
 
                 $this->db->sentencia_ejecutar($sql, $params);    
             }
@@ -173,16 +172,16 @@ class ManagerUnidadGestion extends Manager
      * @return bool
      * @throws UnidadGestionError 
      */
-    public function eliminarServicios($idUnidad, $servicios)
+    public function eliminarServicios($unidadGestion, $servicios)
     {
         try{            
             $sql = "DELETE FROM servicios_unidad_gestion WHERE id_unidad = :id_unidad_gestion and id_servicio = :id_servicio";
             
             $this->db->abrir_transaccion();
-            foreach($servicios['servicios'] as $idServicio){
+            foreach($servicios as $servicio){
                 $params = [
-                    'id_unidad_gestion' => $idUnidad,
-                    'id_servicio' => $idServicio
+                    'id_unidad_gestion' => $unidadGestion->getId(),
+                    'id_servicio' => $servicio->getId()
                 ];    
                 
                 $resultado = $this->db->sentencia_ejecutar($sql, $params);    
-- 
GitLab


From 5193f8b4111f3f885d0bdc8e0d73bf15b6f5494a Mon Sep 17 00:00:00 2001
From: "luciano.cassettai" <luciano.cassettai@unam.edu.ar>
Date: Tue, 4 Apr 2023 11:21:22 -0300
Subject: [PATCH 11/11] ABM funciones

---
 .../API/Endpoints/v1/funciones/funciones.php  | 386 ++++++++++++++++++
 .../API/Endpoints/v1/servicios/servicios.php  |  16 +-
 .../v1/unidades_gestion/unidades_gestion.php  |  13 +-
 .../src/UNAM/Tupa/Backend/API/Factory.php     |  15 +
 ..._autorizantes_test.postman_collection.json |   0
 ...end_funciones_test.postman_collection.json | 358 ++++++++++++++++
 ...kend_permisos_test.postman_collection.json |   0
 ...d_solicitudes_test.postman_collection.json |   0
 api-backend/www/api.php                       |   2 +-
 .../UNAM/Tupa/Core/Manager/ManagerFuncion.php | 314 ++++++++++++++
 .../Negocio/Organizacion/UnidadGestion.php    |   2 +-
 .../Negocio/SolicitudesServicios/Funcion.php  | 157 +++++++
 12 files changed, 1248 insertions(+), 15 deletions(-)
 create mode 100644 api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/funciones/funciones.php
 rename "api-backend/tests/Integration/v1/collections\342\200\216/autorizantes/tupa_api_backend_autorizantes_test.postman_collection.json" => api-backend/tests/Integration/v1/collections/autorizantes/tupa_api_backend_autorizantes_test.postman_collection.json (100%)
 create mode 100644 api-backend/tests/Integration/v1/collections/funciones/tupa_api_backend_funciones_test.postman_collection.json
 rename "api-backend/tests/Integration/v1/collections\342\200\216/permisos/tupa_api_backend_permisos_test.postman_collection.json" => api-backend/tests/Integration/v1/collections/permisos/tupa_api_backend_permisos_test.postman_collection.json (100%)
 rename "api-backend/tests/Integration/v1/collections\342\200\216/solicitudes/tupa_api_backend_solicitudes_test.postman_collection.json" => api-backend/tests/Integration/v1/collections/solicitudes/tupa_api_backend_solicitudes_test.postman_collection.json (100%)
 create mode 100644 core/src/UNAM/Tupa/Core/Manager/ManagerFuncion.php
 create mode 100644 core/src/UNAM/Tupa/Core/Negocio/SolicitudesServicios/Funcion.php

diff --git a/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/funciones/funciones.php b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/funciones/funciones.php
new file mode 100644
index 00000000..e0fdc7a9
--- /dev/null
+++ b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/funciones/funciones.php
@@ -0,0 +1,386 @@
+<?php
+namespace UNAM\Tupa\Backend\API\Endpoints\v1\funciones;
+
+use SIUToba\rest\lib\rest_error;
+use SIUToba\rest\lib\rest_hidratador;
+use SIUToba\rest\lib\rest_validador;
+use SIUToba\rest\rest;
+
+use UNAM\Tupa\Core\Errors\ErrorTupa;
+use UNAM\Tupa\Backend\API\Factory;
+use UNAM\Tupa\Core\Filtros\Filtro;
+use UNAM\Tupa\Core\Negocio\SolicitudesServicios\Funcion;
+
+
+
+class funciones implements \SIUToba\rest\lib\modelable
+{
+    public static function _get_modelos()
+    {
+        $vigenciaFin = [
+            'vigencia_fin' => [
+                'type' => 'date',
+                '_validar' => [
+                    rest_validador::TIPO_DATE => [
+                        'format' => 'Y-m-d'
+                    ],
+                ],
+            ]         
+        ];
+
+        $funcionEdit = [
+            'nombre' => [
+                'type' => 'string',
+                'required' => true,
+                '_validar' => array(
+                    rest_validador::OBLIGATORIO,
+                ),
+            ],
+            'descripcion' => [
+                'type' => 'string',
+                'required' => true,
+                '_validar' => array(
+                    rest_validador::OBLIGATORIO,
+                ),
+            ],
+            'es_agrupador' => [
+                'type' => 'boolean',
+            ],
+            'vigencia_inicio' => [
+                'type' => 'date',
+                '_validar' => [
+                    rest_validador::TIPO_DATE => [
+                        'format' => 'Y-m-d'
+                    ],
+                ],
+            ],
+            'vigencia_fin' => [
+                'type' => 'date',
+                '_validar' => [
+                    rest_validador::TIPO_DATE => [
+                        'format' => 'Y-m-d'
+                    ],
+                ],
+            ],
+            'id_funcion_padre' => [
+                'type' => 'integer',
+            ],
+            'id_funcion_predecesor' => [
+                'type' => 'integer',
+            ],
+            'id_servicio' => [
+                'type' => 'integer',
+                'required' => true,
+                '_validar' => [
+                    rest_validador::OBLIGATORIO,
+                ],
+            ]  
+        ];
+
+        $funcion = array_merge(
+            array(
+                'id_funcion' => array(
+                    'type' => 'integer',
+                ),
+            ),
+            $funcionEdit
+        );
+
+        return $models = array(
+            'Funcion' => $funcion,
+            'FuncionEdit' => $funcionEdit,
+            'VigenciaFin' => $vigenciaFin
+        );
+    }
+
+    protected function get_spec_usuario($tipo = 'Funcion')
+    {
+        $m = $this->_get_modelos();
+
+        return $m[$tipo];
+    }
+
+    /**
+     * Se consume en GET /funciones/{id}.
+     *
+     * @summary Retorna datos de una funcion
+     * @responses 200 {"$ref": "Funcion"} Funcion
+     * @responses 404 No existe el recurso
+     * @responses 500 Error interno del servidor
+     */
+    public function get($id)
+    {
+        try {
+            $manager = Factory::getManagerFuncion();
+
+            $funcion = $manager->getFuncion($id);
+
+            $fila = rest_hidratador::hidratar_fila($this->get_spec_usuario('Funcion'), $funcion->toArray());
+
+            rest::response()->get($fila);
+        } catch (ErrorTupa $e) {
+            Factory::getMainLogger()->error($e->getMessage());
+            rest::response()->not_found();
+        } catch (\Exception $e) {
+            Factory::getMainLogger()->error($e->getMessage());
+            rest::response()->error_negocio('Error interno del servidor', 500);
+        }
+    }
+
+    /**
+     * Se consume en GET /funciones.
+     *
+     * @summary Retorna las funciones existentes
+     * @param_query $nombre string Se define como 'condicion;valor' donde 'condicion' puede ser contiene|no_contiene|comienza_con|termina_con|es_igual_a|es_distinto_de
+     * @param_query $descripcion string Se define como 'condicion;valor' donde 'condicion' puede ser es_menor_que|es_menor_igual_que|es_igual_a|es_distinto_de|es_mayor_igual_que|es_mayor_que|entre
+     * @param_query $id_servicio integer Se define como 'condicion;valor' donde 'condicion' puede ser es_menor_que|es_menor_igual_que|es_igual_a|es_distinto_de|es_mayor_igual_que|es_mayor_que|entre
+     * @param_query $id_funcion_padre string Se define como 'condicion;valor' donde 'condicion' puede ser es_menor_que|es_menor_igual_que|es_igual_a|es_distinto_de|es_mayor_igual_que|es_mayor_que|entre
+     * @param_query $id_funcion_predecesor string Se define como 'condicion;valor' donde 'condicion' puede ser es_menor_que|es_menor_igual_que|es_igual_a|es_distinto_de|es_mayor_igual_que|es_mayor_que|entre
+     * @param_query $vigencia_inicio string Se define como 'condicion;valor' donde 'condicion' puede ser es_menor_que|es_menor_igual_que|es_igual_a|es_distinto_de|es_mayor_igual_que|es_mayor_que|entre
+     * @param_query $vigencia_fin string Se define como 'condicion;valor' donde 'condicion' puede ser es_menor_que|es_menor_igual_que|es_igual_a|es_distinto_de|es_mayor_igual_que|es_mayor_que|entre
+     * @param_query $es_agrupador Boolean Es una funcion agrupadora de otras funciones
+     * @param_query $limit integer Limitar a esta cantidad de servicios
+     * @param_query $page integer Limitar desde esta pagina
+     * @param_query $order string +/-campo,...
+     * @responses 200 array {"$ref":"Funcion"}
+     * @responses 500 Error en los operadores ingresados para el filtro
+     */
+    public function get_list()
+    {
+        try {
+            $filtro = $this->get_filtro_get_list();
+
+            $filtro->setlimit(rest::request()->get('limit', null));
+            $filtro->setPage(rest::request()->get('page', null));
+            $filtro->setOrder(rest::request()->get('order', null));
+
+            $manager = Factory::getManagerFuncion();
+            $funciones = $manager->getFunciones($filtro);
+
+            $resultados = [];            
+            foreach ($funciones as $f) {
+                $resultados[] = $f->toArray();
+            }
+            
+            $registros = rest_hidratador::hidratar($this->get_spec_usuario('Funcion'), $resultados);
+
+            rest::response()->get($registros);
+        } catch (\Exception $e) {
+            Factory::getMainLogger()->error($e->getMessage());
+            rest::response()->error_negocio('Error interno del servidor', 500);
+        }
+    }
+
+    /**
+     * Se consume en GET /funciones/{id_funcion_padre}/hijas
+     *
+     * @summary Retorna las funciones hijas de una funcion padre
+     * @responses 200 array {"$ref":"Funcion"}
+     * @responses 500 Error en los operadores ingresados para el filtro
+     */
+    public function get_hijas_list($id)
+    {
+        try {
+            $filtro = $this->get_filtro_get_list();
+
+            $manager = Factory::getManagerFuncion();
+            $funciones = $manager->getFuncionesHijas($id);
+
+            $resultados = [];            
+            foreach ($funciones as $f) {
+                $resultados[] = $f->toArray();
+            }
+            
+            $registros = rest_hidratador::hidratar($this->get_spec_usuario('Funcion'), $resultados);
+
+            rest::response()->get($registros);
+        } catch (\Exception $e) {
+            Factory::getMainLogger()->error($e->getMessage());
+            rest::response()->error_negocio('Error interno del servidor', 500);
+        }
+    }
+
+    /**
+     * Se consume en GET /funciones/{id_funcion_predecesor}/sucesor
+     *
+     * @summary Retorna todas las funciones sucesoras de una funcion predecesora
+     * @responses 200 array {"$ref":"Funcion"}
+     * @responses 500 Error en los operadores ingresados para el filtro
+     */
+    public function get_sucesor_list($id)
+    {
+        try {
+            $filtro = $this->get_filtro_get_list();
+
+            $manager = Factory::getManagerFuncion();
+            $funciones = $manager->getFuncionesPredecesoras($id);
+
+            $resultados = [];            
+            foreach ($funciones as $f) {
+                $resultados[] = $f->toArray();
+            }
+            
+            $registros = rest_hidratador::hidratar($this->get_spec_usuario('Funcion'), $resultados);
+
+            rest::response()->get($registros);
+        } catch (\Exception $e) {
+            Factory::getMainLogger()->error($e->getMessage());
+            rest::response()->error_negocio('Error interno del servidor', 500);
+        }
+    }
+
+      /**
+     * Se consume en POST /funciones
+     *
+     * @summary Crea una nueva funcion
+     * @param_body $funciones FuncionEdit [required] los datos de la funcion
+     * @responses 201 {"string"} identificador de una funcion
+     * @responses 400 Funcion inválida
+     * @responses 500 Error interno
+     */
+    public function post_list()
+    {
+        // Valido y traduzco los datos al formato de mi modelo
+        $datos = $this->procesar_input_edicion('FuncionEdit',false);
+
+        try {
+            $manager = Factory::getManagerFuncion();
+ 
+            $funcion = new Funcion();
+            $funcion->loadFromDatos($datos);
+
+            $identificador = $manager->crear($funcion);
+     
+            $respuesta = [
+                'id_funcion' => $identificador[0]['id_funcion']
+            ];
+
+            rest::response()->post($respuesta);
+        } catch (rest_error $e) {
+            rest::response()->error_negocio($e->get_datalle(), 400);
+        } catch (\Exception $e) {
+            if ($e->getCode() >= 400 || $e->getCode() < 500) {
+                rest::response()->error_negocio([$e->getMessage()], 400);
+            } else {
+                rest::response()->error_negocio([$e->getMessage()], 500);
+            }
+        }
+    }
+
+      /**
+     * Se consume en PUT /funciones/{id}
+     *
+     * @summary Modifica una funcion
+     * @param_body $FuncinEdit FuncionEdit  [required] los datos a editar de una funcion
+     * @responses 204 El id es correcto
+     * @responses 400 El id no válido
+     * @responses 404 No existe el recurso
+     * @responses 500 Error interno
+     */
+    public function put($id)
+    {
+        $datos = $this->procesar_input_edicion('FuncionEdit');
+
+        try {
+            $manager = Factory::getManagerFuncion();
+            $funcion = $manager->getFuncion($id);
+            $funcion->loadFromDatos($datos);
+            $manager->actualizar($funcion);
+
+            rest::response()->put([ "respuesta" => true ]);
+        } catch (ErrorTupa $e) {
+            rest::response()->not_found();
+        } catch (\Exception $e) {
+            if ($e->getCode() >= 400 || $e->getCode() < 500) {
+                rest::response()->error_negocio([$e->getMessage()], 400);
+            } else {
+                rest::response()->error_negocio([$e->getMessage()], 500);
+            }
+        }
+    }
+
+    /**
+     * Se consume en PUT /funciones/{id}/baja.
+     *
+     * @summary Setea vigencia fin de una funcion
+     * @param_body $vigenciaFin VigenciaFin  [required] la fecha de fin de vigencia de la funcion
+     * @responses 200 Se seteo la baja correctamente
+     * @responses 404 No existe el recurso
+     * @responses 500 Error interno
+     */
+    public function put_baja_list($id)
+    {
+        $datos = $this->procesar_input_edicion('VigenciaFin');
+
+        try {
+            $manager = Factory::getManagerFuncion();
+            $funcion = $manager->getFuncion($id);
+
+            $fechaFin = $datos['vigencia_fin'] ?? date("Y-m-d");
+            $funcion->setVigenciaFin($fechaFin);
+
+            $manager->actualizar($funcion);
+
+            rest::response()->put([ "respuesta" => true ]);
+        } catch (ErrorTupa $e) {
+            Factory::getMainLogger()->error($e->getMessage());
+            rest::response()->not_found();
+        } catch (\Exception $e) {
+            Factory::getMainLogger()->error($e->getMessage());
+            if ($e->getCode() >= 400 || $e->getCode() < 500) {
+                rest::response()->error_negocio($e->getMessage(), 400);
+            } else {
+                rest::response()->error_negocio($e->getMessage(), 500);
+            }
+        }
+    }
+
+    /**
+     * @return Filtro
+     * @throws ErrorTupa
+     */
+    protected function get_filtro_get_list()
+    {
+        /* @var Filtro $filtro */
+        $filtro = new Filtro();
+        $filtro->agregarCampoRest('f.nombre', rest::request()->get('nombre', null));
+        $filtro->agregarCampoRest('f.descripcion', rest::request()->get('descripcion', null));
+        $filtro->agregarCampoRest('f.id_servicio', rest::request()->get('id_servicio', null));
+        $filtro->agregarCampoRest('f.id_funcion_padre', rest::request()->get('id_funcion_padre', null));
+        $filtro->agregarCampoRest('f.id_funcion_predecesor', rest::request()->get('id_funcion_predecesor', null));
+        $filtro->agregarCampoRest('f.vigencia_inicio', rest::request()->get('vigencia_inicio', null));
+        $filtro->agregarCampoRest('f.vigencia_fin', rest::request()->get('vigencia_fin', null));
+        if(rest::request()->get('es_agrupador', null)){
+            $filtro->agregarCampoRest('f.es_agrupador', 'es_igual_a;'.rest::request()->get('es_agrupador', null));
+        }
+        
+        $filtro->agregarCampoOrdenable('nombre');
+        $filtro->agregarCampoOrdenable('vigencia_inicio');
+
+        return $filtro;
+    }
+
+     /**
+     * $relajar_ocultos boolean no checkea campos obligatorios cuando no se especifican.
+     * @param string $modelo
+     * @param bool $relajar_ocultos
+     * @param array $datos
+     * @return array|string
+     * @throws rest_error
+     */
+    protected function procesar_input_edicion($modelo = 'FuncionEdit', $relajar_ocultos = false, $datos = null)
+    {
+        if (! $datos) {
+            $datos = rest::request()->get_body_json();
+        }
+
+        $spec_usuario = $this->get_spec_usuario($modelo);
+
+        rest_validador::validar($datos, $spec_usuario, $relajar_ocultos);
+
+        $resultado = rest_hidratador::deshidratar_fila($datos, $spec_usuario);
+
+        return Factory::getVarios()->arrayToUtf8($resultado);
+    }
+}
+
diff --git a/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/servicios/servicios.php b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/servicios/servicios.php
index e3b3e8cf..5a5b1b7f 100644
--- a/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/servicios/servicios.php
+++ b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/servicios/servicios.php
@@ -1,5 +1,7 @@
 <?php
 
+namespace UNAM\Tupa\Backend\API\Endpoints\v1\servicios;
+
 use SIUToba\rest\lib\rest_error;
 use SIUToba\rest\lib\rest_hidratador;
 use SIUToba\rest\lib\rest_validador;
@@ -11,7 +13,7 @@ use UNAM\Tupa\Core\Filtros\Filtro;
 use UNAM\Tupa\Core\Negocio\SolicitudesServicios\Servicio;
 
 
-class servicios implements SIUToba\rest\lib\modelable
+class servicios implements \SIUToba\rest\lib\modelable
 {
     public static function _get_modelos()
     {
@@ -105,7 +107,7 @@ class servicios implements SIUToba\rest\lib\modelable
         } catch (ErrorTupa $e) {
             Factory::getMainLogger()->error($e->getMessage());
             rest::response()->not_found();
-        } catch (Exception $e) {
+        } catch (\Exception $e) {
             Factory::getMainLogger()->error($e->getMessage());
             rest::response()->error_negocio('Error interno del servidor', 500);
         }
@@ -149,7 +151,7 @@ class servicios implements SIUToba\rest\lib\modelable
             $registros = rest_hidratador::hidratar($this->get_spec_usuario('Servicio'), $resultados);
 
             rest::response()->get($registros);
-        } catch (Exception $e) {
+        } catch (\Exception $e) {
             Factory::getMainLogger()->error($e->getMessage());
             rest::response()->error_negocio('Error interno del servidor', 500);
         }
@@ -162,7 +164,7 @@ class servicios implements SIUToba\rest\lib\modelable
      * @summary Crea un nuevo servicio
      * @param_body $servicios ServicioEdit [required] los datos del servicio
      * @responses 201 {"string"} identificador de un servicio
-     * @responses 400 Servicio inválido
+     * @responses 400 Servicio inv�lido
      * @responses 500 Error interno
      */
     public function post_list()
@@ -186,7 +188,7 @@ class servicios implements SIUToba\rest\lib\modelable
             rest::response()->post($respuesta);
         } catch (rest_error $e) {
             rest::response()->error_negocio($e->get_datalle(), 400);
-        } catch (Exception $e) {
+        } catch (\Exception $e) {
             if ($e->getCode() >= 400 || $e->getCode() < 500) {
                 rest::response()->error_negocio([$e->getMessage()], 400);
             } else {
@@ -201,7 +203,7 @@ class servicios implements SIUToba\rest\lib\modelable
      * @summary Modifica un servicio
      * @param_body $ServicioEdit ServicioEdit  [required] los datos a editar de un servicio
      * @responses 204 El id es correcto
-     * @responses 400 El id no válido
+     * @responses 400 El id no v�lido
      * @responses 404 No existe el recurso
      * @responses 500 Error interno
      */
@@ -256,7 +258,7 @@ class servicios implements SIUToba\rest\lib\modelable
         } catch (ErrorTupa $e) {
             Factory::getMainLogger()->error($e->getMessage());
             rest::response()->not_found();
-        } catch (Exception $e) {
+        } catch (\Exception $e) {
             Factory::getMainLogger()->error($e->getMessage());
             if ($e->getCode() >= 400 || $e->getCode() < 500) {
                 rest::response()->error_negocio($e->getMessage(), 400);
diff --git a/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
index 202ede3b..f84bf8a3 100644
--- a/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
+++ b/api-backend/src/UNAM/Tupa/Backend/API/Endpoints/v1/unidades_gestion/unidades_gestion.php
@@ -1,4 +1,5 @@
 <?php
+namespace UNAM\Tupa\Backend\API\Endpoints\v1\unidades_gestion;
 
 use SIUToba\rest\lib\rest_error;
 use SIUToba\rest\lib\rest_hidratador;
@@ -13,7 +14,7 @@ use UNAM\Tupa\Core\Negocio\Organizacion\UnidadGestion;
 use UNAM\Tupa\Core\Negocio\SolicitudesServicios\Servicio;
 
 
-class unidades_gestion implements SIUToba\rest\lib\modelable
+class unidadesGestion implements \SIUToba\rest\lib\modelable
 {
     public static function _get_modelos()
     {
@@ -126,7 +127,7 @@ class unidades_gestion implements SIUToba\rest\lib\modelable
         } catch (UnidadGestionError $e) {
             Factory::getMainLogger()->error($e->getMessage());
             rest::response()->not_found();
-        } catch (Exception $e) {
+        } catch (\Exception $e) {
             Factory::getMainLogger()->error($e->getMessage());
             rest::response()->error_negocio('Error interno del servidor', 500);
         }
@@ -165,7 +166,7 @@ class unidades_gestion implements SIUToba\rest\lib\modelable
             $registros = rest_hidratador::hidratar($this->get_spec_usuario('UnidadGestion'), $resultados);
 
             rest::response()->get($registros);
-        } catch (Exception $e) {
+        } catch (\Exception $e) {
             Factory::getMainLogger()->error($e->getMessage());
             rest::response()->error_negocio('Error interno del servidor', 500);
         }
@@ -197,7 +198,7 @@ class unidades_gestion implements SIUToba\rest\lib\modelable
         }catch (ErrorTupa $e) {
             Factory::getMainLogger()->error($e->getMessage());
             rest::response()->not_found($e->getMessage());
-        }catch (Exception $e) {
+        }catch (\Exception $e) {
             Factory::getMainLogger()->error($e->getMessage());
             rest::response()->error_negocio('Error interno del servidor', 500);
         }
@@ -226,7 +227,7 @@ class unidades_gestion implements SIUToba\rest\lib\modelable
         } 
         catch (UnidadGestionError $e) {
             rest::response()->error_negocio([$e->getMessage()], 400);
-        } catch (Exception $e) {
+        } catch (\Exception $e) {
             if ($e->getCode() >= 400 || $e->getCode() < 500) {
                 rest::response()->error_negocio([$e->getMessage()], 400);
             } else {
@@ -265,7 +266,7 @@ class unidades_gestion implements SIUToba\rest\lib\modelable
             rest::response()->post($respuesta);
         } catch (rest_error $e) {
             rest::response()->error_negocio($e->get_datalle(), 400);
-        } catch (Exception $e) {
+        } catch (\Exception $e) {
             if ($e->getCode() >= 400 || $e->getCode() < 500) {
                 rest::response()->error_negocio([$e->getMessage()], 400);
             } else {
diff --git a/api-backend/src/UNAM/Tupa/Backend/API/Factory.php b/api-backend/src/UNAM/Tupa/Backend/API/Factory.php
index ae2c8cc2..104953c3 100644
--- a/api-backend/src/UNAM/Tupa/Backend/API/Factory.php
+++ b/api-backend/src/UNAM/Tupa/Backend/API/Factory.php
@@ -30,6 +30,7 @@ use UNAM\Tupa\Core\Manager\ManagerUnidadGestion;
 use UNAM\Tupa\Core\Manager\ManagerPermiso;
 use UNAM\Tupa\Core\Manager\ManagerSolicitud;
 use UNAM\Tupa\Core\Manager\ManagerServicio;
+use UNAM\Tupa\Core\Manager\ManagerFuncion;
 
 class Factory
 {
@@ -252,6 +253,10 @@ class Factory
             return new ManagerTerminosCondiciones($c['db-logger'], $c['db'], $c['codigo']);
         };
 
+        $container['manager-funcion'] = function ($c) {
+            return new ManagerFuncion($c['db-logger'], $c['db'], $c['codigo']);
+        };
+
         $container['manager-autorizante'] = function ($c) {
             return new ManagerAutorizante($c['db-logger'], $c['db'], $c['codigo']);
         };
@@ -384,6 +389,16 @@ class Factory
         return self::getContainer()['manager-terminos-condiciones'];
     }
 
+     /**
+     * Singleton de ManagerSede
+     *
+     * @return ManagerFunciones
+     */
+    public static function getManagerFuncion()
+    {
+        return self::getContainer()['manager-funcion'];
+    }
+
     /**
      * Singleton de ManagerAutorizante
      *
diff --git "a/api-backend/tests/Integration/v1/collections\342\200\216/autorizantes/tupa_api_backend_autorizantes_test.postman_collection.json" b/api-backend/tests/Integration/v1/collections/autorizantes/tupa_api_backend_autorizantes_test.postman_collection.json
similarity index 100%
rename from "api-backend/tests/Integration/v1/collections\342\200\216/autorizantes/tupa_api_backend_autorizantes_test.postman_collection.json"
rename to api-backend/tests/Integration/v1/collections/autorizantes/tupa_api_backend_autorizantes_test.postman_collection.json
diff --git a/api-backend/tests/Integration/v1/collections/funciones/tupa_api_backend_funciones_test.postman_collection.json b/api-backend/tests/Integration/v1/collections/funciones/tupa_api_backend_funciones_test.postman_collection.json
new file mode 100644
index 00000000..4c93b01a
--- /dev/null
+++ b/api-backend/tests/Integration/v1/collections/funciones/tupa_api_backend_funciones_test.postman_collection.json
@@ -0,0 +1,358 @@
+{
+	"info": {
+		"_postman_id": "da74add2-5ee3-464b-9be1-9fd6822f98c6",
+		"name": "Tupa API Backend Funciones",
+		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
+		"_exporter_id": "16366966"
+	},
+	"item": [
+		{
+			"name": "V1",
+			"item": [
+				{
+					"name": "Funciones",
+					"item": [
+						{
+							"name": "Crea una nueva funcion",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"exec": [
+											"pm.test(\"Status code is 201\", () => {",
+											"    pm.response.to.have.status(201);",
+											"});",
+											"",
+											"const schema =  {",
+											"  \"nombre\": \"\",",
+											"  \"descripcion\": \"\",",
+											"  \"es_agrupador\": \"\",",
+											"  \"vigencia_inicio\": \"\",",
+											"  \"vigencia_fin\": \"\",",
+											"  \"id_funcion_padre\": \"\",",
+											"  \"id_funcion_predecesor\": \"\",",
+											"  \"id_servicio\": \"\"",
+											"}",
+											"",
+											"const body = JSON.parse(pm.request.body.raw);",
+											"const sameSchema = JSON.stringify(Object.keys(schema)) === JSON.stringify(Object.keys(body));",
+											"",
+											"pm.test('Estructura del body correcta', () => {",
+											"    pm.expect(true).to.be.eql(sameSchema);",
+											"}); ",
+											"",
+											"var respuesta = pm.response.json();",
+											"",
+											"pm.test(\"funcion creada con ID \" + respuesta.id_funcion, function () {",
+											"    pm.globals.set(\"idFuncion\", respuesta.id_funcion);",
+											"});"
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "POST",
+								"header": [],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"nombre\": \"{{$randomWord}}a\",\n  \"descripcion\": \"{{$randomWords}}a\",\n  \"es_agrupador\": true,\n  \"vigencia_inicio\": \"2020-01-01\",\n  \"vigencia_fin\": \"2020-01-01\",\n  \"id_funcion_padre\": null,\n  \"id_funcion_predecesor\": null,\n  \"id_servicio\": 1\n}",
+									"options": {
+										"raw": {
+											"language": "json"
+										}
+									}
+								},
+								"url": {
+									"raw": "{{baseUrl}}/v1/funciones",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"v1",
+										"funciones"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "Retorna datos de una funcion",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"exec": [
+											"pm.test(\"Status code is 200\", () => {",
+											"    pm.response.to.have.status(200);",
+											"});",
+											"",
+											"",
+											"const response = pm.response.json();",
+											"const hasId = Object.keys(response).includes('id_funcion');",
+											"",
+											"pm.test(\"Funcion con id_funcion \" + response.id_funcion, function () {",
+											"    pm.expect(true).to.be.eql(hasId);",
+											"});",
+											""
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/v1/funciones/{{idFuncion}}",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"v1",
+										"funciones",
+										"{{idFuncion}}"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "Retorna las funciones existentes",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"exec": [
+											"pm.test(\"Status code is 200\", () => {",
+											"    pm.response.to.have.status(200);",
+											"});",
+											"",
+											"const response = pm.response.json();",
+											"",
+											"pm.test(\"Se obtuvieron funciones \", function () {",
+											"    // Si existe al menos 1 servicio",
+											"    pm.expect(response.length).to.not.eql(0);",
+											"",
+											"    if(response.length > 0){",
+											"        const hasId = Object.keys(response[0]).includes('id_funcion');",
+											"",
+											"        // Si la funcion tiene un id_funcion",
+											"        pm.expect(true).to.be.eql(hasId);",
+											"    }",
+											"  ",
+											"});",
+											""
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "{{baseUrl}}/v1/funciones?nombre=contiene;a&descripcion=contiene;a&id_servicio=es_igual_a;1&vigencia_inicio=es_mayor_igual_que;2010-01-01&es_agrupador=true",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"v1",
+										"funciones"
+									],
+									"query": [
+										{
+											"key": "nombre",
+											"value": "contiene;a"
+										},
+										{
+											"key": "descripcion",
+											"value": "contiene;a"
+										},
+										{
+											"key": "id_funcion_padre",
+											"value": "es_igual_a;null",
+											"disabled": true
+										},
+										{
+											"key": "id_funcion_predecesor",
+											"value": "es_distinto_de;999",
+											"disabled": true
+										},
+										{
+											"key": "id_servicio",
+											"value": "es_igual_a;1"
+										},
+										{
+											"key": "vigencia_inicio",
+											"value": "es_mayor_igual_que;2010-01-01"
+										},
+										{
+											"key": "es_agrupador",
+											"value": "true"
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "Modifica una funcion",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"exec": [
+											"pm.test(\"Status code is 200\", () => {",
+											"    pm.response.to.have.status(200);",
+											"});",
+											"",
+											"const schema =  {",
+											"  \"nombre\": \"\",",
+											"  \"descripcion\": \"\",",
+											"  \"es_agrupador\": \"\",",
+											"  \"vigencia_inicio\": \"\",",
+											"  \"vigencia_fin\": \"\",",
+											"  \"id_funcion_padre\": \"\",",
+											"  \"id_funcion_predecesor\": \"\",",
+											"  \"id_servicio\": \"\"",
+											"}",
+											"",
+											"const body = JSON.parse(pm.request.body.raw);",
+											"const sameSchema = JSON.stringify(Object.keys(schema)) === JSON.stringify(Object.keys(body));",
+											"",
+											"pm.test('Estructura del body correcta', () => {",
+											"    pm.expect(true).to.be.eql(sameSchema);",
+											"}); ",
+											""
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "PUT",
+								"header": [],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"nombre\": \"{{$randomWord}}\",\n  \"descripcion\": \"{{$randomWords}}\",\n  \"es_agrupador\": true,\n  \"vigencia_inicio\": \"2020-02-01\",\n  \"vigencia_fin\": \"2025-01-01\",\n  \"id_funcion_padre\": null,\n  \"id_funcion_predecesor\": null,\n  \"id_servicio\": 1\n}",
+									"options": {
+										"raw": {
+											"language": "json"
+										}
+									}
+								},
+								"url": {
+									"raw": "{{baseUrl}}/v1/funciones/{{idFuncion}}",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"v1",
+										"funciones",
+										"{{idFuncion}}"
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "Setea vigencia fin de una funcion",
+							"event": [
+								{
+									"listen": "test",
+									"script": {
+										"exec": [
+											"pm.test(\"Status code is 200\", () => {",
+											"    pm.response.to.have.status(200);",
+											"});",
+											"",
+											"const schema =  {",
+											"  \"vigencia_fin\": \"\"",
+											"}",
+											"",
+											"const body = JSON.parse(pm.request.body.raw);",
+											"const sameSchema = JSON.stringify(Object.keys(schema)) === JSON.stringify(Object.keys(body));",
+											"",
+											"pm.test('Estructura del body correcta', () => {",
+											"    pm.expect(true).to.be.eql(sameSchema);",
+											"});"
+										],
+										"type": "text/javascript"
+									}
+								}
+							],
+							"request": {
+								"method": "PUT",
+								"header": [],
+								"body": {
+									"mode": "raw",
+									"raw": "{\n  \"vigencia_fin\": \"2028-01-01\"\n}",
+									"options": {
+										"raw": {
+											"language": "json"
+										}
+									}
+								},
+								"url": {
+									"raw": "{{baseUrl}}/v1/funciones/{{idFuncion}}/baja",
+									"host": [
+										"{{baseUrl}}"
+									],
+									"path": [
+										"v1",
+										"funciones",
+										"{{idFuncion}}",
+										"baja"
+									]
+								}
+							},
+							"response": []
+						}
+					]
+				}
+			]
+		}
+	],
+	"auth": {
+		"type": "basic",
+		"basic": [
+			{
+				"key": "password",
+				"value": "admin",
+				"type": "string"
+			},
+			{
+				"key": "username",
+				"value": "admin",
+				"type": "string"
+			}
+		]
+	},
+	"event": [
+		{
+			"listen": "prerequest",
+			"script": {
+				"type": "text/javascript",
+				"exec": [
+					""
+				]
+			}
+		},
+		{
+			"listen": "test",
+			"script": {
+				"type": "text/javascript",
+				"exec": [
+					""
+				]
+			}
+		}
+	],
+	"variable": [
+		{
+			"key": "baseUrl",
+			"value": "http://localhost:9002/api"
+		}
+	]
+}
\ No newline at end of file
diff --git "a/api-backend/tests/Integration/v1/collections\342\200\216/permisos/tupa_api_backend_permisos_test.postman_collection.json" b/api-backend/tests/Integration/v1/collections/permisos/tupa_api_backend_permisos_test.postman_collection.json
similarity index 100%
rename from "api-backend/tests/Integration/v1/collections\342\200\216/permisos/tupa_api_backend_permisos_test.postman_collection.json"
rename to api-backend/tests/Integration/v1/collections/permisos/tupa_api_backend_permisos_test.postman_collection.json
diff --git "a/api-backend/tests/Integration/v1/collections\342\200\216/solicitudes/tupa_api_backend_solicitudes_test.postman_collection.json" b/api-backend/tests/Integration/v1/collections/solicitudes/tupa_api_backend_solicitudes_test.postman_collection.json
similarity index 100%
rename from "api-backend/tests/Integration/v1/collections\342\200\216/solicitudes/tupa_api_backend_solicitudes_test.postman_collection.json"
rename to api-backend/tests/Integration/v1/collections/solicitudes/tupa_api_backend_solicitudes_test.postman_collection.json
diff --git a/api-backend/www/api.php b/api-backend/www/api.php
index a2786a22..c94e828e 100644
--- a/api-backend/www/api.php
+++ b/api-backend/www/api.php
@@ -21,7 +21,7 @@ $settings = array(
     'prefijo_controladores' => '',
     'api_version' => $api_version,
     'api_titulo' => 'Tupa (backend)',
-    'url_protegida' => '/pases|registros|sedes|terminos-condiciones|visitantes|visitas|servicios|unidades-gestion/',
+    'url_protegida' => '/pases|registros|sedes|terminos-condiciones|visitantes|visitas|servicios|unidades-gestion|funciones/',
     'debug' => true,
 );
 
diff --git a/core/src/UNAM/Tupa/Core/Manager/ManagerFuncion.php b/core/src/UNAM/Tupa/Core/Manager/ManagerFuncion.php
new file mode 100644
index 00000000..86d89fde
--- /dev/null
+++ b/core/src/UNAM/Tupa/Core/Manager/ManagerFuncion.php
@@ -0,0 +1,314 @@
+<?php
+
+declare(strict_types=1);
+
+namespace UNAM\Tupa\Core\Manager;
+
+use UNAM\Tupa\Core\Errors\ErrorTupa;
+use UNAM\Tupa\Core\Filtros\Filtro;
+use UNAM\Tupa\Core\Negocio\SolicitudesServicios\Funcion;
+
+class ManagerFuncion extends Manager
+{
+    /**
+     * @param $id
+     * @return Funcion|null
+     * @throws ErrorTupa No se pudo recuperar el servicio
+     */
+    public function getFuncion($id)
+    {
+        $params = [
+            "id" => $id
+        ];
+
+        $sql = "SELECT  id_funcion,
+                        nombre,
+                        descripcion,
+                        es_agrupador,
+                        id_funcion_padre,
+                        id_funcion_predecesor,
+                        vigencia_inicio,
+                        vigencia_fin,
+                        id_servicio "
+            . "FROM funciones s "
+            . "WHERE id_funcion = :id";
+
+        $result = $this->db->sentencia_consultar_fila($sql, $params);
+
+        if (!$result) {
+            throw new ErrorTupa("No se pudo recuperar la funcion '$id'");
+        }
+
+        return $this->hidratarFuncion($result);
+    }
+
+    /**
+     * @param Filtro|null $filtro
+     * @param bool $hidratar
+     * @return Funcion[]
+     */
+    public function getFunciones(Filtro $filtro = null, $hidratar = true)
+    {
+        $where = $this->getSqlWhere($filtro);
+        $orderBy = $this->getSqlOrderBy($filtro);
+        $limit = $this->getSqlLimit($filtro);
+
+        $sql = sprintf("
+        SELECT  f.id_funcion,
+                f.nombre,
+                f.descripcion,
+                f.es_agrupador,
+                f.id_funcion_padre,
+                f.id_funcion_predecesor,
+                f.vigencia_inicio,
+                f.vigencia_fin,
+                f.id_servicio 
+        from funciones f
+            %s
+            %s
+            %s;", $where, $orderBy, $limit);
+
+        $result = $this->db->consultar($sql);
+
+        if ($hidratar) {
+            $result = $this->hidratarFunciones($result);
+        }
+
+        return $result;
+    }
+
+    /**
+     * @param Funcion $funcion
+     * @return bool|string
+     * @throws ErrorTupa No se pudo crear el servicio
+     */
+    public function crear(Funcion $funcion)
+    {
+        $sql = "INSERT INTO funciones (nombre, descripcion, es_agrupador, vigencia_inicio, id_funcion_padre, id_funcion_predecesor, id_servicio)
+                VALUES (:nombre, :descripcion, :es_agrupador, :vigencia_inicio, :id_funcion_padre, :id_funcion_predecesor, :id_servicio) RETURNING id_funcion";
+
+        $sqlParams = [
+            "nombre" => $funcion->getNombre(),
+            "descripcion" => $funcion->getDescripcion(),
+            "es_agrupador" => $funcion->getEsAgrupador() ? 1 : 0,
+            "vigencia_inicio" => $funcion->getVigenciaInicio(),
+            "id_funcion_padre" => is_object($funcion->getFuncionPadre()) ? $funcion->getFuncionPadre()->getId() : null,
+            "id_funcion_predecesor" => is_object($funcion->getFuncionPredecesor()) ? $funcion->getFuncionPredecesor()->getId() : null,
+            "id_servicio" => $funcion->getServicio()->getId()
+        ];
+
+        $result = $this->db->sentencia_consultar($sql, $sqlParams);
+
+        if (!$result) {
+            throw new ErrorTupa("No se pudo crear el servicio");
+        }
+
+        return $result;
+    }
+
+    /**
+     * @param Funcion $funcion
+     * @return mixed
+     */
+    public function actualizar($funcion)
+    {
+        $params = [
+            "id_funcion" => $funcion->getId(),
+            "nombre" => $funcion->getNombre(),
+            "descripcion" => $funcion->getDescripcion(),
+            "es_agrupador" => $funcion->getEsAgrupador() ? 1 : 0,
+            "vigencia_inicio" => $funcion->getVigenciaInicio(),
+            "vigencia_fin" => $funcion->getVigenciaFin(),
+            "id_funcion_padre" => is_object($funcion->getFuncionPadre()) ? $funcion->getFuncionPadre()->getId() : null,
+            "id_funcion_predecesor" => is_object($funcion->getFuncionPredecesor()) ? $funcion->getFuncionPredecesor()->getId() : null,
+            "id_servicio" => $funcion->getServicio()->getId()
+        ];
+
+        $sql = "UPDATE funciones
+                SET    nombre = :nombre,
+                       descripcion = :descripcion,
+                       es_agrupador = :es_agrupador,
+                       vigencia_inicio = :vigencia_inicio,
+                       vigencia_fin = :vigencia_fin,
+                       id_funcion_padre = :id_funcion_padre,
+                       id_funcion_predecesor = :id_funcion_predecesor,
+                       id_servicio = :id_servicio
+                WHERE  id_funcion = :id_funcion";
+
+        return $this->db->sentencia_ejecutar($sql, $params);
+    }
+
+
+    /**
+     * Retorna todas las funciones que son hijas de una funcion
+     * @param $id
+     * @return Funcion|null
+     */
+    public function getFuncionesHijas($idFuncionPadre, $hidratar = true)
+    {
+        $sql = sprintf(
+            "WITH RECURSIVE cte_padres_hijos(
+                id_funcion, 
+                nombre, 
+                descripcion,
+                es_agrupador,
+                id_funcion_predecesor,
+                id_funcion_padre, 
+                vigencia_inicio,
+                vigencia_fin,
+                nivel) AS (
+            SELECT 
+                id_funcion, 
+                nombre, 
+                descripcion,
+                es_agrupador,
+                id_funcion_predecesor,
+                id_funcion_padre, 
+                vigencia_inicio,
+                vigencia_fin,
+                1 as nivel
+            FROM funciones 
+            WHERE id_funcion_padre = %s -- obtener los nodos raíz
+            UNION ALL
+            SELECT 
+                funciones.id_funcion, 
+                funciones.nombre, 
+                funciones.descripcion,
+                funciones.es_agrupador,
+                funciones.id_funcion_predecesor,
+                funciones.id_funcion_padre, 
+                funciones.vigencia_inicio,
+                funciones.vigencia_fin,
+                cte_padres_hijos.nivel + 1
+            FROM funciones
+            JOIN cte_padres_hijos ON funciones.id_funcion_padre = cte_padres_hijos.id_funcion
+        )
+        SELECT 
+            id_funcion, 
+            nombre, 
+            descripcion,
+            es_agrupador,
+            id_funcion_predecesor,
+            id_funcion_padre, 
+            vigencia_inicio,
+            vigencia_fin,
+            nivel
+        FROM cte_padres_hijos
+        ORDER BY nivel, id_funcion;",
+            $idFuncionPadre
+        );
+
+        $result = $this->db->consultar($sql);
+
+        if ($hidratar) {
+            $result = $this->hidratarFunciones($result);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Retorna todas las funciones sucesoras de una funcion predecesora
+     * 
+     * @param $id
+     * @return Funcion|null
+     */
+    public function getFuncionesPredecesoras($idFuncionPredecesor, $hidratar = true)
+    {
+        $sql = sprintf(
+            "WITH RECURSIVE cte_predecesores(
+                    id_funcion, 
+                    nombre, 
+                    descripcion,
+                    es_agrupador,
+                    id_funcion_predecesor,
+                    id_funcion_padre, 
+                    vigencia_inicio,
+                    vigencia_fin,
+                    nivel) AS (
+                SELECT 
+                    id_funcion, 
+                    nombre, 
+                    descripcion,
+                    es_agrupador,
+                    id_funcion_predecesor,
+                    id_funcion_padre, 
+                    vigencia_inicio,
+                    vigencia_fin,
+                    1 as nivel
+                FROM funciones
+                WHERE id_funcion_predecesor = %s -- obtener los nodos raíz
+                    
+                UNION ALL
+                    
+                SELECT 
+                    funciones.id_funcion, 
+                    funciones.nombre, 
+                    funciones.descripcion,
+                    funciones.es_agrupador,
+                    funciones.id_funcion_predecesor,
+                    funciones.id_funcion_padre, 
+                    funciones.vigencia_inicio,
+                    funciones.vigencia_fin,
+                    cte_predecesores.nivel + 1
+                FROM funciones
+                JOIN cte_predecesores ON funciones.id_funcion_predecesor = cte_predecesores.id_funcion
+            )
+                
+            SELECT 
+                id_funcion, 
+                nombre, 
+                descripcion,
+                es_agrupador,
+                id_funcion_predecesor,
+                id_funcion_padre, 
+                vigencia_inicio,
+                vigencia_fin,
+                nivel
+            FROM cte_predecesores
+            ORDER BY nivel, id_funcion;",
+            $idFuncionPredecesor
+        );
+
+        $result = $this->db->consultar($sql);
+
+        if ($hidratar) {
+            $result = $this->hidratarFunciones($result);
+        }
+
+        return $result;
+    }
+
+
+    /**
+     * @param $datos
+     * @return Funcion
+     */
+    protected function hidratarFuncion($datos)
+    {
+        $funcion = new Funcion();
+
+        $funcion->loadFromDatos($datos);
+
+        return $funcion;
+    }
+
+    /**
+     * @param $datos
+     * @return Funcion[] colección de Funciones
+     */
+    protected function hidratarFunciones($datos)
+    {
+        $funciones = [];
+
+        if (count($datos) < 1) {
+            return $funciones;
+        }
+
+        foreach ($datos as $dato) {
+            $funciones[] = $this->hidratarFuncion($dato);
+        }
+
+        return $funciones;
+    }
+}
diff --git a/core/src/UNAM/Tupa/Core/Negocio/Organizacion/UnidadGestion.php b/core/src/UNAM/Tupa/Core/Negocio/Organizacion/UnidadGestion.php
index a269a02a..326a911d 100644
--- a/core/src/UNAM/Tupa/Core/Negocio/Organizacion/UnidadGestion.php
+++ b/core/src/UNAM/Tupa/Core/Negocio/Organizacion/UnidadGestion.php
@@ -47,7 +47,7 @@ class UnidadGestion {
         $this->idGrupoArai = $idGrupo;
     }
 
-    public function setServicios(?array $servicios){
+    public function setServicios(array $servicios){
         $this->servicios = $servicios;
     }
 
diff --git a/core/src/UNAM/Tupa/Core/Negocio/SolicitudesServicios/Funcion.php b/core/src/UNAM/Tupa/Core/Negocio/SolicitudesServicios/Funcion.php
new file mode 100644
index 00000000..9f4def59
--- /dev/null
+++ b/core/src/UNAM/Tupa/Core/Negocio/SolicitudesServicios/Funcion.php
@@ -0,0 +1,157 @@
+<?php  declare(strict_types=1);
+
+namespace UNAM\Tupa\Core\Negocio\SolicitudesServicios;
+
+use UNAM\Tupa\Core\Negocio\SolicitudesServicios\Servicio;
+
+class Funcion {
+    private int $id;
+    private string $nombre;
+    private string $descripcion;
+    private bool $esAgrupador;
+    private Funcion $funcionPadre;
+    private Funcion $funcionPredecesor;
+    private string $vigenciaInicio;    
+    private string $vigenciaFin;    
+    private Servicio $servicio;    
+
+    //Getter
+    public function getId():int {
+        return $this->id;
+    }
+
+    public function getNombre():string {
+        return $this->nombre;
+    }
+
+    public function getDescripcion():string {
+        return $this->descripcion;
+    }
+
+    public function getEsAgrupador():bool {
+        return $this->esAgrupador;
+    }
+
+    public function getFuncionPadre():?Funcion {
+        return $this->funcionPadre ?? null;
+    }
+
+    public function getFuncionPredecesor():?Funcion {
+        return $this->funcionPredecesor ?? null;
+    }
+
+    public function getVigenciaInicio():string {
+        return $this->vigenciaInicio;
+    }
+
+    public function getVigenciaFin():?string {
+        return $this->vigenciaFin ?? null;
+    }
+    
+    public function getServicio():?Servicio {
+        return $this->servicio ?? null;
+    }
+
+    //Setter
+    public function setId($id):void {
+        $this->id = $id;
+    }
+
+    public function setNombre(string $nombre):void {
+        $this->nombre = $nombre;
+    }
+
+    public function setDescripcion(string $descripcion):void {
+        $this->descripcion = $descripcion;
+    }
+
+    public function setEsAgrupador(bool $esAgrupador):void {
+        $this->esAgrupador = $esAgrupador;
+    }
+
+    public function setFuncionPadre(Funcion $funcionPadre):void {
+        $this->funcionPadre = $funcionPadre;
+    }
+
+    public function setFuncionPredecesor(Funcion $funcionPredecesor):void {
+        $this->funcionPredecesor = $funcionPredecesor;
+    }
+
+    public function setVigenciaInicio(string $vigenciaInicio):void {
+        $this->vigenciaInicio = $vigenciaInicio;
+    }
+
+    public function setVigenciaFin(string $vigenciaFin):void {
+        $this->vigenciaFin = $vigenciaFin;
+    }
+    
+    public function setServicio(Servicio $servicio):void {
+        $this->servicio =  $servicio;
+    }
+
+    /**
+     * Hidrata los atributos del objeto a partir de $datos
+     *
+     * @param array $datos inicializar directamente con un set de datos
+     */
+    public function loadFromDatos(array $datos)
+    {
+        if (isset($datos['id_funcion'])) {
+            $this->setId($datos['id_funcion']);
+        }
+
+        if (isset($datos['nombre'])) {
+            $this->setNombre($datos['nombre']);
+        }
+
+        if (isset($datos['descripcion'])) {
+            $this->setDescripcion($datos['descripcion']);
+        }
+
+        if (isset($datos['es_agrupador'])) {
+            $this->setEsAgrupador($datos['es_agrupador']);
+        }
+
+        if (isset($datos['id_funcion_padre'])) {
+            $funcionPadre = new Funcion();
+            $funcionPadre->setId($datos['id_funcion_padre']);
+            $this->setFuncionPadre($funcionPadre);
+        }
+
+        if (isset($datos['id_funcion_predecesor'])) {
+            $funcionPredecesor = new Funcion();
+            $funcionPredecesor->setId($datos['id_funcion_predecesor']);
+            $this->setFuncionPredecesor($funcionPredecesor);
+        }
+
+        if (isset($datos['vigencia_inicio'])) {
+            $this->setVigenciaInicio($datos['vigencia_inicio']);
+        }
+
+        if (isset($datos['vigencia_fin'])) {
+            $this->setVigenciaFin($datos['vigencia_fin']);
+        }
+
+        if (isset($datos['id_servicio'])) {
+            $servicio = new Servicio();
+            $servicio->setId($datos['id_servicio']);
+            $this->setServicio($servicio);
+        }
+    }
+
+    public function toArray(): array
+    {
+        return [
+            'id_funcion' => $this->getId(),
+            'nombre' => $this->getNombre(),
+            'descripcion' => $this->getDescripcion(),
+            'es_agrupador' => $this->getEsAgrupador(),
+            'id_funcion_padre' => is_object($this->getFuncionPadre()) ? $this->getFuncionPadre()->getId() : null,
+            'id_funcion_predecesor' => is_object($this->getFuncionPredecesor()) ? $this->getFuncionPredecesor()->getId() : null,
+            'vigencia_inicio' => $this->getVigenciaInicio(),
+            'vigencia_fin' => $this->getVigenciaFin(),
+            'id_servicio' => is_object($this->getServicio()) ? $this->getServicio()->getId() : null
+        ];
+    }
+}
+
-- 
GitLab