diff --git a/Atlas Balance/AGENTS.md b/Atlas Balance/AGENTS.md
index 5dbdd8b..acec0e4 100644
--- a/Atlas Balance/AGENTS.md
+++ b/Atlas Balance/AGENTS.md
@@ -227,7 +227,7 @@ npm run build
# Release Windows x64
cd "Atlas Balance"
-powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\scripts\Build-Release.ps1" -Version V-01.03
+powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\scripts\Build-Release.ps1" -Version V-01.04
# Conectar a PostgreSQL
psql -h localhost -p 5433 -U app_user -d atlas_balance
diff --git a/Atlas Balance/Actualizar Atlas Balance.cmd b/Atlas Balance/Actualizar Atlas Balance.cmd
index e62b936..94a7df4 100644
--- a/Atlas Balance/Actualizar Atlas Balance.cmd
+++ b/Atlas Balance/Actualizar Atlas Balance.cmd
@@ -1,2 +1,3 @@
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\Actualizar-AtlasBalance.ps1" %*
+exit /b %ERRORLEVEL%
diff --git a/Atlas Balance/CLAUDE.md b/Atlas Balance/CLAUDE.md
index c0b6a8a..707d447 100644
--- a/Atlas Balance/CLAUDE.md
+++ b/Atlas Balance/CLAUDE.md
@@ -227,7 +227,7 @@ npm run build
# Release Windows x64
cd "Atlas Balance"
-powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\scripts\Build-Release.ps1" -Version V-01.03
+powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\scripts\Build-Release.ps1" -Version V-01.04
# Conectar a PostgreSQL
psql -h localhost -p 5433 -U app_user -d atlas_balance
diff --git a/Atlas Balance/Directory.Build.props b/Atlas Balance/Directory.Build.props
index aed92e0..17b94e1 100644
--- a/Atlas Balance/Directory.Build.props
+++ b/Atlas Balance/Directory.Build.props
@@ -2,10 +2,10 @@
Atlas Labs
Atlas Balance
- 1.3.0
- 1.3.0.0
- 1.3.0.0
- V-01.03
+ 1.4.0
+ 1.4.0.0
+ 1.4.0.0
+ V-01.04
false
diff --git a/Atlas Balance/Instalar Atlas Balance.cmd b/Atlas Balance/Instalar Atlas Balance.cmd
index 776b8d9..ed3345e 100644
--- a/Atlas Balance/Instalar Atlas Balance.cmd
+++ b/Atlas Balance/Instalar Atlas Balance.cmd
@@ -1,2 +1,3 @@
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\Instalar-AtlasBalance.ps1" %*
+exit /b %ERRORLEVEL%
diff --git a/Atlas Balance/README_RELEASE.md b/Atlas Balance/README_RELEASE.md
index 3254788..14b5b4f 100644
--- a/Atlas Balance/README_RELEASE.md
+++ b/Atlas Balance/README_RELEASE.md
@@ -1,7 +1,9 @@
-# Atlas Balance V-01.03 - release Windows x64
+# Atlas Balance V-01.04 - release Windows x64
Este paquete es autonomo para servidor Windows: el frontend ya esta compilado, el backend y Watchdog van publicados self-contained y la base de datos se prepara desde el instalador.
+El ZIP `main` de GitHub no sirve como instalador. Usa `AtlasBalance-V-01.04-win-x64.zip`; dentro deben existir `api\GestionCaja.API.exe` y `watchdog\GestionCaja.Watchdog.exe`.
+
## Scripts de un clic
- `install.cmd`: instala dependencias, prepara PostgreSQL, crea base de datos, copia API/Watchdog/frontend estatico, genera configuracion de produccion, certificado local, servicios Windows y atajos.
@@ -31,7 +33,11 @@ Por defecto la instalacion queda en `C:\AtlasBalance`. Para usar otra ruta:
## Base de datos
-El instalador intenta preparar PostgreSQL 16 con `winget` cuando no se pasa una instancia existente. Genera passwords fuertes y guarda las credenciales iniciales en:
+El requisito real es PostgreSQL 16+. PostgreSQL 17 es valido.
+
+En Windows Server 2019, instala PostgreSQL manualmente si `winget` falla o no esta disponible. `winget` no es una base fiable para prometer instalacion "one click" en servidores limpios.
+
+El instalador genera passwords fuertes y guarda las credenciales iniciales en:
```text
C:\AtlasBalance\INSTALL_CREDENTIALS_ONCE.txt
@@ -42,7 +48,35 @@ Guarda ese contenido en un gestor de passwords y borra el archivo despues del pr
Si ya tienes PostgreSQL y quieres usarlo:
```powershell
-.\install.cmd -PostgresAdminPassword "PASSWORD_POSTGRES" -PostgresBinPath "C:\Program Files\PostgreSQL\16\bin"
+.\install.cmd -PostgresAdminPassword "PASSWORD_POSTGRES" -PostgresBinPath "C:\Program Files\PostgreSQL\17\bin"
```
No documentes passwords reales en tickets, docs ni chats.
+
+Si reinstalas sobre una BD existente, las credenciales iniciales no se regeneran. Usa el admin ya creado o:
+
+```powershell
+powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\scripts\Reset-AdminPassword.ps1" -InstallPath C:\AtlasBalance -AdminEmail admin@atlasbalance.local -GeneratePassword
+```
+
+Health check recomendado:
+
+```powershell
+curl.exe -k -v https://localhost/api/health
+```
+
+## Actualizar una instalacion existente
+
+Desde la carpeta descomprimida de este paquete:
+
+```powershell
+.\update.cmd -InstallPath C:\AtlasBalance
+```
+
+Si la instalacion ya tiene los scripts nuevos, tambien puedes lanzar desde la carpeta instalada apuntando al paquete:
+
+```powershell
+C:\AtlasBalance\update.cmd -PackagePath C:\Temp\AtlasBalance-V-01.04-win-x64 -InstallPath C:\AtlasBalance
+```
+
+El actualizador crea backup previo, conserva configuracion, reemplaza API/Watchdog, actualiza scripts operativos instalados, actualiza `VERSION`/runtime y valida `/api/health` con `curl.exe -k`.
diff --git a/Atlas Balance/VERSION b/Atlas Balance/VERSION
index 12d3cb0..f179ef7 100644
--- a/Atlas Balance/VERSION
+++ b/Atlas Balance/VERSION
@@ -1 +1 @@
-V-01.03
+V-01.04
diff --git a/Atlas Balance/backend/src/GestionCaja.API/Constants/AuditActions.cs b/Atlas Balance/backend/src/GestionCaja.API/Constants/AuditActions.cs
index df8ee8e..0256966 100644
--- a/Atlas Balance/backend/src/GestionCaja.API/Constants/AuditActions.cs
+++ b/Atlas Balance/backend/src/GestionCaja.API/Constants/AuditActions.cs
@@ -16,6 +16,9 @@ public static class AuditActions
public const string CambioPermisos = "CAMBIO_PERMISOS";
public const string ConfigAlerta = "CONFIG_ALERTA";
public const string AlertaSaldoDisparada = "ALERTA_SALDO_DISPARADA";
+ public const string PlazoFijoProximoVencer = "PLAZO_FIJO_PROXIMO_VENCER";
+ public const string PlazoFijoVencido = "PLAZO_FIJO_VENCIDO";
+ public const string PlazoFijoRenovado = "PLAZO_FIJO_RENOVADO";
public const string BackupGenerado = "BACKUP_GENERADO";
public const string BackupRetencionAutomatica = "BACKUP_RETENCION_AUTOMATICA";
public const string ExportacionGenerada = "EXPORTACION_GENERADA";
diff --git a/Atlas Balance/backend/src/GestionCaja.API/Controllers/AlertasController.cs b/Atlas Balance/backend/src/GestionCaja.API/Controllers/AlertasController.cs
index 55016ed..010a8fd 100644
--- a/Atlas Balance/backend/src/GestionCaja.API/Controllers/AlertasController.cs
+++ b/Atlas Balance/backend/src/GestionCaja.API/Controllers/AlertasController.cs
@@ -43,7 +43,7 @@ join c in _dbContext.Cuentas on a.CuentaId equals c.Id into cuentaJoin
from cuenta in cuentaJoin.DefaultIfEmpty()
join t in _dbContext.Titulares on cuenta.TitularId equals t.Id into titularJoin
from titular in titularJoin.DefaultIfEmpty()
- orderby a.CuentaId == null descending, titular.Nombre, cuenta.Nombre
+ orderby a.CuentaId == null descending, a.TipoTitular, titular.Nombre, cuenta.Nombre
select new
{
Alerta = a,
@@ -85,6 +85,8 @@ where alertaIds.Contains(d.AlertaId)
Id = x.Alerta.Id,
CuentaId = x.Alerta.CuentaId,
CuentaNombre = x.Cuenta?.Nombre,
+ Alcance = ResolveAlertaAlcance(x.Alerta),
+ TipoTitular = x.Alerta.TipoTitular?.ToString(),
TitularId = x.Cuenta?.TitularId,
TitularNombre = x.Titular?.Nombre,
Divisa = x.Cuenta?.Divisa,
@@ -152,46 +154,42 @@ public async Task Activas(CancellationToken cancellationToken)
[Authorize(Roles = "ADMIN")]
public async Task Crear([FromBody] SaveAlertaSaldoRequest request, CancellationToken cancellationToken)
{
- if (request.SaldoMinimo < 0)
+ if (request is null)
{
- return BadRequest(new { error = "Saldo mínimo inválido" });
+ return BadRequest(new { error = "Request invalido" });
}
- if (request.CuentaId.HasValue)
+ if (request.SaldoMinimo < 0)
{
- var cuentaExists = await _dbContext.Cuentas.AnyAsync(
- x => x.Id == request.CuentaId.Value && x.Activa,
- cancellationToken);
- if (!cuentaExists)
- {
- return BadRequest(new { error = "Cuenta inválida o inactiva" });
- }
+ return BadRequest(new { error = "Saldo minimo invalido" });
}
- var duplicate = await _dbContext.AlertasSaldo.AnyAsync(
- x => x.CuentaId == request.CuentaId,
- cancellationToken);
- if (duplicate)
+ var validationError = await ValidateAlertaRequestAsync(request, null, cancellationToken);
+ if (validationError is not null)
{
- return Conflict(new { error = "Ya existe una alerta para esa cuenta (o global)" });
+ return validationError.Status == StatusCodes.Status409Conflict
+ ? Conflict(new { error = validationError.Error })
+ : BadRequest(new { error = validationError.Error });
}
- var invalidUsers = await ValidateDestinatariosAsync(request.DestinatarioUsuarioIds, cancellationToken);
+ var destinatarios = request.DestinatarioUsuarioIds ?? [];
+ var invalidUsers = await ValidateDestinatariosAsync(destinatarios, cancellationToken);
if (invalidUsers.Count > 0)
{
- return BadRequest(new { error = "Hay destinatarios inválidos" });
+ return BadRequest(new { error = "Hay destinatarios invalidos" });
}
var alerta = new AlertaSaldo
{
Id = Guid.NewGuid(),
CuentaId = request.CuentaId,
+ TipoTitular = request.CuentaId.HasValue ? null : request.TipoTitular,
SaldoMinimo = request.SaldoMinimo,
Activa = request.Activa,
FechaCreacion = DateTime.UtcNow
};
_dbContext.AlertasSaldo.Add(alerta);
- await UpsertDestinatariosAsync(alerta.Id, request.DestinatarioUsuarioIds, cancellationToken);
+ await UpsertDestinatariosAsync(alerta.Id, destinatarios, cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);
await LogAlertaAuditAsync(AuditActions.ConfigAlerta, alerta.Id, before: null, after: request, cancellationToken);
@@ -202,6 +200,11 @@ public async Task Crear([FromBody] SaveAlertaSaldoRequest request
[Authorize(Roles = "ADMIN")]
public async Task Actualizar(Guid id, [FromBody] SaveAlertaSaldoRequest request, CancellationToken cancellationToken)
{
+ if (request is null)
+ {
+ return BadRequest(new { error = "Request invalido" });
+ }
+
var alerta = await _dbContext.AlertasSaldo.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
if (alerta is null)
{
@@ -210,46 +213,38 @@ public async Task Actualizar(Guid id, [FromBody] SaveAlertaSaldoR
if (request.SaldoMinimo < 0)
{
- return BadRequest(new { error = "Saldo mínimo inválido" });
+ return BadRequest(new { error = "Saldo minimo invalido" });
}
- if (request.CuentaId.HasValue)
+ var validationError = await ValidateAlertaRequestAsync(request, id, cancellationToken);
+ if (validationError is not null)
{
- var cuentaExists = await _dbContext.Cuentas.AnyAsync(
- x => x.Id == request.CuentaId.Value && x.Activa,
- cancellationToken);
- if (!cuentaExists)
- {
- return BadRequest(new { error = "Cuenta inválida o inactiva" });
- }
+ return validationError.Status == StatusCodes.Status409Conflict
+ ? Conflict(new { error = validationError.Error })
+ : BadRequest(new { error = validationError.Error });
}
- var duplicate = await _dbContext.AlertasSaldo.AnyAsync(
- x => x.Id != id && x.CuentaId == request.CuentaId,
- cancellationToken);
- if (duplicate)
- {
- return Conflict(new { error = "Ya existe una alerta para esa cuenta (o global)" });
- }
-
- var invalidUsers = await ValidateDestinatariosAsync(request.DestinatarioUsuarioIds, cancellationToken);
+ var destinatarios = request.DestinatarioUsuarioIds ?? [];
+ var invalidUsers = await ValidateDestinatariosAsync(destinatarios, cancellationToken);
if (invalidUsers.Count > 0)
{
- return BadRequest(new { error = "Hay destinatarios inválidos" });
+ return BadRequest(new { error = "Hay destinatarios invalidos" });
}
var before = new
{
alerta.CuentaId,
+ alerta.TipoTitular,
alerta.SaldoMinimo,
alerta.Activa,
destinatarios = await _dbContext.AlertaDestinatarios.Where(x => x.AlertaId == id).Select(x => x.UsuarioId).ToListAsync(cancellationToken)
};
alerta.CuentaId = request.CuentaId;
+ alerta.TipoTitular = request.CuentaId.HasValue ? null : request.TipoTitular;
alerta.SaldoMinimo = request.SaldoMinimo;
alerta.Activa = request.Activa;
- await UpsertDestinatariosAsync(alerta.Id, request.DestinatarioUsuarioIds, cancellationToken);
+ await UpsertDestinatariosAsync(alerta.Id, destinatarios, cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);
await LogAlertaAuditAsync(AuditActions.ConfigAlerta, alerta.Id, before, request, cancellationToken);
@@ -269,6 +264,7 @@ public async Task Eliminar(Guid id, CancellationToken cancellatio
var before = new
{
alerta.CuentaId,
+ alerta.TipoTitular,
alerta.SaldoMinimo,
alerta.Activa,
destinatarios = await _dbContext.AlertaDestinatarios.Where(x => x.AlertaId == id).Select(x => x.UsuarioId).ToListAsync(cancellationToken)
@@ -299,6 +295,49 @@ private async Task> ValidateDestinatariosAsync(IReadOnlyList de
return unique.Except(existing).ToList();
}
+ private async Task ValidateAlertaRequestAsync(SaveAlertaSaldoRequest request, Guid? currentId, CancellationToken cancellationToken)
+ {
+ if (request.CuentaId.HasValue && request.TipoTitular.HasValue)
+ {
+ return new AlertaValidationError("La alerta debe ser por cuenta, por tipo de titular o global; no mezcles alcances", StatusCodes.Status400BadRequest);
+ }
+
+ if (request.CuentaId.HasValue)
+ {
+ var cuentaExists = await _dbContext.Cuentas.AnyAsync(
+ x => x.Id == request.CuentaId.Value && x.Activa,
+ cancellationToken);
+ if (!cuentaExists)
+ {
+ return new AlertaValidationError("Cuenta invalida o inactiva", StatusCodes.Status400BadRequest);
+ }
+
+ var duplicateCuenta = await _dbContext.AlertasSaldo.AnyAsync(
+ x => x.Id != currentId && x.CuentaId == request.CuentaId,
+ cancellationToken);
+ return duplicateCuenta
+ ? new AlertaValidationError("Ya existe una alerta para esa cuenta", StatusCodes.Status409Conflict)
+ : null;
+ }
+
+ if (request.TipoTitular.HasValue)
+ {
+ var duplicateTipo = await _dbContext.AlertasSaldo.AnyAsync(
+ x => x.Id != currentId && x.CuentaId == null && x.TipoTitular == request.TipoTitular,
+ cancellationToken);
+ return duplicateTipo
+ ? new AlertaValidationError("Ya existe una alerta para ese tipo de titular", StatusCodes.Status409Conflict)
+ : null;
+ }
+
+ var duplicateGlobal = await _dbContext.AlertasSaldo.AnyAsync(
+ x => x.Id != currentId && x.CuentaId == null && x.TipoTitular == null,
+ cancellationToken);
+ return duplicateGlobal
+ ? new AlertaValidationError("Ya existe una alerta global", StatusCodes.Status409Conflict)
+ : null;
+ }
+
private async Task UpsertDestinatariosAsync(Guid alertaId, IReadOnlyList destinatarioUsuarioIds, CancellationToken cancellationToken)
{
var existing = await _dbContext.AlertaDestinatarios
@@ -335,4 +374,16 @@ await _auditService.LogAsync(
var raw = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub");
return Guid.TryParse(raw, out var userId) ? userId : null;
}
+
+ private static string ResolveAlertaAlcance(AlertaSaldo alerta)
+ {
+ if (alerta.CuentaId.HasValue)
+ {
+ return "CUENTA";
+ }
+
+ return alerta.TipoTitular.HasValue ? "TIPO_TITULAR" : "GLOBAL";
+ }
+
+ private sealed record AlertaValidationError(string Error, int Status);
}
diff --git a/Atlas Balance/backend/src/GestionCaja.API/Controllers/CuentasController.cs b/Atlas Balance/backend/src/GestionCaja.API/Controllers/CuentasController.cs
index 09dd8aa..eb10905 100644
--- a/Atlas Balance/backend/src/GestionCaja.API/Controllers/CuentasController.cs
+++ b/Atlas Balance/backend/src/GestionCaja.API/Controllers/CuentasController.cs
@@ -18,12 +18,14 @@ public sealed class CuentasController : ControllerBase
private readonly AppDbContext _dbContext;
private readonly IUserAccessService _userAccessService;
private readonly IAuditService _auditService;
+ private readonly IPlazoFijoService _plazoFijoService;
- public CuentasController(AppDbContext dbContext, IUserAccessService userAccessService, IAuditService auditService)
+ public CuentasController(AppDbContext dbContext, IUserAccessService userAccessService, IAuditService auditService, IPlazoFijoService plazoFijoService)
{
_dbContext = dbContext;
_userAccessService = userAccessService;
_auditService = auditService;
+ _plazoFijoService = plazoFijoService;
}
[HttpGet("divisas-activas")]
@@ -51,6 +53,8 @@ public async Task Listar(
[FromQuery] string sortDir = "desc",
[FromQuery] string? search = null,
[FromQuery] Guid? titularId = null,
+ [FromQuery] TipoTitular? tipoTitular = null,
+ [FromQuery] TipoCuenta? tipoCuenta = null,
[FromQuery] bool incluirEliminados = false,
CancellationToken cancellationToken = default)
{
@@ -75,6 +79,16 @@ public async Task Listar(
query = query.Where(c => c.TitularId == titularId.Value);
}
+ if (tipoCuenta.HasValue)
+ {
+ query = query.Where(c => c.TipoCuenta == tipoCuenta.Value);
+ }
+
+ if (tipoTitular.HasValue)
+ {
+ query = query.Where(c => _dbContext.Titulares.Any(t => t.Id == c.TitularId && t.Tipo == tipoTitular.Value));
+ }
+
if (!string.IsNullOrWhiteSpace(search))
{
var term = search.Trim().ToLowerInvariant();
@@ -89,32 +103,40 @@ public async Task Listar(
query = ApplySorting(query, sortBy, desc);
var total = await query.CountAsync(cancellationToken);
- var data = await query
+ var pageRows = await query
.Join(
_dbContext.Titulares.IgnoreQueryFilters(),
c => c.TitularId,
t => t.Id,
- (c, t) => new CuentaListItemResponse
- {
- Id = c.Id,
- TitularId = c.TitularId,
- TitularNombre = t.Nombre,
- Nombre = c.Nombre,
- NumeroCuenta = c.NumeroCuenta,
- Iban = c.Iban,
- BancoNombre = c.BancoNombre,
- Divisa = c.Divisa,
- FormatoId = c.FormatoId,
- EsEfectivo = c.EsEfectivo,
- Activa = c.Activa,
- Notas = c.Notas,
- FechaCreacion = c.FechaCreacion,
- DeletedAt = c.DeletedAt
- })
+ (c, t) => new { Cuenta = c, Titular = t })
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
+ var plazoMap = await BuildPlazoFijoMapAsync(pageRows.Select(x => x.Cuenta.Id).ToList(), cancellationToken);
+ var data = pageRows
+ .Select(x => new CuentaListItemResponse
+ {
+ Id = x.Cuenta.Id,
+ TitularId = x.Cuenta.TitularId,
+ TitularNombre = x.Titular.Nombre,
+ TitularTipo = x.Titular.Tipo.ToString(),
+ Nombre = x.Cuenta.Nombre,
+ NumeroCuenta = x.Cuenta.NumeroCuenta,
+ Iban = x.Cuenta.Iban,
+ BancoNombre = x.Cuenta.BancoNombre,
+ Divisa = x.Cuenta.Divisa,
+ FormatoId = x.Cuenta.FormatoId,
+ EsEfectivo = x.Cuenta.EsEfectivo,
+ TipoCuenta = ResolveTipoCuenta(x.Cuenta).ToString(),
+ PlazoFijo = plazoMap.GetValueOrDefault(x.Cuenta.Id),
+ Activa = x.Cuenta.Activa,
+ Notas = x.Cuenta.Notas,
+ FechaCreacion = x.Cuenta.FechaCreacion,
+ DeletedAt = x.Cuenta.DeletedAt
+ })
+ .ToList();
+
return Ok(new PaginatedResponse
{
Data = data,
@@ -152,14 +174,16 @@ public async Task Obtener(Guid id, [FromQuery] bool incluirElimin
var titular = await _dbContext.Titulares.IgnoreQueryFilters()
.Where(t => t.Id == cuenta.TitularId)
- .Select(t => t.Nombre)
+ .Select(t => new { t.Nombre, t.Tipo })
.FirstOrDefaultAsync(cancellationToken);
+ var plazoMap = await BuildPlazoFijoMapAsync([cuenta.Id], cancellationToken);
return Ok(new CuentaListItemResponse
{
Id = cuenta.Id,
TitularId = cuenta.TitularId,
- TitularNombre = titular ?? string.Empty,
+ TitularNombre = titular?.Nombre ?? string.Empty,
+ TitularTipo = titular?.Tipo.ToString() ?? string.Empty,
Nombre = cuenta.Nombre,
NumeroCuenta = cuenta.NumeroCuenta,
Iban = cuenta.Iban,
@@ -167,6 +191,8 @@ public async Task Obtener(Guid id, [FromQuery] bool incluirElimin
Divisa = cuenta.Divisa,
FormatoId = cuenta.FormatoId,
EsEfectivo = cuenta.EsEfectivo,
+ TipoCuenta = ResolveTipoCuenta(cuenta).ToString(),
+ PlazoFijo = plazoMap.GetValueOrDefault(cuenta.Id),
Activa = cuenta.Activa,
Notas = cuenta.Notas,
FechaCreacion = cuenta.FechaCreacion,
@@ -184,12 +210,29 @@ public async Task Resumen(Guid id, [FromQuery] string periodo = "
return Forbid();
}
- var exists = await _dbContext.Cuentas.AnyAsync(c => c.Id == id, cancellationToken);
- if (!exists)
+ var cuenta = await _dbContext.Cuentas
+ .Where(c => c.Id == id)
+ .Select(c => new
+ {
+ c.Id,
+ c.Nombre,
+ c.Divisa,
+ c.EsEfectivo,
+ c.TipoCuenta,
+ c.TitularId,
+ c.Notas
+ })
+ .FirstOrDefaultAsync(cancellationToken);
+ if (cuenta is null)
{
return NotFound(new { error = "Cuenta no encontrada" });
}
+ var titular = await _dbContext.Titulares
+ .Where(t => t.Id == cuenta.TitularId)
+ .Select(t => t.Nombre)
+ .FirstOrDefaultAsync(cancellationToken);
+
var latest = await _dbContext.Extractos
.Where(e => e.CuentaId == id)
.OrderByDescending(e => e.Fecha)
@@ -208,13 +251,29 @@ public async Task Resumen(Guid id, [FromQuery] string periodo = "
Egresos = g.Sum(x => x.Monto < 0 ? -x.Monto : 0)
})
.FirstOrDefaultAsync(cancellationToken);
+ var last = await _dbContext.Extractos
+ .Where(e => e.CuentaId == id)
+ .OrderByDescending(e => e.FechaModificacion ?? e.FechaCreacion)
+ .Select(e => (DateTime?)(e.FechaModificacion ?? e.FechaCreacion))
+ .FirstOrDefaultAsync(cancellationToken);
+ var tipoCuenta = ResolveTipoCuenta(new Cuenta { TipoCuenta = cuenta.TipoCuenta, EsEfectivo = cuenta.EsEfectivo });
+ var plazoMap = await BuildPlazoFijoMapAsync([cuenta.Id], cancellationToken);
return Ok(new CuentaResumenResponse
{
- CuentaId = id,
+ CuentaId = cuenta.Id,
+ CuentaNombre = cuenta.Nombre,
+ Divisa = cuenta.Divisa,
+ TitularId = cuenta.TitularId,
+ TitularNombre = titular ?? string.Empty,
+ EsEfectivo = cuenta.EsEfectivo,
+ TipoCuenta = tipoCuenta.ToString(),
+ PlazoFijo = plazoMap.GetValueOrDefault(cuenta.Id),
+ Notas = cuenta.Notas,
SaldoActual = latest?.Saldo ?? 0m,
IngresosMes = resumenMensual?.Ingresos ?? 0,
- EgresosMes = resumenMensual?.Egresos ?? 0
+ EgresosMes = resumenMensual?.Egresos ?? 0,
+ UltimaActualizacion = last
});
}
@@ -270,23 +329,40 @@ public async Task Crear([FromBody] SaveCuentaRequest request, Can
Iban = validation.Iban,
BancoNombre = validation.BancoNombre,
Divisa = validation.Divisa!,
- FormatoId = request.EsEfectivo ? null : request.FormatoId,
- EsEfectivo = request.EsEfectivo,
+ FormatoId = validation.TipoCuenta == TipoCuenta.NORMAL ? request.FormatoId : null,
+ TipoCuenta = validation.TipoCuenta,
+ EsEfectivo = validation.TipoCuenta == TipoCuenta.EFECTIVO,
Activa = request.Activa,
Notas = NormalizeOptionalText(request.Notas),
FechaCreacion = DateTime.UtcNow
};
_dbContext.Cuentas.Add(cuenta);
+ if (validation.PlazoFijo is not null)
+ {
+ _dbContext.PlazosFijos.Add(new PlazoFijo
+ {
+ Id = Guid.NewGuid(),
+ CuentaId = cuenta.Id,
+ CuentaReferenciaId = validation.PlazoFijo.CuentaReferenciaId,
+ FechaInicio = validation.PlazoFijo.FechaInicio!.Value,
+ FechaVencimiento = validation.PlazoFijo.FechaVencimiento!.Value,
+ InteresPrevisto = validation.PlazoFijo.InteresPrevisto,
+ Renovable = validation.PlazoFijo.Renovable,
+ Estado = EstadoPlazoFijo.ACTIVO,
+ Notas = NormalizeOptionalText(validation.PlazoFijo.Notas),
+ FechaCreacion = DateTime.UtcNow
+ });
+ }
await _dbContext.SaveChangesAsync(cancellationToken);
await _auditService.LogAsync(
GetCurrentUserId(),
- "cuenta_creada",
+ validation.TipoCuenta == TipoCuenta.PLAZO_FIJO ? "cuenta_plazo_fijo_creada" : "cuenta_creada",
"CUENTAS",
cuenta.Id,
HttpContext,
- JsonSerializer.Serialize(new { cuenta.Nombre, cuenta.Divisa, cuenta.EsEfectivo, cuenta.Notas }),
+ JsonSerializer.Serialize(new { cuenta.Nombre, cuenta.Divisa, tipo_cuenta = cuenta.TipoCuenta.ToString(), cuenta.Notas, plazo_fijo = validation.PlazoFijo }),
cancellationToken);
return CreatedAtAction(nameof(Obtener), new { id = cuenta.Id }, new { id = cuenta.Id });
@@ -308,31 +384,90 @@ public async Task Actualizar(Guid id, [FromBody] SaveCuentaReques
return BadRequest(new { error = validation.Error });
}
+ var previousTipoCuenta = ResolveTipoCuenta(cuenta);
+ if (previousTipoCuenta == TipoCuenta.PLAZO_FIJO && validation.TipoCuenta != TipoCuenta.PLAZO_FIJO)
+ {
+ return BadRequest(new { error = "No se puede convertir una cuenta de plazo fijo a otro tipo; crea una cuenta nueva" });
+ }
+
cuenta.TitularId = request.TitularId;
cuenta.Nombre = request.Nombre.Trim();
cuenta.NumeroCuenta = validation.NumeroCuenta;
cuenta.Iban = validation.Iban;
cuenta.BancoNombre = validation.BancoNombre;
cuenta.Divisa = validation.Divisa!;
- cuenta.FormatoId = request.EsEfectivo ? null : request.FormatoId;
- cuenta.EsEfectivo = request.EsEfectivo;
+ cuenta.FormatoId = validation.TipoCuenta == TipoCuenta.NORMAL ? request.FormatoId : null;
+ cuenta.TipoCuenta = validation.TipoCuenta;
+ cuenta.EsEfectivo = validation.TipoCuenta == TipoCuenta.EFECTIVO;
cuenta.Activa = request.Activa;
cuenta.Notas = NormalizeOptionalText(request.Notas);
+ if (validation.TipoCuenta == TipoCuenta.PLAZO_FIJO && validation.PlazoFijo is not null)
+ {
+ var plazo = await _dbContext.PlazosFijos.FirstOrDefaultAsync(p => p.CuentaId == cuenta.Id, cancellationToken);
+ if (plazo is null)
+ {
+ plazo = new PlazoFijo
+ {
+ Id = Guid.NewGuid(),
+ CuentaId = cuenta.Id,
+ FechaCreacion = DateTime.UtcNow
+ };
+ _dbContext.PlazosFijos.Add(plazo);
+ }
+
+ plazo.CuentaReferenciaId = validation.PlazoFijo.CuentaReferenciaId;
+ plazo.FechaInicio = validation.PlazoFijo.FechaInicio!.Value;
+ plazo.FechaVencimiento = validation.PlazoFijo.FechaVencimiento!.Value;
+ plazo.InteresPrevisto = validation.PlazoFijo.InteresPrevisto;
+ plazo.Renovable = validation.PlazoFijo.Renovable;
+ plazo.Notas = NormalizeOptionalText(validation.PlazoFijo.Notas);
+ plazo.FechaModificacion = DateTime.UtcNow;
+ if (plazo.Estado == EstadoPlazoFijo.VENCIDO && plazo.FechaVencimiento > DateOnly.FromDateTime(DateTime.UtcNow.Date))
+ {
+ plazo.Estado = EstadoPlazoFijo.ACTIVO;
+ plazo.FechaUltimaNotificacion = null;
+ }
+ }
+
await _dbContext.SaveChangesAsync(cancellationToken);
await _auditService.LogAsync(
GetCurrentUserId(),
- "cuenta_actualizada",
+ validation.TipoCuenta == TipoCuenta.PLAZO_FIJO ? "cuenta_plazo_fijo_actualizada" : "cuenta_actualizada",
"CUENTAS",
cuenta.Id,
HttpContext,
- JsonSerializer.Serialize(new { cuenta.Nombre, cuenta.Divisa, cuenta.EsEfectivo, cuenta.Activa, cuenta.Notas }),
+ JsonSerializer.Serialize(new { cuenta.Nombre, cuenta.Divisa, tipo_cuenta = cuenta.TipoCuenta.ToString(), cuenta.Activa, cuenta.Notas, plazo_fijo = validation.PlazoFijo }),
cancellationToken);
return Ok(new { message = "Cuenta actualizada" });
}
+ [HttpPost("{id:guid}/plazo-fijo/renovar")]
+ [Authorize(Roles = "ADMIN")]
+ public async Task RenovarPlazoFijo(Guid id, [FromBody] RenovarPlazoFijoRequest request, CancellationToken cancellationToken)
+ {
+ if (request is null)
+ {
+ return BadRequest(new { error = "Request invalido" });
+ }
+
+ try
+ {
+ var result = await _plazoFijoService.RenovarAsync(id, request, GetCurrentUserId(), HttpContext, cancellationToken);
+ return Ok(result);
+ }
+ catch (KeyNotFoundException ex)
+ {
+ return NotFound(new { error = ex.Message });
+ }
+ catch (InvalidOperationException ex)
+ {
+ return BadRequest(new { error = ex.Message });
+ }
+ }
+
[HttpPatch("{id:guid}/notas")]
public async Task ActualizarNotas(Guid id, [FromBody] UpdateCuentaNotasRequest request, CancellationToken cancellationToken)
{
@@ -431,46 +566,83 @@ private static IQueryable ApplySorting(IQueryable query, string
return Guid.TryParse(raw, out var userId) ? userId : null;
}
- private async Task<(string? Error, string? Divisa, string? NumeroCuenta, string? Iban, string? BancoNombre)> ValidateCuentaRequestAsync(
+ private async Task ValidateCuentaRequestAsync(
SaveCuentaRequest request,
Guid? currentId,
CancellationToken cancellationToken)
{
+ var tipoCuenta = ResolveRequestedTipoCuenta(request);
+ var plazoFijo = ResolvePlazoFijoRequest(request);
+
if (string.IsNullOrWhiteSpace(request.Nombre))
{
- return ("Nombre es obligatorio", null, null, null, null);
+ return CuentaValidationResult.Fail("Nombre es obligatorio", tipoCuenta);
}
var titularExists = await _dbContext.Titulares.AnyAsync(t => t.Id == request.TitularId, cancellationToken);
if (!titularExists)
{
- return ("Titular invalido", null, null, null, null);
+ return CuentaValidationResult.Fail("Titular invalido", tipoCuenta);
}
var divisa = request.Divisa?.Trim().ToUpperInvariant();
if (string.IsNullOrWhiteSpace(divisa))
{
- return ("Divisa es obligatoria", null, null, null, null);
+ return CuentaValidationResult.Fail("Divisa es obligatoria", tipoCuenta);
}
var divisaExists = await _dbContext.DivisasActivas.AnyAsync(d => d.Activa && d.Codigo == divisa, cancellationToken);
if (!divisaExists)
{
- return ("La divisa indicada no esta activa", null, null, null, null);
+ return CuentaValidationResult.Fail("La divisa indicada no esta activa", tipoCuenta);
}
- if (!request.EsEfectivo && request.FormatoId.HasValue)
+ if (tipoCuenta == TipoCuenta.NORMAL && request.FormatoId.HasValue)
{
var formato = await _dbContext.FormatosImportacion.FirstOrDefaultAsync(f => f.Id == request.FormatoId.Value, cancellationToken);
if (formato is null)
{
- return ("Formato de importacion invalido", null, null, null, null);
+ return CuentaValidationResult.Fail("Formato de importacion invalido", tipoCuenta);
}
if (!string.IsNullOrWhiteSpace(formato.Divisa) &&
!string.Equals(formato.Divisa, divisa, StringComparison.OrdinalIgnoreCase))
{
- return ("La divisa de la cuenta debe coincidir con la del formato de importacion", null, null, null, null);
+ return CuentaValidationResult.Fail("La divisa de la cuenta debe coincidir con la del formato de importacion", tipoCuenta);
+ }
+ }
+
+ if (tipoCuenta == TipoCuenta.PLAZO_FIJO)
+ {
+ if (plazoFijo?.FechaInicio is null || plazoFijo.FechaVencimiento is null)
+ {
+ return CuentaValidationResult.Fail("Fecha de inicio y fecha de vencimiento son obligatorias para plazo fijo", tipoCuenta);
+ }
+
+ if (plazoFijo.FechaVencimiento < plazoFijo.FechaInicio)
+ {
+ return CuentaValidationResult.Fail("La fecha de vencimiento no puede ser anterior a la fecha de inicio", tipoCuenta);
+ }
+
+ if (plazoFijo.InteresPrevisto.HasValue && plazoFijo.InteresPrevisto.Value < 0)
+ {
+ return CuentaValidationResult.Fail("El interes previsto no puede ser negativo", tipoCuenta);
+ }
+
+ if (plazoFijo.CuentaReferenciaId.HasValue)
+ {
+ if (plazoFijo.CuentaReferenciaId == currentId)
+ {
+ return CuentaValidationResult.Fail("La cuenta de referencia no puede ser la misma cuenta", tipoCuenta);
+ }
+
+ var referencia = await _dbContext.Cuentas.FirstOrDefaultAsync(
+ c => c.Id == plazoFijo.CuentaReferenciaId.Value && c.Activa,
+ cancellationToken);
+ if (referencia is null)
+ {
+ return CuentaValidationResult.Fail("Cuenta de referencia invalida o inactiva", tipoCuenta);
+ }
}
}
@@ -484,15 +656,18 @@ private static IQueryable ApplySorting(IQueryable query, string
if (duplicateName)
{
- return ("Ya existe una cuenta con ese nombre para el titular indicado", null, null, null, null);
+ return CuentaValidationResult.Fail("Ya existe una cuenta con ese nombre para el titular indicado", tipoCuenta);
}
- return (
- null,
- divisa,
- request.EsEfectivo ? null : request.NumeroCuenta?.Trim(),
- request.EsEfectivo ? null : request.Iban?.Trim(),
- request.EsEfectivo ? null : request.BancoNombre?.Trim());
+ return new CuentaValidationResult
+ {
+ TipoCuenta = tipoCuenta,
+ Divisa = divisa,
+ NumeroCuenta = tipoCuenta == TipoCuenta.NORMAL ? request.NumeroCuenta?.Trim() : null,
+ Iban = tipoCuenta == TipoCuenta.NORMAL ? request.Iban?.Trim() : null,
+ BancoNombre = tipoCuenta == TipoCuenta.NORMAL ? request.BancoNombre?.Trim() : null,
+ PlazoFijo = tipoCuenta == TipoCuenta.PLAZO_FIJO ? plazoFijo : null
+ };
}
private async Task CanEditCuentaAsync(UserAccessScope scope, Cuenta cuenta, CancellationToken cancellationToken)
@@ -520,4 +695,101 @@ private async Task CanEditCuentaAsync(UserAccessScope scope, Cuenta cuenta
var normalized = value?.Trim();
return string.IsNullOrWhiteSpace(normalized) ? null : normalized;
}
+
+ private static TipoCuenta ResolveTipoCuenta(Cuenta cuenta)
+ {
+ if (cuenta.TipoCuenta == TipoCuenta.NORMAL && cuenta.EsEfectivo)
+ {
+ return TipoCuenta.EFECTIVO;
+ }
+
+ return cuenta.TipoCuenta;
+ }
+
+ private static TipoCuenta ResolveRequestedTipoCuenta(SaveCuentaRequest request)
+ {
+ if (request.TipoCuenta.HasValue)
+ {
+ return request.TipoCuenta.Value;
+ }
+
+ return request.EsEfectivo ? TipoCuenta.EFECTIVO : TipoCuenta.NORMAL;
+ }
+
+ private static SavePlazoFijoRequest? ResolvePlazoFijoRequest(SaveCuentaRequest request)
+ {
+ if (request.PlazoFijo is not null)
+ {
+ return request.PlazoFijo;
+ }
+
+ if (request.FechaInicio is null &&
+ request.FechaVencimiento is null &&
+ request.InteresPrevisto is null &&
+ request.Renovable is null &&
+ request.CuentaReferenciaId is null &&
+ string.IsNullOrWhiteSpace(request.PlazoFijoNotas))
+ {
+ return null;
+ }
+
+ return new SavePlazoFijoRequest
+ {
+ FechaInicio = request.FechaInicio,
+ FechaVencimiento = request.FechaVencimiento,
+ InteresPrevisto = request.InteresPrevisto,
+ Renovable = request.Renovable ?? false,
+ CuentaReferenciaId = request.CuentaReferenciaId,
+ Notas = request.PlazoFijoNotas
+ };
+ }
+
+ private async Task> BuildPlazoFijoMapAsync(IReadOnlyList cuentaIds, CancellationToken cancellationToken)
+ {
+ if (cuentaIds.Count == 0)
+ {
+ return [];
+ }
+
+ var rows = await (
+ from plazo in _dbContext.PlazosFijos
+ join refCuenta in _dbContext.Cuentas.IgnoreQueryFilters() on plazo.CuentaReferenciaId equals refCuenta.Id into refJoin
+ from cuentaReferencia in refJoin.DefaultIfEmpty()
+ where cuentaIds.Contains(plazo.CuentaId)
+ select new PlazoFijoResponse
+ {
+ Id = plazo.Id,
+ CuentaId = plazo.CuentaId,
+ CuentaReferenciaId = plazo.CuentaReferenciaId,
+ CuentaReferenciaNombre = cuentaReferencia != null ? cuentaReferencia.Nombre : null,
+ FechaInicio = plazo.FechaInicio,
+ FechaVencimiento = plazo.FechaVencimiento,
+ InteresPrevisto = plazo.InteresPrevisto,
+ Renovable = plazo.Renovable,
+ Estado = plazo.Estado.ToString(),
+ FechaUltimaNotificacion = plazo.FechaUltimaNotificacion,
+ FechaRenovacion = plazo.FechaRenovacion,
+ Notas = plazo.Notas
+ })
+ .ToListAsync(cancellationToken);
+
+ return rows.ToDictionary(x => x.CuentaId);
+ }
+
+ private sealed class CuentaValidationResult
+ {
+ public string? Error { get; init; }
+ public TipoCuenta TipoCuenta { get; init; }
+ public string? Divisa { get; init; }
+ public string? NumeroCuenta { get; init; }
+ public string? Iban { get; init; }
+ public string? BancoNombre { get; init; }
+ public SavePlazoFijoRequest? PlazoFijo { get; init; }
+
+ public static CuentaValidationResult Fail(string error, TipoCuenta tipoCuenta) => new()
+ {
+ Error = error,
+ TipoCuenta = tipoCuenta
+ };
+ }
}
diff --git a/Atlas Balance/backend/src/GestionCaja.API/Controllers/ExtractosController.cs b/Atlas Balance/backend/src/GestionCaja.API/Controllers/ExtractosController.cs
index 39fa3aa..b6e6b1a 100644
--- a/Atlas Balance/backend/src/GestionCaja.API/Controllers/ExtractosController.cs
+++ b/Atlas Balance/backend/src/GestionCaja.API/Controllers/ExtractosController.cs
@@ -396,10 +396,10 @@ public async Task GetCuentaResumen(Guid cuentaId, [FromQuery] str
{
if (!TryGetUser(out var actor)) return Unauthorized(new { error = "Usuario no autenticado" });
if (!await CanView(actor, cuentaId, ct)) return Forbid();
- var cuenta = await _db.Cuentas.Where(c => c.Id == cuentaId).Select(c => new { c.Id, c.Nombre, c.Divisa, c.EsEfectivo, c.TitularId, c.Notas }).FirstOrDefaultAsync(ct);
+ var cuenta = await _db.Cuentas.Where(c => c.Id == cuentaId).Select(c => new { c.Id, c.Nombre, c.Divisa, c.EsEfectivo, c.TipoCuenta, c.TitularId, c.Notas }).FirstOrDefaultAsync(ct);
if (cuenta is null) return NotFound(new { error = "Cuenta no encontrada" });
var titular = await _db.Titulares.Where(t => t.Id == cuenta.TitularId).Select(t => t.Nombre).FirstOrDefaultAsync(ct);
- return Ok(await BuildSummary(cuenta.Id, cuenta.Nombre, cuenta.Divisa, cuenta.EsEfectivo, cuenta.TitularId, titular ?? string.Empty, cuenta.Notas, periodo, ct));
+ return Ok(await BuildSummary(cuenta.Id, cuenta.Nombre, cuenta.Divisa, cuenta.EsEfectivo, cuenta.TipoCuenta, cuenta.TitularId, titular ?? string.Empty, cuenta.Notas, periodo, ct));
}
[HttpGet("titulares/{titularId:guid}/cuentas")]
@@ -414,7 +414,7 @@ public async Task GetCuentasTitular(Guid titularId, [FromQuery] s
var summary = new List();
foreach (var c in cuentas)
{
- summary.Add(await BuildSummary(c.Id, c.Nombre, c.Divisa, c.EsEfectivo, titular.Id, titular.Nombre, c.Notas, periodo, ct));
+ summary.Add(await BuildSummary(c.Id, c.Nombre, c.Divisa, c.EsEfectivo, c.TipoCuenta, titular.Id, titular.Nombre, c.Notas, periodo, ct));
}
return Ok(new TitularConCuentasResponse { TitularId = titular.Id, TitularNombre = titular.Nombre, Cuentas = summary });
}
@@ -432,7 +432,7 @@ public async Task GetTitularesResumen([FromQuery] string periodo
{
var tc = cuentas.Where(c => c.TitularId == t.Id).ToList();
var s = new List();
- foreach (var c in tc) s.Add(await BuildSummary(c.Id, c.Nombre, c.Divisa, c.EsEfectivo, t.Id, t.Nombre, c.Notas, periodo, ct));
+ foreach (var c in tc) s.Add(await BuildSummary(c.Id, c.Nombre, c.Divisa, c.EsEfectivo, c.TipoCuenta, t.Id, t.Nombre, c.Notas, periodo, ct));
outData.Add(new TitularConCuentasResponse { TitularId = t.Id, TitularNombre = t.Nombre, Cuentas = s });
}
return Ok(outData);
@@ -481,7 +481,7 @@ public async Task SaveColumnasVisibles([FromBody] SaveColumnasVis
return Ok(new { message = "Preferencias guardadas" });
}
- private async Task BuildSummary(Guid cuentaId, string cuentaNombre, string divisa, bool esEfectivo, Guid titularId, string titularNombre, string? notas, string periodo, CancellationToken ct)
+ private async Task BuildSummary(Guid cuentaId, string cuentaNombre, string divisa, bool esEfectivo, TipoCuenta tipoCuenta, Guid titularId, string titularNombre, string? notas, string periodo, CancellationToken ct)
{
var q = _db.Extractos.Where(e => e.CuentaId == cuentaId);
var latest = await q
@@ -495,6 +495,30 @@ private async Task BuildSummary(Guid cuentaId, string
var ingresos = await periodRows.Where(e => e.Monto > 0).SumAsync(e => (decimal?)e.Monto, ct) ?? 0m;
var egresos = await periodRows.Where(e => e.Monto < 0).SumAsync(e => (decimal?)e.Monto, ct) ?? 0m;
var last = await q.OrderByDescending(e => e.FechaModificacion ?? e.FechaCreacion).Select(e => (DateTime?)(e.FechaModificacion ?? e.FechaCreacion)).FirstOrDefaultAsync(ct);
+ var plazoFijo = tipoCuenta == TipoCuenta.PLAZO_FIJO
+ ? await (
+ from plazo in _db.PlazosFijos
+ join refCuenta in _db.Cuentas.IgnoreQueryFilters() on plazo.CuentaReferenciaId equals refCuenta.Id into refJoin
+ from cuentaReferencia in refJoin.DefaultIfEmpty()
+ where plazo.CuentaId == cuentaId
+ select new PlazoFijoResponse
+ {
+ Id = plazo.Id,
+ CuentaId = plazo.CuentaId,
+ CuentaReferenciaId = plazo.CuentaReferenciaId,
+ CuentaReferenciaNombre = cuentaReferencia != null ? cuentaReferencia.Nombre : null,
+ FechaInicio = plazo.FechaInicio,
+ FechaVencimiento = plazo.FechaVencimiento,
+ InteresPrevisto = plazo.InteresPrevisto,
+ Renovable = plazo.Renovable,
+ Estado = plazo.Estado.ToString(),
+ FechaUltimaNotificacion = plazo.FechaUltimaNotificacion,
+ FechaRenovacion = plazo.FechaRenovacion,
+ Notas = plazo.Notas
+ })
+ .FirstOrDefaultAsync(ct)
+ : null;
+
return new CuentaResumenKpiResponse
{
CuentaId = cuentaId,
@@ -503,6 +527,8 @@ private async Task BuildSummary(Guid cuentaId, string
TitularId = titularId,
TitularNombre = titularNombre,
EsEfectivo = esEfectivo,
+ TipoCuenta = tipoCuenta.ToString(),
+ PlazoFijo = plazoFijo,
Notas = notas,
SaldoActual = latest?.Saldo ?? 0m,
IngresosMes = ingresos,
@@ -588,7 +614,7 @@ private async Task> GetAllowedAccountIds(Actor actor, Cancellation
if (actor.IsAdmin) return [.. await _db.Cuentas.Select(c => c.Id).ToListAsync(ct)];
var perms = await _db.PermisosUsuario.Where(p => p.UsuarioId == actor.Id).ToListAsync(ct);
if (!perms.Any()) return [];
- if (perms.Any(p => p.CuentaId is null && p.TitularId is null && GrantsDataAccess(p)))
+ if (perms.Any(p => p.CuentaId is null && p.TitularId is null && GrantsAccountAccess(p)))
{
return [.. await _db.Cuentas.Select(c => c.Id).ToListAsync(ct)];
}
@@ -614,7 +640,7 @@ private async Task CanViewTitular(Actor actor, Guid titularId, Cancellatio
return false;
}
- if (perms.Any(p => p.CuentaId is null && p.TitularId is null && GrantsDataAccess(p)))
+ if (perms.Any(p => p.CuentaId is null && p.TitularId is null && GrantsAccountAccess(p)))
{
return true;
}
@@ -634,8 +660,8 @@ private async Task CanViewTitular(Actor actor, Guid titularId, Cancellatio
await _db.Cuentas.AnyAsync(c => c.TitularId == titularId && permittedCuentaIds.Contains(c.Id), ct);
}
- private static bool GrantsDataAccess(PermisoUsuario permiso) =>
- permiso.PuedeAgregarLineas || permiso.PuedeEditarLineas || permiso.PuedeEliminarLineas || permiso.PuedeImportar;
+ private static bool GrantsAccountAccess(PermisoUsuario permiso) =>
+ permiso.PuedeVerCuentas || permiso.PuedeAgregarLineas || permiso.PuedeEditarLineas || permiso.PuedeEliminarLineas || permiso.PuedeImportar;
private async Task GetPermission(Actor actor, Cuenta cuenta, CancellationToken ct)
{
diff --git a/Atlas Balance/backend/src/GestionCaja.API/Controllers/ImportacionController.cs b/Atlas Balance/backend/src/GestionCaja.API/Controllers/ImportacionController.cs
index ff34736..48e085d 100644
--- a/Atlas Balance/backend/src/GestionCaja.API/Controllers/ImportacionController.cs
+++ b/Atlas Balance/backend/src/GestionCaja.API/Controllers/ImportacionController.cs
@@ -68,6 +68,30 @@ public async Task Confirmar([FromBody] ImportacionConfirmarReques
}
}
+ [HttpPost("plazo-fijo/movimiento")]
+ public async Task RegistrarMovimientoPlazoFijo([FromBody] ImportacionPlazoFijoMovimientoRequest request, CancellationToken cancellationToken)
+ {
+ if (request is null)
+ {
+ return BadRequest(new { error = "Request invalido" });
+ }
+
+ if (!TryGetActor(out var userId, out var rol))
+ {
+ return Unauthorized(new { error = "Usuario no autenticado" });
+ }
+
+ try
+ {
+ var result = await _importacionService.RegistrarMovimientoPlazoFijoAsync(userId, rol, request, HttpContext, cancellationToken);
+ return Ok(result);
+ }
+ catch (ImportacionException ex)
+ {
+ return StatusCode(ex.StatusCode, new { error = ex.Message });
+ }
+ }
+
private bool TryGetActor(out Guid userId, out string rol)
{
rol = User.FindFirstValue(ClaimTypes.Role) ?? string.Empty;
diff --git a/Atlas Balance/backend/src/GestionCaja.API/Controllers/TitularesController.cs b/Atlas Balance/backend/src/GestionCaja.API/Controllers/TitularesController.cs
index a5dd629..a87d4c6 100644
--- a/Atlas Balance/backend/src/GestionCaja.API/Controllers/TitularesController.cs
+++ b/Atlas Balance/backend/src/GestionCaja.API/Controllers/TitularesController.cs
@@ -33,6 +33,7 @@ public async Task Listar(
[FromQuery] string sortBy = "nombre",
[FromQuery] string sortDir = "asc",
[FromQuery] string? search = null,
+ [FromQuery] TipoTitular? tipoTitular = null,
[FromQuery] bool incluirEliminados = false,
CancellationToken cancellationToken = default)
{
@@ -52,6 +53,11 @@ public async Task Listar(
query = _userAccessService.ApplyTitularScope(query, scope);
+ if (tipoTitular.HasValue)
+ {
+ query = query.Where(t => t.Tipo == tipoTitular.Value);
+ }
+
if (!string.IsNullOrWhiteSpace(search))
{
var term = search.Trim().ToLowerInvariant();
diff --git a/Atlas Balance/backend/src/GestionCaja.API/Controllers/UsuariosController.cs b/Atlas Balance/backend/src/GestionCaja.API/Controllers/UsuariosController.cs
index 363687d..d22700e 100644
--- a/Atlas Balance/backend/src/GestionCaja.API/Controllers/UsuariosController.cs
+++ b/Atlas Balance/backend/src/GestionCaja.API/Controllers/UsuariosController.cs
@@ -219,6 +219,7 @@ public async Task GuardarPermisoCuenta(Guid id, Guid cuentaId, [F
{
CuentaId = cuentaId,
TitularId = request.TitularId,
+ PuedeVerCuentas = request.PuedeVerCuentas,
PuedeAgregarLineas = request.PuedeAgregarLineas,
PuedeEditarLineas = request.PuedeEditarLineas,
PuedeEliminarLineas = request.PuedeEliminarLineas,
@@ -768,6 +769,7 @@ private async Task> LoadPermisosAuditSnapshotAsync(Guid usuarioId,
{
permiso.CuentaId,
permiso.TitularId,
+ permiso.PuedeVerCuentas,
permiso.PuedeAgregarLineas,
permiso.PuedeEditarLineas,
permiso.PuedeEliminarLineas,
@@ -838,6 +840,7 @@ private Task AddPermisoAsync(Guid usuarioId, SavePermisoUsuarioRequest item)
UsuarioId = usuarioId,
CuentaId = item.CuentaId,
TitularId = item.TitularId,
+ PuedeVerCuentas = item.PuedeVerCuentas,
PuedeAgregarLineas = item.PuedeAgregarLineas,
PuedeEditarLineas = item.PuedeEditarLineas,
PuedeEliminarLineas = item.PuedeEliminarLineas,
@@ -887,6 +890,7 @@ private static PermisoUsuarioResponse MapPermiso(PermisoUsuario permiso, Prefere
UsuarioId = permiso.UsuarioId,
CuentaId = permiso.CuentaId,
TitularId = permiso.TitularId,
+ PuedeVerCuentas = permiso.PuedeVerCuentas,
PuedeAgregarLineas = permiso.PuedeAgregarLineas,
PuedeEditarLineas = permiso.PuedeEditarLineas,
PuedeEliminarLineas = permiso.PuedeEliminarLineas,
diff --git a/Atlas Balance/backend/src/GestionCaja.API/DTOs/AlertasDtos.cs b/Atlas Balance/backend/src/GestionCaja.API/DTOs/AlertasDtos.cs
index 4838c22..5ee4246 100644
--- a/Atlas Balance/backend/src/GestionCaja.API/DTOs/AlertasDtos.cs
+++ b/Atlas Balance/backend/src/GestionCaja.API/DTOs/AlertasDtos.cs
@@ -1,8 +1,13 @@
+using System.Text.Json.Serialization;
+using GestionCaja.API.Models;
+
namespace GestionCaja.API.DTOs;
public sealed class SaveAlertaSaldoRequest
{
public Guid? CuentaId { get; set; }
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public TipoTitular? TipoTitular { get; set; }
public decimal SaldoMinimo { get; set; }
public bool Activa { get; set; } = true;
public IReadOnlyList DestinatarioUsuarioIds { get; set; } = [];
@@ -20,6 +25,8 @@ public sealed class AlertaSaldoItemResponse
public Guid Id { get; set; }
public Guid? CuentaId { get; set; }
public string? CuentaNombre { get; set; }
+ public string Alcance { get; set; } = "GLOBAL";
+ public string? TipoTitular { get; set; }
public Guid? TitularId { get; set; }
public string? TitularNombre { get; set; }
public string? Divisa { get; set; }
@@ -37,6 +44,7 @@ public sealed class AlertaActivaItemResponse
public Guid TitularId { get; set; }
public string CuentaNombre { get; set; } = string.Empty;
public string TitularNombre { get; set; } = string.Empty;
+ public string TipoTitular { get; set; } = string.Empty;
public string Divisa { get; set; } = string.Empty;
public decimal SaldoActual { get; set; }
public decimal SaldoMinimo { get; set; }
diff --git a/Atlas Balance/backend/src/GestionCaja.API/DTOs/AuthDtos.cs b/Atlas Balance/backend/src/GestionCaja.API/DTOs/AuthDtos.cs
index ec3d9fd..005a779 100644
--- a/Atlas Balance/backend/src/GestionCaja.API/DTOs/AuthDtos.cs
+++ b/Atlas Balance/backend/src/GestionCaja.API/DTOs/AuthDtos.cs
@@ -37,6 +37,7 @@ public sealed class PermisoUsuarioResponse
public Guid UsuarioId { get; set; }
public Guid? CuentaId { get; set; }
public Guid? TitularId { get; set; }
+ public bool PuedeVerCuentas { get; set; }
public bool PuedeAgregarLineas { get; set; }
public bool PuedeEditarLineas { get; set; }
public bool PuedeEliminarLineas { get; set; }
diff --git a/Atlas Balance/backend/src/GestionCaja.API/DTOs/CuentasDtos.cs b/Atlas Balance/backend/src/GestionCaja.API/DTOs/CuentasDtos.cs
index cc6616a..ac15dbc 100644
--- a/Atlas Balance/backend/src/GestionCaja.API/DTOs/CuentasDtos.cs
+++ b/Atlas Balance/backend/src/GestionCaja.API/DTOs/CuentasDtos.cs
@@ -1,5 +1,24 @@
+using System.Text.Json.Serialization;
+using GestionCaja.API.Models;
+
namespace GestionCaja.API.DTOs;
+public sealed class PlazoFijoResponse
+{
+ public Guid Id { get; set; }
+ public Guid CuentaId { get; set; }
+ public Guid? CuentaReferenciaId { get; set; }
+ public string? CuentaReferenciaNombre { get; set; }
+ public DateOnly FechaInicio { get; set; }
+ public DateOnly FechaVencimiento { get; set; }
+ public decimal? InteresPrevisto { get; set; }
+ public bool Renovable { get; set; }
+ public string Estado { get; set; } = string.Empty;
+ public DateOnly? FechaUltimaNotificacion { get; set; }
+ public DateOnly? FechaRenovacion { get; set; }
+ public string? Notas { get; set; }
+}
+
public sealed class CuentaListItemResponse
{
public Guid Id { get; set; }
@@ -12,6 +31,9 @@ public sealed class CuentaListItemResponse
public string Divisa { get; set; } = "EUR";
public Guid? FormatoId { get; set; }
public bool EsEfectivo { get; set; }
+ public string TipoCuenta { get; set; } = "NORMAL";
+ public string TitularTipo { get; set; } = string.Empty;
+ public PlazoFijoResponse? PlazoFijo { get; set; }
public bool Activa { get; set; }
public string? Notas { get; set; }
public DateTime FechaCreacion { get; set; }
@@ -21,9 +43,18 @@ public sealed class CuentaListItemResponse
public sealed class CuentaResumenResponse
{
public Guid CuentaId { get; set; }
+ public string CuentaNombre { get; set; } = string.Empty;
+ public string Divisa { get; set; } = "EUR";
+ public Guid TitularId { get; set; }
+ public string TitularNombre { get; set; } = string.Empty;
+ public bool EsEfectivo { get; set; }
+ public string TipoCuenta { get; set; } = "NORMAL";
+ public PlazoFijoResponse? PlazoFijo { get; set; }
+ public string? Notas { get; set; }
public decimal SaldoActual { get; set; }
public decimal IngresosMes { get; set; }
public decimal EgresosMes { get; set; }
+ public DateTime? UltimaActualizacion { get; set; }
}
public sealed class SaveCuentaRequest
@@ -35,12 +66,40 @@ public sealed class SaveCuentaRequest
public string? BancoNombre { get; set; }
public string Divisa { get; set; } = "EUR";
public Guid? FormatoId { get; set; }
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public TipoCuenta? TipoCuenta { get; set; }
public bool EsEfectivo { get; set; }
public bool Activa { get; set; } = true;
public string? Notas { get; set; }
+ public SavePlazoFijoRequest? PlazoFijo { get; set; }
+ public DateOnly? FechaInicio { get; set; }
+ public DateOnly? FechaVencimiento { get; set; }
+ public decimal? InteresPrevisto { get; set; }
+ public bool? Renovable { get; set; }
+ public Guid? CuentaReferenciaId { get; set; }
+ public string? PlazoFijoNotas { get; set; }
}
public sealed class UpdateCuentaNotasRequest
{
public string? Notas { get; set; }
}
+
+public sealed class SavePlazoFijoRequest
+{
+ public DateOnly? FechaInicio { get; set; }
+ public DateOnly? FechaVencimiento { get; set; }
+ public decimal? InteresPrevisto { get; set; }
+ public bool Renovable { get; set; }
+ public Guid? CuentaReferenciaId { get; set; }
+ public string? Notas { get; set; }
+}
+
+public sealed class RenovarPlazoFijoRequest
+{
+ public DateOnly NuevaFechaInicio { get; set; }
+ public DateOnly NuevaFechaVencimiento { get; set; }
+ public decimal? InteresPrevisto { get; set; }
+ public bool Renovable { get; set; }
+ public string? Notas { get; set; }
+}
diff --git a/Atlas Balance/backend/src/GestionCaja.API/DTOs/DashboardDtos.cs b/Atlas Balance/backend/src/GestionCaja.API/DTOs/DashboardDtos.cs
index df8c6c8..90c68a6 100644
--- a/Atlas Balance/backend/src/GestionCaja.API/DTOs/DashboardDtos.cs
+++ b/Atlas Balance/backend/src/GestionCaja.API/DTOs/DashboardDtos.cs
@@ -7,10 +7,20 @@ public sealed class DashboardPrincipalResponse
public decimal IngresosMes { get; set; }
public decimal EgresosMes { get; set; }
public decimal TotalConvertido { get; set; }
+ public DashboardPlazosFijosResumenResponse PlazosFijos { get; set; } = new();
public IReadOnlyList SaldosPorTitular { get; set; } = [];
public DashboardChartColorsResponse ChartColors { get; set; } = new();
}
+public sealed class DashboardPlazosFijosResumenResponse
+{
+ public decimal MontoTotalConvertido { get; set; }
+ public decimal InteresesPrevistosConvertidos { get; set; }
+ public int? DiasHastaProximoVencimiento { get; set; }
+ public DateOnly? ProximoVencimiento { get; set; }
+ public int TotalCuentas { get; set; }
+}
+
public sealed class DashboardTitularResponse
{
public Guid TitularId { get; set; }
@@ -28,8 +38,11 @@ public sealed class DashboardSaldoTitularResponse
{
public Guid TitularId { get; set; }
public string TitularNombre { get; set; } = string.Empty;
+ public string TipoTitular { get; set; } = string.Empty;
public IReadOnlyDictionary SaldosPorDivisa { get; set; } = new Dictionary();
public decimal TotalConvertido { get; set; }
+ public decimal SaldoInmovilizadoConvertido { get; set; }
+ public decimal SaldoDisponibleConvertido { get; set; }
}
public sealed class DashboardSaldoCuentaResponse
@@ -39,6 +52,7 @@ public sealed class DashboardSaldoCuentaResponse
public string? BancoNombre { get; set; }
public string Divisa { get; set; } = string.Empty;
public bool EsEfectivo { get; set; }
+ public string TipoCuenta { get; set; } = string.Empty;
public decimal SaldoActual { get; set; }
public decimal SaldoConvertido { get; set; }
}
@@ -55,6 +69,10 @@ public sealed class DashboardSaldoDivisaResponse
public string Divisa { get; set; } = string.Empty;
public decimal Saldo { get; set; }
public decimal SaldoConvertido { get; set; }
+ public decimal SaldoDisponible { get; set; }
+ public decimal SaldoInmovilizado { get; set; }
+ public decimal SaldoTotal { get; set; }
+ public decimal SaldoTotalConvertido { get; set; }
}
public sealed class DashboardEvolucionResponse
diff --git a/Atlas Balance/backend/src/GestionCaja.API/DTOs/ExtractosDtos.cs b/Atlas Balance/backend/src/GestionCaja.API/DTOs/ExtractosDtos.cs
index 1048761..88243c4 100644
--- a/Atlas Balance/backend/src/GestionCaja.API/DTOs/ExtractosDtos.cs
+++ b/Atlas Balance/backend/src/GestionCaja.API/DTOs/ExtractosDtos.cs
@@ -79,6 +79,8 @@ public sealed class CuentaResumenKpiResponse
public Guid TitularId { get; set; }
public string TitularNombre { get; set; } = string.Empty;
public bool EsEfectivo { get; set; }
+ public string TipoCuenta { get; set; } = "NORMAL";
+ public PlazoFijoResponse? PlazoFijo { get; set; }
public string? Notas { get; set; }
public decimal SaldoActual { get; set; }
public decimal IngresosMes { get; set; }
diff --git a/Atlas Balance/backend/src/GestionCaja.API/DTOs/ImportacionDtos.cs b/Atlas Balance/backend/src/GestionCaja.API/DTOs/ImportacionDtos.cs
index a711c0a..aeef154 100644
--- a/Atlas Balance/backend/src/GestionCaja.API/DTOs/ImportacionDtos.cs
+++ b/Atlas Balance/backend/src/GestionCaja.API/DTOs/ImportacionDtos.cs
@@ -35,12 +35,31 @@ public sealed class ImportacionConfirmarRequest
public IReadOnlyList? FilasAImportar { get; set; }
}
+public sealed class ImportacionPlazoFijoMovimientoRequest
+{
+ public Guid CuentaId { get; set; }
+ public string TipoMovimiento { get; set; } = "INGRESO";
+ public DateOnly Fecha { get; set; }
+ public decimal Monto { get; set; }
+ public string? Concepto { get; set; }
+}
+
+public sealed class ImportacionPlazoFijoMovimientoResponse
+{
+ public Guid ExtractoId { get; set; }
+ public int FilaNumero { get; set; }
+ public decimal Monto { get; set; }
+ public decimal SaldoAnterior { get; set; }
+ public decimal SaldoActual { get; set; }
+}
+
public sealed class FilaValidacionResponse
{
public int Indice { get; set; }
public bool Valida { get; set; }
public Dictionary Datos { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public IReadOnlyList Errores { get; set; } = [];
+ public IReadOnlyList Advertencias { get; set; } = [];
}
public sealed class ErrorFilaResponse
@@ -73,6 +92,7 @@ public sealed class CuentaImportacionContextoResponse
public string TitularNombre { get; set; } = string.Empty;
public string Divisa { get; set; } = string.Empty;
public bool EsEfectivo { get; set; }
+ public string TipoCuenta { get; set; } = string.Empty;
public Guid? FormatoId { get; set; }
public MapeoColumnasRequest? FormatoPredefinido { get; set; }
}
diff --git a/Atlas Balance/backend/src/GestionCaja.API/DTOs/UsuariosDtos.cs b/Atlas Balance/backend/src/GestionCaja.API/DTOs/UsuariosDtos.cs
index 44a7542..f59012a 100644
--- a/Atlas Balance/backend/src/GestionCaja.API/DTOs/UsuariosDtos.cs
+++ b/Atlas Balance/backend/src/GestionCaja.API/DTOs/UsuariosDtos.cs
@@ -36,6 +36,7 @@ public sealed class SavePermisoUsuarioRequest
{
public Guid? CuentaId { get; set; }
public Guid? TitularId { get; set; }
+ public bool PuedeVerCuentas { get; set; }
public bool PuedeAgregarLineas { get; set; }
public bool PuedeEditarLineas { get; set; }
public bool PuedeEliminarLineas { get; set; }
diff --git a/Atlas Balance/backend/src/GestionCaja.API/Data/AppDbContext.cs b/Atlas Balance/backend/src/GestionCaja.API/Data/AppDbContext.cs
index 5f55784..fad0886 100644
--- a/Atlas Balance/backend/src/GestionCaja.API/Data/AppDbContext.cs
+++ b/Atlas Balance/backend/src/GestionCaja.API/Data/AppDbContext.cs
@@ -13,6 +13,7 @@ public AppDbContext(DbContextOptions options) : base(options) { }
public DbSet RefreshTokens => Set();
public DbSet Titulares => Set();
public DbSet Cuentas => Set();
+ public DbSet PlazosFijos => Set();
public DbSet FormatosImportacion => Set();
public DbSet Extractos => Set();
public DbSet ExtractosColumnasExtra => Set();
@@ -38,6 +39,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
modelBuilder.HasPostgresExtension("pgcrypto");
modelBuilder.HasPostgresEnum();
modelBuilder.HasPostgresEnum();
+ modelBuilder.HasPostgresEnum();
+ modelBuilder.HasPostgresEnum();
modelBuilder.HasPostgresEnum();
modelBuilder.HasPostgresEnum();
modelBuilder.HasPostgresEnum();
@@ -91,6 +94,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.HasIndex(e => e.TitularId);
entity.HasIndex(e => e.Divisa);
entity.HasIndex(e => e.EsEfectivo);
+ entity.HasIndex(e => e.TipoCuenta);
entity.HasIndex(e => e.Activa);
entity.HasIndex(e => e.DeletedAt);
entity.HasOne(e => e.Titular).WithMany().HasForeignKey(e => e.TitularId).OnDelete(DeleteBehavior.Restrict);
@@ -98,6 +102,21 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.HasOne().WithMany().HasForeignKey(e => e.DeletedById).OnDelete(DeleteBehavior.Restrict);
});
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("PLAZOS_FIJOS");
+ entity.HasKey(e => e.Id);
+ entity.Property(e => e.InteresPrevisto).HasPrecision(18, 2);
+ entity.HasIndex(e => e.CuentaId).IsUnique();
+ entity.HasIndex(e => e.FechaVencimiento);
+ entity.HasIndex(e => e.Estado);
+ entity.HasIndex(e => e.CuentaReferenciaId);
+ entity.HasIndex(e => e.DeletedAt);
+ entity.HasOne(e => e.Cuenta).WithOne().HasForeignKey(e => e.CuentaId).OnDelete(DeleteBehavior.Restrict);
+ entity.HasOne(e => e.CuentaReferencia).WithMany().HasForeignKey(e => e.CuentaReferenciaId).OnDelete(DeleteBehavior.Restrict);
+ entity.HasOne().WithMany().HasForeignKey(e => e.DeletedById).OnDelete(DeleteBehavior.Restrict);
+ });
+
modelBuilder.Entity(entity =>
{
entity.ToTable("FORMATOS_IMPORTACION");
@@ -171,7 +190,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.Property(e => e.SaldoMinimo).HasPrecision(18, 4);
entity.HasIndex(e => e.CuentaId)
.IsUnique()
+ .HasDatabaseName("ix_alertas_saldo_cuenta_id_unique")
.HasFilter("\"cuenta_id\" IS NOT NULL");
+ entity.HasIndex(e => e.TipoTitular)
+ .IsUnique()
+ .HasDatabaseName("ix_alertas_saldo_tipo_titular_unique")
+ .HasFilter("\"cuenta_id\" IS NULL AND \"tipo_titular\" IS NOT NULL");
entity.HasOne().WithMany().HasForeignKey(e => e.CuentaId).OnDelete(DeleteBehavior.Restrict);
});
diff --git a/Atlas Balance/backend/src/GestionCaja.API/Data/SeedData.cs b/Atlas Balance/backend/src/GestionCaja.API/Data/SeedData.cs
index 881eee2..5220b92 100644
--- a/Atlas Balance/backend/src/GestionCaja.API/Data/SeedData.cs
+++ b/Atlas Balance/backend/src/GestionCaja.API/Data/SeedData.cs
@@ -69,7 +69,7 @@ public static void Initialize(AppDbContext context, IConfiguration? configuratio
["backup_retention_weeks"] = ("6", "int", "Semanas de retención de backups"),
["backup_path"] = ("C:/AtlasBalance/backups", "string", "Ruta de almacenamiento de backups"),
["export_path"] = ("C:/AtlasBalance/exports", "string", "Ruta de exportaciones"),
- ["app_version"] = ("V-01.03", "string", "Versión instalada"),
+ ["app_version"] = ("V-01.04", "string", "Versión instalada"),
["app_update_check_url"] = (ConfigurationDefaults.UpdateCheckUrl, "string", "URL del servidor de actualizaciones"),
["smtp_host"] = ("", "string", "Host SMTP"),
["smtp_port"] = ("587", "int", "Puerto SMTP"),
diff --git a/Atlas Balance/backend/src/GestionCaja.API/Jobs/PlazoFijoVencimientoJob.cs b/Atlas Balance/backend/src/GestionCaja.API/Jobs/PlazoFijoVencimientoJob.cs
new file mode 100644
index 0000000..a4360b4
--- /dev/null
+++ b/Atlas Balance/backend/src/GestionCaja.API/Jobs/PlazoFijoVencimientoJob.cs
@@ -0,0 +1,22 @@
+using GestionCaja.API.Services;
+
+namespace GestionCaja.API.Jobs;
+
+public sealed class PlazoFijoVencimientoJob
+{
+ private readonly IPlazoFijoService _plazoFijoService;
+ private readonly ILogger _logger;
+
+ public PlazoFijoVencimientoJob(IPlazoFijoService plazoFijoService, ILogger logger)
+ {
+ _plazoFijoService = plazoFijoService;
+ _logger = logger;
+ }
+
+ public async Task ExecuteAsync()
+ {
+ var hoy = DateOnly.FromDateTime(DateTime.UtcNow.Date);
+ var cambios = await _plazoFijoService.ProcesarVencimientosAsync(hoy, CancellationToken.None);
+ _logger.LogInformation("Job de plazos fijos completado. cambios={Cambios}", cambios);
+ }
+}
diff --git a/Atlas Balance/backend/src/GestionCaja.API/Migrations/20260425130139_AddPuedeVerCuentasPermiso.Designer.cs b/Atlas Balance/backend/src/GestionCaja.API/Migrations/20260425130139_AddPuedeVerCuentasPermiso.Designer.cs
new file mode 100644
index 0000000..08152d8
--- /dev/null
+++ b/Atlas Balance/backend/src/GestionCaja.API/Migrations/20260425130139_AddPuedeVerCuentasPermiso.Designer.cs
@@ -0,0 +1,1538 @@
+//
+using System;
+using System.Net;
+using GestionCaja.API.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace GestionCaja.API.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260425130139_AddPuedeVerCuentasPermiso")]
+ partial class AddPuedeVerCuentasPermiso
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.11")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "estado_proceso", new[] { "pending", "success", "failed" });
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "estado_token_integracion", new[] { "activo", "revocado" });
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "fuente_tipo_cambio", new[] { "api", "manual" });
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "rol_usuario", new[] { "admin", "gerente", "empleado_ultra", "empleado_plus", "empleado" });
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "tipo_proceso", new[] { "auto", "manual" });
+ NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "tipo_titular", new[] { "empresa", "particular" });
+ NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "pgcrypto");
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("GestionCaja.API.Models.AlertaDestinatario", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("AlertaId")
+ .HasColumnType("uuid")
+ .HasColumnName("alerta_id");
+
+ b.Property("UsuarioId")
+ .HasColumnType("uuid")
+ .HasColumnName("usuario_id");
+
+ b.HasKey("Id")
+ .HasName("pk_alerta_destinatarios");
+
+ b.HasIndex("UsuarioId")
+ .HasDatabaseName("ix_alerta_destinatarios_usuario_id");
+
+ b.HasIndex("AlertaId", "UsuarioId")
+ .IsUnique()
+ .HasDatabaseName("ix_alerta_destinatarios_alerta_id_usuario_id");
+
+ b.ToTable("ALERTA_DESTINATARIOS", (string)null);
+ });
+
+ modelBuilder.Entity("GestionCaja.API.Models.AlertaSaldo", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Activa")
+ .HasColumnType("boolean")
+ .HasColumnName("activa");
+
+ b.Property("CuentaId")
+ .HasColumnType("uuid")
+ .HasColumnName("cuenta_id");
+
+ b.Property("FechaCreacion")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("fecha_creacion");
+
+ b.Property("FechaUltimaAlerta")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("fecha_ultima_alerta");
+
+ b.Property("SaldoMinimo")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)")
+ .HasColumnName("saldo_minimo");
+
+ b.HasKey("Id")
+ .HasName("pk_alertas_saldo");
+
+ b.HasIndex("CuentaId")
+ .IsUnique()
+ .HasDatabaseName("ix_alertas_saldo_cuenta_id")
+ .HasFilter("\"cuenta_id\" IS NOT NULL");
+
+ b.ToTable("ALERTAS_SALDO", (string)null);
+ });
+
+ modelBuilder.Entity("GestionCaja.API.Models.Auditoria", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CeldaReferencia")
+ .HasColumnType("text")
+ .HasColumnName("celda_referencia");
+
+ b.Property("ColumnaNombre")
+ .HasColumnType("text")
+ .HasColumnName("columna_nombre");
+
+ b.Property("DetallesJson")
+ .HasColumnType("jsonb")
+ .HasColumnName("detalles_json");
+
+ b.Property("EntidadId")
+ .HasColumnType("uuid")
+ .HasColumnName("entidad_id");
+
+ b.Property("EntidadTipo")
+ .HasColumnType("text")
+ .HasColumnName("entidad_tipo");
+
+ b.Property("IpAddress")
+ .HasColumnType("inet")
+ .HasColumnName("ip_address");
+
+ b.Property("Timestamp")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("timestamp");
+
+ b.Property("TipoAccion")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("tipo_accion");
+
+ b.Property("UsuarioId")
+ .HasColumnType("uuid")
+ .HasColumnName("usuario_id");
+
+ b.Property("ValorAnterior")
+ .HasColumnType("text")
+ .HasColumnName("valor_anterior");
+
+ b.Property("ValorNuevo")
+ .HasColumnType("text")
+ .HasColumnName("valor_nuevo");
+
+ b.HasKey("Id")
+ .HasName("pk_auditorias");
+
+ b.HasIndex("EntidadId")
+ .HasDatabaseName("ix_auditorias_entidad_id");
+
+ b.HasIndex("Timestamp")
+ .HasDatabaseName("ix_auditorias_timestamp");
+
+ b.HasIndex("TipoAccion")
+ .HasDatabaseName("ix_auditorias_tipo_accion");
+
+ b.HasIndex("UsuarioId", "Timestamp")
+ .HasDatabaseName("ix_auditorias_usuario_id_timestamp");
+
+ b.ToTable("AUDITORIAS", (string)null);
+ });
+
+ modelBuilder.Entity("GestionCaja.API.Models.AuditoriaIntegracion", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CodigoRespuesta")
+ .HasColumnType("integer")
+ .HasColumnName("codigo_respuesta");
+
+ b.Property("Endpoint")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("endpoint");
+
+ b.Property("IpAddress")
+ .HasColumnType("inet")
+ .HasColumnName("ip_address");
+
+ b.Property("Metodo")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("metodo");
+
+ b.Property("Parametros")
+ .HasColumnType("jsonb")
+ .HasColumnName("parametros");
+
+ b.Property("TiempoEjecucionMs")
+ .HasColumnType("integer")
+ .HasColumnName("tiempo_ejecucion_ms");
+
+ b.Property("Timestamp")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("timestamp");
+
+ b.Property("TokenId")
+ .HasColumnType("uuid")
+ .HasColumnName("token_id");
+
+ b.HasKey("Id")
+ .HasName("pk_auditoria_integraciones");
+
+ b.HasIndex("CodigoRespuesta")
+ .HasDatabaseName("ix_auditoria_integraciones_codigo_respuesta");
+
+ b.HasIndex("Timestamp")
+ .HasDatabaseName("ix_auditoria_integraciones_timestamp");
+
+ b.HasIndex("TokenId")
+ .HasDatabaseName("ix_auditoria_integraciones_token_id");
+
+ b.ToTable("AUDITORIA_INTEGRACIONES", (string)null);
+ });
+
+ modelBuilder.Entity("GestionCaja.API.Models.Backup", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("Estado")
+ .HasColumnType("integer")
+ .HasColumnName("estado");
+
+ b.Property("FechaCreacion")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("fecha_creacion");
+
+ b.Property("IniciadoPorId")
+ .HasColumnType("uuid")
+ .HasColumnName("iniciado_por_id");
+
+ b.Property("Notas")
+ .HasColumnType("text")
+ .HasColumnName("notas");
+
+ b.Property("RutaArchivo")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("ruta_archivo");
+
+ b.Property("TamanioBytes")
+ .HasColumnType("bigint")
+ .HasColumnName("tamanio_bytes");
+
+ b.Property("Tipo")
+ .HasColumnType("integer")
+ .HasColumnName("tipo");
+
+ b.HasKey("Id")
+ .HasName("pk_backups");
+
+ b.HasIndex("DeletedById")
+ .HasDatabaseName("ix_backups_deleted_by_id");
+
+ b.HasIndex("IniciadoPorId")
+ .HasDatabaseName("ix_backups_iniciado_por_id");
+
+ b.ToTable("BACKUPS", (string)null);
+ });
+
+ modelBuilder.Entity("GestionCaja.API.Models.Configuracion", b =>
+ {
+ b.Property("Clave")
+ .HasColumnType("text")
+ .HasColumnName("clave");
+
+ b.Property("Descripcion")
+ .HasColumnType("text")
+ .HasColumnName("descripcion");
+
+ b.Property("FechaModificacion")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("fecha_modificacion");
+
+ b.Property("Tipo")
+ .HasColumnType("text")
+ .HasColumnName("tipo");
+
+ b.Property("UsuarioModificacionId")
+ .HasColumnType("uuid")
+ .HasColumnName("usuario_modificacion_id");
+
+ b.Property("Valor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("valor");
+
+ b.HasKey("Clave")
+ .HasName("pk_configuracion");
+
+ b.HasIndex("UsuarioModificacionId")
+ .HasDatabaseName("ix_configuracion_usuario_modificacion_id");
+
+ b.ToTable("CONFIGURACION", (string)null);
+ });
+
+ modelBuilder.Entity("GestionCaja.API.Models.Cuenta", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Activa")
+ .HasColumnType("boolean")
+ .HasColumnName("activa");
+
+ b.Property("BancoNombre")
+ .HasColumnType("text")
+ .HasColumnName("banco_nombre");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("Divisa")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("divisa");
+
+ b.Property("EsEfectivo")
+ .HasColumnType("boolean")
+ .HasColumnName("es_efectivo");
+
+ b.Property("FechaCreacion")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("fecha_creacion");
+
+ b.Property("FormatoId")
+ .HasColumnType("uuid")
+ .HasColumnName("formato_id");
+
+ b.Property("Iban")
+ .HasColumnType("text")
+ .HasColumnName("iban");
+
+ b.Property("Nombre")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("nombre");
+
+ b.Property("Notas")
+ .HasColumnType("text")
+ .HasColumnName("notas");
+
+ b.Property("NumeroCuenta")
+ .HasColumnType("text")
+ .HasColumnName("numero_cuenta");
+
+ b.Property("TitularId")
+ .HasColumnType("uuid")
+ .HasColumnName("titular_id");
+
+ b.HasKey("Id")
+ .HasName("pk_cuentas");
+
+ b.HasIndex("Activa")
+ .HasDatabaseName("ix_cuentas_activa");
+
+ b.HasIndex("DeletedAt")
+ .HasDatabaseName("ix_cuentas_deleted_at");
+
+ b.HasIndex("DeletedById")
+ .HasDatabaseName("ix_cuentas_deleted_by_id");
+
+ b.HasIndex("Divisa")
+ .HasDatabaseName("ix_cuentas_divisa");
+
+ b.HasIndex("EsEfectivo")
+ .HasDatabaseName("ix_cuentas_es_efectivo");
+
+ b.HasIndex("FormatoId")
+ .HasDatabaseName("ix_cuentas_formato_id");
+
+ b.HasIndex("TitularId")
+ .HasDatabaseName("ix_cuentas_titular_id");
+
+ b.ToTable("CUENTAS", (string)null);
+ });
+
+ modelBuilder.Entity("GestionCaja.API.Models.DivisaActiva", b =>
+ {
+ b.Property("Codigo")
+ .HasColumnType("text")
+ .HasColumnName("codigo");
+
+ b.Property("Activa")
+ .HasColumnType("boolean")
+ .HasColumnName("activa");
+
+ b.Property("EsBase")
+ .HasColumnType("boolean")
+ .HasColumnName("es_base");
+
+ b.Property("Nombre")
+ .HasColumnType("text")
+ .HasColumnName("nombre");
+
+ b.Property("Simbolo")
+ .HasColumnType("text")
+ .HasColumnName("simbolo");
+
+ b.HasKey("Codigo")
+ .HasName("pk_divisas_activas");
+
+ b.ToTable("DIVISAS_ACTIVAS", (string)null);
+ });
+
+ modelBuilder.Entity("GestionCaja.API.Models.Exportacion", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CuentaId")
+ .HasColumnType("uuid")
+ .HasColumnName("cuenta_id");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("Estado")
+ .HasColumnType("integer")
+ .HasColumnName("estado");
+
+ b.Property("FechaExportacion")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("fecha_exportacion");
+
+ b.Property("IniciadoPorId")
+ .HasColumnType("uuid")
+ .HasColumnName("iniciado_por_id");
+
+ b.Property("RutaArchivo")
+ .HasColumnType("text")
+ .HasColumnName("ruta_archivo");
+
+ b.Property("TamanioBytes")
+ .HasColumnType("bigint")
+ .HasColumnName("tamanio_bytes");
+
+ b.Property("Tipo")
+ .HasColumnType("integer")
+ .HasColumnName("tipo");
+
+ b.HasKey("Id")
+ .HasName("pk_exportaciones");
+
+ b.HasIndex("CuentaId")
+ .HasDatabaseName("ix_exportaciones_cuenta_id");
+
+ b.HasIndex("DeletedById")
+ .HasDatabaseName("ix_exportaciones_deleted_by_id");
+
+ b.HasIndex("IniciadoPorId")
+ .HasDatabaseName("ix_exportaciones_iniciado_por_id");
+
+ b.ToTable("EXPORTACIONES", (string)null);
+ });
+
+ modelBuilder.Entity("GestionCaja.API.Models.Extracto", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Checked")
+ .HasColumnType("boolean")
+ .HasColumnName("checked");
+
+ b.Property("CheckedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("checked_at");
+
+ b.Property("CheckedById")
+ .HasColumnType("uuid")
+ .HasColumnName("checked_by_id");
+
+ b.Property("Comentarios")
+ .HasColumnType("text")
+ .HasColumnName("comentarios");
+
+ b.Property("Concepto")
+ .HasColumnType("text")
+ .HasColumnName("concepto");
+
+ b.Property("CuentaId")
+ .HasColumnType("uuid")
+ .HasColumnName("cuenta_id");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("Fecha")
+ .HasColumnType("date")
+ .HasColumnName("fecha");
+
+ b.Property("FechaCreacion")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("fecha_creacion");
+
+ b.Property("FechaModificacion")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("fecha_modificacion");
+
+ b.Property("FilaNumero")
+ .HasColumnType("integer")
+ .HasColumnName("fila_numero");
+
+ b.Property("Flagged")
+ .HasColumnType("boolean")
+ .HasColumnName("flagged");
+
+ b.Property("FlaggedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("flagged_at");
+
+ b.Property("FlaggedById")
+ .HasColumnType("uuid")
+ .HasColumnName("flagged_by_id");
+
+ b.Property("FlaggedNota")
+ .HasColumnType("text")
+ .HasColumnName("flagged_nota");
+
+ b.Property("Monto")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)")
+ .HasColumnName("monto");
+
+ b.Property("Saldo")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)")
+ .HasColumnName("saldo");
+
+ b.Property("UsuarioCreacionId")
+ .HasColumnType("uuid")
+ .HasColumnName("usuario_creacion_id");
+
+ b.Property("UsuarioModificacionId")
+ .HasColumnType("uuid")
+ .HasColumnName("usuario_modificacion_id");
+
+ b.HasKey("Id")
+ .HasName("pk_extractos");
+
+ b.HasIndex("Checked")
+ .HasDatabaseName("ix_extractos_checked");
+
+ b.HasIndex("CheckedById")
+ .HasDatabaseName("ix_extractos_checked_by_id");
+
+ b.HasIndex("DeletedById")
+ .HasDatabaseName("ix_extractos_deleted_by_id");
+
+ b.HasIndex("Fecha")
+ .HasDatabaseName("ix_extractos_fecha");
+
+ b.HasIndex("Flagged")
+ .HasDatabaseName("ix_extractos_flagged");
+
+ b.HasIndex("FlaggedById")
+ .HasDatabaseName("ix_extractos_flagged_by_id");
+
+ b.HasIndex("UsuarioCreacionId")
+ .HasDatabaseName("ix_extractos_usuario_creacion_id");
+
+ b.HasIndex("UsuarioModificacionId")
+ .HasDatabaseName("ix_extractos_usuario_modificacion_id");
+
+ b.HasIndex("CuentaId", "DeletedAt")
+ .HasDatabaseName("ix_extractos_cuenta_id_deleted_at");
+
+ b.HasIndex("CuentaId", "Fecha")
+ .HasDatabaseName("ix_extractos_cuenta_id_fecha");
+
+ b.HasIndex("CuentaId", "FilaNumero")
+ .IsUnique()
+ .HasDatabaseName("ix_extractos_cuenta_id_fila_numero");
+
+ b.ToTable("EXTRACTOS", (string)null);
+ });
+
+ modelBuilder.Entity("GestionCaja.API.Models.ExtractoColumnaExtra", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("ExtractoId")
+ .HasColumnType("uuid")
+ .HasColumnName("extracto_id");
+
+ b.Property("NombreColumna")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("nombre_columna");
+
+ b.Property("Valor")
+ .HasColumnType("text")
+ .HasColumnName("valor");
+
+ b.HasKey("Id")
+ .HasName("pk_extractos_columnas_extra");
+
+ b.HasIndex("ExtractoId")
+ .HasDatabaseName("ix_extractos_columnas_extra_extracto_id");
+
+ b.HasIndex("NombreColumna")
+ .HasDatabaseName("ix_extractos_columnas_extra_nombre_columna");
+
+ b.ToTable("EXTRACTOS_COLUMNAS_EXTRA", (string)null);
+ });
+
+ modelBuilder.Entity("GestionCaja.API.Models.FormatoImportacion", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Activo")
+ .HasColumnType("boolean")
+ .HasColumnName("activo");
+
+ b.Property("BancoNombre")
+ .HasColumnType("text")
+ .HasColumnName("banco_nombre");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("Divisa")
+ .HasColumnType("text")
+ .HasColumnName("divisa");
+
+ b.Property("FechaCreacion")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("fecha_creacion");
+
+ b.Property("MapeoJson")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("mapeo_json");
+
+ b.Property("Nombre")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("nombre");
+
+ b.Property("UsuarioCreadorId")
+ .HasColumnType("uuid")
+ .HasColumnName("usuario_creador_id");
+
+ b.HasKey("Id")
+ .HasName("pk_formatos_importacion");
+
+ b.HasIndex("DeletedById")
+ .HasDatabaseName("ix_formatos_importacion_deleted_by_id");
+
+ b.HasIndex("UsuarioCreadorId")
+ .HasDatabaseName("ix_formatos_importacion_usuario_creador_id");
+
+ b.ToTable("FORMATOS_IMPORTACION", (string)null);
+ });
+
+ modelBuilder.Entity("GestionCaja.API.Models.IntegrationPermission", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("AccesoTipo")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("acceso_tipo");
+
+ b.Property("CuentaId")
+ .HasColumnType("uuid")
+ .HasColumnName("cuenta_id");
+
+ b.Property("FechaCreacion")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("fecha_creacion");
+
+ b.Property("TitularId")
+ .HasColumnType("uuid")
+ .HasColumnName("titular_id");
+
+ b.Property("TokenId")
+ .HasColumnType("uuid")
+ .HasColumnName("token_id");
+
+ b.HasKey("Id")
+ .HasName("pk_integration_permissions");
+
+ b.HasIndex("CuentaId")
+ .HasDatabaseName("ix_integration_permissions_cuenta_id");
+
+ b.HasIndex("TitularId")
+ .HasDatabaseName("ix_integration_permissions_titular_id");
+
+ b.HasIndex("TokenId")
+ .HasDatabaseName("ix_integration_permissions_token_id");
+
+ b.ToTable("INTEGRATION_PERMISSIONS", (string)null);
+ });
+
+ modelBuilder.Entity("GestionCaja.API.Models.IntegrationToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("Descripcion")
+ .HasColumnType("text")
+ .HasColumnName("descripcion");
+
+ b.Property("Estado")
+ .HasColumnType("integer")
+ .HasColumnName("estado");
+
+ b.Property("FechaCreacion")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("fecha_creacion");
+
+ b.Property("FechaRevocacion")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("fecha_revocacion");
+
+ b.Property("FechaUltimaUso")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("fecha_ultima_uso");
+
+ b.Property("Nombre")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("nombre");
+
+ b.Property("PermisoEscritura")
+ .HasColumnType("boolean")
+ .HasColumnName("permiso_escritura");
+
+ b.Property("PermisoLectura")
+ .HasColumnType("boolean")
+ .HasColumnName("permiso_lectura");
+
+ b.Property("Tipo")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("tipo");
+
+ b.Property("TokenHash")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("token_hash");
+
+ b.Property("UsuarioCreadorId")
+ .HasColumnType("uuid")
+ .HasColumnName("usuario_creador_id");
+
+ b.HasKey("Id")
+ .HasName("pk_integration_tokens");
+
+ b.HasIndex("DeletedById")
+ .HasDatabaseName("ix_integration_tokens_deleted_by_id");
+
+ b.HasIndex("Estado")
+ .HasDatabaseName("ix_integration_tokens_estado");
+
+ b.HasIndex("TokenHash")
+ .IsUnique()
+ .HasDatabaseName("ix_integration_tokens_token_hash");
+
+ b.HasIndex("UsuarioCreadorId")
+ .HasDatabaseName("ix_integration_tokens_usuario_creador_id");
+
+ b.ToTable("INTEGRATION_TOKENS", (string)null);
+ });
+
+ modelBuilder.Entity("GestionCaja.API.Models.NotificacionAdmin", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("DetallesJson")
+ .HasColumnType("jsonb")
+ .HasColumnName("detalles_json");
+
+ b.Property("Fecha")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("fecha");
+
+ b.Property("Leida")
+ .HasColumnType("boolean")
+ .HasColumnName("leida");
+
+ b.Property("Mensaje")
+ .HasColumnType("text")
+ .HasColumnName("mensaje");
+
+ b.Property("Tipo")
+ .HasColumnType("text")
+ .HasColumnName("tipo");
+
+ b.HasKey("Id")
+ .HasName("pk_notificaciones_admin");
+
+ b.ToTable("NOTIFICACIONES_ADMIN", (string)null);
+ });
+
+ modelBuilder.Entity("GestionCaja.API.Models.PermisoUsuario", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CuentaId")
+ .HasColumnType("uuid")
+ .HasColumnName("cuenta_id");
+
+ b.Property("PuedeAgregarLineas")
+ .HasColumnType("boolean")
+ .HasColumnName("puede_agregar_lineas");
+
+ b.Property("PuedeEditarLineas")
+ .HasColumnType("boolean")
+ .HasColumnName("puede_editar_lineas");
+
+ b.Property("PuedeEliminarLineas")
+ .HasColumnType("boolean")
+ .HasColumnName("puede_eliminar_lineas");
+
+ b.Property("PuedeImportar")
+ .HasColumnType("boolean")
+ .HasColumnName("puede_importar");
+
+ b.Property("PuedeVerCuentas")
+ .HasColumnType("boolean")
+ .HasColumnName("puede_ver_cuentas");
+
+ b.Property("PuedeVerDashboard")
+ .HasColumnType("boolean")
+ .HasColumnName("puede_ver_dashboard");
+
+ b.Property("TitularId")
+ .HasColumnType("uuid")
+ .HasColumnName("titular_id");
+
+ b.Property("UsuarioId")
+ .HasColumnType("uuid")
+ .HasColumnName("usuario_id");
+
+ b.HasKey("Id")
+ .HasName("pk_permisos_usuario");
+
+ b.HasIndex("CuentaId")
+ .HasDatabaseName("ix_permisos_usuario_cuenta_id");
+
+ b.HasIndex("TitularId")
+ .HasDatabaseName("ix_permisos_usuario_titular_id");
+
+ b.HasIndex("UsuarioId")
+ .HasDatabaseName("ix_permisos_usuario_usuario_id");
+
+ b.HasIndex("UsuarioId", "CuentaId")
+ .HasDatabaseName("ix_permisos_usuario_usuario_id_cuenta_id");
+
+ b.ToTable("PERMISOS_USUARIO", (string)null);
+ });
+
+ modelBuilder.Entity("GestionCaja.API.Models.PreferenciaUsuarioCuenta", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("ColumnasEditables")
+ .HasColumnType("jsonb")
+ .HasColumnName("columnas_editables");
+
+ b.Property("ColumnasVisibles")
+ .HasColumnType("jsonb")
+ .HasColumnName("columnas_visibles");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("CuentaId")
+ .HasColumnType("uuid")
+ .HasColumnName("cuenta_id");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at");
+
+ b.Property("UsuarioId")
+ .HasColumnType("uuid")
+ .HasColumnName("usuario_id");
+
+ b.HasKey("Id")
+ .HasName("pk_preferencias_usuario_cuenta");
+
+ b.HasIndex("CuentaId")
+ .HasDatabaseName("ix_preferencias_usuario_cuenta_cuenta_id");
+
+ b.HasIndex("UsuarioId")
+ .IsUnique()
+ .HasDatabaseName("ix_preferencias_usuario_cuenta_usuario_id")
+ .HasFilter("\"cuenta_id\" IS NULL");
+
+ b.HasIndex("UsuarioId", "CuentaId")
+ .IsUnique()
+ .HasDatabaseName("ix_preferencias_usuario_cuenta_usuario_id_cuenta_id")
+ .HasFilter("\"cuenta_id\" IS NOT NULL");
+
+ b.ToTable("PREFERENCIAS_USUARIO_CUENTA", (string)null);
+ });
+
+ modelBuilder.Entity("GestionCaja.API.Models.RefreshToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CreadoEn")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("creado_en");
+
+ b.Property("ExpiraEn")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expira_en");
+
+ b.Property("IpAddress")
+ .HasColumnType("inet")
+ .HasColumnName("ip_address");
+
+ b.Property("ReemplazadoPor")
+ .HasColumnType("text")
+ .HasColumnName("reemplazado_por");
+
+ b.Property("RevocadoEn")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("revocado_en");
+
+ b.Property("TokenHash")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("token_hash");
+
+ b.Property("UsuarioId")
+ .HasColumnType("uuid")
+ .HasColumnName("usuario_id");
+
+ b.HasKey("Id")
+ .HasName("pk_refresh_tokens");
+
+ b.HasIndex("ExpiraEn")
+ .HasDatabaseName("ix_refresh_tokens_expira_en");
+
+ b.HasIndex("TokenHash")
+ .IsUnique()
+ .HasDatabaseName("ix_refresh_tokens_token_hash");
+
+ b.HasIndex("UsuarioId")
+ .HasDatabaseName("ix_refresh_tokens_usuario_id");
+
+ b.ToTable("REFRESH_TOKENS", (string)null);
+ });
+
+ modelBuilder.Entity("GestionCaja.API.Models.TipoCambio", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("DivisaDestino")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("divisa_destino");
+
+ b.Property("DivisaOrigen")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("divisa_origen");
+
+ b.Property("FechaActualizacion")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("fecha_actualizacion");
+
+ b.Property