Files
Fuchs_Intranet/Fuchs/Docs/ARCHITECTURE.md
T
Stefan 49e3ed2673 Add unit tests for Fuchs_DataService and related components
- Introduced comprehensive unit tests for the Fuchs_DataService library, covering DATEV header formatting, CSV/XML generation, and FdsMfrClient construction.
- Implemented tests for FdsMfr.UpdateNeed parsing and FdsShared utility helpers, ensuring correct functionality and stability.
- Added tests for FdsConfig and FdsMfrClient to validate configuration resolution and client construction.

Document decisions on backend-authoritative invoice and reminder handling

- Created ADR 0008 to clarify that all invoice types and reminder stages are backend-authoritative during drafting and previewing.
- Established that all calculations and settings must be processed server-side, ensuring consistency between online editor and PDF outputs.

Define irreversible mutations for set-price modes in invoices

- Documented ADR 0009 to specify that the "Set mit Preis" and "Nur Set mit Preis" operations are irreversible mutations affecting service request blocks.
- Clarified that these operations are not display toggles but actual data changes, ensuring clear expectations for invoice handling.

Transition MFR ERP sync to in-process execution within the web app

- Created ADR 0010 to outline the migration of Fuchs_DataService from a standalone service to an in-process library within the Fuchs web application.
- Updated configuration and logging management to be handled by the host application, streamlining the sync process.

Add publish profile and periodic hosted service for job scheduling

- Introduced a publish profile for deployment to a specified folder.
- Implemented PeriodicHostedService to manage multiple independent jobs, including the MFR ERP sync, with configurable execution intervals.

Add dotnet-tools.json for EF Core CLI tools

- Included dotnet-tools.json to manage the version of dotnet-ef for Entity Framework Core migrations and commands.
2026-07-16 13:34:23 +02:00

26 KiB

Fuchs Intranet — Solution Architecture

Auto-generated architecture analysis
.NET 10 · ASP.NET Core MVC · SQL Server


1. Solution Overview

The Fuchs Intranet solution is a line-of-business web application for Sebastian Fuchs Bad und Heizung GmbH & Co. KG (a plumbing/heating company in Düsseldorf). It manages invoices, reminders, service requests, banking transactions, reports, and user authentication — all exposed through a single-page intranet front-end.

Project Type Purpose
Fuchs ASP.NET Core Web (MVC) Main web application — the intranet
Fuchs_DataService Class Library MFR ERP sync logic (entity polling, invoice/DATEV files). Hosted in-process by Fuchs as a PeriodicHostedService (see ADR 0010) — no longer a standalone process.
MFR_RESTClient Class Library REST/OData client for the MFR ERP system. The REST/OData contract is documented in MFR_RESTClient/Docs/mfr_interface_description.md.
Fuchs_Database SSDT (SQL project) Source of truth for the fuchs_fds SQL schema (tables, table types, functions, stored procedures the backend calls).
OCORE Class Library (shared) Core utilities: SQL, crypto, email, IO, logging
OCORE_web Class Library (shared) Web utilities: MVC helpers, middleware, auth, captcha
OCORE_web_pdf Class Library (shared) PDF generation (MigraDoc/PDFsharp, HTML→PDF)
OCORE_Charting Class Library (shared) Data visualization / charting (ported System.Windows.Forms.DataVisualization)
MT940Parser Class Library (external) SWIFT MT940/MT942 bank statement parser
CAMTParser Class Library (in-repo) ISO 20022 CAMT (camt.052/053/054) bank statement parser

All projects target net10.0.


2. Architecture Diagram

┌─────────────────────────────────────────────────────────────────────────┐
│                          CLIENTS (Browser)                              │
│                   SPA-like JS front-end (js/intranet/)                  │
└──────────────────────────────┬──────────────────────────────────────────┘
                               │  HTTP (GET / POST)
                               ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                     Fuchs  (ASP.NET Core MVC)                           │
│                                                                         │
│  Program.cs ─── ConfigureServices / ConfigureApp                        │
│       │                                                                 │
│       ├── Cookie Authentication (scheme: "fuchs_intranet")              │
│       ├── Distributed Memory Cache                                      │
│       └── DI Registrations:                                             │
│            • Fuchs_intranet  (singleton — config + auth + DB helper)    │
│            • IFdsMfr → FdsMfr (singleton — ERP sync)                   │
│                                                                         │
│  Routes:                                                                │
│    /{fn?}/{id?}/{code?}      → IntranetController.Index  (SPA shell)   │
│    /do/{fn?}/{id?}/{code?}   → IntranetController.Do     (API)         │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │            IntranetController  (partial class)                  │    │
│  │                                                                 │    │
│  │  .cs          — core: Index, Do dispatcher, Auth, Login/Logout │    │
│  │  .Invoices.cs — invoice CRUD, DATEV export, PDF                │    │
│  │  .Invoices2.cs— invoice sub-handlers (MFR refresh, get, list)  │    │
│  │  .Reminder.cs — payment reminder CRUD + PDF                    │    │
│  │  .Requests.cs — service request management                     │    │
│  │  .Banking.cs  — MT940 upload, transaction queries              │    │
│  │  .Reports.cs  — report catalog, execution                      │    │
│  └──────────┬──────────────────────────────────────────────────────┘    │
│             │ calls                                                      │
│  ┌──────────▼──────────────────────────────────────────────────────┐    │
│  │                code/  (Business Logic)                          │    │
│  │                                                                 │    │
│  │  FuchsIntranet.cs   — Fuchs_intranet singleton (config, auth,  │    │
│  │                        DB connections, debug logging)           │    │
│  │  FdsInvoiceData.cs  — Invoice data model + PDF generation      │    │
│  │  FdsReminderData.cs — Reminder data model + PDF generation     │    │
│  │  FuchsPdf.cs        — PDF layout/rendering (MigraDoc)          │    │
│  │  FuchsWidgets.cs    — Dashboard widget data (SQL-driven)       │    │
│  │  FuchsReports.cs    — Report dispatch                          │    │
│  │  FuchsFdsEmail.cs   — Email sending + DB logging               │    │
│  │  Banking.cs         — MT940 parsing to DataTable               │    │
│  │  MigraDocExtensions — MigraDoc helper extensions               │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                                                                         │
│  Logging/FuchsLoggerProvider.cs — custom ILoggerProvider                │
└────────┬────────────────┬──────────────────┬───────────────────────┬────┘
         │                │                  │                       │
         ▼                ▼                  ▼                       ▼
┌──────────────┐ ┌──────────────┐ ┌────────────────┐  ┌──────────────────┐
│  OCORE       │ │  OCORE_web   │ │ OCORE_web_pdf  │  │   MT940Parser    │
│              │ │              │ │                 │  │                  │
│ • SQL helper │ │ • MVC helper │ │ • MigraDoc/     │  │ • SWIFT MT940/   │
│   (ADO.NET)  │ │   (JSON,File)│ │   PDFsharp      │  │   MT942 parser   │
│ • Email      │ │ • Cookie Auth│ │ • HTML→PDF      │  │                  │
│ • IO/Files   │ │ • Middleware │ │ • Font resolver  │  │                  │
│ • Crypto     │ │ • Captcha    │ │                 │  │                  │
│ • Logging    │ │ • Background │ │                 │  │                  │
│ • CSV/XML    │ │   services   │ │                 │  │                  │
│ • DateTime   │ │ • Security   │ │                 │  │                  │
└──────────────┘ └──────────────┘ └────────────────┘  └──────────────────┘
         │                │
         ▼                ▼
┌─────────────────────────────────────────────────────────────────────────┐
│           Fuchs_DataService  (Class Library, hosted in-process)         │
│                                                                         │
│  PeriodicHostedService — in Fuchs/Services; BackgroundService +         │
│                          PeriodicTimer, registered in Program.cs         │
│                          when Fds:SyncEnabled (ADR 0010)                 │
│  FdsMfr.cs (IFdsMfr)  — MFR sync orchestration                         │
│  FdsMfrClient.cs       — MFR REST client wrapper                        │
│  FdsShared.cs          — FdsConfig (reads the host's IConfiguration)    │
│  (DATEV zip)           — OCORE.zip (System.IO.Compression), no 7-Zip    │
│  FdsDebug.cs           — Debug/file logging                             │
│                                                                         │
│  Jobs:  MfrSync (every Fds:ExecutionFrequency_Minutes)                  │
│    → UpdateIfNecessary_async  (entity table sync)                       │
│    → UpdateRequested_async   (on-demand entity refresh)                 │
│    → GetInvoiceFiles_async   (invoice PDF download)                     │
└────────────────────────┬────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                     MFR_RESTClient                                      │
│                                                                         │
│  MFRClient.cs          — RestSharp-based REST/OData client              │
│  MFRClientModels.cs    — Config, credentials, entity types              │
│  ODataEnvelope.cs      — OData response wrapper                         │
│  Entities/MfrGeneric.cs— Generic entity helpers                         │
└────────────────────────┬────────────────────────────────────────────────┘
                         │
                         ▼
              ┌─────────────────────┐
              │   MFR ERP System    │
              │   (External REST)   │
              └─────────────────────┘

                         ┌─────────────────────┐
     All projects ──────►│   SQL Server         │
                         │   (fuchs_fds DB)     │
                         │   Stored procedures  │
                         │   Symmetric key enc. │
                         └─────────────────────┘

3. Dependency Graph

Fuchs (Web)
 ├── OCORE
 ├── OCORE_web ──► OCORE
 ├── OCORE_web_pdf
 ├── MT940Parser
 ├── MFR_RESTClient
 └── Fuchs_DataService
      ├── OCORE
      ├── OCORE_web
      └── MFR_RESTClient

OCORE_Charting  (standalone — referenced by solution but no direct project reference)

4. Key Architectural Patterns

4.1 Partial Controller Pattern

IntranetController is split across 7 partial-class files, each handling a business domain (invoices, reminders, banking, etc.). The main Do() action uses a switch expression to dispatch to domain-specific methods.

4.2 Singleton Configuration Object

Fuchs_intranet is a manually-managed singleton (via FuchsOcmsIntranet) initialized at startup with IConfiguration. It holds connection strings, app settings, auth helpers, and DB connection factory methods.

4.3 Service Layer (Dependency Injection)

Business logic lives in DI-registered services under Fuchs/Services/ behind interfaces, injected into IntranetController: IComService, IPdfService, IInvoiceService, IReminderService, IReportService, IWidgetService, IBankingService, IMfrClientFactory. Stateless services (IPdfService, IBankingService, IMfrClientFactory) are singletons; DB/request-scoped services are scoped (see Program.cs). FdsInvoiceData / FdsReminderData are now pure data holders (parse + properties); loading, persistence and PDF generation live in the services (fully async — no Task.Run(...).Wait()). FuchsPdf / FuchsVisualization remain as static rendering libraries used by the services. The earlier static, controller-coupled helpers (FuchsWidgets, FuchsReports, Banking, FuchsFdsEmail) have been removed.

4.4 SQL-First Data Access

There is no ORM (no EF Core). All data access uses ADO.NET via OCORE SQL helpers (getSQLDatatable_async, getSQLDataSet_async, setSQLValue_async) calling stored procedures and inline SQL. DataTable/DataRow is the primary data transfer mechanism.

4.5 Background Service

The MFR ERP sync runs in-process inside the web app as a PeriodicHostedService (Fuchs/Services/PeriodicHostedService.cs), registered in Program.cs when Fds:SyncEnabled is true. It polls the MFR ERP on a timer (Fds:ExecutionFrequency_Minutes), syncing entities and downloading invoice files via the sync logic in the Fuchs_DataService library. See ADR 0010.

4.6 Authentication

Cookie-based authentication (CookieAuthenticationDefaults) with custom claims (FuchsUserIdentity). SQL-based user/password verification.


5. Service Layer (implemented)

Status: DONE. The services below are implemented and DI-registered in Program.cs. The original extraction rationale is retained for reference / history. FuchsFdsEmailIComService (ProcessWeb Mailer API, inline base64 attachments), FuchsWidgetsIWidgetService, FuchsPdfIPdfService, BankingIBankingService (now MT940 and CAMT), FdsInvoiceData/FdsReminderDataIInvoiceService/IReminderService (data classes are now pure POCOs), FuchsReportsIReportService (backed by the ported FuchsVisualization engine), FdsMfrClientIMfrClientFactory.


5.1 FuchsFdsEmailIEmailService

Current state: Static class using System.Configuration.ConfigurationManager (legacy!) and receiving Fuchs_intranet as a parameter.

Problem: Uses cfg.AppSettings["FDS_EmailSettings"] — violates the project's own rule to not use System.Configuration.ConfigurationManager. Untestable. Static state (_settings cache).

Proposed service:

public interface IEmailService
{
    Task<bool> SendEmailAsync(string reference, string subject, string html,
        string email, string name, Dictionary<string, byte[]>? attachments);
}

public class FuchsEmailService : IEmailService
{
    private readonly EmailServerSettings _settings;
    private readonly Fuchs_intranet _intranet;
    // Inject IConfiguration, Fuchs_intranet, ILogger<FuchsEmailService>
}

Registration: builder.Services.AddSingleton<IEmailService, FuchsEmailService>();

Files affected: Fuchs\code\FuchsFdsEmail.cs, all callers in controller partials.


5.2 FuchsPdfIPdfService

Current state: Large static class with static helper methods, hardcoded license key, company-specific constants.

Problem: Not injectable, not testable, mixes configuration (colors, company data) with PDF rendering logic.

Proposed service:

public interface IPdfService
{
    Document CreateInvoicePdf(FdsInvoiceData invoice);
    Document CreateReminderPdf(FdsReminderData reminder);
    Task<PdfImageCollection> DocToImageCollectionAsync(Document doc);
}

public class FuchsPdfService : IPdfService
{
    // Inject ILogger<FuchsPdfService>
    // Company data could come from IOptions<FuchsPdfOptions>
}

Registration: builder.Services.AddSingleton<IPdfService, FuchsPdfService>();

Files affected: Fuchs\code\FuchsPdf.cs, FdsInvoiceData.cs, FdsReminderData.cs, controller partials.


5.3 FuchsWidgetsIWidgetService

Current state: Static class that receives the entire IntranetController as a parameter to access _intranet, UserAccountID, DbSec, etc.

Problem: Tight coupling to controller — passes the whole controller instance. Cannot be unit tested independently.

Proposed service:

public interface IWidgetService
{
    Task<object> GetUserWidgetsAsync(string userAccountId, DatabaseSecurity dbSec);
    Task<object> GetWidgetDataAsync(string widgetId, string userAccountId, DatabaseSecurity dbSec);
    Task<object> GetSingleWidgetAsync(string shortName, string userAccountId, DatabaseSecurity dbSec);
}

public class FuchsWidgetService : IWidgetService
{
    private readonly Fuchs_intranet _intranet;
    // Inject Fuchs_intranet, ILogger<FuchsWidgetService>
}

Registration: builder.Services.AddScoped<IWidgetService, FuchsWidgetService>();

Files affected: Fuchs\code\FuchsWidgets.cs, IntranetController.cs (Do method).


5.4 BankingIBankingService

Current state: Static class with MT940 parsing logic.

Problem: Minor — already fairly stateless, but takes ILogger as parameter instead of injection.

Proposed service:

public interface IBankingService
{
    DataTable ParseMT940(Stream stream, DataTable? schema = null);
}

public class BankingService : IBankingService
{
    private readonly ILogger<BankingService> _logger;
    // Inject ILogger
}

Registration: builder.Services.AddSingleton<IBankingService, BankingService>();

Files affected: Fuchs\code\Banking.cs, IntranetController.Banking.cs.


5.5 FdsInvoiceData / FdsReminderData → Factory Services

Current state: Data model classes that directly call stored procedures and PDF generation in their constructors/methods. They receive IntranetController as a parameter for DB access.

Problem: Business objects doing their own persistence (Active Record anti-pattern). Tightly coupled to controller.

Proposed services:

public interface IInvoiceService
{
    Task<FdsInvoiceData> LoadInvoiceAsync(string id, string userAccountId);
    Task<string> RegisterInvoiceAsync(FdsInvoiceData invoice, bool change);
    Task<Document> GenerateInvoicePdfAsync(FdsInvoiceData invoice);
}

public interface IReminderService
{
    Task<FdsReminderData> LoadReminderAsync(string id, string userAccountId);
    Task<string> RegisterReminderAsync(FdsReminderData reminder, bool change);
    Task<Document> GenerateReminderPdfAsync(FdsReminderData reminder);
}

Registration: builder.Services.AddScoped<IInvoiceService, InvoiceService>();

Files affected: FdsInvoiceData.cs, FdsReminderData.cs, all controller partials that create these objects.


5.6 FuchsReportsIReportService

Current state: Static class with a single dispatch method, receives controller.

Proposed service:

public interface IReportService
{
    Task<IActionResult> ProcessRequestAsync(string action, string id,
        string userAccountId, DatabaseSecurity dbSec);
}

5.7 FdsMfrClient → Injectable MFR Client

Current state: Created with new FdsMfrClient() directly in controller code (e.g., IntranetController.Invoices2.cs line 26). Uses static FdsConfig for credentials.

Problem: Cannot be mocked for testing. Credentials hardwired to static config.

Proposed service:

public interface IMfrClientFactory
{
    FdsMfrClient Create();
}

public class MfrClientFactory : IMfrClientFactory, IDisposable
{
    private readonly ILoggerFactory _loggerFactory;
    private readonly MFRClientCredentials _credentials;
    // Inject ILoggerFactory, IOptions<MfrSettings>
}

Registration: builder.Services.AddSingleton<IMfrClientFactory, MfrClientFactory>();

Files affected: FdsMfrClient.cs, IntranetController.Invoices2.cs, any code doing new FdsMfrClient().


5.8 Fuchs_intranet — Decompose the God Object

Current state: Single class handling configuration, DB connections, authentication, module auth queries, debug logging, and PDF licensing.

Recommended split:

Responsibility Proposed Service Lifetime
Configuration (conn strings, app settings) IOptions<FuchsSettings> Singleton
DB connection factory IDbConnectionFactory Singleton
User authentication IAuthenticationService Scoped
Module authorization IAuthorizationService (custom) Scoped
Debug/error logging Use built-in ILogger<T>

6. Priority Ranking

Priority Candidate Impact Effort
🔴 1 FuchsFdsEmailIEmailService Fixes ConfigurationManager violation, high testability gain Low
🔴 2 FuchsWidgetsIWidgetService Removes controller coupling Low
🟡 3 FdsMfrClientIMfrClientFactory Removes new in controllers, enables mocking Medium
🟡 4 BankingIBankingService Clean DI pattern, minor effort Low
🟡 5 FuchsPdfIPdfService Large file, significant but high-value refactor Medium
🟠 6 FdsInvoiceData/FdsReminderData → Services Major architectural improvement, most effort High
🟠 7 Fuchs_intranet decomposition God-object split, foundational but risky High
🟢 8 FuchsReportsIReportService Minimal current logic, prep for future Low

7. Additional Observations

  1. Resolved — email/SMS moved off ConfigurationManager into IComService (ProcessWeb Mailer API).
  2. ResolvedFdsInvoiceData/FdsReminderData are now pure data holders; DB + PDF logic moved to IInvoiceService/IReminderService.
  3. ResolvedFdsMfrClient is created via IMfrClientFactory (no new in controllers).
  4. ResolvedOCORE_Charting is now used (transitively, via OCORE_web's chart engine) by the report renderer (FuchsVisualization).
  5. ResolvedTopshelf removed; Fuchs_DataService is now a class library and the MFR sync is hosted in-process by the web app as a PeriodicHostedService (ADR 0010).

8. Observability (OpenTelemetry)

  • Instrumentation is centralised in Fuchs/Observability/FuchsTelemetry.cs: one ActivitySource and one Meter (Fuchs.Intranet).
  • Metrics — counters (fuchs.invoices.rendered, fuchs.reminders.rendered, fuchs.reports.rendered, fuchs.emails.sent/.failed, fuchs.sms.sent, fuchs.banking.mt940.rows, fuchs.mfr.calls) and duration histograms (fuchs.pdf.render.duration, fuchs.report.render.duration, fuchs.email.send.duration).
  • Tracing — ASP.NET Core, HttpClient and SqlClient instrumentation plus the app ActivitySource; services start spans for their key operations.
  • Configured in Program.cs. Always collected in-process; OTLP export is opt-in via Fuchs:Telemetry:OtlpEndpoint (and can be disabled with Fuchs:Telemetry:Enabled=false), so a missing collector never affects the app.
  • All services + handlers log entry/result/timing/errors via ILogger<T> with structured placeholders.

9. Bank Statement Parsing (MT940 + CAMT)

  • BankingService (IBankingService) accepts both MT940 (SWIFT text, via the external MT940Parser) and CAMT (ISO 20022 camt.052/053/054 XML, via the in-repo CAMTParser).
  • ParseToDatatable auto-detects the format from content (XML → CAMT, else MT940) and maps either into the fds__tt__bankingtransactions schema; the bam/up handler and the frontend upload accept both.
  • CAMTParser matches elements by local name (namespace-agnostic) so it works across every camt schema version. When the banking schema changes, keep the MT940 and CAMT column mappings in BankingService aligned.

10. MFR ERP Integration

  • MFR_RESTClient is the REST/OData client for the mfr (Mobile Field Report) ERP. Its integration contract — base URLs, auth, OData conventions, pagination, error/retry semantics, deep-create and document-upload endpoints — is documented in MFR_RESTClient/Docs/mfr_interface_description.md. Consult that file before changing the client.
  • The client uses HTTP Basic auth, a configurable timeout/user-agent, and retries idempotent GETs on transient failures (HTTP 429/5xx, network/timeout) with exponential backoff + jitter (honouring Retry-After). ReadODataAllPages follows @odata.nextLink pagination.
  • Fuchs_DataService (Topshelf worker) polls MFR on a timer and syncs entities + invoice files; the web app creates clients via IMfrClientFactory.

11. Database

  • The SQL schema lives in the Fuchs_Database SSDT project (source of truth). The backend is SQL-first: it calls stored procedures, table-valued types (e.g. fds__tt__bankingtransactions) and functions via OCORE ADO.NET helpers.
  • When changing a stored procedure's name/parameters or a table type, update both the SSDT project and the calling C# in the same change, and keep the MT940/CAMT banking column mappings aligned with fds__tt__bankingtransactions.