Enhance logging in FdsSqlOptions and related classes

- Updated FdsSqlOptions to accept an optional ILogger parameter for improved error logging.
- Modified FdsMfr and FdsMfrClient classes to pass the logger instance to FdsSqlOptions.
- Added detailed error logging in various methods to capture SQL execution issues and file handling errors.
- Improved documentation for FdsSqlOptions to clarify logging behavior.
- Updated Archive class to log compression errors, enhancing traceability of failures.
- Adjusted project configuration to suppress specific warnings related to transitive dependencies.
- Added NuGet.config to define package sources for dependency management.
- Updated submodule references for OCORE and related projects.
This commit is contained in:
Stefan
2026-07-03 20:22:05 +02:00
parent 1a3bf30442
commit 882e97509a
57 changed files with 2121 additions and 106 deletions
+34
View File
@@ -0,0 +1,34 @@
namespace Fuchs.Notifications;
public enum DomainEventType
{
InvoiceDraftCreated,
InvoiceDraftUpdated,
InvoiceFileCreated,
InvoiceSentToCustomer,
InvoiceResentToCustomer,
InvoiceMarkedSent,
InvoiceCreationFailed,
InvoiceFileCreationFailed,
InvoiceSendFailed,
ReminderDraftCreated,
ReminderFileCreated,
ReminderSentToCustomer,
ReminderResentToCustomer,
ReminderMarkedSent,
ReminderCreationFailed,
ReminderFileCreationFailed,
ReminderSendFailed,
BankingTransactionsImported,
BankingImportFailed,
UserIssue
}
public sealed record DomainEvent(
DomainEventType Type,
string UserAccountId,
string Title,
IReadOnlyDictionary<string, object?> Context)
{
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
}
+275
View File
@@ -0,0 +1,275 @@
using Fuchs.intranet;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using static OCORE.OCORE_dictionaries;
namespace Fuchs.Notifications;
public sealed class EventService : IEventService
{
private readonly IHubContext<NotificationHub> _hub;
private readonly ILogger<EventService> _logger;
public EventService(IHubContext<NotificationHub> hub, ILogger<EventService> logger)
{
_hub = hub;
_logger = logger;
}
public async Task PublishAsync(DomainEvent domainEvent, CancellationToken cancellationToken = default)
{
try
{
GuiNotification notification = BuildNotification(domainEvent);
await _hub.Clients
.All
.SendAsync("notification", notification, cancellationToken);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Notification publish failed for {EventType}", domainEvent.Type);
}
}
public Task InvoiceDraftRegisteredAsync(FdsInvoiceData invoice, bool changed, string userAccountId)
{
var type = changed ? DomainEventType.InvoiceDraftUpdated : DomainEventType.InvoiceDraftCreated;
return PublishAsync(new DomainEvent(type, userAccountId, "Rechnungsentwurf", InvoiceContext(invoice)));
}
public Task InvoiceFileCreatedAsync(FdsInvoiceData invoice, string fileName, string userAccountId)
{
var ctx = InvoiceContext(invoice);
ctx["fileName"] = fileName;
return PublishAsync(new DomainEvent(DomainEventType.InvoiceFileCreated, userAccountId, "Rechnungsdatei", ctx));
}
public Task InvoiceSentToCustomerAsync(FdsInvoiceData invoice, string email, string userAccountId, bool resent = false)
{
var ctx = InvoiceContext(invoice);
ctx["email"] = email;
return PublishAsync(new DomainEvent(
resent ? DomainEventType.InvoiceResentToCustomer : DomainEventType.InvoiceSentToCustomer,
userAccountId,
"Rechnung versandt",
ctx));
}
public Task InvoiceMarkedSentAsync(string invoiceId, string invoiceNumber, string userAccountId)
{
Dictionary<string, object?> ctx = new()
{
["id"] = invoiceId,
["invoiceNumber"] = string.IsNullOrWhiteSpace(invoiceNumber) ? invoiceId : invoiceNumber
};
return PublishAsync(new DomainEvent(DomainEventType.InvoiceMarkedSent, userAccountId, "Rechnung markiert", ctx));
}
public Task InvoiceIssueAsync(string message, string userAccountId, string invoiceId = "")
=> PublishAsync(new DomainEvent(
DomainEventType.InvoiceCreationFailed,
userAccountId,
"Rechnung",
new Dictionary<string, object?> { ["id"] = invoiceId, ["message"] = message }));
public Task ReminderDraftCreatedAsync(FdsReminderData reminder, string userAccountId)
=> PublishAsync(new DomainEvent(DomainEventType.ReminderDraftCreated, userAccountId, "Mahnentwurf", ReminderContext(reminder)));
public Task ReminderFileCreatedAsync(FdsReminderData reminder, string fileName, string userAccountId)
{
var ctx = ReminderContext(reminder);
ctx["fileName"] = fileName;
return PublishAsync(new DomainEvent(DomainEventType.ReminderFileCreated, userAccountId, "Mahndatei", ctx));
}
public Task ReminderSentToCustomerAsync(FdsReminderData reminder, string email, string userAccountId, bool resent = false)
{
var ctx = ReminderContext(reminder);
ctx["email"] = email;
return PublishAsync(new DomainEvent(
resent ? DomainEventType.ReminderResentToCustomer : DomainEventType.ReminderSentToCustomer,
userAccountId,
"Mahnung versandt",
ctx));
}
public Task ReminderMarkedSentAsync(string reminderId, string reminderTitle, string userAccountId)
{
Dictionary<string, object?> ctx = new()
{
["id"] = reminderId,
["title"] = string.IsNullOrWhiteSpace(reminderTitle) ? reminderId : reminderTitle
};
return PublishAsync(new DomainEvent(DomainEventType.ReminderMarkedSent, userAccountId, "Mahnung markiert", ctx));
}
public Task ReminderIssueAsync(string message, string userAccountId, string reminderId = "")
=> PublishAsync(new DomainEvent(
DomainEventType.ReminderCreationFailed,
userAccountId,
"Mahnung",
new Dictionary<string, object?> { ["id"] = reminderId, ["message"] = message }));
public Task BankingTransactionsImportedAsync(DateTime? from, DateTime? to, int rows, string fileName, string userAccountId)
=> PublishAsync(new DomainEvent(
DomainEventType.BankingTransactionsImported,
userAccountId,
"Banking",
new Dictionary<string, object?>
{
["from"] = from,
["to"] = to,
["rows"] = rows,
["fileName"] = fileName
}));
public Task BankingImportIssueAsync(string message, string fileName, string userAccountId)
=> PublishAsync(new DomainEvent(
DomainEventType.BankingImportFailed,
userAccountId,
"Banking",
new Dictionary<string, object?> { ["fileName"] = fileName, ["message"] = message }));
public Task UserIssueAsync(string title, string message, string userAccountId, IReadOnlyDictionary<string, object?>? context = null)
{
Dictionary<string, object?> ctx = context == null
? new Dictionary<string, object?>()
: new Dictionary<string, object?>(context);
ctx["message"] = message;
return PublishAsync(new DomainEvent(DomainEventType.UserIssue, userAccountId, title, ctx));
}
private static GuiNotification BuildNotification(DomainEvent domainEvent)
{
string message = domainEvent.Type switch
{
DomainEventType.InvoiceDraftCreated =>
$"Rechnungsentwurf {Ctx(domainEvent, "invoiceNumber")} wurde erstellt.",
DomainEventType.InvoiceDraftUpdated =>
$"Rechnungsentwurf {Ctx(domainEvent, "invoiceNumber")} wurde aktualisiert.",
DomainEventType.InvoiceFileCreated =>
$"Rechnungsdatei {Ctx(domainEvent, "fileName")} wurde erstellt.",
DomainEventType.InvoiceSentToCustomer =>
$"Rechnung {Ctx(domainEvent, "invoiceNumber")} wurde an den Kunden mit der E-Mail {Ctx(domainEvent, "email")} versandt.",
DomainEventType.InvoiceResentToCustomer =>
$"Rechnung {Ctx(domainEvent, "invoiceNumber")} wurde erneut an {Ctx(domainEvent, "email")} versandt.",
DomainEventType.InvoiceMarkedSent =>
$"Rechnung {Ctx(domainEvent, "invoiceNumber")} wurde als versandt markiert.",
DomainEventType.InvoiceCreationFailed =>
Ctx(domainEvent, "message"),
DomainEventType.InvoiceFileCreationFailed =>
Ctx(domainEvent, "message"),
DomainEventType.InvoiceSendFailed =>
Ctx(domainEvent, "message"),
DomainEventType.ReminderDraftCreated =>
$"Mahnentwurf {Ctx(domainEvent, "title")} wurde erstellt.",
DomainEventType.ReminderFileCreated =>
$"Mahndatei {Ctx(domainEvent, "fileName")} wurde erstellt.",
DomainEventType.ReminderSentToCustomer =>
$"Mahnung {Ctx(domainEvent, "title")} wurde an den Kunden mit der E-Mail {Ctx(domainEvent, "email")} versandt.",
DomainEventType.ReminderResentToCustomer =>
$"Mahnung {Ctx(domainEvent, "title")} wurde erneut an {Ctx(domainEvent, "email")} versandt.",
DomainEventType.ReminderMarkedSent =>
$"Mahnung {Ctx(domainEvent, "title")} wurde als versandt markiert.",
DomainEventType.ReminderCreationFailed =>
Ctx(domainEvent, "message"),
DomainEventType.ReminderFileCreationFailed =>
Ctx(domainEvent, "message"),
DomainEventType.ReminderSendFailed =>
Ctx(domainEvent, "message"),
DomainEventType.BankingTransactionsImported =>
BankingImportMessage(domainEvent),
DomainEventType.BankingImportFailed =>
Ctx(domainEvent, "message"),
DomainEventType.UserIssue =>
Ctx(domainEvent, "message"),
_ => domainEvent.Title
};
return new GuiNotification(
Guid.NewGuid().ToString("N"),
domainEvent.Type.ToString(),
domainEvent.Title,
message,
IsFailure(domainEvent.Type) ? "error" : "info",
domainEvent.CreatedAt,
domainEvent.Context);
}
private static bool IsFailure(DomainEventType type) =>
type is DomainEventType.InvoiceCreationFailed
or DomainEventType.InvoiceFileCreationFailed
or DomainEventType.InvoiceSendFailed
or DomainEventType.ReminderCreationFailed
or DomainEventType.ReminderFileCreationFailed
or DomainEventType.ReminderSendFailed
or DomainEventType.BankingImportFailed
or DomainEventType.UserIssue;
private static string BankingImportMessage(DomainEvent domainEvent)
{
int rows = int.TryParse(Ctx(domainEvent, "rows"), out int r) ? r : 0;
string movement = rows == 1 ? "Kontobewegung" : "Kontobewegungen";
string period = BankingPeriod(domainEvent);
return string.IsNullOrEmpty(period)
? $"{rows} {movement} wurden importiert."
: $"{movement} für {period} wurden importiert.";
}
private static string BankingPeriod(DomainEvent domainEvent)
{
DateTime? from = DateCtx(domainEvent, "from");
DateTime? to = DateCtx(domainEvent, "to");
if (from == null && to == null) return "";
if (from != null && to != null)
{
string fromFmt = from.Value.Year == to.Value.Year
? from.Value.ToString("d.M.")
: from.Value.ToString("d.M.yyyy");
string toFmt = from.Value.Year == to.Value.Year
? to.Value.ToString("dd.MM.")
: to.Value.ToString("dd.MM.yyyy");
return $"{fromFmt} - {toFmt}";
}
return (from ?? to)!.Value.ToString("dd.MM.yyyy");
}
private static DateTime? DateCtx(DomainEvent domainEvent, string key)
{
if (!domainEvent.Context.TryGetValue(key, out var value) || value == null) return null;
if (value is DateTime dt) return dt;
if (value is DateTimeOffset dto) return dto.DateTime;
return DateTime.TryParse(value.ToString(), out var parsed) ? parsed : null;
}
private static Dictionary<string, object?> InvoiceContext(FdsInvoiceData invoice)
{
string invoiceNumber = invoice.InvoiceId;
return new Dictionary<string, object?>
{
["id"] = invoice.Id,
["invoiceNumber"] = string.IsNullOrWhiteSpace(invoiceNumber) ? invoice.Id : invoiceNumber,
["documentName"] = invoice.InvoiceRegistration?.getString("DocumentName") ?? "",
["email"] = invoice.InvoiceRegistration?.getString("SendToEmail") ?? "",
["title"] = invoice.InvoiceTitle
};
}
private static Dictionary<string, object?> ReminderContext(FdsReminderData reminder)
{
return new Dictionary<string, object?>
{
["id"] = reminder.Id,
["invoiceNumber"] = reminder.InvoiceId,
["title"] = string.IsNullOrWhiteSpace(reminder.ReminderTitle) ? reminder.Id : reminder.ReminderTitle,
["documentName"] = reminder.ReminderRegistration?.getString("DocumentName") ?? "",
["email"] = reminder.InvoiceEmail
};
}
private static string Ctx(DomainEvent domainEvent, string key)
{
if (!domainEvent.Context.TryGetValue(key, out var value)) return "";
return value?.ToString() ?? "";
}
}
+10
View File
@@ -0,0 +1,10 @@
namespace Fuchs.Notifications;
public sealed record GuiNotification(
string Id,
string Type,
string Title,
string Message,
string Severity,
DateTimeOffset CreatedAt,
IReadOnlyDictionary<string, object?> Context);
+25
View File
@@ -0,0 +1,25 @@
using Fuchs.intranet;
namespace Fuchs.Notifications;
public interface IEventService
{
Task PublishAsync(DomainEvent domainEvent, CancellationToken cancellationToken = default);
Task InvoiceDraftRegisteredAsync(FdsInvoiceData invoice, bool changed, string userAccountId);
Task InvoiceFileCreatedAsync(FdsInvoiceData invoice, string fileName, string userAccountId);
Task InvoiceSentToCustomerAsync(FdsInvoiceData invoice, string email, string userAccountId, bool resent = false);
Task InvoiceMarkedSentAsync(string invoiceId, string invoiceNumber, string userAccountId);
Task InvoiceIssueAsync(string message, string userAccountId, string invoiceId = "");
Task ReminderDraftCreatedAsync(FdsReminderData reminder, string userAccountId);
Task ReminderFileCreatedAsync(FdsReminderData reminder, string fileName, string userAccountId);
Task ReminderSentToCustomerAsync(FdsReminderData reminder, string email, string userAccountId, bool resent = false);
Task ReminderMarkedSentAsync(string reminderId, string reminderTitle, string userAccountId);
Task ReminderIssueAsync(string message, string userAccountId, string reminderId = "");
Task BankingTransactionsImportedAsync(DateTime? from, DateTime? to, int rows, string fileName, string userAccountId);
Task BankingImportIssueAsync(string message, string fileName, string userAccountId);
Task UserIssueAsync(string title, string message, string userAccountId, IReadOnlyDictionary<string, object?>? context = null);
}
+9
View File
@@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace Fuchs.Notifications;
[Authorize]
public sealed class NotificationHub : Hub
{
}