diff --git a/Fuchs.Tests/BankingTests.cs b/Fuchs.Tests/BankingTests.cs
index 225b5f3..b6904e7 100644
--- a/Fuchs.Tests/BankingTests.cs
+++ b/Fuchs.Tests/BankingTests.cs
@@ -1,17 +1,20 @@
-using System.Data;
+using System.Data;
using System.IO;
using System.Text;
-using Fuchs.intranet;
+using Fuchs.Services;
+using Microsoft.Extensions.Logging.Abstractions;
using programmersdigest.MT940Parser;
using Xunit;
namespace Fuchs.Tests;
///
-/// Banking helper robustness tests.
+/// Banking service robustness tests.
///
public class BankingDebitCreditMarkTests
{
+ private static readonly BankingService Svc = new(NullLogger.Instance);
+
[Theory]
[InlineData(DebitCreditMark.Credit, "C")]
[InlineData(DebitCreditMark.Debit, "D")]
@@ -19,18 +22,20 @@ public class BankingDebitCreditMarkTests
[InlineData(DebitCreditMark.ReverseDebit, "RD")]
public void DebitCreditMarkAbb_ReturnsExpected(DebitCreditMark mark, string expected)
{
- Assert.Equal(expected, Banking.DebitCreditMarkAbb(mark));
+ Assert.Equal(expected, Svc.DebitCreditMarkAbb(mark));
}
[Fact]
public void DebitCreditMarkAbb_UndefinedValue_ReturnsEmpty()
{
- Assert.Equal("", Banking.DebitCreditMarkAbb((DebitCreditMark)999));
+ Assert.Equal("", Svc.DebitCreditMarkAbb((DebitCreditMark)999));
}
}
public class BankingParseToDatatableTests
{
+ private static readonly BankingService Svc = new(NullLogger.Instance);
+
private static readonly string MinimalMT940 =
"\r\n:20:STARTUMSE\r\n" +
":25:DE12345678901234567890\r\n" +
@@ -48,7 +53,7 @@ public class BankingParseToDatatableTests
public void ParseToDatatable_ValidMT940_ReturnsOneRow()
{
using var stream = ToStream(MinimalMT940);
- var table = Banking.ParseToDatatable(stream);
+ var table = Svc.ParseToDatatable(stream);
Assert.Equal(1, table.Rows.Count);
}
@@ -56,7 +61,7 @@ public class BankingParseToDatatableTests
public void ParseToDatatable_ValidMT940_HasAccountColumn()
{
using var stream = ToStream(MinimalMT940);
- var table = Banking.ParseToDatatable(stream);
+ var table = Svc.ParseToDatatable(stream);
Assert.True(table.Columns.Contains("AccountIdentification"));
Assert.Equal("DE12345678901234567890", table.Rows[0]["AccountIdentification"]);
}
@@ -65,7 +70,7 @@ public class BankingParseToDatatableTests
public void ParseToDatatable_ValidMT940_HasAmountColumn()
{
using var stream = ToStream(MinimalMT940);
- var table = Banking.ParseToDatatable(stream);
+ var table = Svc.ParseToDatatable(stream);
Assert.True(table.Columns.Contains("Amount"));
Assert.Equal(500m, table.Rows[0]["Amount"]);
}
@@ -74,7 +79,7 @@ public class BankingParseToDatatableTests
public void ParseToDatatable_ValidMT940_HasDebitCreditMark()
{
using var stream = ToStream(MinimalMT940);
- var table = Banking.ParseToDatatable(stream);
+ var table = Svc.ParseToDatatable(stream);
Assert.Equal("C", table.Rows[0]["DebitCreditMark"]);
}
@@ -82,7 +87,7 @@ public class BankingParseToDatatableTests
public void ParseToDatatable_EmptyStream_ReturnsEmptyTable()
{
using var stream = ToStream("");
- var table = Banking.ParseToDatatable(stream);
+ var table = Svc.ParseToDatatable(stream);
Assert.Equal(0, table.Rows.Count);
}
@@ -90,7 +95,7 @@ public class BankingParseToDatatableTests
public void ParseToDatatable_EmptyStream_HasDefaultSchema()
{
using var stream = ToStream("");
- var table = Banking.ParseToDatatable(stream);
+ var table = Svc.ParseToDatatable(stream);
Assert.True(table.Columns.Contains("AccountIdentification"));
Assert.True(table.Columns.Contains("Amount"));
Assert.True(table.Columns.Contains("DebitCreditMark"));
@@ -104,7 +109,7 @@ public class BankingParseToDatatableTests
schema.Columns.Add("Amount", typeof(decimal));
using var stream = ToStream(MinimalMT940);
- var table = Banking.ParseToDatatable(stream, schemaDatatable: schema);
+ var table = Svc.ParseToDatatable(stream, schemaDatatable: schema);
Assert.Equal(2, table.Columns.Count);
}
@@ -113,7 +118,7 @@ public class BankingParseToDatatableTests
{
var multi = MinimalMT940 + "\n" + MinimalMT940;
using var stream = ToStream(multi);
- var table = Banking.ParseToDatatable(stream);
+ var table = Svc.ParseToDatatable(stream);
Assert.Equal(2, table.Rows.Count);
}
@@ -121,7 +126,7 @@ public class BankingParseToDatatableTests
public void ParseToDatatable_MalformedContent_DoesNotThrow()
{
using var stream = ToStream("This is not MT940 data at all");
- var ex = Record.Exception(() => Banking.ParseToDatatable(stream));
+ var ex = Record.Exception(() => Svc.ParseToDatatable(stream));
Assert.Null(ex);
}
}
diff --git a/Fuchs.Tests/FdsDataTests.cs b/Fuchs.Tests/FdsDataTests.cs
new file mode 100644
index 0000000..5533cff
--- /dev/null
+++ b/Fuchs.Tests/FdsDataTests.cs
@@ -0,0 +1,83 @@
+using Fuchs.intranet;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Xunit;
+
+namespace Fuchs.Tests;
+
+///
+/// Tests for the refactored data holders (FdsInvoiceData / FdsReminderData).
+/// These are now pure data objects — persistence/PDF moved to the DI services.
+///
+public class FdsDataTests
+{
+ [Fact]
+ public void FdsInvoiceData_Empty_DefaultsToDraftWithNoRegistration()
+ {
+ var inv = new FdsInvoiceData();
+ Assert.True(inv.IsDraft);
+ Assert.Null(inv.InvoiceRegistration);
+ Assert.Equal("", inv.Id);
+ Assert.Equal("R", inv.InvoiceType); // fallback when no registration
+ }
+
+ [Fact]
+ public void FdsInvoiceData_ParsesFormSections()
+ {
+ var jo = JObject.Parse(
+ "{ \"admin\": { \"type\": \"R\", \"customerid\": \"42\" }, " +
+ " \"new\": { \"title\": \"Test\", \"total_gross\": \"119.00\" }, " +
+ " \"sms\": { \"tscn\": \"10\" }, " +
+ " \"req\": [] }");
+ var inv = new FdsInvoiceData(jo);
+
+ Assert.NotNull(inv.Admin);
+ Assert.NotNull(inv.NewValues);
+ Assert.NotNull(inv.Sms);
+ Assert.Equal("R", inv.Admin!.getString("type"));
+ Assert.Equal("Test", inv.NewValues!.getString("title"));
+ Assert.True(inv.IsDraft);
+ }
+
+ [Fact]
+ public void FdsInvoiceData_BuildInvoiceParams_PicksHighestVatAndMapsFields()
+ {
+ // two line items with different VAT rates → highest (19) selected
+ var jo = JObject.Parse(
+ "{ \"admin\": { \"type\": \"R\", \"customerid\": \"7\", \"p13b\": false }, " +
+ " \"new\": { \"title\": \"Bad\", \"total_gross\": \"119\", \"total_net\": \"100\", \"vat_19_net\": \"100\", \"paymentterm\": \"10d\" }, " +
+ " \"req\": [ { \"items\": [ { \"vat\": \"7%\" }, { \"vat\": \"19%\" } ] } ] }");
+ var inv = new FdsInvoiceData(jo);
+
+ var pl = inv.BuildInvoiceParams(change: false, invId: "");
+ var vat = pl.Find(p => p.ParameterName == "@InvoiceVAT_1");
+ var title = pl.Find(p => p.ParameterName == "@InvoiceTitle");
+
+ Assert.NotNull(vat);
+ Assert.Equal("19", vat!.Value);
+ Assert.Equal("Bad", title!.Value);
+ }
+
+ [Fact]
+ public void FdsReminderData_Empty_DefaultsToDraft()
+ {
+ var rem = new FdsReminderData();
+ Assert.True(rem.IsDraft);
+ Assert.Null(rem.ReminderRegistration);
+ Assert.Equal("", rem.Id);
+ }
+
+ [Fact]
+ public void FdsReminderData_ParsesFormSections()
+ {
+ var jo = JObject.Parse(
+ "{ \"new\": { \"amount\": \"50\", \"invoiceemail\": \"a@b.de\" }, " +
+ " \"rem\": { \"type\": \"f\", \"invid\": \"123\" } }");
+ var rem = new FdsReminderData(jo);
+
+ Assert.NotNull(rem.NewValues);
+ Assert.NotNull(rem.Rem);
+ Assert.Equal("50", rem.NewValues!.getString("amount"));
+ Assert.Equal("f", rem.Rem!.getString("type"));
+ }
+}
diff --git a/Fuchs/Controllers/IntranetController.Banking.cs b/Fuchs/Controllers/IntranetController.Banking.cs
index c5dcda6..36c4e4a 100644
--- a/Fuchs/Controllers/IntranetController.Banking.cs
+++ b/Fuchs/Controllers/IntranetController.Banking.cs
@@ -29,7 +29,7 @@ public partial class IntranetController
_intranet.Intranet__SQLConnectionString,
Security: DbSec, options: SqlOpt(fn, id, code))).DataTable;
- var tbl = Banking.ParseToDatatable(stream, schemaDt);
+ var tbl = _banking.ParseToDatatable(stream, schemaDt);
var tmptbl = "bs_" + Guid.NewGuid().ToString().Replace("-", "");
var dtwa = new DatatableWriterAsync(tbl, _intranet.Intranet__SQLConnectionString)
diff --git a/Fuchs/Controllers/IntranetController.Invoices.cs b/Fuchs/Controllers/IntranetController.Invoices.cs
index 26efcca..328a2d7 100644
--- a/Fuchs/Controllers/IntranetController.Invoices.cs
+++ b/Fuchs/Controllers/IntranetController.Invoices.cs
@@ -146,7 +146,7 @@ public partial class IntranetController
return BadRequest400();
}
_logger.LogInformation("mfrrel: resetting MFR relation for invoice {InvoiceId}, user={User}", relId, UserAccountID);
- using (var mfr = new fds.FdsMfrClient())
+ using (var mfr = _mfrFactory.Create())
await mfr.Update__entitytable(EntityTypes.Invoice,
fds.FdsMfr.UpdateNeed.Reset, new[] { relId });
return Ok();
diff --git a/Fuchs/Controllers/IntranetController.Invoices2.cs b/Fuchs/Controllers/IntranetController.Invoices2.cs
index 76505e8..fbc1019 100644
--- a/Fuchs/Controllers/IntranetController.Invoices2.cs
+++ b/Fuchs/Controllers/IntranetController.Invoices2.cs
@@ -22,7 +22,7 @@ public partial class IntranetController
if (!long.TryParse(Form("id"), out long tgtid)) { _logger.LogWarning("HandleInvoicePget: invalid 'id' value='{Value}' user={User}", Form("id"), UserAccountID); return BadRequest400(); }
_logger.LogDebug("HandleInvoicePget tgtid={TgtId} user={User}", tgtid, UserAccountID);
- using (var mfr = new fds.FdsMfrClient())
+ using (var mfr = _mfrFactory.Create())
{
_logger.LogDebug("HandleInvoicePget resetting invoice entity tgtid={TgtId}", tgtid);
await mfr.Update__entitytable(EntityTypes.Invoice,
@@ -50,7 +50,7 @@ public partial class IntranetController
}
}
_logger.LogDebug("HandleInvoicePget resetting {InvCount} invoices and {SrqCount} service requests", invIds.Count, srqIds.Count);
- using var mfr2 = new fds.FdsMfrClient();
+ using var mfr2 = _mfrFactory.Create();
foreach (var iid in invIds)
await mfr2.Update__entitytable(EntityTypes.Invoice, fds.FdsMfr.UpdateNeed.Reset, new[] { iid });
foreach (var iid in srqIds)
@@ -413,9 +413,9 @@ public partial class IntranetController
return ldic;
}
- private static async Task BuildPdfImageArray(byte[] content)
+ private async Task BuildPdfImageArray(byte[] content)
{
- var imgcol = await FuchsPdf.BytesToImageCollection(content);
+ var imgcol = await _pdf.BytesToImageCollectionAsync(content);
return imgcol.ImgB64Array;
}
}
diff --git a/Fuchs/Controllers/IntranetController.Reminder.cs b/Fuchs/Controllers/IntranetController.Reminder.cs
index 7ecaafe..3d2a1d1 100644
--- a/Fuchs/Controllers/IntranetController.Reminder.cs
+++ b/Fuchs/Controllers/IntranetController.Reminder.cs
@@ -38,11 +38,11 @@ public partial class IntranetController
{
if (!HasForm("remc")) return BadRequest400();
var ctd = JsonConvert.DeserializeObject(Form("remc"))!;
- var fdRem = new FdsReminderData(ctd);
- fdRem.RegisterReminder(this, change: false, remId: "");
+ var fdRem = await _reminders.RegisterReminderAsync(
+ new FdsReminderData(ctd), change: false, remId: "", UserAccountID, DbSec);
if (!string.IsNullOrEmpty(fdRem.Id))
{
- var imgcol = await FuchsPdf.DocToImageCollection(fdRem.ReminderPDF(this));
+ 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" });
@@ -64,12 +64,11 @@ public partial class IntranetController
case "rdoc":
{
if (!HasForm("id")) return BadRequest400();
- byte[]? fc = null;
- var file = FdsReminderData.GetStoredFile(ref fc, Form("id"), this);
- if (file == null) return StatusCode(404, new { error = "Dokument wurde nicht gefunden" });
+ var (file, fc) = await _reminders.GetStoredFileAsync(Form("id"), UserAccountID, DbSec);
+ if (file == null || fc == null) return StatusCode(404, new { error = "Dokument wurde nicht gefunden" });
return Form("typ") != "img"
- ? await FileContentResultAsync(fc!, file.MimeType(), file.Name)
- : await JSONAsync(new { id = Form("id"), img = await BuildPdfImageArray(fc!) });
+ ? await FileContentResultAsync(fc, file.MimeType(), file.Name)
+ : await JSONAsync(new { id = Form("id"), img = await BuildPdfImageArray(fc) });
}
case "idoc": return await HandleReminderIdoc(fn, id, code);
@@ -107,8 +106,8 @@ public partial class IntranetController
if (frdic.TryGetValue("IsFinal", out var isFinal) && isFinal is true)
{
string remId = frdic["Id"]?.ToString() ?? "";
- var fdRem = new FdsReminderData(remId, this);
- byte[] filebyte = await fdRem.StoreReminderDocumentFile(this);
+ var fdRem = await _reminders.LoadReminderAsync(remId, UserAccountID, DbSec);
+ byte[] filebyte = await _reminders.StoreReminderDocumentFileAsync(fdRem, fdRem.IsDraft, UserAccountID, DbSec);
string email = frdic.nz("SendToEmail", "");
if (!string.IsNullOrEmpty(email) && filebyte.Length > 0)
{
@@ -140,17 +139,17 @@ public partial class IntranetController
private async Task HandleReminderIdoc(string fn, string id, string code)
{
if (!HasForm("id") || string.IsNullOrEmpty(Form("id"))) return StatusCode(404);
- var fdRem = new FdsReminderData(Form("id"), this);
+ var fdRem = await _reminders.LoadReminderAsync(Form("id"), UserAccountID, DbSec);
if (string.IsNullOrEmpty(fdRem.Id)) return StatusCode(404, new { error = "Erinnerung wurde nicht gefunden" });
- string filename = fdRem.ReminderRegistration.nz("DocumentName").ne($"Zahlungserinnerung_{fdRem.Id}.pdf");
+ string filename = fdRem.ReminderRegistration!.nz("DocumentName").ne($"Zahlungserinnerung_{fdRem.Id}.pdf");
if (Form("typ") != "img")
{
byte[] ct = Form("create", "0") != "1"
- ? (await fdRem.GetReminderFile(this)) is { Length: > 0 } f1 ? f1 : await fdRem.StoreReminderDocumentFile(this)
- : FuchsPdf.DocToPdfBytes(fdRem.ReminderPDF(this));
+ ? (await _reminders.GetReminderFileAsync(fdRem, fdRem.IsDraft, _mfr, UserAccountID, DbSec)) is { Length: > 0 } f1 ? f1 : await _reminders.StoreReminderDocumentFileAsync(fdRem, fdRem.IsDraft, UserAccountID, DbSec)
+ : _pdf.DocToPdfBytes(_reminders.GenerateReminderPdf(fdRem, fdRem.IsDraft));
return await FileContentResultAsync(ct, "application/pdf", filename, inline: true);
}
- var imgcol = await FuchsPdf.DocToImageCollection(fdRem.ReminderPDF(this));
+ var imgcol = await _pdf.DocToImageCollectionAsync(_reminders.GenerateReminderPdf(fdRem, fdRem.IsDraft));
return await JSONAsync(new { id = fdRem.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages });
}
diff --git a/Fuchs/Controllers/IntranetController.Reports.cs b/Fuchs/Controllers/IntranetController.Reports.cs
index aa90b53..aa88b06 100644
--- a/Fuchs/Controllers/IntranetController.Reports.cs
+++ b/Fuchs/Controllers/IntranetController.Reports.cs
@@ -46,7 +46,7 @@ public partial class IntranetController
}
default:
- return await FuchsReports.ProcessFdsRequest(this, id.ToLower(), code);
+ return await _reports.ProcessRequestAsync(id.ToLower(), code, UserAccountID, DbSec, RequestParamsDict());
}
}
}
diff --git a/Fuchs/Controllers/IntranetController.Requests.cs b/Fuchs/Controllers/IntranetController.Requests.cs
index f8f7539..49c2004 100644
--- a/Fuchs/Controllers/IntranetController.Requests.cs
+++ b/Fuchs/Controllers/IntranetController.Requests.cs
@@ -45,8 +45,9 @@ public partial class IntranetController
case "save":
{
if (!HasForm("invc")) return BadRequest400();
- var fdInv = new FdsInvoiceData(JsonConvert.DeserializeObject(Form("invc"))!);
- fdInv.RegisterInvoice(this, change: !string.IsNullOrEmpty(Form("id")), invId: Form("id"));
+ var fdInv = await _invoices.RegisterInvoiceAsync(
+ new FdsInvoiceData(JsonConvert.DeserializeObject(Form("invc"))!),
+ change: !string.IsNullOrEmpty(Form("id")), invId: Form("id"), UserAccountID, DbSec);
return !string.IsNullOrEmpty(fdInv.Id)
? await JSONAsync(new { id = fdInv.Id })
: StatusCode(500, new { error = "Rechnung wurde nicht gespeichert" });
@@ -55,11 +56,12 @@ public partial class IntranetController
case "sprep":
{
if (!HasForm("invc")) return BadRequest400();
- var fdInv = new FdsInvoiceData(JsonConvert.DeserializeObject(Form("invc"))!);
- fdInv.RegisterInvoice(this, change: false, invId: "");
+ var fdInv = await _invoices.RegisterInvoiceAsync(
+ new FdsInvoiceData(JsonConvert.DeserializeObject(Form("invc"))!),
+ change: false, invId: "", UserAccountID, DbSec);
if (!string.IsNullOrEmpty(fdInv.Id))
{
- var imgcol = await FuchsPdf.DocToImageCollection(fdInv.InvoicePDF(this));
+ 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" });
@@ -68,11 +70,12 @@ public partial class IntranetController
case "sedit":
{
if (!HasForm("id", "invc")) return BadRequest400();
- var fdInv = new FdsInvoiceData(JsonConvert.DeserializeObject(Form("invc"))!);
- fdInv.RegisterInvoice(this, change: true, invId: Form("id"));
+ var fdInv = await _invoices.RegisterInvoiceAsync(
+ new FdsInvoiceData(JsonConvert.DeserializeObject(Form("invc"))!),
+ change: true, invId: Form("id"), UserAccountID, DbSec);
if (!string.IsNullOrEmpty(fdInv.Id))
{
- var imgcol = await FuchsPdf.DocToImageCollection(fdInv.InvoicePDF(this));
+ 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" });
@@ -171,7 +174,7 @@ public partial class IntranetController
[EntityHelper.EntityName(EntityTypes.ServiceRequest)] =
new fds.FdsMfrClient.DatabaseSchema(EntityTypes.ServiceRequest)
};
- using var mfr = new fds.FdsMfrClient();
+ using var mfr = _mfrFactory.Create();
await mfr.Update__entitytable(EntityTypes.ServiceRequest,
fds.FdsMfr.UpdateNeed.Reset, ids.ToArray(), schemaDic: schemaDic);
return Ok();
@@ -258,8 +261,8 @@ public partial class IntranetController
if (frdic.TryGetValue("IsFinal", out var isFinal) && isFinal is true)
{
string invId = frdic["Id"]?.ToString() ?? "";
- var fdInv = new FdsInvoiceData(invId, this);
- byte[] filebyte = await fdInv.StoreInvoiceDocumentFile(this);
+ var fdInv = await _invoices.LoadInvoiceAsync(invId, UserAccountID, DbSec);
+ byte[] filebyte = await _invoices.StoreInvoiceDocumentFileAsync(fdInv, fdInv.IsDraft, UserAccountID, DbSec);
var dtset = await getSQLDataSet_async(
"EXECUTE [dbo].[fds__getInvoice] @Id, @authuser;",
_intranet.Intranet__SQLConnectionString,
@@ -293,19 +296,19 @@ public partial class IntranetController
private async Task HandleRequestIdoc(string fn, string id, string code)
{
if (!HasForm("id") || string.IsNullOrEmpty(Form("id"))) return StatusCode(404);
- var fdInv = new FdsInvoiceData(Form("id"), this);
+ var fdInv = await _invoices.LoadInvoiceAsync(Form("id"), UserAccountID, DbSec);
if (string.IsNullOrEmpty(fdInv.Id)) return StatusCode(404, new { error = "Rechnung wurde nicht gefunden" });
- string filename = fdInv.InvoiceRegistration.nz("DocumentName").ne($"Rechnung_{fdInv.Id}.pdf");
+ string filename = fdInv.InvoiceRegistration!.nz("DocumentName").ne($"Rechnung_{fdInv.Id}.pdf");
if (Form("typ") != "img")
{
byte[]? ct = Form("create", "0") != "1"
- ? await fdInv.GetInvoiceFile(this) is { Length: > 0 } f1 ? f1 : await fdInv.StoreInvoiceDocumentFile(this)
- : FuchsPdf.DocToPdfBytes(fdInv.InvoicePDF(this));
+ ? await _invoices.GetInvoiceFileAsync(fdInv, fdInv.IsDraft, _mfr) is { Length: > 0 } f1 ? f1 : await _invoices.StoreInvoiceDocumentFileAsync(fdInv, fdInv.IsDraft, UserAccountID, DbSec)
+ : _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" });
}
- var imgcol = await FuchsPdf.DocToImageCollection(fdInv.InvoicePDF(this));
+ var imgcol = await _pdf.DocToImageCollectionAsync(_invoices.GenerateInvoicePdf(fdInv, fdInv.IsDraft));
return await JSONAsync(new { id = fdInv.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages });
}
@@ -322,8 +325,8 @@ public partial class IntranetController
if (frdic.TryGetValue("IsFinal", out var isFinal) && isFinal is true)
{
string invId = frdic["Id"]?.ToString() ?? "";
- var fdInv = new FdsInvoiceData(invId, this);
- byte[] filebyte = FuchsPdf.DocToPdfBytes(fdInv.InvoicePDF(this));
+ var fdInv = await _invoices.LoadInvoiceAsync(invId, UserAccountID, DbSec);
+ byte[] filebyte = await _invoices.RenderInvoicePdfBytesAsync(fdInv, fdInv.IsDraft);
string email = frdic.nz("SendToEmail", "");
if (!string.IsNullOrEmpty(email) && filebyte.Length > 0)
{
diff --git a/Fuchs/Controllers/IntranetController.cs b/Fuchs/Controllers/IntranetController.cs
index 81633ea..4ec74b7 100644
--- a/Fuchs/Controllers/IntranetController.cs
+++ b/Fuchs/Controllers/IntranetController.cs
@@ -26,6 +26,13 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
internal readonly fds.IFdsMfr _mfr;
private readonly ILogger _logger;
private readonly IComService _comService;
+ private readonly IBankingService _banking;
+ private readonly IPdfService _pdf;
+ private readonly IMfrClientFactory _mfrFactory;
+ private readonly IWidgetService _widgets;
+ private readonly IReportService _reports;
+ private readonly IInvoiceService _invoices;
+ private readonly IReminderService _reminders;
private readonly List _allowedNonAuth = new() { "spwc", "spw" };
private readonly List _allowedGet = new()
{
@@ -41,12 +48,40 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
public string UserAccountID => UserIdent.UserAccountId;
public string AuthAccount => UserIdent.Email;
- public IntranetController(Fuchs_intranet intranet, fds.IFdsMfr mfr, ILogger logger, IComService comService)
+ public IntranetController(
+ Fuchs_intranet intranet,
+ fds.IFdsMfr mfr,
+ ILogger logger,
+ IComService comService,
+ IBankingService banking,
+ IPdfService pdf,
+ IMfrClientFactory mfrFactory,
+ IWidgetService widgets,
+ IReportService reports,
+ IInvoiceService invoices,
+ IReminderService reminders)
{
_intranet = intranet;
_mfr = mfr;
_logger = logger;
_comService = comService;
+ _banking = banking;
+ _pdf = pdf;
+ _mfrFactory = mfrFactory;
+ _widgets = widgets;
+ _reports = reports;
+ _invoices = invoices;
+ _reminders = reminders;
+ }
+
+ /// Merged query-string + form parameters (form wins) for report processing.
+ internal Dictionary RequestParamsDict()
+ {
+ var prms = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var kv in Request.Query) prms[kv.Key] = kv.Value.ToString();
+ if (Request.HasFormContentType)
+ foreach (var kv in Request.Form) prms[kv.Key] = kv.Value.ToString();
+ return prms;
}
// ── Standard param list (pre-populates @authuser) ────────────────────────
@@ -106,7 +141,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
IActionResult? result = fn.ToLower() switch
{
"ping" => Ok(),
- "wdg" => await FuchsWidgets.IntranetWdg(this, id),
+ "wdg" => await _widgets.GetWidgetAsync(id, UserAccountID, DbSec, Request),
"todos" => new PhysicalFileResult(
Path.Combine(Directory.GetCurrentDirectory(), "Data", "ProjectToDos.html"),
"text/html"),
@@ -374,7 +409,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
if (string.IsNullOrEmpty(id))
{
// Empty id → return the OData EDMX schema ($metadata), matching legacy fds.getSchema()
- using var mfrSchema = new fds.FdsMfrClient();
+ using var mfrSchema = _mfrFactory.Create();
string schema = await mfrSchema.ReadAnything(
mfrSchema.ClientConfig.BaseUrl + "$metadata", throwErrorIfNotOk: false);
return Content(schema, "text/xml", System.Text.Encoding.UTF8);
@@ -383,7 +418,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
{
string path = id + (!string.IsNullOrEmpty(code) ? "/" + code : HttpUtility.UrlDecode(Request.QueryString.Value ?? ""));
_logger.LogDebug("HandleMfr reading OData path={Path} user={User}", path, UserAccountID);
- using var mfrRead = new fds.FdsMfrClient();
+ using var mfrRead = _mfrFactory.Create();
var result = await mfrRead.ReadOData(path, throwErrorIfNotOk: false);
_logger.LogDebug("HandleMfr OData read complete for path={Path} user={User}", path, UserAccountID);
return Content(JsonConvert.SerializeObject(result), "application/json");
@@ -402,7 +437,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
if (et != EntityTypes.none && string.IsNullOrEmpty(Request.Form["need"]))
{
_logger.LogInformation("MfrUpdate entity={EntityType} need=Short user={User}", et, UserAccountID);
- using var mfrSingle = new fds.FdsMfrClient();
+ using var mfrSingle = _mfrFactory.Create();
await mfrSingle.Update__entitytable(et, fds.FdsMfr.UpdateNeed.Short);
_logger.LogDebug("MfrUpdate Short completed for entity={EntityType}", et);
return Ok();
@@ -411,7 +446,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
{
var need = fds.FdsMfr.UpdateNeedValue(needParam);
_logger.LogInformation("MfrUpdate entity={EntityType} need={Need} user={User}", et, need, UserAccountID);
- using var mfr = new fds.FdsMfrClient();
+ using var mfr = _mfrFactory.Create();
await mfr.Update__entitytable(et, updateNeed: need, debugDetails: false);
_logger.LogDebug("MfrUpdate completed for entity={EntityType} need={Need}", et, need);
return Ok();
diff --git a/Fuchs/Fuchs.csproj b/Fuchs/Fuchs.csproj
index b690b6a..8ac955d 100644
--- a/Fuchs/Fuchs.csproj
+++ b/Fuchs/Fuchs.csproj
@@ -7,6 +7,10 @@
db-dev.processweb.de;Debug;Release;server02.processweb.de
CA1416
+
+
+
+
x64
diff --git a/Fuchs/Program.cs b/Fuchs/Program.cs
index 7fcfe62..4aa62ab 100644
--- a/Fuchs/Program.cs
+++ b/Fuchs/Program.cs
@@ -75,6 +75,15 @@ public class Program
builder.Services.Configure(builder.Configuration.GetSection("Fuchs:Mailer"));
builder.Services.AddHttpClient("ProcessWebMailer");
builder.Services.AddScoped();
+
+ // Business services (DI migration — replaces the static helper / Active-Record pattern)
+ builder.Services.AddSingleton(); // stateless parser
+ builder.Services.AddSingleton(); // stateless renderer (sets license)
+ builder.Services.AddSingleton();
+ builder.Services.AddScoped();
+ builder.Services.AddScoped();
+ builder.Services.AddScoped();
+ builder.Services.AddScoped();
}
private static void ConfigureApp(WebApplication app)
diff --git a/Fuchs/Services/FuchsReportService.cs b/Fuchs/Services/FuchsReportService.cs
index 843b53e..e941f5e 100644
--- a/Fuchs/Services/FuchsReportService.cs
+++ b/Fuchs/Services/FuchsReportService.cs
@@ -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;
///
-/// Report service implementation. Replaces the static FuchsReports 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 . Replaces the static FuchsReports.
///
public class FuchsReportService : IReportService
{
+ private const int DefaultReloadSeconds = 60 * 10;
+
+ private readonly Fuchs_intranet _intranet;
private readonly ILogger _logger;
- public FuchsReportService(ILogger logger)
+ public FuchsReportService(Fuchs_intranet intranet, ILogger logger)
{
+ _intranet = intranet;
_logger = logger;
}
- public Task ProcessRequestAsync(string action, string id,
- string userAccountId, DatabaseSecurity dbSec)
+ private string Conn => _intranet.Intranet__SQLConnectionString;
+
+ public async Task ProcessRequestAsync(string fnc, string reportId,
+ string userAccountId, DatabaseSecurity dbSec, IDictionary parameters)
{
- // Specific report actions are dispatched here.
- // Extend with additional cases as needed.
- return Task.FromResult(new OkResult());
+ var prms = new Dictionary(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 { 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(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 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;
}
}
diff --git a/Fuchs/Services/FuchsWidgetService.cs b/Fuchs/Services/FuchsWidgetService.cs
index 6ece54a..5adfe6d 100644
--- a/Fuchs/Services/FuchsWidgetService.cs
+++ b/Fuchs/Services/FuchsWidgetService.cs
@@ -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;
///
-/// Widget service implementation. Replaces the static FuchsWidgets class.
-/// No longer depends on IntranetController.
+/// Widget service for the Fuchs intranet dashboard. Port of fuchs_fds_widgets.vb —
+/// SQL-driven widget cases. Replaces the static FuchsWidgets class
+/// (no longer depends on IntranetController).
///
public class FuchsWidgetService : IWidgetService
{
@@ -26,6 +26,8 @@ public class FuchsWidgetService : IWidgetService
_logger = logger;
}
+ private string Conn => _intranet.Intranet__SQLConnectionString;
+
public async Task 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 MakeParams(string userAccountId, params SqlParameter[] extra)
+ private static List Params(string userAccountId,
+ params Microsoft.Data.SqlClient.SqlParameter[] extra)
{
- var list = new List { SQL_VarChar("@authuser", userAccountId) };
+ var list = new List { SQL_VarChar("@authuser", userAccountId) };
list.AddRange(extra);
return list;
}
+ // ── "my" — list of widget short-names for the current user ───────────────
private async Task 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()
.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 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 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 BuildWidgetResponse(string userAccountId,
- DatabaseSecurity dbSec, Dictionary wdg)
+ // ── Widget renderer dispatcher ────────────────────────────────────────────
+ private async Task BuildWidgetResponse(string userAccountId, DatabaseSecurity dbSec,
+ string shortName, Dictionary 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()
- .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()
+ .Select(c => c.ColumnName).ToArray(),
+ data = dt.DataTable.Rows.Cast()
+ .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();
+ 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 { [shortName] = widgetData });
}
+
+ private static string[] ParseRenderingOptions(string raw) =>
+ string.IsNullOrWhiteSpace(raw)
+ ? Array.Empty()
+ : raw.Split(new[] { ',', ';', '|' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(s => s.Trim())
+ .ToArray();
}
diff --git a/Fuchs/Services/IDbConnectionFactory.cs b/Fuchs/Services/IDbConnectionFactory.cs
deleted file mode 100644
index f9a398a..0000000
--- a/Fuchs/Services/IDbConnectionFactory.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using Microsoft.Data.SqlClient;
-
-namespace Fuchs.Services;
-
-///
-/// Factory for creating SQL connections to the Fuchs database.
-///
-public interface IDbConnectionFactory
-{
- /// Gets the primary FDS connection string.
- string ConnectionString { get; }
-
- /// Creates and opens a new SQL connection.
- SqlConnection CreateConnection();
-
- /// Creates a new SQL connection (not opened).
- SqlConnection CreateClosedConnection();
-}
diff --git a/Fuchs/Services/IInvoiceService.cs b/Fuchs/Services/IInvoiceService.cs
index cee593f..23b8668 100644
--- a/Fuchs/Services/IInvoiceService.cs
+++ b/Fuchs/Services/IInvoiceService.cs
@@ -13,8 +13,8 @@ public interface IInvoiceService
/// Loads an existing invoice by ID.
Task LoadInvoiceAsync(string id, string userAccountId, DatabaseSecurity dbSec);
- /// Registers (creates or updates) an invoice from form data.
- Task RegisterInvoiceAsync(object formData, bool change, string invId,
+ /// Registers (creates or updates) an invoice from a parsed data object.
+ Task RegisterInvoiceAsync(FdsInvoiceData invoice, bool change, string invId,
string userAccountId, DatabaseSecurity dbSec);
/// Generates a PDF document for an invoice.
diff --git a/Fuchs/Services/IReminderService.cs b/Fuchs/Services/IReminderService.cs
index 6e41296..32d4b3c 100644
--- a/Fuchs/Services/IReminderService.cs
+++ b/Fuchs/Services/IReminderService.cs
@@ -13,8 +13,8 @@ public interface IReminderService
/// Loads an existing reminder by ID.
Task LoadReminderAsync(string id, string userAccountId, DatabaseSecurity dbSec);
- /// Registers (creates) a reminder from form data.
- Task RegisterReminderAsync(object formData, bool change, string remId,
+ /// Registers (creates) a reminder from a parsed data object.
+ Task RegisterReminderAsync(FdsReminderData reminder, bool change, string remId,
string userAccountId, DatabaseSecurity dbSec);
/// Generates a PDF document for a reminder.
diff --git a/Fuchs/Services/IReportService.cs b/Fuchs/Services/IReportService.cs
index 8d9945d..a99459c 100644
--- a/Fuchs/Services/IReportService.cs
+++ b/Fuchs/Services/IReportService.cs
@@ -1,15 +1,21 @@
-using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc;
using OCORE.security;
-using OCORE.SQL;
namespace Fuchs.Services;
///
-/// Abstraction for report processing.
+/// Abstraction for SQL-driven report processing (HTML page / fragment / PNG chart).
///
public interface IReportService
{
- /// Processes a report request.
- Task ProcessRequestAsync(string action, string id,
- string userAccountId, DatabaseSecurity dbSec);
+ ///
+ /// Processes a report request.
+ ///
+ /// Target function: "generic", "generic_content"/"gct", "chart", or a query-type name.
+ /// Report name/id (the URL code segment).
+ /// Authenticated user id.
+ /// Database security context.
+ /// Merged query-string + form parameters.
+ Task ProcessRequestAsync(string fnc, string reportId,
+ string userAccountId, DatabaseSecurity dbSec, IDictionary parameters);
}
diff --git a/Fuchs/Services/InvoiceService.cs b/Fuchs/Services/InvoiceService.cs
index 1b05a85..1107a95 100644
--- a/Fuchs/Services/InvoiceService.cs
+++ b/Fuchs/Services/InvoiceService.cs
@@ -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;
///
-/// Invoice service implementation. Extracts DB operations from FdsInvoiceData
-/// 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 .
///
public class InvoiceService : IInvoiceService
{
private readonly Fuchs_intranet _intranet;
- private readonly IPdfService _pdfService;
+ private readonly IPdfService _pdf;
private readonly ILogger _logger;
- public InvoiceService(Fuchs_intranet intranet, IPdfService pdfService, ILogger logger)
+ public InvoiceService(Fuchs_intranet intranet, IPdfService pdf, ILogger logger)
{
_intranet = intranet;
- _pdfService = pdfService;
+ _pdf = pdf;
_logger = logger;
}
+ private string Conn => _intranet.Intranet__SQLConnectionString;
+
public async Task 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
+ {
+ 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 RegisterInvoiceAsync(object formData, bool change, string invId,
+ public async Task 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 { SQL_VarChar("@authuser", userAccountId) };
+ pl.AddRange(invoice.BuildInvoiceParams(change, invId));
+
+ var sqlParts = new List { "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("