// Exemplo de endereço de entrega do Softeum Pedidos, em C# (.NET 8, ASP.NET Core minimal API). // // Criar: dotnet new web -n ReceptorSofteum e troque o Program.cs gerado por este arquivo. // Rodar: SOFTEUM_SEGREDO=whsec_... dotnet run (no Windows: set SOFTEUM_SEGREDO=whsec_...) // Testar: node simular-aviso.mjs http://localhost:8080/softeum/pedidos whsec_... --pedido // // Em produção: atrás do IIS/Kestrel com HTTPS público. Este exemplo não foi executado no ambiente da Softeum // (os de Node, Python e PHP foram); a conferência da assinatura é a mesma dos outros três. using System.Collections.Concurrent; using System.Security.Cryptography; using System.Text; using System.Text.Json; var segredo = Environment.GetEnvironmentVariable("SOFTEUM_SEGREDO") ?? throw new InvalidOperationException("Defina SOFTEUM_SEGREDO com o segredo do painel (Integração → Como o ERP recebe → Aviso no endereço do ERP)."); var porta = Environment.GetEnvironmentVariable("PORTA") ?? "8080"; // pedido.id → número do pedido no ERP. No ERP de verdade: coluna do pedido de venda com UNIQUE. var lancados = new ConcurrentDictionary(); var app = WebApplication.Create(args); app.MapPost("/softeum/pedidos", async (HttpRequest req) => { // 1. O corpo BRUTO: a assinatura é calculada sobre os bytes que chegaram, antes de desserializar. using var leitor = new StreamReader(req.Body, Encoding.UTF8); var corpo = await leitor.ReadToEndAsync(); if (!AssinaturaValida(req.Headers["Softeum-Assinatura"].ToString(), corpo, segredo)) return Results.Json(new { erro = "Assinatura inválida." }, statusCode: 401); JsonElement aviso; try { aviso = JsonDocument.Parse(corpo).RootElement; } catch (JsonException) { return Results.Json(new { erro = "JSON inválido." }, statusCode: 400); } // 2. Aviso de teste e tipos que o ERP ainda não conhece: só 2xx, nada lançado. if (!aviso.TryGetProperty("tipo", out var tipo) || tipo.GetString() != "pedido.aprovado") return Results.Json(new { }); // 3. Pedido repetido: não lança de novo, devolve o mesmo número. var pedido = aviso.GetProperty("pedido"); var id = pedido.GetProperty("id").GetString()!; if (lancados.TryGetValue(id, out var existente)) return Results.Json(new { numero_erp = existente }); // 4. Lança e devolve o número: o pedido fica "confirmado" no Softeum na hora. try { var numero = LancarNoErp(pedido, lancados.Count); lancados[id] = numero; return Results.Json(new { numero_erp = numero }); } catch (Exception erro) { Console.Error.WriteLine($"Não deu para lançar: {erro.Message}"); return Results.Json(new { erro = "Não deu para lançar agora." }, statusCode: 503); // o Softeum tenta de novo depois } }); Console.WriteLine($"Esperando avisos do Softeum em http://localhost:{porta}"); app.Run($"http://0.0.0.0:{porta}"); // Aqui entra o lançamento de verdade no ERP. Devolve o número do pedido de venda criado. static string LancarNoErp(JsonElement pedido, int jaLancados) { Console.WriteLine($"Lançando o pedido {pedido.GetProperty("numero_comprador").GetString()} " + $"de {pedido.GetProperty("comprador").GetProperty("razao_social").GetString()}: " + $"{pedido.GetProperty("itens").GetArrayLength()} item(ns), R$ {pedido.GetProperty("valor_total").GetDecimal()}"); return (500000 + jaLancados + 1).ToString(); } // Confere o cabeçalho Softeum-Assinatura contra o corpo exatamente como chegou. static bool AssinaturaValida(string? cabecalho, string corpoBruto, string segredo, long? agora = null) { if (string.IsNullOrEmpty(cabecalho)) return false; var partes = new Dictionary(); foreach (var parte in cabecalho.Split(',')) { var kv = parte.Trim().Split('=', 2); if (kv.Length == 2) partes[kv[0]] = kv[1]; } if (!partes.TryGetValue("t", out var tTexto) || !long.TryParse(tTexto, out var t)) return false; if (!partes.TryGetValue("v1", out var v1)) return false; var now = agora ?? DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (Math.Abs(now - t) > 300) return false; using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(segredo)); var esperado = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes($"{t}.{corpoBruto}"))).ToLowerInvariant(); return CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(esperado), Encoding.UTF8.GetBytes(v1)); }