diff --git a/Controllers/LinesController.cs b/Controllers/LinesController.cs index 0a0cd0a..fcfb02f 100644 --- a/Controllers/LinesController.cs +++ b/Controllers/LinesController.cs @@ -1339,22 +1339,31 @@ namespace line_gestao_api.Controllers var canManageFullLine = User.IsInRole(AppRoles.SysAdmin) || User.IsInRole(AppRoles.Gestor); if (!canManageFullLine) { + var tenantId = x.TenantId != Guid.Empty + ? x.TenantId + : (_tenantProvider.ActorTenantId ?? Guid.Empty); + if (tenantId == Guid.Empty) + { + return BadRequest(new { message = "Tenant inválido para atualizar linha." }); + } + + if (x.TenantId == Guid.Empty) + { + x.TenantId = tenantId; + } + x.Usuario = NormalizeOptionalText(req.Usuario); x.CentroDeCustos = NormalizeOptionalText(req.CentroDeCustos); + + await ApplySetorToLineAsync(x, tenantId, req.SetorId, req.SetorNome); await ApplyAparelhoToLineAsync( x, - x.TenantId, + tenantId, req.AparelhoId, req.AparelhoNome, req.AparelhoCor, req.AparelhoImei); - await UpsertVigenciaFromMobileLineAsync( - x, - dtEfetivacaoServico: null, - dtTerminoFidelizacao: null, - overrideDates: false, - previousLinha: null); x.UpdatedAt = DateTime.UtcNow; try diff --git a/Controllers/SolicitacoesLinhasController.cs b/Controllers/SolicitacoesLinhasController.cs new file mode 100644 index 0000000..32159df --- /dev/null +++ b/Controllers/SolicitacoesLinhasController.cs @@ -0,0 +1,240 @@ +using System.Globalization; +using System.Security.Claims; +using line_gestao_api.Data; +using line_gestao_api.Dtos; +using line_gestao_api.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace line_gestao_api.Controllers; + +[ApiController] +[Route("api/solicitacoes-linhas")] +[Authorize] +public class SolicitacoesLinhasController : ControllerBase +{ + private const string TipoAlteracaoFranquia = "ALTERACAO_FRANQUIA"; + private const string TipoBloqueio = "BLOQUEIO"; + private readonly AppDbContext _db; + + public SolicitacoesLinhasController(AppDbContext db) + { + _db = db; + } + + [HttpPost] + [Authorize(Roles = "sysadmin,gestor,cliente")] + public async Task> Create([FromBody] CreateSolicitacaoLinhaRequestDto req) + { + if (req.LineId == Guid.Empty) + { + return BadRequest(new { message = "Linha inválida para solicitação." }); + } + + var line = await _db.MobileLines + .AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == req.LineId); + if (line == null) + { + return NotFound(new { message = "Linha não encontrada." }); + } + + var tipoSolicitacao = NormalizeTipoSolicitacao(req.TipoSolicitacao); + if (tipoSolicitacao == null) + { + return BadRequest(new { message = "Tipo de solicitação inválido. Use 'alteracao-franquia' ou 'bloqueio'." }); + } + + decimal? franquiaLineNova = null; + if (tipoSolicitacao == TipoAlteracaoFranquia) + { + if (!req.FranquiaLineNova.HasValue) + { + return BadRequest(new { message = "Informe a nova franquia para solicitar alteração." }); + } + + franquiaLineNova = decimal.Round(req.FranquiaLineNova.Value, 2, MidpointRounding.AwayFromZero); + if (franquiaLineNova < 0) + { + return BadRequest(new { message = "A nova franquia não pode ser negativa." }); + } + } + + var solicitanteNome = ResolveSolicitanteNome(); + var usuarioLinha = NormalizeOptionalText(line.Usuario) ?? solicitanteNome; + var linha = NormalizeOptionalText(line.Linha) ?? "-"; + var mensagem = tipoSolicitacao == TipoAlteracaoFranquia + ? $"O Usuário \"{usuarioLinha}\" solicitou alteração da linha \"{linha}\" \"{FormatFranquia(line.FranquiaLine)}\" -> \"{FormatFranquia(franquiaLineNova)}\"" + : $"O Usuário \"{usuarioLinha}\" solicitou bloqueio da linha \"{linha}\""; + + var solicitacao = new SolicitacaoLinha + { + TenantId = line.TenantId, + MobileLineId = line.Id, + Linha = NormalizeOptionalText(line.Linha), + UsuarioLinha = NormalizeOptionalText(line.Usuario), + TipoSolicitacao = tipoSolicitacao, + FranquiaLineAtual = line.FranquiaLine, + FranquiaLineNova = franquiaLineNova, + SolicitanteUserId = ResolveSolicitanteUserId(), + SolicitanteNome = solicitanteNome, + Mensagem = mensagem, + Status = "PENDENTE", + CreatedAt = DateTime.UtcNow + }; + + _db.SolicitacaoLinhas.Add(solicitacao); + await _db.SaveChangesAsync(); + + var tenantNome = await _db.Tenants + .AsNoTracking() + .Where(t => t.Id == solicitacao.TenantId) + .Select(t => t.NomeOficial) + .FirstOrDefaultAsync(); + + return Ok(ToDto(solicitacao, tenantNome)); + } + + [HttpGet] + [Authorize(Roles = "sysadmin,gestor")] + public async Task>> List( + [FromQuery] string? search, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20) + { + page = page < 1 ? 1 : page; + pageSize = pageSize < 1 ? 20 : Math.Min(pageSize, 200); + + var query = + from solicitacao in _db.SolicitacaoLinhas.AsNoTracking() + join tenant in _db.Tenants.AsNoTracking() + on solicitacao.TenantId equals tenant.Id into tenantJoin + from tenant in tenantJoin.DefaultIfEmpty() + select new + { + Solicitacao = solicitacao, + TenantNome = tenant != null ? tenant.NomeOficial : null + }; + + if (!string.IsNullOrWhiteSpace(search)) + { + var term = search.Trim(); + query = query.Where(x => + EF.Functions.ILike(x.Solicitacao.Linha ?? "", $"%{term}%") || + EF.Functions.ILike(x.Solicitacao.UsuarioLinha ?? "", $"%{term}%") || + EF.Functions.ILike(x.Solicitacao.SolicitanteNome ?? "", $"%{term}%") || + EF.Functions.ILike(x.Solicitacao.Mensagem ?? "", $"%{term}%")); + } + + var total = await query.CountAsync(); + var items = await query + .OrderByDescending(x => x.Solicitacao.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(x => new SolicitacaoLinhaListDto + { + Id = x.Solicitacao.Id, + TenantId = x.Solicitacao.TenantId, + TenantNome = x.TenantNome, + MobileLineId = x.Solicitacao.MobileLineId, + Linha = x.Solicitacao.Linha, + UsuarioLinha = x.Solicitacao.UsuarioLinha, + TipoSolicitacao = x.Solicitacao.TipoSolicitacao, + FranquiaLineAtual = x.Solicitacao.FranquiaLineAtual, + FranquiaLineNova = x.Solicitacao.FranquiaLineNova, + SolicitanteNome = x.Solicitacao.SolicitanteNome, + Mensagem = x.Solicitacao.Mensagem, + Status = x.Solicitacao.Status, + CreatedAt = x.Solicitacao.CreatedAt + }) + .ToListAsync(); + + return Ok(new PagedResult + { + Page = page, + PageSize = pageSize, + Total = total, + Items = items + }); + } + + private static string? NormalizeTipoSolicitacao(string? tipoSolicitacao) + { + var value = (tipoSolicitacao ?? string.Empty).Trim().ToLowerInvariant(); + return value switch + { + "alteracao-franquia" => TipoAlteracaoFranquia, + "alteracao_franquia" => TipoAlteracaoFranquia, + "alteracaofranquia" => TipoAlteracaoFranquia, + "franquia" => TipoAlteracaoFranquia, + "bloqueio" => TipoBloqueio, + "solicitar-bloqueio" => TipoBloqueio, + _ => null + }; + } + + private string ResolveSolicitanteNome() + { + var fromClaim = User.FindFirstValue("name"); + if (!string.IsNullOrWhiteSpace(fromClaim)) + { + return fromClaim.Trim(); + } + + var fromIdentity = User.Identity?.Name; + if (!string.IsNullOrWhiteSpace(fromIdentity)) + { + return fromIdentity.Trim(); + } + + var fromEmail = User.FindFirstValue(ClaimTypes.Email) ?? User.FindFirstValue("email"); + if (!string.IsNullOrWhiteSpace(fromEmail)) + { + return fromEmail.Trim(); + } + + return "Usuário"; + } + + private Guid? ResolveSolicitanteUserId() + { + var raw = + User.FindFirstValue(ClaimTypes.NameIdentifier) ?? + User.FindFirstValue("sub"); + + return Guid.TryParse(raw, out var parsed) ? parsed : null; + } + + private static string? NormalizeOptionalText(string? value) + { + return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + } + + private static string FormatFranquia(decimal? value) + { + return value.HasValue + ? value.Value.ToString("0.##", CultureInfo.GetCultureInfo("pt-BR")) + : "-"; + } + + private static SolicitacaoLinhaListDto ToDto(SolicitacaoLinha solicitacao, string? tenantNome) + { + return new SolicitacaoLinhaListDto + { + Id = solicitacao.Id, + TenantId = solicitacao.TenantId, + TenantNome = tenantNome, + MobileLineId = solicitacao.MobileLineId, + Linha = solicitacao.Linha, + UsuarioLinha = solicitacao.UsuarioLinha, + TipoSolicitacao = solicitacao.TipoSolicitacao, + FranquiaLineAtual = solicitacao.FranquiaLineAtual, + FranquiaLineNova = solicitacao.FranquiaLineNova, + SolicitanteNome = solicitacao.SolicitanteNome, + Mensagem = solicitacao.Mensagem, + Status = solicitacao.Status, + CreatedAt = solicitacao.CreatedAt + }; + } +} diff --git a/Data/AppDbContext.cs b/Data/AppDbContext.cs index a20efd2..876adf8 100644 --- a/Data/AppDbContext.cs +++ b/Data/AppDbContext.cs @@ -50,6 +50,7 @@ public class AppDbContext : IdentityDbContext Notifications => Set(); + public DbSet SolicitacaoLinhas => Set(); // ✅ tabela RESUMO public DbSet ResumoMacrophonyPlans => Set(); @@ -281,6 +282,26 @@ public class AppDbContext : IdentityDbContext(e => + { + e.HasIndex(x => x.TenantId); + e.HasIndex(x => x.CreatedAt); + e.HasIndex(x => x.TipoSolicitacao); + e.HasIndex(x => x.Status); + e.HasIndex(x => x.MobileLineId); + e.HasIndex(x => x.SolicitanteUserId); + + e.HasOne(x => x.MobileLine) + .WithMany() + .HasForeignKey(x => x.MobileLineId) + .OnDelete(DeleteBehavior.SetNull); + + e.HasOne(x => x.SolicitanteUser) + .WithMany() + .HasForeignKey(x => x.SolicitanteUserId) + .OnDelete(DeleteBehavior.SetNull); + }); + // ========================= // ✅ PARCELAMENTOS // ========================= @@ -380,6 +401,7 @@ public class AppDbContext : IdentityDbContext().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); + modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId)); diff --git a/Dtos/SolicitacaoLinhaDtos.cs b/Dtos/SolicitacaoLinhaDtos.cs new file mode 100644 index 0000000..c4ab40c --- /dev/null +++ b/Dtos/SolicitacaoLinhaDtos.cs @@ -0,0 +1,25 @@ +namespace line_gestao_api.Dtos; + +public class CreateSolicitacaoLinhaRequestDto +{ + public Guid LineId { get; set; } + public string TipoSolicitacao { get; set; } = string.Empty; + public decimal? FranquiaLineNova { get; set; } +} + +public class SolicitacaoLinhaListDto +{ + public Guid Id { get; set; } + public Guid TenantId { get; set; } + public string? TenantNome { get; set; } + public Guid? MobileLineId { get; set; } + public string? Linha { get; set; } + public string? UsuarioLinha { get; set; } + public string TipoSolicitacao { get; set; } = string.Empty; + public decimal? FranquiaLineAtual { get; set; } + public decimal? FranquiaLineNova { get; set; } + public string? SolicitanteNome { get; set; } + public string Mensagem { get; set; } = string.Empty; + public string Status { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } +} diff --git a/Migrations/20260305193000_AddSolicitacaoLinhas.cs b/Migrations/20260305193000_AddSolicitacaoLinhas.cs new file mode 100644 index 0000000..19e5037 --- /dev/null +++ b/Migrations/20260305193000_AddSolicitacaoLinhas.cs @@ -0,0 +1,59 @@ +using line_gestao_api.Data; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace line_gestao_api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260305193000_AddSolicitacaoLinhas")] + public class AddSolicitacaoLinhas : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + CREATE TABLE IF NOT EXISTS "SolicitacaoLinhas" ( + "Id" uuid NOT NULL, + "TenantId" uuid NOT NULL, + "MobileLineId" uuid NULL, + "Linha" character varying(30) NULL, + "UsuarioLinha" character varying(200) NULL, + "TipoSolicitacao" character varying(60) NOT NULL, + "FranquiaLineAtual" numeric NULL, + "FranquiaLineNova" numeric NULL, + "SolicitanteUserId" uuid NULL, + "SolicitanteNome" character varying(200) NULL, + "Mensagem" character varying(1000) NOT NULL, + "Status" character varying(30) NOT NULL, + "CreatedAt" timestamp with time zone NOT NULL, + CONSTRAINT "PK_SolicitacaoLinhas" PRIMARY KEY ("Id"), + CONSTRAINT "FK_SolicitacaoLinhas_AspNetUsers_SolicitanteUserId" + FOREIGN KEY ("SolicitanteUserId") REFERENCES "AspNetUsers" ("Id") + ON DELETE SET NULL, + CONSTRAINT "FK_SolicitacaoLinhas_MobileLines_MobileLineId" + FOREIGN KEY ("MobileLineId") REFERENCES "MobileLines" ("Id") + ON DELETE SET NULL + ); + """); + + migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_SolicitacaoLinhas_TenantId" ON "SolicitacaoLinhas" ("TenantId");"""); + migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_SolicitacaoLinhas_CreatedAt" ON "SolicitacaoLinhas" ("CreatedAt");"""); + migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_SolicitacaoLinhas_TipoSolicitacao" ON "SolicitacaoLinhas" ("TipoSolicitacao");"""); + migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_SolicitacaoLinhas_Status" ON "SolicitacaoLinhas" ("Status");"""); + migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_SolicitacaoLinhas_MobileLineId" ON "SolicitacaoLinhas" ("MobileLineId");"""); + migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_SolicitacaoLinhas_SolicitanteUserId" ON "SolicitacaoLinhas" ("SolicitanteUserId");"""); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_SolicitacaoLinhas_SolicitanteUserId";"""); + migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_SolicitacaoLinhas_MobileLineId";"""); + migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_SolicitacaoLinhas_Status";"""); + migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_SolicitacaoLinhas_TipoSolicitacao";"""); + migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_SolicitacaoLinhas_CreatedAt";"""); + migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_SolicitacaoLinhas_TenantId";"""); + migrationBuilder.Sql("""DROP TABLE IF EXISTS "SolicitacaoLinhas";"""); + } + } +} diff --git a/Migrations/AppDbContextModelSnapshot.cs b/Migrations/AppDbContextModelSnapshot.cs index 2a0857a..a48fcf6 100644 --- a/Migrations/AppDbContextModelSnapshot.cs +++ b/Migrations/AppDbContextModelSnapshot.cs @@ -950,6 +950,74 @@ namespace line_gestao_api.Migrations b.ToTable("Notifications"); }); + modelBuilder.Entity("line_gestao_api.Models.SolicitacaoLinha", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FranquiaLineAtual") + .HasColumnType("numeric"); + + b.Property("FranquiaLineNova") + .HasColumnType("numeric"); + + b.Property("Linha") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("Mensagem") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("MobileLineId") + .HasColumnType("uuid"); + + b.Property("SolicitanteNome") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SolicitanteUserId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TipoSolicitacao") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("UsuarioLinha") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("MobileLineId"); + + b.HasIndex("SolicitanteUserId"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.HasIndex("TipoSolicitacao"); + + b.ToTable("SolicitacaoLinhas"); + }); + modelBuilder.Entity("line_gestao_api.Models.ParcelamentoLine", b => { b.Property("Id") @@ -1813,6 +1881,23 @@ namespace line_gestao_api.Migrations b.Navigation("VigenciaLine"); }); + modelBuilder.Entity("line_gestao_api.Models.SolicitacaoLinha", b => + { + b.HasOne("line_gestao_api.Models.MobileLine", "MobileLine") + .WithMany() + .HasForeignKey("MobileLineId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("line_gestao_api.Models.ApplicationUser", "SolicitanteUser") + .WithMany() + .HasForeignKey("SolicitanteUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("MobileLine"); + + b.Navigation("SolicitanteUser"); + }); + modelBuilder.Entity("line_gestao_api.Models.ParcelamentoMonthValue", b => { b.HasOne("line_gestao_api.Models.ParcelamentoLine", "ParcelamentoLine") diff --git a/Models/SolicitacaoLinha.cs b/Models/SolicitacaoLinha.cs new file mode 100644 index 0000000..a91391d --- /dev/null +++ b/Models/SolicitacaoLinha.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.DataAnnotations; + +namespace line_gestao_api.Models; + +public class SolicitacaoLinha : ITenantEntity +{ + public Guid Id { get; set; } = Guid.NewGuid(); + + public Guid TenantId { get; set; } + + public Guid? MobileLineId { get; set; } + public MobileLine? MobileLine { get; set; } + + [MaxLength(30)] + public string? Linha { get; set; } + + [MaxLength(200)] + public string? UsuarioLinha { get; set; } + + [Required] + [MaxLength(60)] + public string TipoSolicitacao { get; set; } = string.Empty; + + public decimal? FranquiaLineAtual { get; set; } + public decimal? FranquiaLineNova { get; set; } + + public Guid? SolicitanteUserId { get; set; } + public ApplicationUser? SolicitanteUser { get; set; } + + [MaxLength(200)] + public string? SolicitanteNome { get; set; } + + [Required] + [MaxLength(1000)] + public string Mensagem { get; set; } = string.Empty; + + [Required] + [MaxLength(30)] + public string Status { get; set; } = "PENDENTE"; + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/uploads/aparelhos/096f0a5384c44051bbbe6d5101de6583/33e879fab0544c3d8c44725286d7652a/nota-fiscal/20260305205540415_4a39cd61c19c482b9e702b0cf115d855.pdf b/uploads/aparelhos/096f0a5384c44051bbbe6d5101de6583/33e879fab0544c3d8c44725286d7652a/nota-fiscal/20260305205540415_4a39cd61c19c482b9e702b0cf115d855.pdf new file mode 100644 index 0000000..b44ab61 Binary files /dev/null and b/uploads/aparelhos/096f0a5384c44051bbbe6d5101de6583/33e879fab0544c3d8c44725286d7652a/nota-fiscal/20260305205540415_4a39cd61c19c482b9e702b0cf115d855.pdf differ diff --git a/uploads/aparelhos/096f0a5384c44051bbbe6d5101de6583/33e879fab0544c3d8c44725286d7652a/recibo/20260305205540426_c39b3b8f12264abebb9f5ff316f5eede.pdf b/uploads/aparelhos/096f0a5384c44051bbbe6d5101de6583/33e879fab0544c3d8c44725286d7652a/recibo/20260305205540426_c39b3b8f12264abebb9f5ff316f5eede.pdf new file mode 100644 index 0000000..c507e9e Binary files /dev/null and b/uploads/aparelhos/096f0a5384c44051bbbe6d5101de6583/33e879fab0544c3d8c44725286d7652a/recibo/20260305205540426_c39b3b8f12264abebb9f5ff316f5eede.pdf differ diff --git a/uploads/aparelhos/096f0a5384c44051bbbe6d5101de6583/75a8d34aa4114d7e9282e84951d1ebd4/nota-fiscal/20260305185744573_425e70d949454ee9a59c9122626a0404.webp b/uploads/aparelhos/096f0a5384c44051bbbe6d5101de6583/75a8d34aa4114d7e9282e84951d1ebd4/nota-fiscal/20260305185744573_425e70d949454ee9a59c9122626a0404.webp new file mode 100644 index 0000000..bc48652 Binary files /dev/null and b/uploads/aparelhos/096f0a5384c44051bbbe6d5101de6583/75a8d34aa4114d7e9282e84951d1ebd4/nota-fiscal/20260305185744573_425e70d949454ee9a59c9122626a0404.webp differ