diff --git a/.aspnet-keys/key-df4b6848-d6f8-4258-b66f-efc9c9908378.xml b/.aspnet-keys/key-df4b6848-d6f8-4258-b66f-efc9c9908378.xml
new file mode 100644
index 0000000..f1eaeb4
--- /dev/null
+++ b/.aspnet-keys/key-df4b6848-d6f8-4258-b66f-efc9c9908378.xml
@@ -0,0 +1,16 @@
+
+
+ 2026-02-11T18:29:16.4250416Z
+ 2026-02-11T18:29:16.4250416Z
+ 2026-05-12T18:29:16.4250416Z
+
+
+
+
+
+
+ ZPHp/1oEBziSImYed0VIGR6ghUDI9+xXPXqb8t5Hs91BPBBvLM2NCReZtsmfKfmoqP0uFJ0jK+t/7tr6XxBAaA==
+
+
+
+
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 0808c4a..ce2b5ee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@
# dotenv files
.env
+appsettings.Local.json
# User-specific files
*.rsuser
diff --git a/Controllers/AuthController.cs b/Controllers/AuthController.cs
index 34ffe6b..fe2251d 100644
--- a/Controllers/AuthController.cs
+++ b/Controllers/AuthController.cs
@@ -8,6 +8,7 @@ using line_gestao_api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
@@ -60,7 +61,8 @@ public class AuthController : ControllerBase
UserName = email,
TenantId = tenantId.Value,
IsActive = true,
- EmailConfirmed = true
+ EmailConfirmed = true,
+ LockoutEnabled = true
};
var createResult = await _userManager.CreateAsync(user, req.Password);
@@ -78,6 +80,7 @@ public class AuthController : ControllerBase
}
[HttpPost("login")]
+ [EnableRateLimiting("auth-login")]
public async Task Login(LoginRequest req)
{
var email = (req.Email ?? "").Trim().ToLowerInvariant();
@@ -115,9 +118,24 @@ public class AuthController : ControllerBase
if (!user.IsActive)
return Unauthorized("Usuário desativado.");
+ if (await _userManager.IsLockedOutAsync(user))
+ return Unauthorized("Usuário temporariamente bloqueado por tentativas inválidas. Tente novamente em alguns minutos.");
+
var valid = await _userManager.CheckPasswordAsync(user, password);
if (!valid)
+ {
+ if (user.LockoutEnabled)
+ {
+ await _userManager.AccessFailedAsync(user);
+ }
+
return Unauthorized("Credenciais inválidas.");
+ }
+
+ if (user.LockoutEnabled)
+ {
+ await _userManager.ResetAccessFailedCountAsync(user);
+ }
var effectiveTenantId = await EnsureValidTenantIdAsync(user);
if (!effectiveTenantId.HasValue)
diff --git a/Controllers/ChipsVirgensController.cs b/Controllers/ChipsVirgensController.cs
index 57c8d49..1f18b17 100644
--- a/Controllers/ChipsVirgensController.cs
+++ b/Controllers/ChipsVirgensController.cs
@@ -93,6 +93,7 @@ namespace line_gestao_api.Controllers
}
[HttpPost]
+ [Authorize(Roles = "admin,gestor")]
public async Task> Create([FromBody] CreateChipVirgemDto req)
{
var now = DateTime.UtcNow;
diff --git a/Controllers/ControleRecebidosController.cs b/Controllers/ControleRecebidosController.cs
index 0b33c57..e0e2a75 100644
--- a/Controllers/ControleRecebidosController.cs
+++ b/Controllers/ControleRecebidosController.cs
@@ -138,6 +138,7 @@ namespace line_gestao_api.Controllers
}
[HttpPost]
+ [Authorize(Roles = "admin,gestor")]
public async Task> Create([FromBody] CreateControleRecebidoDto req)
{
var now = DateTime.UtcNow;
diff --git a/Controllers/LinesController.cs b/Controllers/LinesController.cs
index 441a052..cd52c5d 100644
--- a/Controllers/LinesController.cs
+++ b/Controllers/LinesController.cs
@@ -686,6 +686,7 @@ namespace line_gestao_api.Controllers
// ✅ 5. CREATE
// ==========================================================
[HttpPost]
+ [Authorize(Roles = "admin,gestor")]
public async Task> Create([FromBody] CreateMobileLineDto req)
{
if (string.IsNullOrWhiteSpace(req.Cliente))
@@ -788,6 +789,7 @@ namespace line_gestao_api.Controllers
// ✅ 6. UPDATE
// ==========================================================
[HttpPut("{id:guid}")]
+ [Authorize(Roles = "admin,gestor")]
public async Task Update(Guid id, [FromBody] UpdateMobileLineRequest req)
{
var x = await _db.MobileLines.FirstOrDefaultAsync(a => a.Id == id);
@@ -3649,19 +3651,124 @@ namespace line_gestao_api.Controllers
return new DateTime(dt.Year, dt.Month, dt.Day, 12, 0, 0, DateTimeKind.Utc);
}
- private static decimal? TryDecimal(string? s)
+ private static decimal? TryDecimal(string? input)
{
- if (string.IsNullOrWhiteSpace(s)) return null;
+ if (string.IsNullOrWhiteSpace(input)) return null;
- s = s.Replace("R$", "", StringComparison.OrdinalIgnoreCase).Trim();
+ var normalized = NormalizeDecimalInput(input);
+ if (string.IsNullOrWhiteSpace(normalized)) return null;
- if (decimal.TryParse(s, NumberStyles.Any, new CultureInfo("pt-BR"), out var d)) return d;
- if (decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out d)) return d;
+ return decimal.TryParse(normalized, NumberStyles.Float, CultureInfo.InvariantCulture, out var value)
+ ? value
+ : null;
+ }
- var s2 = s.Replace(".", "").Replace(",", ".");
- if (decimal.TryParse(s2, NumberStyles.Any, CultureInfo.InvariantCulture, out d)) return d;
+ private static string NormalizeDecimalInput(string raw)
+ {
+ var s = raw
+ .Replace("R$", "", StringComparison.OrdinalIgnoreCase)
+ .Replace("%", "", StringComparison.OrdinalIgnoreCase)
+ .Replace(" ", "")
+ .Replace("\u00A0", "")
+ .Trim();
- return null;
+ if (string.IsNullOrWhiteSpace(s)) return string.Empty;
+
+ var negativeByParentheses = s.StartsWith("(") && s.EndsWith(")");
+ if (negativeByParentheses && s.Length >= 2)
+ {
+ s = s[1..^1];
+ }
+
+ var allowed = new StringBuilder(s.Length);
+ foreach (var ch in s)
+ {
+ if (char.IsDigit(ch) || ch == '.' || ch == ',' || ch == '-' || ch == '+' || ch == 'e' || ch == 'E')
+ {
+ allowed.Append(ch);
+ }
+ }
+
+ s = allowed.ToString();
+ if (string.IsNullOrWhiteSpace(s)) return string.Empty;
+
+ string normalized;
+ var commaCount = s.Count(c => c == ',');
+ var dotCount = s.Count(c => c == '.');
+
+ if (s.IndexOf('e') >= 0 || s.IndexOf('E') >= 0)
+ {
+ normalized = s.Replace(",", ".");
+ }
+ else if (commaCount > 0 && dotCount > 0)
+ {
+ var lastComma = s.LastIndexOf(',');
+ var lastDot = s.LastIndexOf('.');
+
+ if (lastComma > lastDot)
+ {
+ // Ex.: 1.234,56 -> 1234.56
+ normalized = s.Replace(".", "").Replace(",", ".");
+ }
+ else
+ {
+ // Ex.: 1,234.56 -> 1234.56
+ normalized = s.Replace(",", "");
+ }
+ }
+ else if (commaCount > 0)
+ {
+ normalized = NormalizeSingleSeparatorNumber(s, ',');
+ }
+ else if (dotCount > 0)
+ {
+ normalized = NormalizeSingleSeparatorNumber(s, '.');
+ }
+ else
+ {
+ normalized = s;
+ }
+
+ if (negativeByParentheses && !normalized.StartsWith("-"))
+ {
+ normalized = "-" + normalized;
+ }
+
+ return normalized;
+ }
+
+ private static string NormalizeSingleSeparatorNumber(string value, char separator)
+ {
+ var separatorCount = value.Count(c => c == separator);
+ if (separatorCount <= 0) return value;
+
+ if (separatorCount == 1)
+ {
+ var idx = value.IndexOf(separator);
+ var leftDigits = value[..idx].Count(char.IsDigit);
+ var rightDigits = value[(idx + 1)..].Count(char.IsDigit);
+
+ // "1.234"/"1,234" normalmente representa milhar.
+ var looksLikeThousandsSeparator = rightDigits == 3 && leftDigits > 0 && leftDigits <= 3;
+ if (looksLikeThousandsSeparator)
+ {
+ return value.Replace(separator.ToString(), "");
+ }
+
+ return separator == ',' ? value.Replace(",", ".") : value;
+ }
+
+ var parts = value.Split(separator);
+ var allGroupsAreThousands = parts.Skip(1).All(p => p.Length == 3 && p.All(char.IsDigit));
+ if (allGroupsAreThousands)
+ {
+ return value.Replace(separator.ToString(), "");
+ }
+
+ var last = value.LastIndexOf(separator);
+ var whole = value[..last].Replace(separator.ToString(), "");
+ var fraction = value[(last + 1)..];
+ return $"{whole}.{fraction}";
}
private static int TryInt(string s)
diff --git a/Controllers/MuregController.cs b/Controllers/MuregController.cs
index 111ba4a..1ebd16b 100644
--- a/Controllers/MuregController.cs
+++ b/Controllers/MuregController.cs
@@ -187,6 +187,7 @@ namespace line_gestao_api.Controllers
}
[HttpPost]
+ [Authorize(Roles = "admin,gestor")]
public async Task> Create([FromBody] CreateMuregDto req)
{
if (req.MobileLineId == Guid.Empty)
@@ -288,6 +289,7 @@ namespace line_gestao_api.Controllers
}
[HttpPut("{id:guid}")]
+ [Authorize(Roles = "admin,gestor")]
public async Task Update(Guid id, [FromBody] UpdateMuregDto req)
{
var entity = await _db.MuregLines.FirstOrDefaultAsync(x => x.Id == id);
@@ -375,6 +377,7 @@ namespace line_gestao_api.Controllers
// ✅ POST: /api/mureg/import-excel (mantido)
// ==========================================================
[HttpPost("import-excel")]
+ [Authorize(Roles = "admin,gestor")]
[Consumes("multipart/form-data")]
[RequestSizeLimit(50_000_000)]
public async Task ImportExcel([FromForm] ImportExcelForm form)
diff --git a/Controllers/NotificationsController.cs b/Controllers/NotificationsController.cs
index 0c17b55..d580e13 100644
--- a/Controllers/NotificationsController.cs
+++ b/Controllers/NotificationsController.cs
@@ -66,6 +66,12 @@ public class NotificationsController : ControllerBase
})
.ToListAsync();
+ var todayUtc = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Utc);
+ foreach (var item in items)
+ {
+ ApplyEffectiveType(item, todayUtc);
+ }
+
return Ok(items);
}
@@ -160,6 +166,11 @@ public class NotificationsController : ControllerBase
notification.Tipo))
.ToListAsync();
+ var todayUtc = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Utc);
+ rows = rows
+ .Select(r => r with { Tipo = ResolveEffectiveType(r.Tipo, r.DataReferencia, todayUtc) })
+ .ToList();
+
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Notificacoes");
@@ -291,6 +302,38 @@ public class NotificationsController : ControllerBase
return filter?.Trim().ToLowerInvariant();
}
+ private static void ApplyEffectiveType(NotificationDto item, DateTime todayUtc)
+ {
+ item.Tipo = ResolveEffectiveType(item.Tipo, item.DtTerminoFidelizacao ?? item.ReferenciaData, todayUtc);
+ var effectiveDate = item.DtTerminoFidelizacao ?? item.ReferenciaData;
+ if (!effectiveDate.HasValue)
+ {
+ return;
+ }
+
+ var endDateUtc = DateTime.SpecifyKind(effectiveDate.Value.Date, DateTimeKind.Utc);
+ var daysUntil = (endDateUtc - todayUtc).Days;
+ item.DiasParaVencer = daysUntil < 0 ? 0 : daysUntil;
+ }
+
+ private static string ResolveEffectiveType(string currentType, DateTime? referenceDate, DateTime todayUtc)
+ {
+ if (!referenceDate.HasValue)
+ {
+ return currentType;
+ }
+
+ var isKnownType = currentType.Equals("AVencer", StringComparison.OrdinalIgnoreCase)
+ || currentType.Equals("Vencido", StringComparison.OrdinalIgnoreCase);
+ if (!isKnownType)
+ {
+ return currentType;
+ }
+
+ var endDateUtc = DateTime.SpecifyKind(referenceDate.Value.Date, DateTimeKind.Utc);
+ return endDateUtc < todayUtc ? "Vencido" : "AVencer";
+ }
+
private sealed record NotificationExportRow(
string? Conta,
string? Linha,
diff --git a/Controllers/ParcelamentosController.cs b/Controllers/ParcelamentosController.cs
index 4f655ca..65ac100 100644
--- a/Controllers/ParcelamentosController.cs
+++ b/Controllers/ParcelamentosController.cs
@@ -165,6 +165,7 @@ public class ParcelamentosController : ControllerBase
}
[HttpPost]
+ [Authorize(Roles = "admin,gestor")]
public async Task> Create([FromBody] ParcelamentoUpsertDto req)
{
var now = DateTime.UtcNow;
@@ -201,6 +202,7 @@ public class ParcelamentosController : ControllerBase
}
[HttpPut("{id:guid}")]
+ [Authorize(Roles = "admin,gestor")]
public async Task Update(Guid id, [FromBody] ParcelamentoUpsertDto req)
{
var entity = await _db.ParcelamentoLines
diff --git a/Controllers/TrocaNumeroController.cs b/Controllers/TrocaNumeroController.cs
index 00e44f5..759764a 100644
--- a/Controllers/TrocaNumeroController.cs
+++ b/Controllers/TrocaNumeroController.cs
@@ -112,6 +112,7 @@ namespace line_gestao_api.Controllers
// ✅ CREATE
// ==========================================================
[HttpPost]
+ [Authorize(Roles = "admin,gestor")]
public async Task> Create([FromBody] CreateTrocaNumeroDto req)
{
var now = DateTime.UtcNow;
@@ -140,6 +141,7 @@ namespace line_gestao_api.Controllers
// ✅ UPDATE
// ==========================================================
[HttpPut("{id:guid}")]
+ [Authorize(Roles = "admin,gestor")]
public async Task Update(Guid id, [FromBody] UpdateTrocaNumeroRequest req)
{
var x = await _db.TrocaNumeroLines.FirstOrDefaultAsync(a => a.Id == id);
diff --git a/Data/SeedData.cs b/Data/SeedData.cs
index 66adf30..52a9132 100644
--- a/Data/SeedData.cs
+++ b/Data/SeedData.cs
@@ -8,10 +8,12 @@ namespace line_gestao_api.Data;
public class SeedOptions
{
+ public bool Enabled { get; set; } = true;
public string DefaultTenantName { get; set; } = "Default";
public string AdminName { get; set; } = "Administrador";
public string AdminEmail { get; set; } = "admin@linegestao.local";
- public string AdminPassword { get; set; } = "Admin123!";
+ public string AdminPassword { get; set; } = "DevAdmin123!";
+ public bool ReapplyAdminCredentialsOnStartup { get; set; } = false;
}
public static class SeedData
@@ -29,6 +31,11 @@ public static class SeedData
await db.Database.MigrateAsync();
+ if (!options.Enabled)
+ {
+ return;
+ }
+
var roles = new[] { "admin", "gestor", "operador", "leitura" };
foreach (var role in roles)
{
@@ -69,7 +76,8 @@ public static class SeedData
Name = options.AdminName,
TenantId = tenant.Id,
EmailConfirmed = true,
- IsActive = true
+ IsActive = true,
+ LockoutEnabled = true
};
var createResult = await userManager.CreateAsync(adminUser, options.AdminPassword);
@@ -78,21 +86,28 @@ public static class SeedData
await userManager.AddToRoleAsync(adminUser, "admin");
}
}
- else
+ else if (options.ReapplyAdminCredentialsOnStartup)
{
- existingAdmin.UserName = options.AdminEmail;
- existingAdmin.Email = options.AdminEmail;
existingAdmin.Name = options.AdminName;
- existingAdmin.TenantId = tenant.Id;
+ existingAdmin.Email = options.AdminEmail;
+ existingAdmin.UserName = options.AdminEmail;
existingAdmin.EmailConfirmed = true;
existingAdmin.IsActive = true;
+ existingAdmin.LockoutEnabled = true;
+ await userManager.SetLockoutEndDateAsync(existingAdmin, null);
+ await userManager.ResetAccessFailedCountAsync(existingAdmin);
await userManager.UpdateAsync(existingAdmin);
- if (!await userManager.CheckPasswordAsync(existingAdmin, options.AdminPassword))
+ var resetToken = await userManager.GeneratePasswordResetTokenAsync(existingAdmin);
+ var resetPasswordResult = await userManager.ResetPasswordAsync(existingAdmin, resetToken, options.AdminPassword);
+ if (!resetPasswordResult.Succeeded)
{
- var resetToken = await userManager.GeneratePasswordResetTokenAsync(existingAdmin);
- await userManager.ResetPasswordAsync(existingAdmin, resetToken, options.AdminPassword);
+ var removePasswordResult = await userManager.RemovePasswordAsync(existingAdmin);
+ if (removePasswordResult.Succeeded)
+ {
+ await userManager.AddPasswordAsync(existingAdmin, options.AdminPassword);
+ }
}
if (!await userManager.IsInRoleAsync(existingAdmin, "admin"))
diff --git a/Dtos/GeralDashboardInsightsDto.cs b/Dtos/GeralDashboardInsightsDto.cs
index a2597ad..72ed164 100644
--- a/Dtos/GeralDashboardInsightsDto.cs
+++ b/Dtos/GeralDashboardInsightsDto.cs
@@ -16,6 +16,15 @@ namespace line_gestao_api.Dtos
public GeralDashboardVivoKpiDto Vivo { get; set; } = new();
public GeralDashboardTravelKpiDto TravelMundo { get; set; } = new();
public GeralDashboardAdditionalKpiDto Adicionais { get; set; } = new();
+ public List TotaisLine { get; set; } = new();
+ }
+
+ public class GeralDashboardLineTotalDto
+ {
+ public string Tipo { get; set; } = string.Empty;
+ public int QtdLinhas { get; set; }
+ public decimal ValorTotalLine { get; set; }
+ public decimal LucroTotalLine { get; set; }
}
public class GeralDashboardVivoKpiDto
diff --git a/Program.cs b/Program.cs
index 0e2ea7d..291cc3d 100644
--- a/Program.cs
+++ b/Program.cs
@@ -1,17 +1,29 @@
-using System.Text;
+using System.IO;
+using System.Text;
+using System.Threading.RateLimiting;
using line_gestao_api.Data;
using line_gestao_api.Models;
using line_gestao_api.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
+builder.Configuration.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: true);
+
+var dataProtectionKeyPath = builder.Environment.IsProduction()
+ ? "/var/www/html/line-gestao-api/publish/.aspnet-keys"
+ : Path.Combine(builder.Environment.ContentRootPath, ".aspnet-keys");
builder.Services.AddControllers();
+builder.Services.AddDataProtection()
+ .PersistKeysToFileSystem(new DirectoryInfo(dataProtectionKeyPath))
+ .SetApplicationName("line-gestao-api");
builder.Services.Configure(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
@@ -24,7 +36,7 @@ builder.Services.Configure(o =>
o.MultipartBodyLengthLimit = 50_000_000;
});
-var corsOrigins = builder.Configuration
+var configuredCorsOrigins = builder.Configuration
.GetSection("Cors:AllowedOrigins")
.Get()?
.Where(o => !string.IsNullOrWhiteSpace(o))
@@ -33,18 +45,43 @@ var corsOrigins = builder.Configuration
.ToArray()
?? [];
-if (corsOrigins.Length == 0)
+var allowAnyCorsOrigin = configuredCorsOrigins.Any(o => o == "*");
+var corsOrigins = configuredCorsOrigins
+ .Where(o => o != "*")
+ .ToArray();
+
+var isProduction = builder.Environment.IsProduction();
+if (isProduction && allowAnyCorsOrigin)
{
+ throw new InvalidOperationException("CORS with wildcard '*' is not allowed in production. Configure explicit origins in 'Cors:AllowedOrigins'.");
+}
+
+if (!allowAnyCorsOrigin && corsOrigins.Length == 0)
+{
+ if (isProduction)
+ {
+ throw new InvalidOperationException("No CORS origins configured for production. Set 'Cors:AllowedOrigins' with explicit trusted origins.");
+ }
+
corsOrigins = ["http://localhost:4200"];
}
builder.Services.AddCors(options =>
{
options.AddPolicy("Front", p =>
+ {
+ if (allowAnyCorsOrigin)
+ {
+ p.AllowAnyOrigin()
+ .AllowAnyHeader()
+ .AllowAnyMethod();
+ return;
+ }
+
p.WithOrigins(corsOrigins)
.AllowAnyHeader()
- .AllowAnyMethod()
- );
+ .AllowAnyMethod();
+ });
});
builder.Services.AddDbContext(options =>
@@ -63,6 +100,9 @@ builder.Services.AddIdentityCore(options =>
{
options.Password.RequiredLength = 6;
options.User.RequireUniqueEmail = false;
+ options.Lockout.AllowedForNewUsers = true;
+ options.Lockout.MaxFailedAccessAttempts = 5;
+ options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
})
.AddRoles>()
.AddEntityFrameworkStores()
@@ -77,6 +117,12 @@ if (string.IsNullOrWhiteSpace(jwtKey))
throw new InvalidOperationException("Configuration 'Jwt:Key' is required.");
}
+var jwtKeyBytes = Encoding.UTF8.GetByteCount(jwtKey);
+if (jwtKeyBytes < 16)
+{
+ throw new InvalidOperationException($"Configuration 'Jwt:Key' must be at least 16 bytes (128 bits). Current length: {jwtKeyBytes} bytes.");
+}
+
var issuer = builder.Configuration["Jwt:Issuer"];
var audience = builder.Configuration["Jwt:Audience"];
@@ -97,6 +143,21 @@ builder.Services
});
builder.Services.AddAuthorization();
+builder.Services.AddRateLimiter(options =>
+{
+ options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
+ options.AddPolicy("auth-login", httpContext =>
+ RateLimitPartition.GetFixedWindowLimiter(
+ partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
+ factory: _ => new FixedWindowRateLimiterOptions
+ {
+ PermitLimit = 8,
+ Window = TimeSpan.FromMinutes(1),
+ QueueLimit = 0,
+ QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
+ AutoReplenishment = true
+ }));
+});
builder.Services.Configure(builder.Configuration.GetSection("Notifications"));
builder.Services.AddHostedService();
@@ -119,6 +180,7 @@ if (useHttpsRedirection)
}
app.UseCors("Front");
+app.UseRateLimiter();
app.UseAuthentication();
app.UseMiddleware();
diff --git a/Services/GeralDashboardInsightsService.cs b/Services/GeralDashboardInsightsService.cs
index dc705ed..a696546 100644
--- a/Services/GeralDashboardInsightsService.cs
+++ b/Services/GeralDashboardInsightsService.cs
@@ -35,6 +35,32 @@ namespace line_gestao_api.Services
{
TotalLinhas = g.Count(),
TotalAtivas = g.Count(x => (x.Status ?? "").ToLower().Contains("ativo")),
+ PfAtivas = g.Count(x =>
+ (x.Status ?? "").ToLower().Contains("ativo") &&
+ ((x.Skil ?? "").ToLower().Contains("fís") || (x.Skil ?? "").ToLower().Contains("fis") || (x.Skil ?? "").ToLower().Contains("pf"))),
+ PjAtivas = g.Count(x =>
+ (x.Status ?? "").ToLower().Contains("ativo") &&
+ ((x.Skil ?? "").ToLower().Contains("jur") || (x.Skil ?? "").ToLower().Contains("pj"))),
+ PfValorTotalLineAtivas = g.Sum(x =>
+ (x.Status ?? "").ToLower().Contains("ativo") &&
+ ((x.Skil ?? "").ToLower().Contains("fís") || (x.Skil ?? "").ToLower().Contains("fis") || (x.Skil ?? "").ToLower().Contains("pf"))
+ ? (x.ValorContratoLine ?? 0m)
+ : 0m),
+ PjValorTotalLineAtivas = g.Sum(x =>
+ (x.Status ?? "").ToLower().Contains("ativo") &&
+ ((x.Skil ?? "").ToLower().Contains("jur") || (x.Skil ?? "").ToLower().Contains("pj"))
+ ? (x.ValorContratoLine ?? 0m)
+ : 0m),
+ PfLucroTotalLineAtivas = g.Sum(x =>
+ (x.Status ?? "").ToLower().Contains("ativo") &&
+ ((x.Skil ?? "").ToLower().Contains("fís") || (x.Skil ?? "").ToLower().Contains("fis") || (x.Skil ?? "").ToLower().Contains("pf"))
+ ? (x.Lucro ?? 0m)
+ : 0m),
+ PjLucroTotalLineAtivas = g.Sum(x =>
+ (x.Status ?? "").ToLower().Contains("ativo") &&
+ ((x.Skil ?? "").ToLower().Contains("jur") || (x.Skil ?? "").ToLower().Contains("pj"))
+ ? (x.Lucro ?? 0m)
+ : 0m),
TotalBloqueados = g.Count(x =>
(x.Status ?? "").ToLower().Contains("bloque") ||
(x.Status ?? "").ToLower().Contains("perda") ||
@@ -662,7 +688,8 @@ namespace line_gestao_api.Services
new() { ServiceName = ServiceVivoSync, CountLines = totals.NotPaidSync, TotalValue = 0m },
new() { ServiceName = ServiceVivoGestaoDispositivo, CountLines = totals.NotPaidGestaoDispositivo, TotalValue = 0m }
}
- }
+ },
+ TotaisLine = BuildTotaisLineRows(totals)
};
}
@@ -730,6 +757,43 @@ namespace line_gestao_api.Services
};
}
+ private static List BuildTotaisLineRows(TotalsProjection? totals)
+ {
+ if (totals == null)
+ {
+ return new List();
+ }
+
+ var diffQtd = totals.PjAtivas - totals.PfAtivas;
+ var diffValor = totals.PjValorTotalLineAtivas - totals.PfValorTotalLineAtivas;
+ var diffLucro = totals.PjLucroTotalLineAtivas - totals.PfLucroTotalLineAtivas;
+
+ return new List
+ {
+ new()
+ {
+ Tipo = "PESSOA FISICA",
+ QtdLinhas = totals.PfAtivas,
+ ValorTotalLine = totals.PfValorTotalLineAtivas,
+ LucroTotalLine = totals.PfLucroTotalLineAtivas
+ },
+ new()
+ {
+ Tipo = "PESSOA JURIDICA",
+ QtdLinhas = totals.PjAtivas,
+ ValorTotalLine = totals.PjValorTotalLineAtivas,
+ LucroTotalLine = totals.PjLucroTotalLineAtivas
+ },
+ new()
+ {
+ Tipo = "DIFERENCA PJ X PF",
+ QtdLinhas = diffQtd,
+ ValorTotalLine = diffValor,
+ LucroTotalLine = diffLucro
+ }
+ };
+ }
+
private static List BuildClientGroups(IEnumerable rows)
{
var list = new List();
@@ -1037,6 +1101,12 @@ namespace line_gestao_api.Services
{
public int TotalLinhas { get; set; }
public int TotalAtivas { get; set; }
+ public int PfAtivas { get; set; }
+ public int PjAtivas { get; set; }
+ public decimal PfValorTotalLineAtivas { get; set; }
+ public decimal PjValorTotalLineAtivas { get; set; }
+ public decimal PfLucroTotalLineAtivas { get; set; }
+ public decimal PjLucroTotalLineAtivas { get; set; }
public int TotalBloqueados { get; set; }
public int VivoLinhas { get; set; }
public decimal VivoFranquiaTotalGb { get; set; }
diff --git a/appsettings.Development.json b/appsettings.Development.json
index 2ce8bdf..d3bf1c2 100644
--- a/appsettings.Development.json
+++ b/appsettings.Development.json
@@ -1,9 +1,9 @@
{
"ConnectionStrings": {
- "Default": "Host=localhost;Port=5432;Database=linegestao;Username=linegestao_app;Password=255851Ed@"
+ "Default": "Host=localhost;Port=5432;Database=linegestao;Username=linegestao_app;Password=1231234512Ed@"
},
"Jwt": {
- "Key": "dev-only-please-replace-with-env-variable-in-production",
+ "Key": "linegestao-local-jwt-key-2026-dev-strong-123456789",
"Issuer": "LineGestao",
"Audience": "LineGestao",
"ExpiresMinutes": 360
@@ -28,9 +28,11 @@
"ReminderDays": [30, 15, 7]
},
"Seed": {
+ "Enabled": true,
+ "ReapplyAdminCredentialsOnStartup": true,
"DefaultTenantName": "Default",
"AdminName": "Administrador",
"AdminEmail": "admin@linegestao.local",
- "AdminPassword": "Admin123!"
+ "AdminPassword": "DevAdmin123!"
}
}
diff --git a/appsettings.Local.example.json b/appsettings.Local.example.json
new file mode 100644
index 0000000..b3c813f
--- /dev/null
+++ b/appsettings.Local.example.json
@@ -0,0 +1,19 @@
+{
+ "ConnectionStrings": {
+ "Default": "Host=localhost;Port=5432;Database=linegestao;Username=linegestao_app;Password=1231234512Ed@"
+ },
+ "Jwt": {
+ "Key": "YOUR_LOCAL_STRONG_JWT_KEY_MIN_32_CHARS",
+ "Issuer": "LineGestao",
+ "Audience": "LineGestao",
+ "ExpiresMinutes": 360
+ },
+ "Seed": {
+ "Enabled": true,
+ "ReapplyAdminCredentialsOnStartup": false,
+ "DefaultTenantName": "Default",
+ "AdminName": "Administrador",
+ "AdminEmail": "admin@linegestao.local",
+ "AdminPassword": "YOUR_LOCAL_ADMIN_SEED_PASSWORD"
+ }
+}
diff --git a/appsettings.json b/appsettings.json
index f5bbc10..73f507e 100644
--- a/appsettings.json
+++ b/appsettings.json
@@ -1,28 +1,38 @@
{
"ConnectionStrings": {
- "Default": "Host=localhost;Port=5432;Database=linegestao;Username=linegestao_app;Password=CHANGE_ME"
+ "Default": "Host=localhost;Port=5432;Database=linegestao;Username=linegestao_app;Password=1231234512Ed@"
},
"Jwt": {
- "Key": "",
+ "Key": "linegestao-local-jwt-key-2026-dev-strong-123456789",
"Issuer": "LineGestao",
"Audience": "LineGestao",
"ExpiresMinutes": 360
},
"Cors": {
- "AllowedOrigins": [ "http://localhost:4200" ]
+ "AllowedOrigins": [
+ "http://localhost:4200"
+ ]
},
"App": {
- "UseHttpsRedirection": true
+ "UseHttpsRedirection": false
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
},
"Notifications": {
"CheckIntervalMinutes": 60,
"NotifyAllFutureDates": true,
- "ReminderDays": [30, 15, 7]
+ "ReminderDays": [ 30, 15, 7 ]
},
"Seed": {
+ "Enabled": true,
+ "ReapplyAdminCredentialsOnStartup": true,
"DefaultTenantName": "Default",
"AdminName": "Administrador",
"AdminEmail": "admin@linegestao.local",
- "AdminPassword": "CHANGE_ME"
+ "AdminPassword": "DevAdmin123!"
}
}
diff --git a/line-gestao-api.csproj b/line-gestao-api.csproj
index 9fddb98..179951f 100644
--- a/line-gestao-api.csproj
+++ b/line-gestao-api.csproj
@@ -17,6 +17,13 @@
+
+
+
+
+
+
+