## ¡Advertencia!

***Este proyecto está en .NET 10. Los SDK de versiones anteriores no son compatibles. Ten en cuenta lo anterior para desarrollar, compilar y ejecutar código, así como para las implementaciones en Docker y Jenkins.***

## Introducción

Este proyecto es un servicio gRPC .NET 10, que implementa una conexión a la base de datos del proyecto **Nube 3.0**
mediante un canal seguro (Certificados OpenSSL).

Expone un CRUD dinámico sobre las 41 tablas de la base de datos `cloud_services` sin necesidad de definir un RPC por entidad — consulta la [documentación del CRUD dinámico](CRUD_DOCUMENTATION.md) para ver los métodos disponibles y ejemplos de consumo.

## Requisitos 

- [SDK de .NET 10 para Linux, macOS y Windows](https://dotnet.microsoft.com/es-es/download/dotnet/10.0)  **Obligatorio**
- [Visual Studio 2022 para macOS y Windows](https://visualstudio.microsoft.com/es/vs/)  **Opcional**

Se considera obligatorio el SDK ya que es necesario para compilar y ejecutar este proyecto.

Lo recomendable es utilizar el IDE oficial de Visual Studio para poder ejecutar, compilar, debuggear, administrar referencias y librerías.

## Contenido

La raíz del repositorio contiene la solución del proyecto servidor. (No se incluyen los certificados de seguridad).

Se configuró el archivo .gitignore para mantener el repositorio limpio de archivos de compilación innecesarios.

## Ejecución 

### Credenciales de base de datos (obligatorio)

**Sin este paso la aplicación no arranca.** Las cadenas de conexión viven en `server/appsettings.secrets.json`, un archivo no versionado (está en `.gitignore`) que se carga como obligatorio en `Program.cs`. Si falta, el arranque falla con excepción antes de cualquier otra inicialización.

Para crearlo, copiar la plantilla y rellenar los valores reales:

```
cd server
cp appsettings.secrets.example.json appsettings.secrets.json     # Linux/macOS
copy appsettings.secrets.example.json appsettings.secrets.json  # Windows
```

El archivo tiene esta forma:

```json
{
	"ConnectionStrings": {
		"Main": "server=<host>;Port=<port>;user=<user>;password=<password>;database=<database>;...",
		"Local": "server=127.0.0.1;Port=<port>;user=<user>;password=<password>;database=<database>;..."
	}
}
```

`Main` es la cadena que consume tanto el `CloudServicesDbContext` (EF Core) como la capa legacy `DataAccess`. Ambas la reciben por inyección de dependencias desde `IConfiguration`.

> **Nunca commitear este archivo.** Si necesitas agregar una clave nueva, agrégala a `appsettings.secrets.example.json` con valores de plantilla, no con credenciales reales.

#### Pruebas

> Para poner las pruebas a correr en una máquina nueva —túnel SOCKS, contexto de kubectl, port-forward y diagnóstico de fallas— ver [`tests/DynamicCrud.Tests/README.md`](tests/DynamicCrud.Tests/README.md).

`tests/DynamicCrud.Tests` contiene **dos suites independientes**, separadas por el trait `suite`:

| Suite | Contra qué corre | Comando |
|---|---|---|
| `db` | Base de datos real, en proceso | `make test-db` |
| `grpc` | El DAO ya desplegado, por gRPC | `make test-grpc` |

`make test` corre ambas.

##### Suite de base de datos (`db`)

Reutiliza **el mismo** `server/appsettings.secrets.json` — se copia a su carpeta de salida durante el build, así que no hay una segunda copia de credenciales que mantener. Usa la cadena `Local`.

En CI, donde no existe el archivo, se puede definir la variable de entorno equivalente (tiene prioridad sobre el JSON):

```
ConnectionStrings__Local=server=...;Port=...;user=...;password=...;database=...;
```

Si no hay ninguna de las dos, las pruebas fallan con un mensaje que indica exactamente qué falta.

##### Suite de caja negra por gRPC (`grpc`)

Habla gRPC contra una instancia ya desplegada usando los clientes tipados generados de los protos. No abre conexión a la base de datos ni levanta el servidor en proceso.

**Es de solo consulta.** Ningún caso invoca `Create`, `Update`, `Delete`, `Upsert`, `BulkCreate`, `BulkUpdate`, `BulkDelete` ni `ExecuteBatch`, así que es idempotente y se puede correr contra QA cuantas veces haga falta sin ensuciar datos. Los 25 rpcs de escritura del servicio quedan **fuera de cobertura** por diseño.

Levanta el port-forward y corre:

```bash
kubectl port-forward svc/csharp-dao-service 8440:8440 -n <namespace>
make test-grpc
```

El endpoint por defecto es `https://127.0.0.1:8440`. El certificado del servidor está emitido para su nombre dentro del cluster, así que a través de un port-forward la suite cifra pero **no valida el nombre del certificado** — el equivalente de `grpcurl -insecure`. Para usar el puerto sin TLS del pod:

```bash
kubectl port-forward pod/<pod> 50051:50051 -n <namespace>
make test-grpc DAO_ENDPOINT=http://127.0.0.1:50051
```

Si el port-forward no está arriba, la suite falla de inmediato con un mensaje que lo dice, en vez de dejar cientos de timeouts.

Qué verifica, sobre los 43 modelos del `ModelRegistry`:

- `GetFields` declara nombre, tipo y columna de cada campo, y `GetAll` no devuelve campos fuera de esa declaración.
- `Search` paginado y `Count` reportan **el mismo total** — verificación cruzada entre dos rpcs distintos.
- Páginas consecutivas no comparten registros y el total se mantiene estable entre ellas.
- `orderBy` asc/desc produce secuencias efectivamente ordenadas.
- `GetById` con un id tomado de `GetAll` devuelve ese mismo registro.
- Un filtro `eq` sobre la clave primaria devuelve exactamente un registro.
- La proyección de columnas devuelve solo lo pedido.
- Rutas de error (modelo inexistente, columna inválida, operador inválido, `order_by` inválido) responden con `success=false` y **no** con una excepción gRPC.

Más los rpcs de lectura de `servicioDAOService`, `administrationService`, `ProfileService` y `CatalogService`. Sus parámetros no van hardcodeados: se descubren con el CRUD dinámico antes de cada llamada, porque varios stored procedures interpretan el filtro nulo como "no coincide con nada" en vez de "sin filtro", y llamarlos en vacío no ejercería su lógica real.

La capa legacy atrapa las excepciones de MySQL y las devuelve como `NotFound`, así que un procedure inexistente se ve desde fuera igual que una tabla vacía. Para poder distinguirlos, los rpcs que consultan **una sola tabla** con un id recién leído de esa misma tabla tratan `NotFound` como fallo: la fila existe, así que "sin resultados" significa que la consulta está rota. Los que dependen de joins conservan el criterio laxo.

##### Estado actual contra QA

362 de 367 pasan. Los 5 fallos son defectos reales, no ruido de las pruebas:

| Fallo | Causa |
|---|---|
| `StatusSupplyService`, `ServiceStatusService` | Declarados en `cloudServiceDAO.proto` pero **sin override en C#**; el servidor responde `Unimplemented` |
| `GetServicesCatalogEF` | Igual: declarado en `EntityFrameworkDAOServices.proto`, `CatalogService.cs` solo implementa los otros dos |
| `getUserVCD` | `ServicesDAO.cs:378` usa `org_id > 0 ? org_id : 1` — sin `org_id` filtra por la organización 1 en lugar de no filtrar |
| `getStatusCatalog` | `spskGetStatusCatalog` usa `or (status_catalog_id is not null)` donde debía ser `is null`; el filtro nunca se cumple |

> Cobertura no alcanzada: `getTermsandConditions` invoca `spskGetTermsAndConditions` y en la base el procedure se llama `spskGetTermsConditions` (sin el "And"), pero `terms_conditions` está vacía en QA, así que el caso termina temprano y el defecto no se manifiesta. Lo mismo aplica a cualquier rpc cuya tabla origen no tenga filas.

### Certificados 

Los certificados **no se distribuyen en el repositorio**. En `server/` solo se versionan `ca.crt` y `ca.key`; el par del servidor (`server.pem` y `server.key`) debe solicitarse al equipo y colocarse en esa misma carpeta.

Las rutas se configuran en `appsettings.json`, sección `GrpcServer:Certificates`.

> **Importante:** si faltan `server.pem` o `server.key`, la aplicación imprime `[ERROR FATAL] No se encontraron certificados` **pero continúa arrancando**, dejando el puerto seguro (8440) escuchando en texto plano — el log lo delata como `http://` en lugar de `https://`. Para desarrollo local usar el puerto inseguro (50051), que está declarado como tal. No desplegar a un entorno compartido sin verificar que el arranque anuncie `https://` en el puerto seguro.

### Terminal/Línea de comandos

En este método tendremos que compilar y ejecutar de manera individual cada proyecto comenzando por el servidor.

Una vez clonado el repositorio ejecutamos lo siguiente:

Primero comprobar la versión de SDK, si el comando no es reconocido es debido que no se instaló correctamente el SDK
```
dotnet --version
```


En caso de no ser alguna versión 10.x.x, listamos las versiones instaladas
```
dotnet --list-sdks
```

Utilizaremos el número de cualquier versión 10.x.x (de preferencia la más reciente disponible) para armar el siguiente comando:

```
dotnet new globaljson --sdk-version 10.x.xx --force
```

Y revisamos que se esté utilizando una versión 10.x.x

```
dotnet --version
```
 
Nos ubicamos en la carpeta de servidor, compilamos y después ejecutamos.

```
cd server
dotnet build
dotnet run
```

Recordatorio: si no creaste `appsettings.secrets.json` (ver sección [Credenciales de base de datos](#credenciales-de-base-de-datos-obligatorio)), `dotnet run` falla de inmediato con una excepción de configuración. El `dotnet build` sí compila sin él — el archivo solo se necesita en tiempo de ejecución.

Nota: Si en el momento de ejecutar el proyecto genera un error de que no ubica algunos de los certificados, se deberá copiar los certifcados de la ruta raíz a la ruta donde esté buscando, esto varia en cada sistema operativo.

Nota: Es normal que se generen advertencias, lo importante es que no se generen errores durante el build.

Nota: El problema más común está relacionado a los puertos. Para cambiarlos, modificar el archivo `Program.cs` en la raíz de la carpeta `server`.

## Ejecución contenedores 

Una vez validado la funcionalidad con los pasos anteriores, procederemos a crear la imagen, nos ubicamos en la carpeta servidor para compilar y publicar.

 ```
cd server
dotnet build 
dotnet publish
```

El comando dotnet build es opcional si ya lo has ejecutado anteriormente, se recomienda en caso de una modificación al código o si no se ha utilizado por primera vez.

Dentro de la carpeta server se encuentra el archivo Dockerfile, procederemos acrear la imagen y posteriormente ejecutar el contenedor.

```
docker build -t server-image -f Dockerfile .   
docker run -p 50051:50051 -p 443:443 --name server server-image  
```
Nota: El problema más común está relacionado a los puertos. Para cambiarlos, modificar el archivo `Program.cs` en la raíz de la carpeta `server`.

## Visual Studio 2022

Abrimos el archivo `cloudservicesDAO.sln`, seleccionamos el proyecto `server` e iniciamos con el botón ejecutar ▷.

Nota: Si al ejecutar el proyecto genera un error de certificados, copiar los certificados de la raíz al directorio donde los esté buscando — esto varía según el sistema operativo.

---

## Guía de Extensión

### Consulta personalizada con Entity Framework (INNER JOIN)

Cuando se necesita una consulta que cruce varias tablas, proyecte campos de múltiples entidades o encapsule lógica de negocio, no se usa el CRUD dinámico. Se sigue el mismo patrón que `ProfileRepository` + `CatalogService`: implementar en la capa `ServicesDAO` y exponer desde `server` vía gRPC.

El flujo completo es:

```
CloudServicesDbContext  →  Repositorio (ServicesDAO)  →  Servicio gRPC (server)  →  .proto
```

El ejemplo implementa una consulta que obtiene los servicios contratados por una organización, cruzando `contracted_services` + `services` + `product_type`.

#### Paso 1 — Crear el DTO e interfaz en `ServicesDAO/Repositories/`

El DTO es el objeto de transferencia que sale del repositorio hacia el servicio gRPC. No es una entidad EF y no se registra en el `DbContext`.

```csharp
// ServicesDAO/Repositories/IContractedServicesRepository.cs
namespace ServicesDAO.Repositories
{
    public class ContractedServiceDetailDto
    {
        public int       ContractId      { get; set; }
        public string    ServiceName     { get; set; }
        public string    ProductTypeName { get; set; }
        public bool      Active          { get; set; }
        public DateTime? ContractedAt    { get; set; }
    }

    public interface IContractedServicesRepository
    {
        Task<IEnumerable<ContractedServiceDetailDto>> GetByOrgAsync(int orgId);
    }
}
```

#### Paso 2 — Implementar el repositorio con INNER JOIN

```csharp
// ServicesDAO/Repositories/ContractedServicesRepository.cs
using Microsoft.EntityFrameworkCore;
using ServicesDAO.Data;

namespace ServicesDAO.Repositories
{
    public class ContractedServicesRepository : IContractedServicesRepository
    {
        private readonly CloudServicesDbContext _context;

        public ContractedServicesRepository(CloudServicesDbContext context)
            => _context = context;

        public async Task<IEnumerable<ContractedServiceDetailDto>> GetByOrgAsync(int orgId)
        {
            // INNER JOIN: contracted_services → services → product_type
            return await _context.ContractedServices
                .AsNoTracking()
                .Where(cs => cs.OrgId == orgId && cs.Active)
                .Join(
                    _context.Services,
                    cs => cs.ServiceId,
                    s  => s.ServicesID,
                    (cs, s) => new { cs, s }
                )
                .Join(
                    _context.ProductTypes,
                    x  => x.s.ProductTypeID,
                    pt => pt.Id,
                    (x, pt) => new ContractedServiceDetailDto
                    {
                        ContractId      = x.cs.Id,
                        ServiceName     = x.s.Name,
                        ProductTypeName = pt.ProductName,
                        Active          = x.cs.Active,
                        ContractedAt    = x.cs.CreatedAt
                    }
                )
                .OrderBy(d => d.ServiceName)
                .ToListAsync();
        }
    }
}
```

> **Alternativa con `Include`:** Si los modelos tienen propiedades de navegación configuradas en `OnModelCreating`, se puede reemplazar `.Join()` por `.Include(cs => cs.Service).ThenInclude(s => s.ProductType)`. `.Join()` explícito es la opción segura cuando las FKs no tienen propiedad de navegación declarada.

#### Paso 3 — Registrar en DI (`server/Program.cs`)

En la sección de repositorios, junto a `IProfileRepository`:

```csharp
builder.Services.AddScoped<IContractedServicesRepository, ContractedServicesRepository>();
```

#### Paso 4 — Inyectar en el servicio gRPC (`server/Services/`)

El servicio gRPC recibe el repositorio por constructor, igual que `CatalogService` recibe `IProfileRepository`:

```csharp
// server/Services/CatalogService.cs
public class CatalogService : EntityFrameworkDAOServices.CatalogService.CatalogServiceBase
{
    private readonly IProfileRepository _profileRepo;
    private readonly IContractedServicesRepository _contractedRepo; // nuevo
    private readonly ILogger<CatalogService> _logger;

    public CatalogService(
        IProfileRepository profileRepo,
        IContractedServicesRepository contractedRepo,  // nuevo
        ILogger<CatalogService> logger)
    {
        _profileRepo    = profileRepo;
        _contractedRepo = contractedRepo;
        _logger         = logger;
    }

    public override async Task<GetContractedServicesByOrgResponse> GetContractedServicesByOrg(
        GetContractedServicesByOrgRequest request, ServerCallContext context)
    {
        try
        {
            var results  = await _contractedRepo.GetByOrgAsync(request.OrgId);
            var response = new GetContractedServicesByOrgResponse();

            foreach (var d in results)
            {
                response.Services.Add(new ContractedServiceDetailInfo
                {
                    ContractId      = d.ContractId,
                    ServiceName     = d.ServiceName,
                    ProductTypeName = d.ProductTypeName,
                    Active          = d.Active,
                    ContractedAt    = d.ContractedAt?.ToString("o") ?? ""
                });
            }
            return response;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error en GetContractedServicesByOrg");
            throw new RpcException(new Status(StatusCode.Internal, ex.Message));
        }
    }
}
```

---

### Publicar la consulta en un archivo `.proto`

#### ¿En qué `.proto` agregar el nuevo RPC?

| Archivo | Cuándo usarlo |
|---|---|
| `EntityFrameworkDAOServices.proto` | Consultas EF de **catálogo**: servicios, tipos de producto, planes, imágenes — lo que extiende `CatalogService` |
| `administrationservices.proto` | Consultas de **perfiles, roles, permisos, menús** |
| Nuevo `.proto` | Cuando el dominio no encaja en los anteriores (facturación, backups, reportes) |
| `cloudServiceDAO.proto` | **No agregar nuevos RPCs** — es el servicio legacy |

#### Opción A — Respuesta tipada (patrón `CatalogService`)

Para consultas que devuelven una estructura conocida y fija. Es el estilo que ya usa `EntityFrameworkDAOServices.proto`:

```protobuf
// EntityFrameworkDAOServices.proto — agregar antes del bloque service { }

message ContractedServiceDetailInfo {
    int32  contract_id        = 1;
    string service_name       = 2;
    string product_type_name  = 3;
    bool   active             = 4;
    string contracted_at      = 5;
}

message GetContractedServicesByOrgRequest {
    int32 org_id = 1;
}

message GetContractedServicesByOrgResponse {
    repeated ContractedServiceDetailInfo services = 1;
}

// Dentro del bloque service CatalogService { ... }
rpc GetContractedServicesByOrg (GetContractedServicesByOrgRequest)
    returns (GetContractedServicesByOrgResponse);
```

#### Opción B — Homologar a `CrudResponse` (patrón `DynamicCrudService`)

Si se requiere que la respuesta sea consistente con el `DynamicCrudService` — por ejemplo, porque el cliente ya maneja `CrudResponse` y quiere un único modelo de respuesta — se define un mensaje equivalente en el `.proto` destino. `CrudResponse` no se importa entre archivos `.proto`, por lo que el patrón es replicar la estructura:

```protobuf
// EntityFrameworkDAOServices.proto — alternativa homologada
// Requiere agregar al inicio del archivo:
// import "google/protobuf/struct.proto";

message EFCrudResponse {
    bool                  success = 1;
    string                message = 2;
    google.protobuf.Value data    = 3;
    string                detail  = 4;
    string                status  = 5;  // "success" | "info" | "error"
    int32                 total   = 6;
}

message GetContractedByOrgRequest { int32 org_id = 1; }

// Dentro del bloque service CatalogService { ... }
rpc GetContractedServicesByOrg (GetContractedByOrgRequest) returns (EFCrudResponse);
```

En el servicio gRPC, serializar el resultado a `google.protobuf.Value`:

```csharp
using Google.Protobuf.WellKnownTypes;
using System.Text.Json;

var results   = await _contractedRepo.GetByOrgAsync(request.OrgId);
var json      = JsonSerializer.Serialize(results);
var dataValue = Value.Parser.ParseJson(json);

return new EFCrudResponse
{
    Success = true,
    Message = "OK",
    Status  = "success",
    Total   = results.Count(),
    Data    = dataValue
};
```

> **¿Cuándo usar cada opción?**
> - **Opción A (tipada)** — cuando la estructura del resultado es estable y el cliente puede generar el stub con el tipo concreto. Más explícita y más fácil de versionar.
> - **Opción B (homologada)** — cuando el cliente ya consume `CrudResponse` y necesita un contrato uniforme, o cuando la estructura puede variar.

#### Ciclo de compilación tras modificar un `.proto`

```bash
cd server
dotnet build   # Grpc.Tools regenera las clases C# automáticamente en obj/
```

Los archivos generados viven en `server/obj/` — nunca editarlos a mano. Si el build falla con `CS0246` (tipo no encontrado), verificar que el `package` en el `.proto` coincide con el namespace que importa el servicio.

Si se crea un **nuevo archivo `.proto`**, hay dos pasos adicionales:

**1. Referenciarlo en `server/server.csproj`:**

```xml
<ItemGroup>
  <Protobuf Include="..\MiNuevoDominio.proto" GrpcServices="Server" />
</ItemGroup>
```

**2. Mapear el servicio en `server/Services/GrpcServerHost.cs`** (método `MapAllGrpcServices`):

```csharp
app.MapGrpcService<MiNuevoServiceImpl>();
```
