Complete DI migration: wire all business services end-to-end

Move the intranet off the static-helper / Active-Record pattern onto
constructor-injected services, removing controller coupling and the
sync-over-async (Task.Run().Wait()) hot spots in the data classes.

Services now registered and consumed via DI:
- IBankingService, IPdfService, IMfrClientFactory (singletons)
- IWidgetService, IReportService, IInvoiceService, IReminderService (scoped)

Key changes:
- FuchsWidgetService: real widget logic (sql_table/indicator/html +
  rendering_options) ported from the static class, which is deleted.
- FuchsReportService + FuchsVisualization: report engine decoupled from
  IntranetController (takes connStr/dbSec/userAccountId); static
  FuchsReports deleted.
- InvoiceService / ReminderService: implement load/register/render/store
  (previously NotImplementedException stubs). FdsInvoiceData /
  FdsReminderData are now pure data holders — all DB + PDF work moved into
  the services, async throughout (no Task.Run().Wait()).
- Controllers inject and call the services; all `new FdsMfrClient()` calls
  go through IMfrClientFactory.
- Deleted dead code: static Banking, FuchsWidgets, FuchsReports, and the
  unused IDbConnectionFactory.
- InternalsVisibleTo("Fuchs.Tests") for testing internal mapping logic.

Tests: 127 passing (Banking tests moved to the service; added data-holder
tests for FdsInvoiceData/FdsReminderData). Full solution builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 12:57:59 +02:00
parent c81619fa53
commit 8dee630abb
25 changed files with 711 additions and 947 deletions
+117 -8
View File
@@ -1,27 +1,136 @@
using Microsoft.AspNetCore.Mvc;
using Fuchs.intranet;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using OCORE.security;
using OCORE.SQL;
using static OCORE.commons;
using static OCORE.OCORE_dictionaries;
using static OCORE.SQL.sql;
namespace Fuchs.Services;
/// <summary>
/// Report service implementation. Replaces the static <c>FuchsReports</c> class.
/// Report processing for the Fuchs intranet — SQL-driven reports from the
/// fds__ report catalog, rendered as HTML pages, HTML fragments, or PNG charts
/// via <see cref="FuchsVisualization"/>. Replaces the static <c>FuchsReports</c>.
/// </summary>
public class FuchsReportService : IReportService
{
private const int DefaultReloadSeconds = 60 * 10;
private readonly Fuchs_intranet _intranet;
private readonly ILogger<FuchsReportService> _logger;
public FuchsReportService(ILogger<FuchsReportService> logger)
public FuchsReportService(Fuchs_intranet intranet, ILogger<FuchsReportService> logger)
{
_intranet = intranet;
_logger = logger;
}
public Task<IActionResult> ProcessRequestAsync(string action, string id,
string userAccountId, DatabaseSecurity dbSec)
private string Conn => _intranet.Intranet__SQLConnectionString;
public async Task<IActionResult> ProcessRequestAsync(string fnc, string reportId,
string userAccountId, DatabaseSecurity dbSec, IDictionary<string, string> parameters)
{
// Specific report actions are dispatched here.
// Extend with additional cases as needed.
return Task.FromResult<IActionResult>(new OkResult());
var prms = new Dictionary<string, string>(parameters, StringComparer.OrdinalIgnoreCase)
{
["@authuser"] = userAccountId
};
string tgt = (string.IsNullOrEmpty(fnc)
? (prms.TryGetValue("fnc", out var f) ? f : fnc)
: fnc).Replace("gct", "generic_content");
string report = prms.TryGetValue("report", out var r) && !string.IsNullOrEmpty(r)
? r
: (!string.IsNullOrEmpty(reportId) ? reportId : "");
string templatePath = Path.Combine(AppContext.BaseDirectory, "Content", "FDS_Template.html");
// Report configuration (refresh interval + cache flag) from the catalog.
int ciRefresh = -2;
bool ciCache = false;
try
{
var catalog = await getSQLDatatable_async(
"EXECUTE [dbo].[fds__admin_getReportCatalog] @report_name, @authuser;",
Conn,
new List<SqlParameter> { SQL_VarChar("@report_name", report), SQL_VarChar("@authuser", userAccountId) },
Security: dbSec, options: new FIS_SQLOptions());
var cfg = catalog.FirstRow.toObjectDictionary();
if (cfg.TryGetValue("refresh", out var rf) && rf is not null && rf is not DBNull &&
int.TryParse(rf.ToString(), out var rfi)) ciRefresh = rfi;
if (cfg.TryGetValue("functions", out var fn) && fn is not null)
ciCache = (fn.ToString() ?? "").Split(',').Contains("cache");
}
catch (Exception cex)
{
_logger.LogError(cex, "Report catalog read failed for report {Report}", report);
}
bool ciForce = prms.TryGetValue("cache", out var ca) && ca.ToLower() == "0";
try
{
switch (tgt)
{
case "generic_content":
if (string.IsNullOrEmpty(report)) return new StatusCodeResult(300);
return new ContentResult
{
Content = await FuchsVisualization.RenderContentAsync(
Conn, dbSec, userAccountId, report, FdsQueryType.generic, prms),
ContentType = "text/html"
};
case "generic":
{
if (string.IsNullOrEmpty(report)) return new StatusCodeResult(300);
var page = await FuchsVisualization.RenderPageAsync(
Conn, dbSec, userAccountId, report, report, FdsQueryType.generic, prms,
FdsDestination.web, templatePath, allowcache: ciCache, forceReload: ciForce);
ApplyReload(page, prms, ciRefresh);
return new ContentResult { Content = page.ToHtml(FdsDestination.web), ContentType = "text/html" };
}
case "chart":
byte[]? png = await FuchsVisualization.RenderQueryAsChartAsync(
Conn, dbSec, userAccountId, report, FdsQueryType.generic, prms);
if (png is null) return new StatusCodeResult(500);
return new FileContentResult(png, "image/png")
{
FileDownloadName = $"{report.Replace(" ", "_")}_{DateTime.Now:yyyyMMdd_HHmm}.png"
};
case "xls":
return new StatusCodeResult(501); // not implemented (matches legacy NotImplementedException)
default:
if (Enum.TryParse<FdsQueryType>(fnc, ignoreCase: true, out var qt))
{
if (string.IsNullOrEmpty(report)) return new StatusCodeResult(300);
var page = await FuchsVisualization.RenderPageAsync(
Conn, dbSec, userAccountId, report, report, qt, prms,
FdsDestination.web, templatePath);
ApplyReload(page, prms, -2);
return new ContentResult { Content = page.ToHtml(FdsDestination.web), ContentType = "text/html" };
}
return new StatusCodeResult(300);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Report processing failed fnc={Fnc} report={Report} tgt={Tgt}", fnc, report, tgt);
return new StatusCodeResult(500);
}
}
private static void ApplyReload(FuchsHtmlPage page, IDictionary<string, string> prms, int ciRefresh)
{
if (prms.TryGetValue("reload", out var rl) && int.TryParse(rl, out var rs)) page.ReloadSeconds = rs;
else if (ciRefresh > -2) page.ReloadSeconds = ciRefresh;
else if (DefaultReloadSeconds > 0) page.ReloadSeconds = DefaultReloadSeconds;
if (page.QueryDuration > 180 && page.ReloadSeconds is > 0 and < 3600) page.ReloadSeconds = 1200;
else if (page.QueryDuration > 60 && page.ReloadSeconds is > 0 and < 1200) page.ReloadSeconds = 1200;
}
}
+109 -37
View File
@@ -1,7 +1,6 @@
using Fuchs.intranet;
using Fuchs.intranet;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using OCORE.security;
using OCORE.SQL;
@@ -12,8 +11,9 @@ using static OCORE.web.mvc_helper_async;
namespace Fuchs.Services;
/// <summary>
/// Widget service implementation. Replaces the static <c>FuchsWidgets</c> class.
/// No longer depends on <c>IntranetController</c>.
/// Widget service for the Fuchs intranet dashboard. Port of fuchs_fds_widgets.vb —
/// SQL-driven widget cases. Replaces the static <c>FuchsWidgets</c> class
/// (no longer depends on <c>IntranetController</c>).
/// </summary>
public class FuchsWidgetService : IWidgetService
{
@@ -26,6 +26,8 @@ public class FuchsWidgetService : IWidgetService
_logger = logger;
}
private string Conn => _intranet.Intranet__SQLConnectionString;
public async Task<IActionResult> GetWidgetAsync(string widgetId, string userAccountId,
DatabaseSecurity dbSec, HttpRequest request)
{
@@ -33,9 +35,9 @@ public class FuchsWidgetService : IWidgetService
{
return widgetId.ToLower() switch
{
"my" => await HandleWidgetMy(userAccountId, dbSec),
"my" => await HandleWidgetMy(userAccountId, dbSec),
"one" => await HandleWidgetOne(userAccountId, dbSec, request),
_ => await HandleWidgetGeneric(widgetId, userAccountId, dbSec)
_ => await HandleWidgetGeneric(widgetId, userAccountId, dbSec)
};
}
catch (Exception ex)
@@ -45,20 +47,20 @@ public class FuchsWidgetService : IWidgetService
}
}
private List<SqlParameter> MakeParams(string userAccountId, params SqlParameter[] extra)
private static List<Microsoft.Data.SqlClient.SqlParameter> Params(string userAccountId,
params Microsoft.Data.SqlClient.SqlParameter[] extra)
{
var list = new List<SqlParameter> { SQL_VarChar("@authuser", userAccountId) };
var list = new List<Microsoft.Data.SqlClient.SqlParameter> { SQL_VarChar("@authuser", userAccountId) };
list.AddRange(extra);
return list;
}
// ── "my" — list of widget short-names for the current user ───────────────
private async Task<IActionResult> HandleWidgetMy(string userAccountId, DatabaseSecurity dbSec)
{
var dt = await getSQLDatatable_async(
"SELECT * FROM [dbo].[fis_getONEPersonWidgets] (@authuser);",
_intranet.Intranet__SQLConnectionString,
MakeParams(userAccountId, SQL_VarChar("@account", "fis")),
Security: dbSec);
Conn, Params(userAccountId, SQL_VarChar("@account", "fis")), Security: dbSec);
var names = dt.DataTable.Rows
.Cast<System.Data.DataRow>()
.OrderBy(r => dt.DataTable.Columns.Contains("order") ? r.nz("order") : "")
@@ -67,6 +69,7 @@ public class FuchsWidgetService : IWidgetService
return await JSONAsync(names);
}
// ── "one" — full widget data for a single widget ──────────────────────────
private async Task<IActionResult> HandleWidgetOne(string userAccountId, DatabaseSecurity dbSec,
HttpRequest request)
{
@@ -75,55 +78,124 @@ public class FuchsWidgetService : IWidgetService
var dt = await getSQLDatatable_async(
"SELECT * FROM [dbo].[fis_getONEPersonWidgets] (@authuser) WHERE [short_name] = @shortname;",
_intranet.Intranet__SQLConnectionString,
MakeParams(userAccountId,
Conn,
Params(userAccountId,
SQL_VarChar("@shortname", shortName),
SQL_VarChar("@account", "fis")),
SQL_VarChar("@account", "fis")),
Security: dbSec);
if (dt.Count != 1) return new StatusCodeResult(404);
var wdg = dt.FirstRow.toObjectDictionary();
return await BuildWidgetResponse(userAccountId, dbSec, wdg);
return await BuildWidgetResponse(userAccountId, dbSec, shortName, wdg);
}
// ── Generic widget by id ──────────────────────────────────────────────────
private async Task<IActionResult> HandleWidgetGeneric(string widgetId, string userAccountId,
DatabaseSecurity dbSec)
{
var pl = MakeParams(userAccountId, SQL_VarChar("@widget", widgetId, dbNull_IfEmpty: true));
var pl = Params(userAccountId, SQL_VarChar("@widget", widgetId, dbNull_IfEmpty: true));
var dset = await getSQLDataSet_async(
"EXECUTE [dbo].[fds__getWidget] @widget, @authuser;",
_intranet.Intranet__SQLConnectionString, pl,
tablenames: new[] { "admin", "data" },
Security: dbSec);
Conn, pl, tablenames: new[] { "admin", "data" }, Security: dbSec);
return await JSONAsync(new
{
admin = dset.Table("admin").FirstRow.toObjectDictionary(),
data = dset.Tables("data").toArrayofObjectDictionaries()
data = dset.Tables("data").toArrayofObjectDictionaries()
});
}
private async Task<IActionResult> BuildWidgetResponse(string userAccountId,
DatabaseSecurity dbSec, Dictionary<string, object?> wdg)
// ── Widget renderer dispatcher ────────────────────────────────────────────
private async Task<IActionResult> BuildWidgetResponse(string userAccountId, DatabaseSecurity dbSec,
string shortName, Dictionary<string, object?> wdg)
{
string type = (wdg.nz("type", "") ?? "").ToLower();
string sql = wdg.nz("sql", "") ?? "";
string dbType = (wdg.nz("type", "") ?? "").ToLower();
string sql = wdg.nz("sql", "") ?? "";
var ropts = ParseRenderingOptions(wdg.nz("rendering_options", "") ?? "");
string name = wdg.nz("name", "") ?? "";
string descr = wdg.nz("description", "") ?? "";
if (type.StartsWith("sql") && !string.IsNullOrEmpty(sql))
object widgetData;
switch (dbType)
{
var dt = await getSQLDatatable_async(sql,
_intranet.Intranet__SQLConnectionString,
MakeParams(userAccountId), Security: dbSec);
return await JSONAsync(new
case "sql_table":
{
name = wdg.nz("name", ""),
type,
rows = dt.DataTable.Rows
.Cast<System.Data.DataRow>()
.Select(r => r.toObjectDictionary())
.ToArray()
});
var dt = await getSQLDatatable_async(sql, Conn, Params(userAccountId), Security: dbSec);
widgetData = new
{
name,
description = descr,
type = "table",
rendering_options = ropts,
columns = dt.DataTable.Columns.Cast<System.Data.DataColumn>()
.Select(c => c.ColumnName).ToArray(),
data = dt.DataTable.Rows.Cast<System.Data.DataRow>()
.Select(r => r.toObjectDictionary()).ToArray()
};
break;
}
case "sql_indicator":
{
var dt = await getSQLDatatable_async(sql, Conn, Params(userAccountId), Security: dbSec);
var firstRow = dt.DataTable.Rows.Count > 0
? dt.DataTable.Rows[0].toObjectDictionary()
: new Dictionary<string, object?>();
widgetData = new
{
name,
description = descr,
type = "ind",
rendering_options = ropts,
data = new
{
status = firstRow.nz("status", "") ?? "",
value = firstRow.nz("value", "") ?? "",
label = firstRow.nz("label", "") ?? ""
}
};
break;
}
case "html":
widgetData = new
{
name,
description = descr,
type = "html",
rendering_options = ropts,
html = wdg.nz("html", "") ?? ""
};
break;
default:
widgetData = new
{
name,
description = descr,
type = dbType,
rendering_options = ropts,
html = wdg.nz("html", "") ?? "",
url = wdg.nz("url", "") ?? "",
image = wdg.nz("image", "") ?? "",
data = (object)new
{
status = wdg.nz("status", "") ?? "",
value = wdg.nz("value", "") ?? "",
label = wdg.nz("label", "") ?? ""
}
};
break;
}
return await JSONAsync(wdg);
// Wrap under short_name key so the front-end can do response[wi]
return await JSONAsync(new Dictionary<string, object> { [shortName] = widgetData });
}
private static string[] ParseRenderingOptions(string raw) =>
string.IsNullOrWhiteSpace(raw)
? Array.Empty<string>()
: raw.Split(new[] { ',', ';', '|' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.ToArray();
}
-18
View File
@@ -1,18 +0,0 @@
using Microsoft.Data.SqlClient;
namespace Fuchs.Services;
/// <summary>
/// Factory for creating SQL connections to the Fuchs database.
/// </summary>
public interface IDbConnectionFactory
{
/// <summary>Gets the primary FDS connection string.</summary>
string ConnectionString { get; }
/// <summary>Creates and opens a new SQL connection.</summary>
SqlConnection CreateConnection();
/// <summary>Creates a new SQL connection (not opened).</summary>
SqlConnection CreateClosedConnection();
}
+2 -2
View File
@@ -13,8 +13,8 @@ public interface IInvoiceService
/// <summary>Loads an existing invoice by ID.</summary>
Task<FdsInvoiceData> LoadInvoiceAsync(string id, string userAccountId, DatabaseSecurity dbSec);
/// <summary>Registers (creates or updates) an invoice from form data.</summary>
Task<FdsInvoiceData> RegisterInvoiceAsync(object formData, bool change, string invId,
/// <summary>Registers (creates or updates) an invoice from a parsed data object.</summary>
Task<FdsInvoiceData> RegisterInvoiceAsync(FdsInvoiceData invoice, bool change, string invId,
string userAccountId, DatabaseSecurity dbSec);
/// <summary>Generates a PDF document for an invoice.</summary>
+2 -2
View File
@@ -13,8 +13,8 @@ public interface IReminderService
/// <summary>Loads an existing reminder by ID.</summary>
Task<FdsReminderData> LoadReminderAsync(string id, string userAccountId, DatabaseSecurity dbSec);
/// <summary>Registers (creates) a reminder from form data.</summary>
Task<FdsReminderData> RegisterReminderAsync(object formData, bool change, string remId,
/// <summary>Registers (creates) a reminder from a parsed data object.</summary>
Task<FdsReminderData> RegisterReminderAsync(FdsReminderData reminder, bool change, string remId,
string userAccountId, DatabaseSecurity dbSec);
/// <summary>Generates a PDF document for a reminder.</summary>
+12 -6
View File
@@ -1,15 +1,21 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using OCORE.security;
using OCORE.SQL;
namespace Fuchs.Services;
/// <summary>
/// Abstraction for report processing.
/// Abstraction for SQL-driven report processing (HTML page / fragment / PNG chart).
/// </summary>
public interface IReportService
{
/// <summary>Processes a report request.</summary>
Task<IActionResult> ProcessRequestAsync(string action, string id,
string userAccountId, DatabaseSecurity dbSec);
/// <summary>
/// Processes a report request.
/// </summary>
/// <param name="fnc">Target function: "generic", "generic_content"/"gct", "chart", or a query-type name.</param>
/// <param name="reportId">Report name/id (the URL <c>code</c> segment).</param>
/// <param name="userAccountId">Authenticated user id.</param>
/// <param name="dbSec">Database security context.</param>
/// <param name="parameters">Merged query-string + form parameters.</param>
Task<IActionResult> ProcessRequestAsync(string fnc, string reportId,
string userAccountId, DatabaseSecurity dbSec, IDictionary<string, string> parameters);
}
+98 -20
View File
@@ -1,62 +1,140 @@
using Fuchs.intranet;
using System.Data;
using Fuchs.intranet;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using MigraDoc.DocumentObjectModel;
using MigraDoc.Rendering;
using OCORE.security;
using Newtonsoft.Json;
using OCORE.SQL;
using static OCORE.commons;
using static OCORE.OCORE_dictionaries;
using static OCORE.SQL.sql;
namespace Fuchs.Services;
/// <summary>
/// Invoice service implementation. Extracts DB operations from <c>FdsInvoiceData</c>
/// and PDF generation into a proper DI service.
/// Invoice service — load, register, render and store invoices.
/// Replaces the controller-coupled, sync-over-async logic that previously lived
/// inside <see cref="FdsInvoiceData"/>.
/// </summary>
public class InvoiceService : IInvoiceService
{
private readonly Fuchs_intranet _intranet;
private readonly IPdfService _pdfService;
private readonly IPdfService _pdf;
private readonly ILogger<InvoiceService> _logger;
public InvoiceService(Fuchs_intranet intranet, IPdfService pdfService, ILogger<InvoiceService> logger)
public InvoiceService(Fuchs_intranet intranet, IPdfService pdf, ILogger<InvoiceService> logger)
{
_intranet = intranet;
_pdfService = pdfService;
_pdf = pdf;
_logger = logger;
}
private string Conn => _intranet.Intranet__SQLConnectionString;
public async Task<FdsInvoiceData> LoadInvoiceAsync(string id, string userAccountId, DatabaseSecurity dbSec)
{
// TODO: Complete after FdsInvoiceData is refactored to remove IntranetController dependency
throw new NotImplementedException("InvoiceService.LoadInvoiceAsync pending FdsInvoiceData refactor.");
var inv = new FdsInvoiceData();
if (string.IsNullOrEmpty(id)) return inv;
var pl = new List<SqlParameter>
{
SQL_VarChar("@authuser", userAccountId),
SQL_VarChar("@Id", id),
SQL_Bit("@includefile", false)
};
var dset = await getSQLDataSet_async(
"EXECUTE [dbo].[fds__getInvoice] @Id, @includefile, @authuser;",
Conn, pl, tablenames: new[] { "admin", "inv", "req", "itm" },
Security: dbSec, options: new FIS_SQLOptions());
if (!string.IsNullOrEmpty(dset.Exception))
_logger.LogError("LoadInvoiceAsync sql exception for {Id}: {Ex}", id, dset.Exception);
inv.InvoiceRegistration = new GenericObjectDictionary(dset.Table("inv").FirstRow.toObjectDictionary());
inv.IsDraft = inv.InvoiceRegistration.getItem("IsFinal", false) is not true;
return inv;
}
public async Task<FdsInvoiceData> RegisterInvoiceAsync(object formData, bool change, string invId,
public async Task<FdsInvoiceData> RegisterInvoiceAsync(FdsInvoiceData invoice, bool change, string invId,
string userAccountId, DatabaseSecurity dbSec)
{
throw new NotImplementedException("InvoiceService.RegisterInvoiceAsync pending FdsInvoiceData refactor.");
if (invoice.NewValues == null) return invoice;
var pl = new List<SqlParameter> { SQL_VarChar("@authuser", userAccountId) };
pl.AddRange(invoice.BuildInvoiceParams(change, invId));
var sqlParts = new List<string> { "DECLARE @Id varchar(10);" };
if (!change)
{
sqlParts.Add("EXECUTE [dbo].[fds__createInvoice] @InvoiceType, @InvoiceTitle, @InvoiceBalance, @InvoiceBalance_net, @InvoiceVAT_net1, @InvoiceVAT_1, @PaymentTerm, @CustomerId, @SendToAddress, @SendToEmail, @ProvisionPeriod, @CustomValues, @authuser, @Id OUTPUT;");
sqlParts.Add("EXECUTE [dbo].[fds__createInvoice_Details] @Id, @InvoiceService_net, @InvoiceService_VAT, @InvoiceOptions, @authuser;");
}
else
{
pl.Add(SQL_VarChar("@InvId", invId));
sqlParts.Add("EXECUTE [dbo].[fds__setInvoice] @InvId, @InvoiceType, @InvoiceTitle, @InvoiceBalance, @InvoiceBalance_net, @InvoiceVAT_net1, @InvoiceVAT_1, @PaymentTerm, @CustomerId, @SendToAddress, @SendToEmail, @ProvisionPeriod, @CustomValues, @authuser, @Id OUTPUT;");
sqlParts.Add("EXECUTE [dbo].[fds__createInvoice_Details] @Id, @InvoiceService_net, @InvoiceService_VAT, @InvoiceOptions, @authuser;");
}
if (invoice.RawProvisionLocation.Length > 0)
{
pl.Add(SQL_NVarChar("@ProvisionLocation",
string.Join("\n", invoice.RawProvisionLocation).LeftToFirst("<!--", emptyIfNotFound: false)));
sqlParts.Add("UPDATE [dbo].[fds__invoices] SET [ProvisionLocation] = LEFT(@ProvisionLocation,1000) WHERE [Id] = @Id AND [isFinal] = 0;");
}
var invdset = await getSQLDataSet_async(string.Join("\n", sqlParts),
Conn, pl, tablenames: new[] { "inv", "det", "req", "itm" },
Security: dbSec, options: new FIS_SQLOptions());
if (!string.IsNullOrEmpty(invdset.Exception))
_logger.LogError("RegisterInvoiceAsync sql exception: {Ex}", invdset.Exception);
invoice.InvoiceRegistration = new GenericObjectDictionary(invdset.Table("inv").FirstRow.toObjectDictionary());
return invoice;
}
public Document GenerateInvoicePdf(FdsInvoiceData invoice, bool draft)
{
throw new NotImplementedException("InvoiceService.GenerateInvoicePdf pending FdsInvoiceData refactor.");
var reg = invoice.InvoiceRegistration;
var tb = new FuchsPdf.FdsTextBlocks
{
AdminRef = reg?.getString("Id") ?? "",
Address = reg?.getString("SendToAddress") is { Length: > 0 } sa
? sa.Replace("<br>", "\n").Replace("<br/>", "\n").Split('\n').Select(t => t.Trim()).ToArray()
: Array.Empty<string>(),
AdminUser = reg?.getString("UserNameFinalized") ?? "",
AdminUserEmail = reg?.getString("UserEmailFinalized") ?? "",
AdminDatumValue = reg?.getString("DateCreated") is { Length: > 0 } dc ? DateTime.Parse(dc) : DateTime.Now
};
// WriteLetter is effectively synchronous; no Task.Run thread-hop (safe: no sync context in ASP.NET Core).
var doc = _pdf.WriteLetterAsync(tb, draft).GetAwaiter().GetResult();
doc.Info.Title = reg?.getString("InvoiceTitle") ?? "";
_pdf.ApplyInvoice(doc, tb, invoice, draft);
return doc;
}
public async Task<byte[]> RenderInvoicePdfBytesAsync(FdsInvoiceData invoice, bool draft)
{
throw new NotImplementedException("InvoiceService.RenderInvoicePdfBytesAsync pending FdsInvoiceData refactor.");
}
public Task<byte[]> RenderInvoicePdfBytesAsync(FdsInvoiceData invoice, bool draft)
=> Task.FromResult(_pdf.DocToPdfBytes(GenerateInvoicePdf(invoice, draft)));
public async Task<byte[]> StoreInvoiceDocumentFileAsync(FdsInvoiceData invoice, bool draft,
string userAccountId, DatabaseSecurity dbSec)
{
throw new NotImplementedException("InvoiceService.StoreInvoiceDocumentFileAsync pending FdsInvoiceData refactor.");
byte[] ba;
try { ba = await RenderInvoicePdfBytesAsync(invoice, draft); }
catch (Exception ex) { _logger.LogError(ex, "StoreInvoiceDocumentFileAsync render failed for {Id}", invoice.Id); ba = Array.Empty<byte>(); }
if (ba.Length == 0) return Array.Empty<byte>();
var pl = new List<SqlParameter>
{
SQL_VarChar("@authuser", userAccountId),
SQL_VarChar("@Id", invoice.Id),
new("@file", SqlDbType.VarBinary) { Value = ba }
};
bool r = await setSQLValue_async(
"EXECUTE [dbo].[fds__setInvoiceFile] @Id, @file;",
Conn, pl, Security: dbSec, options: new FIS_SQLOptions());
return r ? ba : Array.Empty<byte>();
}
public async Task<byte[]?> GetInvoiceFileAsync(FdsInvoiceData invoice, bool draft,
fds.IFdsMfr mfr)
public async Task<byte[]?> GetInvoiceFileAsync(FdsInvoiceData invoice, bool draft, fds.IFdsMfr mfr)
{
if (invoice.InvoiceRegistration?.getItem("IsFinal", false) is true)
{
+119 -17
View File
@@ -1,67 +1,169 @@
using Fuchs.intranet;
using System.Data;
using Fuchs.intranet;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using MigraDoc.DocumentObjectModel;
using OCORE.security;
using OCORE.SQL;
using static OCORE.commons;
using static OCORE.OCORE_dictionaries;
using static OCORE.SQL.sql;
namespace Fuchs.Services;
/// <summary>
/// Reminder service implementation. Extracts DB operations from <c>FdsReminderData</c>
/// into a proper DI service.
/// Reminder service — load, register, render and store reminders.
/// Replaces the controller-coupled, sync-over-async logic that previously lived
/// inside <see cref="FdsReminderData"/>.
/// </summary>
public class ReminderService : IReminderService
{
private readonly Fuchs_intranet _intranet;
private readonly IPdfService _pdfService;
private readonly IPdfService _pdf;
private readonly ILogger<ReminderService> _logger;
public ReminderService(Fuchs_intranet intranet, IPdfService pdfService, ILogger<ReminderService> logger)
public ReminderService(Fuchs_intranet intranet, IPdfService pdf, ILogger<ReminderService> logger)
{
_intranet = intranet;
_pdfService = pdfService;
_pdf = pdf;
_logger = logger;
}
private string Conn => _intranet.Intranet__SQLConnectionString;
public async Task<FdsReminderData> LoadReminderAsync(string id, string userAccountId, DatabaseSecurity dbSec)
{
throw new NotImplementedException("ReminderService.LoadReminderAsync pending FdsReminderData refactor.");
var rem = new FdsReminderData();
if (string.IsNullOrEmpty(id)) return rem;
var pl = new List<SqlParameter>
{
SQL_VarChar("Id", id),
SQL_VarChar("@authuser", userAccountId),
SQL_Bit("@includefile", false)
};
var dset = await getSQLDataSet_async(
"EXECUTE [dbo].[fds__getReminder] @Id, @includefile, @authuser;",
Conn, pl, tablenames: new[] { "admin", "rem" },
Security: dbSec, options: new FIS_SQLOptions());
if (!string.IsNullOrEmpty(dset.Exception))
_logger.LogError("LoadReminderAsync sql exception for {Id}: {Ex}", id, dset.Exception);
rem.ReminderRegistration = new GenericObjectDictionary(dset.Table("rem").FirstRow.toObjectDictionary());
rem.IsDraft = rem.ReminderRegistration.getItem("IsFinal") is not true;
return rem;
}
public async Task<FdsReminderData> RegisterReminderAsync(object formData, bool change, string remId,
public async Task<FdsReminderData> RegisterReminderAsync(FdsReminderData reminder, bool change, string remId,
string userAccountId, DatabaseSecurity dbSec)
{
throw new NotImplementedException("ReminderService.RegisterReminderAsync pending FdsReminderData refactor.");
if (reminder.Rem == null || reminder.Rem.Count == 0) return reminder;
var pl = new List<SqlParameter>
{
SQL_VarChar("@authuser", userAccountId),
SQL_VarChar("InvId", reminder.RawInvId),
SQL_Char("type", reminder.Rem["type"]?.ToString() ?? ""),
SQL_Float("amount", stringvalue: reminder.NewValues?.nz("amount") ?? ""),
SQL_Float("amount_payed", stringvalue: reminder.NewValues?.nz("amount_payed") ?? ""),
SQL_VarChar("SendToAddress", string.Join("\n", reminder.RawInvoiceAddress)),
SQL_NVarChar("SendToEmail", reminder.RawInvoiceEmail),
SQL_NVarChar("subject", reminder.NewValues?.getString("subject") ?? "", dbNull_IfEmpty: true),
SQL_NVarChar("text", reminder.NewValues?.nz("text") ?? "", dbNull_IfEmpty: true)
};
var sqlParts = new List<string> { "DECLARE @Id varchar(10);" };
if (!change || string.IsNullOrEmpty(remId))
sqlParts.Add("EXECUTE [dbo].[fds__createReminder] @InvId, @type, @amount, @amount_payed, @SendToAddress, @SendToEmail, @subject, @text, @authuser, @Id OUTPUT;");
else
pl.Add(SQL_VarChar("RemId", remId));
var remdset = await getSQLDataSet_async(string.Join("\n", sqlParts),
Conn, pl, tablenames: new[] { "rem" },
Security: dbSec, options: new FIS_SQLOptions());
if (!string.IsNullOrEmpty(remdset.Exception))
_logger.LogError("RegisterReminderAsync sql exception: {Ex}", remdset.Exception);
reminder.ReminderRegistration = new GenericObjectDictionary(remdset.Table("rem").FirstRow.toObjectDictionary());
return reminder;
}
public Document GenerateReminderPdf(FdsReminderData reminder, bool draft)
{
throw new NotImplementedException("ReminderService.GenerateReminderPdf pending FdsReminderData refactor.");
var tb = new FuchsPdf.FdsTextBlocks
{
AdminRef = "",
Address = reminder.InvoiceAddress,
AdminUser = reminder.UserNameFinalized,
AdminUserEmail = reminder.UserEmailFinalized,
AdminDatumValue = reminder.DateCreated ?? DateTime.Now
};
if (!string.IsNullOrEmpty(reminder.RawCustomValues))
{
var o = new GenericObjectDictionary(reminder.RawCustomValues);
string oEmail = (string?)o.getItem("contactEmail") ?? "";
string oName = (string?)o.getItem("contactName") ?? "";
if (!string.IsNullOrEmpty(oEmail) || !string.IsNullOrEmpty(oName))
{
tb.AdminUser = oName;
tb.AdminUserEmail = oEmail;
}
}
var doc = _pdf.WriteLetterAsync(tb, draft).GetAwaiter().GetResult();
doc.Info.Title = reminder.ReminderTitle;
_pdf.ApplyReminder(doc, tb, reminder, draft);
return doc;
}
public async Task<byte[]> RenderReminderPdfBytesAsync(FdsReminderData reminder, bool draft)
{
throw new NotImplementedException("ReminderService.RenderReminderPdfBytesAsync pending FdsReminderData refactor.");
}
public Task<byte[]> RenderReminderPdfBytesAsync(FdsReminderData reminder, bool draft)
=> Task.FromResult(_pdf.DocToPdfBytes(GenerateReminderPdf(reminder, draft)));
public async Task<byte[]> StoreReminderDocumentFileAsync(FdsReminderData reminder, bool draft,
string userAccountId, DatabaseSecurity dbSec)
{
throw new NotImplementedException("ReminderService.StoreReminderDocumentFileAsync pending FdsReminderData refactor.");
byte[] ba;
try { ba = await RenderReminderPdfBytesAsync(reminder, draft); }
catch (Exception ex) { _logger.LogError(ex, "StoreReminderDocumentFileAsync render failed for {Id}", reminder.Id); ba = Array.Empty<byte>(); }
if (ba.Length == 0) return Array.Empty<byte>();
var pl = new List<SqlParameter>
{
SQL_VarChar("Id", reminder.Id),
SQL_VarChar("@authuser", userAccountId),
new("@file", SqlDbType.VarBinary) { Value = ba }
};
bool r = await setSQLValue_async(
"EXECUTE [dbo].[fds__setReminderFile] @Id, @file;",
Conn, pl, Security: dbSec, options: new FIS_SQLOptions());
return r ? ba : Array.Empty<byte>();
}
public async Task<byte[]> GetReminderFileAsync(FdsReminderData reminder, bool draft,
fds.IFdsMfr mfr, string userAccountId, DatabaseSecurity dbSec)
{
throw new NotImplementedException("ReminderService.GetReminderFileAsync pending FdsReminderData refactor.");
if (reminder.ReminderRegistration?.getItem("IsFinal", false) is true)
{
if (!reminder.ReminderRegistration.ContainsKey("hasFile") ||
reminder.ReminderRegistration.getItem("hasFile", false) is not true)
await StoreReminderDocumentFileAsync(reminder, draft, userAccountId, dbSec);
byte[]? ba = null;
mfr.GetFdsDoc(ref ba, reminder.Id, "reminder");
return ba ?? Array.Empty<byte>();
}
return await RenderReminderPdfBytesAsync(reminder, draft);
}
public async Task<(FileInfo? file, byte[]? content)> GetStoredFileAsync(string reminderId,
string userAccountId, DatabaseSecurity dbSec)
{
throw new NotImplementedException("ReminderService.GetStoredFileAsync pending FdsReminderData refactor.");
var dset = await getSQLDataSet_async(
"SELECT TOP(1) * FROM [dbo].[fds__reminder] WHERE [Id] = @Id AND [file] is not null;",
Conn,
new List<SqlParameter> { SQL_VarChar("@authuser", userAccountId), SQL_VarChar("@Id", reminderId) },
Security: dbSec, options: new FIS_SQLOptions());
var row = dset.FirstTable().FirstRow.toObjectDictionary();
if (row.Count > 0 && !string.IsNullOrEmpty(row.nz("DocumentName")) && row.no("file", null!) is byte[] b)
return (new FileInfo(row.nz("DocumentName")), b);
return (null, null);
}
}