merge: resolve conflicts with remote — keep local CORS config, merge webhook notifications
- Resolved merge conflicts in Program.cs (kept configurable CORS from local) - Resolved merge conflicts in DocumentController.cs (kept full controller with list/download endpoints from local) - Integrated remote webhook notification system: * INotificationService interface + WebhookNotificationService * InvoiceService now sends Teams/Slack webhook on status changes * appsettings.json: added Notifications:WebhookUrl config * Program.cs: registered INotificationService + Webhook HttpClient
This commit is contained in:
commit
12b1755e86
|
|
@ -4,6 +4,9 @@ obj/
|
|||
/packages/
|
||||
out/
|
||||
|
||||
## Secrets
|
||||
appsettings.Development.json
|
||||
|
||||
## IDE - Rider / IntelliJ
|
||||
.idea/
|
||||
*.iml
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using DoliMiddlewareApi.Exceptions;
|
|||
using DoliMiddlewareApi.Services;
|
||||
using DoliMiddlewareApi.Services.Auth;
|
||||
using DoliMiddlewareApi.Services.Clients;
|
||||
using DoliMiddlewareApi.Services.Notifications;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
|
@ -193,6 +194,15 @@ builder.Services.AddMemoryCache();
|
|||
// HttpContext accessor para acceder a User.Claims en servicios
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
builder.Services.AddHttpClient("Webhook");
|
||||
|
||||
// =========================================
|
||||
// NOTIFICACIONES — Webhook (Teams / Slack)
|
||||
// Configura Notifications:WebhookUrl en appsettings o variable de entorno
|
||||
// Si no está configurado, las notificaciones se ignoran silenciosamente
|
||||
// =========================================
|
||||
builder.Services.AddScoped<INotificationService, WebhookNotificationService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// =========================================
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ using DoliMiddlewareApi.Dtos.query;
|
|||
using DoliMiddlewareApi.Exceptions;
|
||||
using DoliMiddlewareApi.Mappers;
|
||||
using DoliMiddlewareApi.Services.Clients;
|
||||
using DoliMiddlewareApi.Services.Notifications;
|
||||
|
||||
namespace DoliMiddlewareApi.Services;
|
||||
|
||||
public class InvoiceService(IDolibarrApiClient apiClient)
|
||||
public class InvoiceService(IDolibarrApiClient apiClient, INotificationService notifications)
|
||||
{
|
||||
public async Task<InvoiceDetailDto> GetInvoiceAsync(int id)
|
||||
{
|
||||
|
|
@ -128,6 +129,10 @@ public class InvoiceService(IDolibarrApiClient apiClient)
|
|||
};
|
||||
|
||||
await apiClient.PostAsync(endpoint, new { });
|
||||
|
||||
// Notificación a Teams/Slack — fire and forget desde el punto de vista del caller
|
||||
var invoice = await apiClient.GetResourceAsync<InvoiceDetailResponse>($"invoices/{id}");
|
||||
await notifications.NotifyInvoiceStatusChangedAsync(id, invoice.@ref ?? $"#{id}", normalized);
|
||||
}
|
||||
|
||||
public async Task ValidateInvoiceAsync(int id)
|
||||
|
|
@ -136,6 +141,8 @@ public class InvoiceService(IDolibarrApiClient apiClient)
|
|||
if (invoice.statut != "0") throw new ForbiddenException("Solo se pueden validar facturas en borrador (draft)");
|
||||
|
||||
await apiClient.PostAsync($"invoices/{id}/validate", new { });
|
||||
|
||||
await notifications.NotifyInvoiceStatusChangedAsync(id, invoice.@ref ?? $"#{id}", "unpaid");
|
||||
}
|
||||
|
||||
public async Task DeleteInvoiceAsync(int id)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
namespace DoliMiddlewareApi.Services.Notifications;
|
||||
|
||||
public interface INotificationService
|
||||
{
|
||||
Task NotifyInvoiceStatusChangedAsync(int invoiceId, string invoiceRef, string newStatus);
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace DoliMiddlewareApi.Services.Notifications;
|
||||
|
||||
/// <summary>
|
||||
/// Envía notificaciones a un webhook de Teams o Slack cuando cambia el estado de una factura.
|
||||
/// Compatible con Teams (Adaptive Cards) y Slack (Block Kit).
|
||||
/// Configurar la URL en appsettings: Notifications:WebhookUrl
|
||||
/// </summary>
|
||||
public class WebhookNotificationService(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IConfiguration configuration,
|
||||
ILogger<WebhookNotificationService> logger) : INotificationService
|
||||
{
|
||||
private static readonly Dictionary<string, (string label, string color)> StatusMeta = new()
|
||||
{
|
||||
["draft"] = ("Borrador", "#8a7dff"),
|
||||
["unpaid"] = ("Pendiente de pago", "#ffb454"),
|
||||
["paid"] = ("Pagada ✓", "#5dd39e"),
|
||||
};
|
||||
|
||||
public async Task NotifyInvoiceStatusChangedAsync(int invoiceId, string invoiceRef, string newStatus)
|
||||
{
|
||||
var webhookUrl = configuration["Notifications:WebhookUrl"];
|
||||
if (string.IsNullOrWhiteSpace(webhookUrl))
|
||||
return; // Notificaciones desactivadas — no hay webhook configurado
|
||||
|
||||
var (label, color) = StatusMeta.GetValueOrDefault(newStatus, (newStatus, "#888"));
|
||||
|
||||
var payload = BuildPayload(invoiceId, invoiceRef, label, color);
|
||||
|
||||
try
|
||||
{
|
||||
var client = httpClientFactory.CreateClient("Webhook");
|
||||
var content = new StringContent(
|
||||
JsonSerializer.Serialize(payload),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
var response = await client.PostAsync(webhookUrl, content);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
logger.LogWarning(
|
||||
"Webhook respondió {Status} para factura {InvoiceId}",
|
||||
response.StatusCode, invoiceId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Nunca rompemos el flujo principal por un fallo en notificaciones
|
||||
logger.LogError(ex,
|
||||
"Error enviando notificación para factura {InvoiceId}", invoiceId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Construye un payload compatible con Teams (Adaptive Card via Incoming Webhook)
|
||||
/// y con Slack (message payload básico con attachments).
|
||||
/// Teams espera { "type": "message", "attachments": [...] } con Adaptive Card.
|
||||
/// Slack espera { "text": "...", "attachments": [...] }.
|
||||
/// Detectamos por URL cuál usar.
|
||||
/// </summary>
|
||||
private object BuildPayload(int invoiceId, string invoiceRef, string label, string color)
|
||||
{
|
||||
var webhookUrl = configuration["Notifications:WebhookUrl"] ?? "";
|
||||
|
||||
if (webhookUrl.Contains("slack.com"))
|
||||
{
|
||||
return new
|
||||
{
|
||||
text = $"🧾 Factura *{invoiceRef}* ha cambiado de estado",
|
||||
attachments = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
color,
|
||||
fields = new[]
|
||||
{
|
||||
new { title = "Factura", value = invoiceRef, @short = true },
|
||||
new { title = "Estado", value = label, @short = true },
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Teams — Adaptive Card
|
||||
return new
|
||||
{
|
||||
type = "message",
|
||||
attachments = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
contentType = "application/vnd.microsoft.card.adaptive",
|
||||
content = new
|
||||
{
|
||||
type = "AdaptiveCard",
|
||||
version = "1.4",
|
||||
body = new object[]
|
||||
{
|
||||
new
|
||||
{
|
||||
type = "TextBlock",
|
||||
text = "🧾 Estado de factura actualizado",
|
||||
weight = "Bolder",
|
||||
size = "Medium"
|
||||
},
|
||||
new
|
||||
{
|
||||
type = "FactSet",
|
||||
facts = new[]
|
||||
{
|
||||
new { title = "Factura", value = invoiceRef },
|
||||
new { title = "ID", value = invoiceId.ToString() },
|
||||
new { title = "Estado", value = label },
|
||||
}
|
||||
}
|
||||
},
|
||||
msteams = new { width = "Full" }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -18,5 +18,7 @@
|
|||
"Secret": "",
|
||||
"Issuer": "DoliMiddleware",
|
||||
"Audience": "DoliClients"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Notifications":{
|
||||
"WebhookUrl": ""
|
||||
}}
|
||||
|
|
|
|||
Loading…
Reference in New Issue