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 @@ + + + + + + +