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
+19 -14
View File
@@ -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;
/// <summary>
/// Banking helper robustness tests.
/// Banking service robustness tests.
/// </summary>
public class BankingDebitCreditMarkTests
{
private static readonly BankingService Svc = new(NullLogger<BankingService>.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<BankingService>.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);
}
}
+83
View File
@@ -0,0 +1,83 @@
using Fuchs.intranet;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;
namespace Fuchs.Tests;
/// <summary>
/// Tests for the refactored data holders (FdsInvoiceData / FdsReminderData).
/// These are now pure data objects — persistence/PDF moved to the DI services.
/// </summary>
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"));
}
}