using System.Data.OleDb; using System.Text; using System.Text.Json; using System.Threading.RateLimiting; using Faturas; using Microsoft.EntityFrameworkCore; namespace Webhook_4docs { public class Program { private static readonly RateLimiter connRateLimiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions { PermitLimit = 1 }); public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); builder.Configuration.AddJsonFile("appsettings.json"); var connectionString = builder.Configuration.GetConnectionString("WebhookDbContext"); var appFolder = builder.Configuration["PathBase"]; builder.Services.AddDbContext(options => options.UseNpgsql(connectionString) ); builder.Services.AddLogging(loggingBuilder => { loggingBuilder.ClearProviders(); loggingBuilder.AddConsole(); }); builder.Services.AddRazorPages(); var app = builder.Build(); var logger = app.Services.GetRequiredService>(); using (var scope = app.Services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService(); dbContext.Database.Migrate(); } if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); app.UseHsts(); } if (!string.IsNullOrWhiteSpace(appFolder)) { app.UsePathBase(appFolder); } app.UseRouting(); app.UseStaticFiles(); app.UseAuthorization(); app.MapRazorPages(); // PATCH /api -> health ou invoice (json) app.MapPatch("/api", async context => { var logger = context.RequestServices.GetRequiredService>(); var bodyText = await SafeReadBodyAsync(context.Request, logger); if (bodyText is null) { context.Response.StatusCode = StatusCodes.Status400BadRequest; await context.Response.WriteAsync("Invalid JSON"); return; } if (!TryParseJson(bodyText, out var root, logger)) { context.Response.StatusCode = StatusCodes.Status400BadRequest; await context.Response.WriteAsync("Invalid JSON"); return; } // Health: healthy + blame + locationID bool isHealth = root.TryGetProperty("healthy", out _) && root.TryGetProperty("blame", out _) && root.TryGetProperty("locationID", out _); if (isHealth) { await ProcessHealthAsync(root, GetCaminhoDb(), logger); context.Response.StatusCode = StatusCodes.Status200OK; return; } // Invoice: requestID/requestId + json bool hasRequestId = root.TryGetProperty("requestID", out var reqIdEl); bool hasJson = root.TryGetProperty("json", out var dataEl); if (hasRequestId && hasJson) { await ProcessInvoiceAsync(root, dataEl, GetCaminhoDb(), logger, app, reqIdEl.ToString()); context.Response.StatusCode = StatusCodes.Status200OK; return; } logger.LogDebug("PATCH /api payload não reconhecido."); context.Response.StatusCode = StatusCodes.Status202Accepted; }); // PUT /api -> somente invoice (result) app.MapPut("/api", async context => { var logger = context.RequestServices.GetRequiredService>(); var bodyText = await SafeReadBodyAsync(context.Request, logger); if (bodyText is null) { context.Response.StatusCode = StatusCodes.Status400BadRequest; await context.Response.WriteAsync("Invalid JSON"); return; } if (!TryParseJson(bodyText, out var root, logger)) { context.Response.StatusCode = StatusCodes.Status400BadRequest; await context.Response.WriteAsync("Invalid JSON"); return; } // Invoice: requestID/requestId + result bool hasRequestId = root.TryGetProperty("requestId", out var reqIdEl); bool hasResult = root.TryGetProperty("result", out var dataEl); if (!(hasRequestId && hasResult)) { logger.LogDebug("PUT /api payload inválido para este endpoint."); context.Response.StatusCode = StatusCodes.Status400BadRequest; return; } await ProcessInvoiceAsync(root, dataEl, GetCaminhoDb(), logger, app, reqIdEl.ToString()); context.Response.StatusCode = StatusCodes.Status200OK; }); app.Run(); string GetCaminhoDb() => @"X:/Middle/Informativo Setorial/Modelo Word/BD1_dados cadastrais e faturas.accdb"; } // ===== Utilitários de request/JSON ===== static async Task SafeReadBodyAsync(HttpRequest request, ILogger logger, int maxBytes = 10 * 1024 * 1024) { request.EnableBuffering(); using var ms = new MemoryStream(capacity: Math.Min(maxBytes, 1024 * 64)); await request.Body.CopyToAsync(ms); if (ms.Length > maxBytes) { logger.LogWarning("Payload excede o limite de {MaxBytes} bytes.", maxBytes); return null; } ms.Position = 0; using var reader = new StreamReader(ms, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true); return await reader.ReadToEndAsync(); } static bool TryParseJson(string text, out JsonElement root, ILogger logger) { try { root = JsonDocument.Parse(text).RootElement.Clone(); return true; } catch (JsonException ex) { logger.LogWarning(ex, "Falha ao parsear JSON."); root = default; return false; } } // ===== Processamento de Invoice ===== static async Task ProcessInvoiceAsync(JsonElement root, JsonElement dataEl, string caminhoDB, ILogger logger, WebApplication app, string fatura_ID) { var tipoDocumento = dataEl.Get("documentType")?.GetString()?.ToLowerInvariant() ?? string.Empty; var deveProcessar = string.IsNullOrEmpty(tipoDocumento) || tipoDocumento == "nota_fiscal"; if (!deveProcessar) { logger.LogInformation("Invoice ignorada por tipoDocumento: {TipoDocumento}", tipoDocumento); return; } var hasPath = !string.IsNullOrEmpty(root.GetProperty("clientData").ToString()); if (hasPath) { root.GetProperty("clientData").TryGetProperty("fatura_PATH", out var pathEl); await ProcessInvoiceFromPath(root, fatura_ID, pathEl!.GetString()!, caminhoDB, logger, app); } else { await ProcessInvoiceFromJson(root, fatura_ID, caminhoDB, logger, app); } } static async Task ProcessInvoiceFromPath(JsonElement root, string fatura_ID, string pdfPath, string caminhoDB, ILogger logger, WebApplication app) { var fatura = new Fatura(fatura_ID, root, pdfPath); var completed = false; while (!completed) { var connLease = await connRateLimiter.AcquireAsync(); if (!connLease.IsAcquired) continue; try { using var conn = new OleDbConnection(@"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + caminhoDB + ";Jet OLEDB:Database Password=gds21"); if (conn.State == System.Data.ConnectionState.Closed) { await conn.OpenAsync(); } fatura.Processar(conn); completed = true; } catch (Exception ex) { completed = true; throw new Exception(ex.Message); } finally { connLease.Dispose(); } } await FinalizarPersistenciaAsync(fatura, fatura_ID, async targetPath => { try { if (!File.Exists(targetPath)) { fatura.Mover(false); } } catch (IOException ioEx) { logger.LogWarning(ioEx, "Falha ao copiar arquivo para {Target}", targetPath); } await Task.CompletedTask; }, logger, app); } static async Task ProcessInvoiceFromJson(JsonElement root, string fatura_ID, string caminhoDB, ILogger logger, WebApplication app) { var fatura = new Fatura(fatura_ID, root); var completed = false; while (!completed) { var connLease = await connRateLimiter.AcquireAsync(); if (!connLease.IsAcquired) continue; try { using var conn = new OleDbConnection(@"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + caminhoDB + ";Jet OLEDB:Database Password=gds21"); if (conn.State == System.Data.ConnectionState.Closed) { await conn.OpenAsync(); } fatura.Processar(conn); completed = true; } catch (Exception ex) { completed = true; throw new Exception(ex.Message); } finally { connLease.Dispose(); } } await FinalizarPersistenciaAsync(fatura, fatura_ID, async targetPath => { var base64 = root.Get("pdfFile")?.GetString(); if (string.IsNullOrWhiteSpace(base64)) { logger.LogWarning("Campo pdfFile/pdfFileBase64 ausente no payload."); return; } CriarArquivo(targetPath, base64!); await Task.CompletedTask; }, logger, app); } static async Task FinalizarPersistenciaAsync(Fatura fatura, string fatura_ID, Func persistFile, ILogger logger, WebApplication app) { var status = fatura.Status; var status_id = status switch { "FATURA DUPLICADA NO BD" => 4, "UNIDADE CONSUMIDORA NÃO LOCALIZADA NO BD" => 5, "FATURA INCLUIDA NO BD" => 6, _ => 7 }; status = status_id == 7 ? "ERRO" : status; try { var pathBase = $@"X:\Middle\Carteira {fatura.Gestao![0]}\Carteira {fatura.Gestao}\Faturas fourdocs\{status_id} - {status}"; if (status_id == 6 && fatura.PastaTUSD!.Exists) { pathBase = fatura.PastaTUSD!.FullName; } if (status_id != 4) { Directory.CreateDirectory(pathBase); var targetFile = IndexedFilename($@"{pathBase}\ID {fatura_ID!} - Mês {fatura.Mes} - Empresa {fatura.Empresa} - Unidade {fatura.Unidade}", "pdf"); await persistFile(targetFile); var dbModel = new ProcessedInvoices { InvoiceId = int.Parse(fatura_ID), DateTimeProcessed = DateTime.UtcNow, Status = status, InvoicePath = targetFile }; logger.LogInformation("Fatura incluída no BD"); logger.LogInformation("Fatura salva na pasta {Arquivo}", targetFile); using var scope = app.Services.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); dbContext.ProcessedInvoices.Add(dbModel); await dbContext.SaveChangesAsync(); } else { logger.LogInformation("Fatura duplicada no BD"); logger.LogInformation("Fatura descartada"); } } catch (Exception ex) { logger.LogError(ex, "Erro no processamento da fatura"); } } public static void CriarArquivo(string fatura_arquivo, string pdfFile64) { if (!File.Exists(fatura_arquivo)) { byte[] bytes = Convert.FromBase64String(pdfFile64); using var stream = new FileStream(fatura_arquivo, FileMode.CreateNew, FileAccess.Write, FileShare.None); using var writer = new BinaryWriter(stream); writer.Write(bytes, 0, bytes.Length); } } static string IndexedFilename(string stub, string extension) { int ix = 0; string? filename; if (File.Exists($"{stub}.{extension}")) { do { ix++; filename = $"{stub} ({ix}).{extension}"; } while (File.Exists(filename)); } else { filename = $"{stub}.{extension}"; } return filename; } // ===== Processamento de Health ===== static async Task ProcessHealthAsync(JsonElement jsonBody, string caminhoDB, ILogger logger) { var healthy = jsonBody.GetProperty("healthy").GetBoolean(); var blame = jsonBody.GetProperty("blame").GetString()?.ToLowerInvariant() ?? string.Empty; var locationId = jsonBody.GetProperty("locationID").ToString(); var (errorID, logAction) = (healthy, blame) switch { (false, "user") => (1d, (Action)(() => logger.LogError("Loc ID: {LocationId} Sem acesso", locationId))), (false, "system") => (2d, (Action)(() => logger.LogError("Loc ID: {LocationId} Com erro de sistema", locationId))), (false, var other) => (3d, (Action)(() => logger.LogError("Loc ID: {LocationId} Com erro {Blame}", locationId, other))), (true, _) => (0d, (Action)(() => { })) }; logAction(); var errorText = GetErrorText(jsonBody); await UpdateErrorIdStatusAsync(caminhoDB, jsonBody.GetProperty("locationID").GetInt64(), errorID, errorText, logger); } //tenta pegar o deactivationReport, se não conseguir ou for null, tenta pegar o websiteText, se não conseguir ou for null, tenta pegar o systemText. As propriedades podem não exisir public static string GetErrorText(JsonElement root) { string errorText; if (root.TryGetProperty("deactivationReport", out JsonElement deactivationReportElement) && deactivationReportElement.ValueKind != JsonValueKind.Null) { errorText = deactivationReportElement.ToString(); } else if (root.TryGetProperty("websiteText", out JsonElement websiteTextElement) && websiteTextElement.ValueKind != JsonValueKind.Null) { errorText = websiteTextElement.ToString(); } else if (root.TryGetProperty("systemText", out JsonElement systemTextElement) && systemTextElement.ValueKind != JsonValueKind.Null) { errorText = systemTextElement.ToString(); } else { errorText = "Erro desconhecido"; } return errorText; } public static async Task UpdateErrorIdStatusAsync(string CaminhoDB, double location_id, double errorID, string systemText, ILogger logger) { int test = 0; int maxRetries = 3; int attempt = 0; while (attempt < maxRetries) { var connLease = await connRateLimiter.AcquireAsync(); if (connLease.IsAcquired) { try { using (OleDbConnection conn = new(@"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + CaminhoDB + ";Jet OLEDB:Database Password=gds21")) { await conn.OpenAsync(); using (OleDbCommand cmd = new( @"UPDATE AgVirtual4Docs SET errorID = @errorID, status = @status WHERE location_id = @location_id", conn)) { cmd.Parameters.AddWithValue("@errorID", errorID); cmd.Parameters.AddWithValue("@status", systemText); cmd.Parameters.AddWithValue("@location_id", location_id); test = await cmd.ExecuteNonQueryAsync(); } } break; } catch (OleDbException ex) { logger.LogInformation($"Erro no OleDb update: {ex.Message} (Tentativa {attempt + 1} de {maxRetries})"); if (attempt < maxRetries - 1) { await Task.Delay(1000 * (int)Math.Pow(2, attempt)); } else { throw; } } finally { connLease.Dispose(); } attempt++; } else { await Task.Delay(200); } } return test; } } public static partial class JsonExtensions { public static JsonElement? Get(this JsonElement element, string name) => element.ValueKind != JsonValueKind.Null && element.ValueKind != JsonValueKind.Undefined && element.TryGetProperty(name, out var value) ? value : (JsonElement?)null; public static JsonElement? Get(this JsonElement element, int index) { if (element.ValueKind == JsonValueKind.Null || element.ValueKind == JsonValueKind.Undefined) return null; return index < element.GetArrayLength() ? element[index] : null; } } }