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:
@@ -22,8 +22,9 @@ public partial class IntranetController
|
||||
return await JSONAsync(new { manage = 1 });
|
||||
|
||||
case "up":
|
||||
_logger.LogInformation("Banking MT940 upload: {FileCount} file(s) user={User}",
|
||||
_logger.LogInformation("Banking statement upload: {FileCount} file(s) user={User}",
|
||||
Request.Form.Files.Count, UserAccountID);
|
||||
var uploadResults = new List<object>();
|
||||
foreach (var fle in Request.Form.Files)
|
||||
{
|
||||
using var stream = fle.OpenReadStream();
|
||||
@@ -34,6 +35,9 @@ public partial class IntranetController
|
||||
|
||||
var tbl = _banking.ParseToDatatable(stream, schemaDt);
|
||||
var tmptbl = "bs_" + Guid.NewGuid().ToString().Replace("-", "");
|
||||
var (importFrom, importTo) = BankingDateRange(tbl);
|
||||
bool importFailed = false;
|
||||
string importFailure = "";
|
||||
|
||||
var dtwa = new DatatableWriterAsync(tbl, _intranet.Intranet__SQLConnectionString)
|
||||
{
|
||||
@@ -48,16 +52,80 @@ public partial class IntranetController
|
||||
dtwa.CommandAfterError = new SqlCommand(
|
||||
$"SELECT * INTO [{tmptbl}] FROM {dtwa.DestinationTableName};");
|
||||
dtwa.OnError += (_, exc, _) =>
|
||||
{
|
||||
importFailed = true;
|
||||
importFailure = exc.Message;
|
||||
_logger.LogError(exc,
|
||||
"Banking upload SQL exception — file={File} destTable={DestTable} user={User}",
|
||||
fle.FileName, dtwa.DestinationTableName, UserAccountID);
|
||||
_intranet.debug_log("IntranetController.bam.up - sql exception",
|
||||
exc, UserAccountID, new { uid = dtwa.InstanceGUID, tmptbl });
|
||||
};
|
||||
dtwa.OnCommandAfterError += (_, exc) =>
|
||||
{
|
||||
importFailed = true;
|
||||
importFailure = exc.Message;
|
||||
_logger.LogError(exc,
|
||||
"Banking upload merge-command exception — file={File} destTable={DestTable} " +
|
||||
"rescueTable={RescueTable} user={User}",
|
||||
fle.FileName, dtwa.DestinationTableName, tmptbl, UserAccountID);
|
||||
_intranet.debug_log("IntranetController.bam.up - command-after exception",
|
||||
exc, UserAccountID, new { uid = dtwa.InstanceGUID, tmptbl });
|
||||
};
|
||||
_logger.LogDebug("Banking upload parsed {Rows} rows → temp table submit (user={User})",
|
||||
tbl.Rows.Count, UserAccountID);
|
||||
dtwa.DoSubmit();
|
||||
if (dtwa.SubmitException != null)
|
||||
{
|
||||
importFailed = true;
|
||||
importFailure = dtwa.SubmitException.Message;
|
||||
_logger.LogError(dtwa.SubmitException,
|
||||
"Banking upload submit exception — file={File} destTable={DestTable} user={User}",
|
||||
fle.FileName, dtwa.DestinationTableName, UserAccountID);
|
||||
}
|
||||
|
||||
if (importFailed)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Banking import failed — file={File} rows={Rows} reason={Reason} user={User}",
|
||||
fle.FileName, tbl.Rows.Count, importFailure, UserAccountID);
|
||||
await _events.BankingImportIssueAsync(
|
||||
$"Kontobewegungen aus {fle.FileName} konnten nicht importiert werden: {importFailure}",
|
||||
fle.FileName, UserAccountID);
|
||||
}
|
||||
else if (tbl.Rows.Count == 0)
|
||||
{
|
||||
// Parsing produced zero rows — check the preceding "Bank statement parsed"
|
||||
// warning from BankingService for the reason (missing account element,
|
||||
// unsupported schema variant, empty file, ...).
|
||||
_logger.LogWarning(
|
||||
"Banking import: 0 rows parsed from {File} — nothing to import. user={User}",
|
||||
fle.FileName, UserAccountID);
|
||||
await _events.BankingImportIssueAsync(
|
||||
$"Aus {fle.FileName} konnten keine Kontobewegungen importiert werden.",
|
||||
fle.FileName, UserAccountID);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Banking import succeeded — file={File} rows={Rows} from={From} to={To} user={User}",
|
||||
fle.FileName, tbl.Rows.Count, importFrom, importTo, UserAccountID);
|
||||
await _events.BankingTransactionsImportedAsync(
|
||||
importFrom, importTo, tbl.Rows.Count, fle.FileName, UserAccountID);
|
||||
}
|
||||
|
||||
uploadResults.Add(new
|
||||
{
|
||||
fileName = fle.FileName,
|
||||
rows = tbl.Rows.Count,
|
||||
success = !importFailed && tbl.Rows.Count > 0,
|
||||
error = importFailed ? importFailure : ""
|
||||
});
|
||||
}
|
||||
return Ok();
|
||||
// Return a JSON body: the frontend posts with dataType 'json', so an
|
||||
// empty 200 would be reported as a parse error and surface the generic
|
||||
// "auth failed" alert even though the import actually succeeded.
|
||||
return await JSONAsync(new { ok = true, files = uploadResults });
|
||||
|
||||
case "qtl":
|
||||
{
|
||||
@@ -126,7 +194,9 @@ public partial class IntranetController
|
||||
"EXECUTE [dbo].[fds__setBankingtransaction_done] @taID, @authuser;",
|
||||
_intranet.Intranet__SQLConnectionString, pl,
|
||||
Security: DbSec, options: SqlOpt(fn, id, code));
|
||||
return res.Result is true ? Ok() : StatusCode(500, new { error = "not successful" });
|
||||
return res.Result is true
|
||||
? await JSONAsync(new { ok = true })
|
||||
: StatusCode(500, new { error = "not successful" });
|
||||
}
|
||||
|
||||
case "ati":
|
||||
@@ -139,7 +209,9 @@ public partial class IntranetController
|
||||
"EXECUTE [dbo].[fds__setBankingtransaction_assignToIvoice] @taID, @invoice_id, @authuser;",
|
||||
_intranet.Intranet__SQLConnectionString, pl,
|
||||
Security: DbSec, options: SqlOpt(fn, id, code));
|
||||
return res.Result is true ? Ok() : StatusCode(500, new { error = "not successful" });
|
||||
return res.Result is true
|
||||
? await JSONAsync(new { ok = true })
|
||||
: StatusCode(500, new { error = "not successful" });
|
||||
}
|
||||
|
||||
case "vfi":
|
||||
@@ -165,4 +237,26 @@ public partial class IntranetController
|
||||
|
||||
protected string Form(string key, string fallback = "") =>
|
||||
Request.Form.TryGetValue(key, out var v) ? v.ToString() : fallback;
|
||||
|
||||
private static (DateTime? From, DateTime? To) BankingDateRange(System.Data.DataTable tbl)
|
||||
{
|
||||
DateTime? from = null;
|
||||
DateTime? to = null;
|
||||
foreach (System.Data.DataRow row in tbl.Rows)
|
||||
{
|
||||
DateTime? date = BankingRowDate(row, tbl.Columns.Contains("EntryDate") ? "EntryDate" : "")
|
||||
?? BankingRowDate(row, tbl.Columns.Contains("ValueDate") ? "ValueDate" : "");
|
||||
if (date == null) continue;
|
||||
from = from == null || date.Value < from.Value ? date.Value : from;
|
||||
to = to == null || date.Value > to.Value ? date.Value : to;
|
||||
}
|
||||
return (from, to);
|
||||
}
|
||||
|
||||
private static DateTime? BankingRowDate(System.Data.DataRow row, string column)
|
||||
{
|
||||
if (string.IsNullOrEmpty(column) || row[column] == DBNull.Value) return null;
|
||||
if (row[column] is DateTime dt) return dt.Date;
|
||||
return DateTime.TryParse(row[column]?.ToString(), out var parsed) ? parsed.Date : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,14 @@ public partial class IntranetController
|
||||
_intranet.Intranet__SQLConnectionString, pl,
|
||||
Security: DbSec, options: SqlOpt(fn, id, code));
|
||||
if (!string.IsNullOrEmpty(dt2.Exception))
|
||||
{
|
||||
_logger.LogError("sis: SQL error for invoice {InvoiceId}: {SqlError}, user={User}", invoiceId, dt2.Exception, UserAccountID);
|
||||
await _events.InvoiceIssueAsync(
|
||||
$"Rechnung {invoiceId} konnte nicht als versandt markiert werden.",
|
||||
UserAccountID, invoiceId);
|
||||
}
|
||||
else
|
||||
await _events.InvoiceMarkedSentAsync(invoiceId, invoiceId, UserAccountID);
|
||||
return string.IsNullOrEmpty(dt2.Exception) ? Ok() : StatusCode(500);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,10 +43,11 @@ public partial class IntranetController
|
||||
new FdsReminderData(ctd), change: false, remId: "", UserAccountID, DbSec);
|
||||
if (!string.IsNullOrEmpty(fdRem.Id))
|
||||
{
|
||||
await _events.ReminderDraftCreatedAsync(fdRem, UserAccountID);
|
||||
var imgcol = await _pdf.DocToImageCollectionAsync(_reminders.GenerateReminderPdf(fdRem, fdRem.IsDraft));
|
||||
return await JSONAsync(new { id = fdRem.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages });
|
||||
}
|
||||
return StatusCode(500, new { error = "Erinnerung wurde nicht registriert" });
|
||||
return await ReminderIssueResult("Die Mahnung konnte aufgrund eines Fehlers nicht erstellt werden.");
|
||||
}
|
||||
|
||||
case "conf": return await HandleReminderConf(fn, id, code);
|
||||
@@ -59,6 +60,12 @@ public partial class IntranetController
|
||||
"EXECUTE [dbo].[fds__setReminderSent] @Id, @auto, @authuser;",
|
||||
_intranet.Intranet__SQLConnectionString, pl,
|
||||
Security: DbSec, options: SqlOpt(fn, id, code));
|
||||
if (string.IsNullOrEmpty(dt2.Exception))
|
||||
await _events.ReminderMarkedSentAsync(Form("id"), Form("id"), UserAccountID);
|
||||
else
|
||||
await _events.ReminderIssueAsync(
|
||||
$"Mahnung {Form("id")} konnte nicht als versandt markiert werden.",
|
||||
UserAccountID, Form("id"));
|
||||
return string.IsNullOrEmpty(dt2.Exception) ? Ok() : StatusCode(500);
|
||||
}
|
||||
|
||||
@@ -127,16 +134,35 @@ public partial class IntranetController
|
||||
email.Trim(), "", remdoc);
|
||||
if (sent)
|
||||
{
|
||||
await _events.ReminderSentToCustomerAsync(fdRem, email.Trim(), UserAccountID);
|
||||
var pls = StdParamlist(SQL_VarChar("@Id", remId), SQL_Bit("@auto", true));
|
||||
await getSQLDatatable_async(
|
||||
"EXECUTE [dbo].[fds__setReminderSent] @Id, @auto, @authuser;",
|
||||
_intranet.Intranet__SQLConnectionString, pls,
|
||||
Security: DbSec, options: SqlOpt(fn, id, code));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError(
|
||||
"Reminder email send failed — reminderId={ReminderId} email={Email} user={User}",
|
||||
remId, email.Trim(), UserAccountID);
|
||||
await _events.ReminderIssueAsync(
|
||||
$"Mahnung {frdic.nz("subject").ne(remId)} konnte nicht an {email.Trim()} versandt werden.",
|
||||
UserAccountID, remId);
|
||||
}
|
||||
}
|
||||
else if (filebyte.Length == 0)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Reminder PDF render returned 0 bytes — reminderId={ReminderId} user={User}",
|
||||
remId, UserAccountID);
|
||||
await _events.ReminderIssueAsync(
|
||||
$"Die Mahn-PDF {frdic.nz("DocumentName", "").ne($"Zahlungserinnerung_{remId}.pdf")} konnte nicht erstellt werden.",
|
||||
UserAccountID, remId);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
return StatusCode(500, new { error = "Aktion war nicht erfolgreich" });
|
||||
return await ReminderIssueResult("Die Mahnung konnte aufgrund eines Fehlers nicht erstellt werden.");
|
||||
}
|
||||
|
||||
private async Task<IActionResult> HandleReminderIdoc(string fn, string id, string code)
|
||||
@@ -178,14 +204,39 @@ public partial class IntranetController
|
||||
if (!string.IsNullOrEmpty(frdic.nz("InvoiceFileName")) &&
|
||||
frdic.no("InvoiceFile", null!) is byte[] invFile)
|
||||
remdoc[frdic.nz("InvoiceFileName")] = invFile;
|
||||
await _comService.SendEmailAsync($"rem_{remId}",
|
||||
bool sent = await _comService.SendEmailAsync($"rem_{remId}",
|
||||
$"SanitärFuchs - {frdic.nz("subject").ne(frdic.nz("DocumentName"))}",
|
||||
BuildReminderBody(Convert.ToDouble(frdic.no("amount_open", 0))),
|
||||
email.Trim(), "", remdoc);
|
||||
if (sent)
|
||||
{
|
||||
var fdRem = await _reminders.LoadReminderAsync(remId, UserAccountID, DbSec);
|
||||
await _events.ReminderSentToCustomerAsync(fdRem, email.Trim(), UserAccountID, resent: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError(
|
||||
"Reminder resend email send failed — reminderId={ReminderId} email={Email} user={User}",
|
||||
remId, email.Trim(), UserAccountID);
|
||||
await _events.ReminderIssueAsync(
|
||||
$"Mahnung {frdic.nz("subject").ne(remId)} konnte nicht erneut an {email.Trim()} versandt werden.",
|
||||
UserAccountID, remId);
|
||||
}
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
return StatusCode(500, new { error = "Aktion war nicht erfolgreich" });
|
||||
return await ReminderIssueResult("Die Mahnung konnte aufgrund eines Fehlers nicht versandt werden.");
|
||||
}
|
||||
|
||||
private async Task<IActionResult> ReminderIssueResult(string message, string reminderId = "")
|
||||
{
|
||||
// Mirrors the SignalR toast in a durable app log: without this, a reminder
|
||||
// save/create/send failure was only visible as a GUI notification nobody was
|
||||
// necessarily watching at the time.
|
||||
_logger.LogError("Reminder issue — reminderId={ReminderId} user={User} message={Message}",
|
||||
reminderId, UserAccountID, message);
|
||||
await _events.ReminderIssueAsync(message, UserAccountID, reminderId);
|
||||
return StatusCode(500, new { error = message });
|
||||
}
|
||||
|
||||
private static string BuildReminderBody(double amountOpen) =>
|
||||
|
||||
@@ -36,7 +36,15 @@ public partial class IntranetController
|
||||
ri["params"] = dset.Tables("params")
|
||||
.toArrayofObjectDictionaries($"[object_id] = {ri["object_id"]} AND [name] <> '@authuser'");
|
||||
}
|
||||
catch { ri["params"] = Array.Empty<Dictionary<string, object>>(); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Without this, a genuinely broken params filter/query is indistinguishable
|
||||
// from the expected "this report has no params" case in the response.
|
||||
_logger.LogWarning(ex,
|
||||
"Report catalog: failed to load params for object_id={ObjectId} user={User}",
|
||||
ri["object_id"], UserAccountID);
|
||||
ri["params"] = Array.Empty<Dictionary<string, object>>();
|
||||
}
|
||||
}
|
||||
return await JSONAsync(new
|
||||
{
|
||||
|
||||
@@ -49,9 +49,11 @@ public partial class IntranetController
|
||||
var fdInv = await _invoices.RegisterInvoiceAsync(
|
||||
new FdsInvoiceData(JsonConvert.DeserializeObject(Form("invc"))!),
|
||||
change: !string.IsNullOrEmpty(Form("id")), invId: Form("id"), UserAccountID, DbSec);
|
||||
if (!string.IsNullOrEmpty(fdInv.Id))
|
||||
await _events.InvoiceDraftRegisteredAsync(fdInv, !string.IsNullOrEmpty(Form("id")), UserAccountID);
|
||||
return !string.IsNullOrEmpty(fdInv.Id)
|
||||
? await JSONAsync(new { id = fdInv.Id })
|
||||
: StatusCode(500, new { error = "Rechnung wurde nicht gespeichert" });
|
||||
: await InvoiceIssueResult("Die Rechnung konnte aufgrund eines Fehlers nicht gespeichert werden.");
|
||||
}
|
||||
|
||||
case "sprep":
|
||||
@@ -62,10 +64,11 @@ public partial class IntranetController
|
||||
change: false, invId: "", UserAccountID, DbSec);
|
||||
if (!string.IsNullOrEmpty(fdInv.Id))
|
||||
{
|
||||
await _events.InvoiceDraftRegisteredAsync(fdInv, changed: false, userAccountId: UserAccountID);
|
||||
var imgcol = await _pdf.DocToImageCollectionAsync(_invoices.GenerateInvoicePdf(fdInv, fdInv.IsDraft));
|
||||
return await JSONAsync(new { id = fdInv.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages });
|
||||
}
|
||||
return StatusCode(500, new { error = "Rechnung wurde nicht registriert" });
|
||||
return await InvoiceIssueResult("Die Rechnung konnte aufgrund eines Fehlers nicht erstellt werden.");
|
||||
}
|
||||
|
||||
case "sedit":
|
||||
@@ -76,10 +79,11 @@ public partial class IntranetController
|
||||
change: true, invId: Form("id"), UserAccountID, DbSec);
|
||||
if (!string.IsNullOrEmpty(fdInv.Id))
|
||||
{
|
||||
await _events.InvoiceDraftRegisteredAsync(fdInv, changed: true, userAccountId: UserAccountID);
|
||||
var imgcol = await _pdf.DocToImageCollectionAsync(_invoices.GenerateInvoicePdf(fdInv, fdInv.IsDraft));
|
||||
return await JSONAsync(new { id = fdInv.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages });
|
||||
}
|
||||
return StatusCode(500, new { error = "Rechnung wurde nicht registriert" });
|
||||
return await InvoiceIssueResult("Die Rechnung konnte aufgrund eines Fehlers nicht aktualisiert werden.");
|
||||
}
|
||||
|
||||
case "sdel":
|
||||
@@ -141,13 +145,20 @@ public partial class IntranetController
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Dictionary<string, object?>> AttachReports(SQLDataSet dset)
|
||||
private List<Dictionary<string, object?>> AttachReports(SQLDataSet dset)
|
||||
{
|
||||
var req = new List<Dictionary<string, object?>>(dset.Tables("requests").toArrayofObjectDictionaries()!);
|
||||
foreach (var r in req)
|
||||
{
|
||||
try { r["reports"] = dset.Tables("reports").toArrayofObjectDictionaries($"[requestID] = {r["Id"]}"); }
|
||||
catch { /* no reports table */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
// "reports" table absent is expected for some queries; but a real failure while
|
||||
// joining (e.g. malformed filter) looked identical to that with no way to tell them apart.
|
||||
_logger.LogWarning(ex,
|
||||
"AttachReports: failed to join reports for requestId={RequestId} user={User}",
|
||||
r["Id"], UserAccountID);
|
||||
}
|
||||
}
|
||||
return req;
|
||||
}
|
||||
@@ -285,15 +296,34 @@ public partial class IntranetController
|
||||
body, email.Trim(), "", inv);
|
||||
if (sent)
|
||||
{
|
||||
await _events.InvoiceSentToCustomerAsync(fdInv, email.Trim(), UserAccountID);
|
||||
var pls = StdParamlist(SQL_VarChar("@Id", invId), SQL_Bit("@auto", true));
|
||||
await getSQLDatatable_async("EXECUTE [dbo].[fds__setInvoiceSent] @Id, @auto, @authuser;",
|
||||
_intranet.Intranet__SQLConnectionString, pls,
|
||||
Security: DbSec, options: SqlOpt(fn, id, code));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError(
|
||||
"Invoice email send failed — invoiceId={InvoiceId} email={Email} user={User}",
|
||||
invId, email.Trim(), UserAccountID);
|
||||
await _events.InvoiceIssueAsync(
|
||||
$"Rechnung {frdic.nz("InvoiceId").ne(invId)} konnte nicht an {email.Trim()} versandt werden.",
|
||||
UserAccountID, invId);
|
||||
}
|
||||
}
|
||||
else if (filebyte.Length == 0)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Invoice PDF render returned 0 bytes — invoiceId={InvoiceId} user={User}",
|
||||
invId, UserAccountID);
|
||||
await _events.InvoiceIssueAsync(
|
||||
$"Die Rechnungs-PDF {frdic.nz("DocumentName").ne($"Rechnung_{invId}.pdf")} konnte nicht erstellt werden.",
|
||||
UserAccountID, invId);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
return StatusCode(500, new { error = "Aktion war nicht erfolgreich" });
|
||||
return await InvoiceIssueResult("Die Rechnung konnte aufgrund eines Fehlers nicht erstellt werden.");
|
||||
}
|
||||
|
||||
private async Task<IActionResult> HandleRequestIdoc(string fn, string id, string code)
|
||||
@@ -309,7 +339,7 @@ public partial class IntranetController
|
||||
: _pdf.DocToPdfBytes(_invoices.GenerateInvoicePdf(fdInv, fdInv.IsDraft));
|
||||
return ct != null
|
||||
? await FileContentResultAsync(ct, "application/pdf", filename, inline: true)
|
||||
: StatusCode(500, new { error = "Rechnungs-PDF konnte nicht erstellt werden" });
|
||||
: await InvoiceIssueResult("Die Rechnungs-PDF konnte aufgrund eines Fehlers nicht erstellt werden.", fdInv.Id);
|
||||
}
|
||||
var imgcol = await _pdf.DocToImageCollectionAsync(_invoices.GenerateInvoicePdf(fdInv, fdInv.IsDraft));
|
||||
return await JSONAsync(new { id = fdInv.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages });
|
||||
@@ -335,14 +365,35 @@ public partial class IntranetController
|
||||
{
|
||||
double bal = Convert.ToDouble(frdic.no("InvoiceBalance", 0));
|
||||
string terms = fdInv.PaymentTerms.Replace("wd", " Werktagen").Replace("d", " Tagen").Replace("wk", " Wochen").ne("10 Tagen");
|
||||
await _comService.SendEmailAsync(
|
||||
bool sent = await _comService.SendEmailAsync(
|
||||
$"inv_{invId}", $"Sanit\u00e4rFuchs - {frdic.nz("DocumentName")}",
|
||||
BuildInvoiceBody(bal, terms), email.Trim(), "",
|
||||
new Dictionary<string, byte[]> { [frdic.nz("DocumentName")] = filebyte });
|
||||
if (sent)
|
||||
await _events.InvoiceSentToCustomerAsync(fdInv, email.Trim(), UserAccountID, resent: true);
|
||||
else
|
||||
{
|
||||
_logger.LogError(
|
||||
"Invoice resend email send failed — invoiceId={InvoiceId} email={Email} user={User}",
|
||||
invId, email.Trim(), UserAccountID);
|
||||
await _events.InvoiceIssueAsync(
|
||||
$"Rechnung {frdic.nz("InvoiceId").ne(invId)} konnte nicht erneut an {email.Trim()} versandt werden.",
|
||||
UserAccountID, invId);
|
||||
}
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
return StatusCode(500, new { error = "Aktion war nicht erfolgreich" });
|
||||
return await InvoiceIssueResult("Die Rechnung konnte aufgrund eines Fehlers nicht versandt werden.");
|
||||
}
|
||||
|
||||
private async Task<IActionResult> InvoiceIssueResult(string message, string invoiceId = "")
|
||||
{
|
||||
// Mirrors the SignalR toast in a durable app log: without this, an invoice save/create/send
|
||||
// failure was only visible as a GUI notification nobody was necessarily watching at the time.
|
||||
_logger.LogError("Invoice issue — invoiceId={InvoiceId} user={User} message={Message}",
|
||||
invoiceId, UserAccountID, message);
|
||||
await _events.InvoiceIssueAsync(message, UserAccountID, invoiceId);
|
||||
return StatusCode(500, new { error = message });
|
||||
}
|
||||
|
||||
private static string BuildInvoiceBody(double balance, string paymentTerms) =>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Web;
|
||||
using Fuchs.intranet;
|
||||
using Fuchs.Notifications;
|
||||
using Fuchs.Services;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -33,6 +34,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
|
||||
private readonly IReportService _reports;
|
||||
private readonly IInvoiceService _invoices;
|
||||
private readonly IReminderService _reminders;
|
||||
private readonly IEventService _events;
|
||||
private readonly List<string> _allowedNonAuth = new() { "spwc", "spw" };
|
||||
private readonly List<string> _allowedGet = new()
|
||||
{
|
||||
@@ -59,7 +61,8 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
|
||||
IWidgetService widgets,
|
||||
IReportService reports,
|
||||
IInvoiceService invoices,
|
||||
IReminderService reminders)
|
||||
IReminderService reminders,
|
||||
IEventService events)
|
||||
{
|
||||
_intranet = intranet;
|
||||
_mfr = mfr;
|
||||
@@ -72,6 +75,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
|
||||
_reports = reports;
|
||||
_invoices = invoices;
|
||||
_reminders = reminders;
|
||||
_events = events;
|
||||
}
|
||||
|
||||
/// <summary>Merged query-string + form parameters (form wins) for report processing.</summary>
|
||||
@@ -102,7 +106,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
|
||||
public DatabaseSecurity DbSec => _intranet.GetDbSecurity(UserAccountID);
|
||||
|
||||
public FIS_SQLOptions SqlOpt(string fn, string id, string code) =>
|
||||
new(new Dictionary<string, object> { ["fn"] = fn, ["id"] = id, ["code"] = code });
|
||||
new(new Dictionary<string, object> { ["fn"] = fn, ["id"] = id, ["code"] = code }, _logger);
|
||||
|
||||
// ── Action helpers ────────────────────────────────────────────────────────
|
||||
protected IActionResult Unauthorized401() => StatusCode(401);
|
||||
|
||||
Reference in New Issue
Block a user