Refatoração dos endpoints PATCH e PUT em /api para melhor separação entre health check e processamento de invoices. Modularização do fluxo de processamento de faturas, melhorias no tratamento de arquivos PDF e leitura de requisições. Ajuste no construtor da classe Fatura para receber JsonElement. Logging e tratamento de erros aprimorados. Alteração do endpoint Kestrel para permitir acesso externo. Remoção de código duplicado e ajustes em PathBase.
513 lines
20 KiB
C#
513 lines
20 KiB
C#
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<WebhookDbContext>(options =>
|
|
options.UseNpgsql(connectionString)
|
|
);
|
|
|
|
builder.Services.AddLogging(loggingBuilder =>
|
|
{
|
|
loggingBuilder.ClearProviders();
|
|
loggingBuilder.AddConsole();
|
|
});
|
|
|
|
builder.Services.AddRazorPages();
|
|
|
|
var app = builder.Build();
|
|
var logger = app.Services.GetRequiredService<ILogger<Program>>();
|
|
|
|
using (var scope = app.Services.CreateScope())
|
|
{
|
|
var dbContext = scope.ServiceProvider.GetRequiredService<WebhookDbContext>();
|
|
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<ILogger<Program>>();
|
|
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<ILogger<Program>>();
|
|
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<string?> 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<string, Task> 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<WebhookDbContext>();
|
|
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<int> 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;
|
|
}
|
|
}
|
|
} |