Initial Commit after switching from SVN to git

This commit is contained in:
2026-05-03 01:43:52 +02:00
parent ab8638e5bb
commit a4284234b2
910 changed files with 359931 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
using System.Data;
using Microsoft.Extensions.Logging;
using programmersdigest.MT940Parser;
namespace Fuchs.Services;
/// <summary>
/// MT940 bank statement parsing service. Replaces the static <c>Banking</c> class.
/// </summary>
public class BankingService : IBankingService
{
private readonly ILogger<BankingService> _logger;
public BankingService(ILogger<BankingService> logger)
{
_logger = logger;
}
public string DebitCreditMarkAbb(DebitCreditMark mark) => mark switch
{
DebitCreditMark.Credit => "C",
DebitCreditMark.Debit => "D",
DebitCreditMark.ReverseCredit => "RC",
DebitCreditMark.ReverseDebit => "RD",
_ => ""
};
public DataTable ParseToDatatable(Stream stream, DataTable? schemaDatatable = null)
{
var tbl = schemaDatatable?.Clone() ?? BuildDefaultSchema();
void SetNfo(DataRow nr, string key, object? value)
{
if (tbl.Columns.Contains(key) && value != null)
nr[key] = value;
}
using var ps = new Parser(stream: stream);
try
{
foreach (var statement in ps.Parse())
{
if (string.IsNullOrEmpty(statement.AccountIdentification)) continue;
foreach (var line in statement.Lines)
{
try
{
var nr = tbl.NewRow();
SetNfo(nr, "AccountIdentification", statement.AccountIdentification);
if (line.Amount.HasValue) SetNfo(nr, "Amount", line.Amount);
if (line.EntryDate.HasValue) SetNfo(nr, "EntryDate", line.EntryDate);
if (line.FundsCode.HasValue) SetNfo(nr, "FundsCode", line.FundsCode.ToString());
SetNfo(nr, "BankReference", line.BankReference);
var info = line.InformationToOwner;
SetNfo(nr, "AccountNumberOfPayer", info.AccountNumberOfPayer);
SetNfo(nr, "BankCodeOfPayer", info.BankCodeOfPayer);
SetNfo(nr, "CompensationAmount", info.CompensationAmount);
SetNfo(nr, "CreditorReference", info.CreditorReference);
SetNfo(nr, "CreditorsReferenceParty", info.CreditorsReferenceParty);
SetNfo(nr, "CustomerReference", info.CustomerReference);
SetNfo(nr, "EndToEndReference", info.EndToEndReference);
SetNfo(nr, "JournalNumber", info.JournalNumber);
SetNfo(nr, "MandateReference", info.MandateReference);
SetNfo(nr, "NameOfPayer", info.NameOfPayer);
SetNfo(nr, "OriginalAmount", info.OriginalAmount);
SetNfo(nr, "OriginatorsIdentificationCode", info.OriginatorsIdentificationCode);
SetNfo(nr, "PayersReferenceParty", info.PayersReferenceParty);
SetNfo(nr, "PostingText", info.PostingText);
SetNfo(nr, "SepaRemittanceInformation", info.SepaRemittanceInformation);
if (info.TextKeyAddition.HasValue) SetNfo(nr, "TextKeyAddition", info.TextKeyAddition);
SetNfo(nr, "TransactionCode", info.TransactionCode);
SetNfo(nr, "IsUnstructuredData", info.IsUnstructuredData);
SetNfo(nr, "UnstructuredData", info.UnstructuredData);
SetNfo(nr, "UnstructuredRemittanceInformation", info.UnstructuredRemittanceInformation);
SetNfo(nr, "DebitCreditMark", DebitCreditMarkAbb(line.Mark));
SetNfo(nr, "SupplementaryDetails", line.SupplementaryDetails);
SetNfo(nr, "TransactionTypeIdCode", line.TransactionTypeIdCode);
SetNfo(nr, "ValueDate", line.ValueDate);
tbl.Rows.Add(nr);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "MT940 line parse error — account={Account}", statement.AccountIdentification);
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "MT940 statement parse failed.");
}
tbl.AcceptChanges();
return tbl;
}
private static DataTable BuildDefaultSchema()
{
var t = new DataTable();
var cols = t.Columns;
cols.Add("AccountIdentification", typeof(string));
cols.Add("Amount", typeof(decimal));
cols.Add("BankReference", typeof(string));
cols.Add("EntryDate", typeof(DateTime));
cols.Add("FundsCode", typeof(string));
cols.Add("AccountNumberOfPayer", typeof(string));
cols.Add("BankCodeOfPayer", typeof(string));
cols.Add("CompensationAmount", typeof(string));
cols.Add("CreditorReference", typeof(string));
cols.Add("CreditorsReferenceParty", typeof(string));
cols.Add("CustomerReference", typeof(string));
cols.Add("EndToEndReference", typeof(string));
cols.Add("JournalNumber", typeof(string));
cols.Add("MandateReference", typeof(string));
cols.Add("NameOfPayer", typeof(string));
cols.Add("OriginalAmount", typeof(string));
cols.Add("OriginatorsIdentificationCode", typeof(string));
cols.Add("PayersReferenceParty", typeof(string));
cols.Add("PostingText", typeof(string));
cols.Add("SepaRemittanceInformation", typeof(string));
cols.Add("TextKeyAddition", typeof(int));
cols.Add("TransactionCode", typeof(int));
cols.Add("IsUnstructuredData", typeof(bool));
cols.Add("UnstructuredData", typeof(string));
cols.Add("UnstructuredRemittanceInformation", typeof(string));
cols.Add("DebitCreditMark", typeof(string));
cols.Add("SupplementaryDetails", typeof(string));
cols.Add("TransactionTypeIdCode", typeof(string));
cols.Add("ValueDate", typeof(DateTime));
return t;
}
}
+197
View File
@@ -0,0 +1,197 @@
using Fuchs.intranet;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MimeKit;
using Newtonsoft.Json;
using OCORE.SQL;
using static OCORE.SQL.sql;
namespace Fuchs.Services;
/// <summary>
/// Email service implementation. Replaces the static <c>FuchsFdsEmail</c> class.
/// Reads settings from <c>IOptions&lt;FuchsEmailSettings&gt;</c> (appsettings.json)
/// instead of <c>System.Configuration.ConfigurationManager</c>.
/// </summary>
public class FuchsEmailService : IEmailService
{
private readonly ILogger<FuchsEmailService> _logger;
private readonly Fuchs_intranet _intranet;
private readonly FuchsEmailSettings _emailSettings;
private OCORE.email.EmailServerSettings? _cachedSettings;
private const string ReplyToName = "Sebastian Fuchs - Bad und Heizung";
private const string ReplyToAddress = "info@sanitaerfuchs.de";
private const string SignatureIntro =
"<p>&nbsp;</p><p style=\"margin:24px 0 16px 0;line-height:140%;\">" +
"Herzliche Gr\u00fc\u00dfe aus D\u00fcsseldorf-Bilk<br/>" +
"Ihr Team der Firma Sebastian Fuchs</p>";
public FuchsEmailService(
ILogger<FuchsEmailService> logger,
Fuchs_intranet intranet,
IOptions<FuchsEmailSettings> emailSettings)
{
_logger = logger;
_intranet = intranet;
_emailSettings = emailSettings.Value;
}
public async Task<bool> SendEmailAsync(string reference, string subject, string html,
string email, string name, Dictionary<string, byte[]>? attachments)
{
var errors = new List<string>();
string guid = "";
string config = "";
DateTime sent = default;
if (!IsValidEmail(email))
errors.Add("Die Email-Adresse ist nicht g\u00fcltig.");
if (string.IsNullOrEmpty(html))
errors.Add("Bitte geben Sie eine Nachricht ein.");
if (errors.Count == 0)
{
try
{
var settings = GetEmailSettings();
string body = html + BuildSignature();
// Development redirect: replace To with DevRedirectAddress, suppress CC/BCC
string devRedirect = _emailSettings.DevRedirectAddress;
bool isDevRedirect = !string.IsNullOrWhiteSpace(devRedirect);
if (isDevRedirect)
{
_logger.LogWarning("DEV redirect: email to '{Original}' redirected to '{Redirect}'", email, devRedirect);
email = devRedirect;
name = "Dev Redirect";
}
if (settings != null)
{
var msg = new OCORE.email.Email(
Mode: OCORE.email.EmailMode.DirectMode,
type: settings.type)
{
EmailSettings = settings,
Subject = subject
};
msg.AddTo(email, name);
msg.AddReplyTo(new MailboxAddress(ReplyToName, ReplyToAddress));
msg.SetBody(body);
if (attachments != null)
foreach (var kv in attachments)
msg.AttachFile(filecontent: kv.Value, kv.Key);
var result = await msg.SendAsync(
(dref, ex) => _logger.LogError(ex, "Email send error {Reference}", dref));
guid = msg.MessageId ?? "";
config = msg.EmailConfig_serialized;
sent = result.Timestamp;
errors.AddRange(result.ErrorMessages);
}
else
{
var main = _emailSettings.Main;
string host = !string.IsNullOrEmpty(main.Host) ? main.Host : _emailSettings.SmtpHost;
string user = !string.IsNullOrEmpty(main.Username) ? main.Username : _emailSettings.SmtpUser;
string pass = !string.IsNullOrEmpty(main.Password) ? main.Password : _emailSettings.SmtpPass;
string from = !string.IsNullOrEmpty(main.From) ? main.From
: (!string.IsNullOrEmpty(_emailSettings.SmtpFrom) ? _emailSettings.SmtpFrom : user);
string fromName = !string.IsNullOrEmpty(main.Alias) ? main.Alias : _emailSettings.SmtpFromName;
int port = main.Port > 0 ? main.Port : _emailSettings.SmtpPort;
var message = new MimeMessage();
message.From.Add(new MailboxAddress(fromName, from));
message.To.Add(new MailboxAddress(name, email));
message.ReplyTo.Add(new MailboxAddress(ReplyToName, ReplyToAddress));
message.Subject = subject;
var builder = new BodyBuilder { HtmlBody = body };
if (attachments != null)
foreach (var kv in attachments)
builder.Attachments.Add(kv.Key, kv.Value);
message.Body = builder.ToMessageBody();
using var client = new MailKit.Net.Smtp.SmtpClient();
await client.ConnectAsync(host, port, MailKit.Security.SecureSocketOptions.Auto);
if (!string.IsNullOrEmpty(user))
await client.AuthenticateAsync(user, pass);
await client.SendAsync(message);
await client.DisconnectAsync(true);
guid = message.MessageId!;
sent = DateTime.UtcNow;
}
}
catch (Exception ex)
{
errors.Add("Beim Versenden ist ein Fehler aufgetreten.");
_logger.LogError(ex, "SendEmailAsync failed for {Reference}", reference);
}
}
if (errors.Count > 0)
_logger.LogWarning("SendEmailAsync errors for {Reference}: {Errors}", reference, errors);
// SQL audit log
try
{
var pl = new List<SqlParameter>
{
SQL_VarChar("@Ref", reference),
SQL_VarChar("@guid", guid),
SQL_DateTime("@DateSent", sent == default ? DBNull.Value : (object)sent),
SQL_NVarChar("@config", config, dbNull_IfEmpty: true),
SQL_Bit("@success", errors.Count == 0),
SQL_NVarChar("@log", JsonConvert.SerializeObject(errors))
};
await setSQLValue_async(
"EXECUTE [dbo].[fds__logEmail] @Ref, @guid, @DateSent, @config, @success, @log;",
_intranet.Intranet__SQLConnectionString, pl,
Security: _intranet.GetDbSecurity());
}
catch (Exception logEx)
{
_logger.LogError(logEx, "Failed to log email audit for {Reference}", reference);
}
return errors.Count == 0;
}
private OCORE.email.EmailServerSettings? GetEmailSettings()
{
if (_cachedSettings != null) return _cachedSettings;
var main = _emailSettings.Main;
if (string.IsNullOrWhiteSpace(main.Host)) return null;
_cachedSettings = new OCORE.email.EmailServerSettings(main.Type, main.ToOcoreJson());
return _cachedSettings;
}
private static string BuildSignature()
{
try
{
string sigPath = Path.Combine(AppContext.BaseDirectory,
"email_signature", "sanitaerfuchs_email_signature.txt");
if (File.Exists(sigPath))
return SignatureIntro + File.ReadAllText(sigPath);
}
catch { /* signature is optional */ }
return "";
}
private static bool IsValidEmail(string email)
{
try
{
var a = new System.Net.Mail.MailAddress(email);
return string.Equals(a.Address, email, StringComparison.OrdinalIgnoreCase);
}
catch { return false; }
}
}
+34
View File
@@ -0,0 +1,34 @@
namespace Fuchs.Services;
/// <summary>
/// SMTP / email server settings bound from appsettings.json → "Fuchs:Email" section.
/// </summary>
public class FuchsEmailSettings
{
/// <summary>Main account for outgoing customer emails (anfrage@sanitaerfuchs.de).</summary>
public SmtpAccountSettings Main { get; set; } = new();
/// <summary>FDS/invoicing account (rechnungen@sanitaerfuchs.de).</summary>
public SmtpAccountSettings Fds { get; set; } = new();
/// <summary>Internal OCORE service account used for system notifications.</summary>
public SmtpAccountSettings Service { get; set; } = new();
/// <summary>Comma-separated list of addresses used as test recipients.</summary>
public string TestAddresses { get; set; } = "";
// ── MailKit fallback settings (used when Main is not configured) ──
public string SmtpHost { get; set; } = "";
public int SmtpPort { get; set; } = 587;
public string SmtpUser { get; set; } = "";
public string SmtpPass { get; set; } = "";
public string SmtpFrom { get; set; } = "";
public string SmtpFromName { get; set; } = "SanitärFuchs";
/// <summary>
/// Development only: when set, all outgoing emails are redirected to this address;
/// CC and BCC are cleared. Leave empty in production.
/// </summary>
public string DevRedirectAddress { get; set; } = "";
}
+45
View File
@@ -0,0 +1,45 @@
using Fuchs.intranet;
using Microsoft.Extensions.Logging;
using MigraDoc.DocumentObjectModel;
namespace Fuchs.Services;
/// <summary>
/// PDF service implementation. Delegates to <see cref="FuchsPdf"/> static methods
/// while providing a DI-friendly injectable wrapper.
/// </summary>
public class FuchsPdfService : IPdfService
{
private readonly ILogger<FuchsPdfService> _logger;
public FuchsPdfService(ILogger<FuchsPdfService> logger)
{
_logger = logger;
FuchsPdf.SetLicense();
}
public Task<Document> WriteLetterAsync(FuchsPdf.FdsTextBlocks textBlocks, bool draft)
{
return FuchsPdf.WriteLetter(textBlocks, draft, FuchsPdf.DeCulture);
}
public void ApplyInvoice(Document doc, FuchsPdf.FdsTextBlocks textBlocks,
FdsInvoiceData invoice, bool draft = false)
{
FuchsPdf.ApplyInvoice(doc, textBlocks, invoice, draft);
}
public void ApplyReminder(Document doc, FuchsPdf.FdsTextBlocks textBlocks,
FdsReminderData reminder, bool draft = false)
{
FuchsPdf.ApplyReminder(doc, textBlocks, reminder, draft);
}
public byte[] DocToPdfBytes(Document doc) => FuchsPdf.DocToPdfBytes(doc);
public Task<OCORE.pdf._pdf.ImageCollection> DocToImageCollectionAsync(Document doc) =>
FuchsPdf.DocToImageCollection(doc);
public Task<OCORE.pdf._pdf.ImageCollection> BytesToImageCollectionAsync(byte[] pdfBytes) =>
FuchsPdf.BytesToImageCollection(pdfBytes);
}
+27
View File
@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using OCORE.security;
using OCORE.SQL;
namespace Fuchs.Services;
/// <summary>
/// Report service implementation. Replaces the static <c>FuchsReports</c> class.
/// </summary>
public class FuchsReportService : IReportService
{
private readonly ILogger<FuchsReportService> _logger;
public FuchsReportService(ILogger<FuchsReportService> logger)
{
_logger = logger;
}
public Task<IActionResult> ProcessRequestAsync(string action, string id,
string userAccountId, DatabaseSecurity dbSec)
{
// Specific report actions are dispatched here.
// Extend with additional cases as needed.
return Task.FromResult<IActionResult>(new OkResult());
}
}
+129
View File
@@ -0,0 +1,129 @@
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;
using static OCORE.commons;
using static OCORE.SQL.sql;
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>.
/// </summary>
public class FuchsWidgetService : IWidgetService
{
private readonly Fuchs_intranet _intranet;
private readonly ILogger<FuchsWidgetService> _logger;
public FuchsWidgetService(Fuchs_intranet intranet, ILogger<FuchsWidgetService> logger)
{
_intranet = intranet;
_logger = logger;
}
public async Task<IActionResult> GetWidgetAsync(string widgetId, string userAccountId,
DatabaseSecurity dbSec, HttpRequest request)
{
try
{
return widgetId.ToLower() switch
{
"my" => await HandleWidgetMy(userAccountId, dbSec),
"one" => await HandleWidgetOne(userAccountId, dbSec, request),
_ => await HandleWidgetGeneric(widgetId, userAccountId, dbSec)
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Widget error for {WidgetId}, user {UserAccountId}", widgetId, userAccountId);
return new StatusCodeResult(500);
}
}
private List<SqlParameter> MakeParams(string userAccountId, params SqlParameter[] extra)
{
var list = new List<SqlParameter> { SQL_VarChar("@authuser", userAccountId) };
list.AddRange(extra);
return list;
}
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);
var names = dt.DataTable.Rows
.Cast<System.Data.DataRow>()
.OrderBy(r => dt.DataTable.Columns.Contains("order") ? r.nz("order") : "")
.Select(r => r.nz("short_name"))
.ToArray();
return await JSONAsync(names);
}
private async Task<IActionResult> HandleWidgetOne(string userAccountId, DatabaseSecurity dbSec,
HttpRequest request)
{
string shortName = request.Form["short_name"].ToString() ?? "";
if (string.IsNullOrEmpty(shortName)) return new BadRequestResult();
var dt = await getSQLDatatable_async(
"SELECT * FROM [dbo].[fis_getONEPersonWidgets] (@authuser) WHERE [short_name] = @shortname;",
_intranet.Intranet__SQLConnectionString,
MakeParams(userAccountId,
SQL_VarChar("@shortname", shortName),
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);
}
private async Task<IActionResult> HandleWidgetGeneric(string widgetId, string userAccountId,
DatabaseSecurity dbSec)
{
var pl = MakeParams(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);
return await JSONAsync(new
{
admin = dset.Table("admin").FirstRow.toObjectDictionary(),
data = dset.Tables("data").toArrayofObjectDictionaries()
});
}
private async Task<IActionResult> BuildWidgetResponse(string userAccountId,
DatabaseSecurity dbSec, Dictionary<string, object?> wdg)
{
string type = (wdg.nz("type", "") ?? "").ToLower();
string sql = wdg.nz("sql", "") ?? "";
if (type.StartsWith("sql") && !string.IsNullOrEmpty(sql))
{
var dt = await getSQLDatatable_async(sql,
_intranet.Intranet__SQLConnectionString,
MakeParams(userAccountId), Security: dbSec);
return await JSONAsync(new
{
name = wdg.nz("name", ""),
type,
rows = dt.DataTable.Rows
.Cast<System.Data.DataRow>()
.Select(r => r.toObjectDictionary())
.ToArray()
});
}
return await JSONAsync(wdg);
}
}
+15
View File
@@ -0,0 +1,15 @@
using System.Data;
namespace Fuchs.Services;
/// <summary>
/// Abstraction for MT940 bank statement parsing.
/// </summary>
public interface IBankingService
{
/// <summary>Abbreviation for a debit/credit mark.</summary>
string DebitCreditMarkAbb(programmersdigest.MT940Parser.DebitCreditMark mark);
/// <summary>Parses an MT940 stream into a DataTable.</summary>
DataTable ParseToDatatable(Stream stream, DataTable? schemaDatatable = null);
}
+18
View File
@@ -0,0 +1,18 @@
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();
}
+20
View File
@@ -0,0 +1,20 @@
namespace Fuchs.Services;
/// <summary>
/// Abstraction for sending emails with optional attachments and audit logging.
/// </summary>
public interface IEmailService
{
/// <summary>
/// Sends an email and logs the result to the database.
/// </summary>
/// <param name="reference">Audit reference key (e.g. "inv_123").</param>
/// <param name="subject">Email subject line.</param>
/// <param name="html">HTML body content (signature is appended automatically).</param>
/// <param name="email">Recipient email address.</param>
/// <param name="name">Recipient display name.</param>
/// <param name="attachments">Optional file attachments (filename → content).</param>
/// <returns><c>true</c> when the email was sent successfully.</returns>
Task<bool> SendEmailAsync(string reference, string subject, string html,
string email, string name, Dictionary<string, byte[]>? attachments);
}
+33
View File
@@ -0,0 +1,33 @@
using Fuchs.intranet;
using MigraDoc.DocumentObjectModel;
using OCORE.security;
using OCORE.SQL;
namespace Fuchs.Services;
/// <summary>
/// Abstraction for invoice operations (load, register, PDF generation).
/// </summary>
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,
string userAccountId, DatabaseSecurity dbSec);
/// <summary>Generates a PDF document for an invoice.</summary>
Document GenerateInvoicePdf(FdsInvoiceData invoice, bool draft);
/// <summary>Renders invoice PDF to bytes.</summary>
Task<byte[]> RenderInvoicePdfBytesAsync(FdsInvoiceData invoice, bool draft);
/// <summary>Stores the invoice PDF file to the database.</summary>
Task<byte[]> StoreInvoiceDocumentFileAsync(FdsInvoiceData invoice, bool draft,
string userAccountId, DatabaseSecurity dbSec);
/// <summary>Gets the invoice file (stored or rendered).</summary>
Task<byte[]?> GetInvoiceFileAsync(FdsInvoiceData invoice, bool draft,
fds.IFdsMfr mfr);
}
+11
View File
@@ -0,0 +1,11 @@
namespace Fuchs.Services;
/// <summary>
/// Factory for creating <see cref="fds.FdsMfrClient"/> instances.
/// Replaces direct <c>new FdsMfrClient()</c> calls in controllers.
/// </summary>
public interface IMfrClientFactory
{
/// <summary>Creates a new MFR client instance.</summary>
fds.FdsMfrClient Create();
}
+28
View File
@@ -0,0 +1,28 @@
using Fuchs.intranet;
using MigraDoc.DocumentObjectModel;
namespace Fuchs.Services;
/// <summary>
/// Abstraction for PDF generation.
/// </summary>
public interface IPdfService
{
/// <summary>Creates a letter document with standard Fuchs layout.</summary>
Task<Document> WriteLetterAsync(FuchsPdf.FdsTextBlocks textBlocks, bool draft);
/// <summary>Applies invoice data to a letter document.</summary>
void ApplyInvoice(Document doc, FuchsPdf.FdsTextBlocks textBlocks, FdsInvoiceData invoice, bool draft = false);
/// <summary>Applies reminder data to a letter document.</summary>
void ApplyReminder(Document doc, FuchsPdf.FdsTextBlocks textBlocks, FdsReminderData reminder, bool draft = false);
/// <summary>Renders a MigraDoc Document to a PDF/A byte array.</summary>
byte[] DocToPdfBytes(Document doc);
/// <summary>Renders a MigraDoc Document to an image collection for preview.</summary>
Task<OCORE.pdf._pdf.ImageCollection> DocToImageCollectionAsync(Document doc);
/// <summary>Converts PDF bytes to an image collection.</summary>
Task<OCORE.pdf._pdf.ImageCollection> BytesToImageCollectionAsync(byte[] pdfBytes);
}
+37
View File
@@ -0,0 +1,37 @@
using Fuchs.intranet;
using MigraDoc.DocumentObjectModel;
using OCORE.security;
using OCORE.SQL;
namespace Fuchs.Services;
/// <summary>
/// Abstraction for reminder operations (load, register, PDF generation).
/// </summary>
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,
string userAccountId, DatabaseSecurity dbSec);
/// <summary>Generates a PDF document for a reminder.</summary>
Document GenerateReminderPdf(FdsReminderData reminder, bool draft);
/// <summary>Renders reminder PDF to bytes.</summary>
Task<byte[]> RenderReminderPdfBytesAsync(FdsReminderData reminder, bool draft);
/// <summary>Stores the reminder PDF file to the database.</summary>
Task<byte[]> StoreReminderDocumentFileAsync(FdsReminderData reminder, bool draft,
string userAccountId, DatabaseSecurity dbSec);
/// <summary>Gets the reminder file (stored or rendered).</summary>
Task<byte[]> GetReminderFileAsync(FdsReminderData reminder, bool draft,
fds.IFdsMfr mfr, string userAccountId, DatabaseSecurity dbSec);
/// <summary>Gets a stored reminder file by ID.</summary>
Task<(FileInfo? file, byte[]? content)> GetStoredFileAsync(string reminderId,
string userAccountId, DatabaseSecurity dbSec);
}
+15
View File
@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc;
using OCORE.security;
using OCORE.SQL;
namespace Fuchs.Services;
/// <summary>
/// Abstraction for report processing.
/// </summary>
public interface IReportService
{
/// <summary>Processes a report request.</summary>
Task<IActionResult> ProcessRequestAsync(string action, string id,
string userAccountId, DatabaseSecurity dbSec);
}
+16
View File
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using OCORE.security;
using OCORE.SQL;
namespace Fuchs.Services;
/// <summary>
/// Abstraction for dashboard widget data retrieval.
/// </summary>
public interface IWidgetService
{
/// <summary>Dispatches a widget request by widget ID.</summary>
Task<IActionResult> GetWidgetAsync(string widgetId, string userAccountId,
DatabaseSecurity dbSec, HttpRequest request);
}
+69
View File
@@ -0,0 +1,69 @@
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.SQL.sql;
namespace Fuchs.Services;
/// <summary>
/// Invoice service implementation. Extracts DB operations from <c>FdsInvoiceData</c>
/// and PDF generation into a proper DI service.
/// </summary>
public class InvoiceService : IInvoiceService
{
private readonly Fuchs_intranet _intranet;
private readonly IPdfService _pdfService;
private readonly ILogger<InvoiceService> _logger;
public InvoiceService(Fuchs_intranet intranet, IPdfService pdfService, ILogger<InvoiceService> logger)
{
_intranet = intranet;
_pdfService = pdfService;
_logger = logger;
}
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.");
}
public async Task<FdsInvoiceData> RegisterInvoiceAsync(object formData, bool change, string invId,
string userAccountId, DatabaseSecurity dbSec)
{
throw new NotImplementedException("InvoiceService.RegisterInvoiceAsync pending FdsInvoiceData refactor.");
}
public Document GenerateInvoicePdf(FdsInvoiceData invoice, bool draft)
{
throw new NotImplementedException("InvoiceService.GenerateInvoicePdf pending FdsInvoiceData refactor.");
}
public async Task<byte[]> RenderInvoicePdfBytesAsync(FdsInvoiceData invoice, bool draft)
{
throw new NotImplementedException("InvoiceService.RenderInvoicePdfBytesAsync pending FdsInvoiceData refactor.");
}
public async Task<byte[]> StoreInvoiceDocumentFileAsync(FdsInvoiceData invoice, bool draft,
string userAccountId, DatabaseSecurity dbSec)
{
throw new NotImplementedException("InvoiceService.StoreInvoiceDocumentFileAsync pending FdsInvoiceData refactor.");
}
public async Task<byte[]?> GetInvoiceFileAsync(FdsInvoiceData invoice, bool draft,
fds.IFdsMfr mfr)
{
if (invoice.InvoiceRegistration?.getItem("IsFinal", false) is true)
{
byte[]? ba = null;
mfr.GetFdsDoc(ref ba, invoice.Id, "invoice");
return ba;
}
return await RenderInvoicePdfBytesAsync(invoice, draft);
}
}
+22
View File
@@ -0,0 +1,22 @@
using Microsoft.Extensions.Logging;
namespace Fuchs.Services;
/// <summary>
/// Factory implementation for <see cref="fds.FdsMfrClient"/>.
/// Centralizes MFR client creation and supplies logger from DI.
/// </summary>
public class MfrClientFactory : IMfrClientFactory
{
private readonly ILoggerFactory _loggerFactory;
public MfrClientFactory(ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory;
}
public fds.FdsMfrClient Create()
{
return new fds.FdsMfrClient(_loggerFactory);
}
}
+67
View File
@@ -0,0 +1,67 @@
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.SQL.sql;
namespace Fuchs.Services;
/// <summary>
/// Reminder service implementation. Extracts DB operations from <c>FdsReminderData</c>
/// into a proper DI service.
/// </summary>
public class ReminderService : IReminderService
{
private readonly Fuchs_intranet _intranet;
private readonly IPdfService _pdfService;
private readonly ILogger<ReminderService> _logger;
public ReminderService(Fuchs_intranet intranet, IPdfService pdfService, ILogger<ReminderService> logger)
{
_intranet = intranet;
_pdfService = pdfService;
_logger = logger;
}
public async Task<FdsReminderData> LoadReminderAsync(string id, string userAccountId, DatabaseSecurity dbSec)
{
throw new NotImplementedException("ReminderService.LoadReminderAsync pending FdsReminderData refactor.");
}
public async Task<FdsReminderData> RegisterReminderAsync(object formData, bool change, string remId,
string userAccountId, DatabaseSecurity dbSec)
{
throw new NotImplementedException("ReminderService.RegisterReminderAsync pending FdsReminderData refactor.");
}
public Document GenerateReminderPdf(FdsReminderData reminder, bool draft)
{
throw new NotImplementedException("ReminderService.GenerateReminderPdf pending FdsReminderData refactor.");
}
public async Task<byte[]> RenderReminderPdfBytesAsync(FdsReminderData reminder, bool draft)
{
throw new NotImplementedException("ReminderService.RenderReminderPdfBytesAsync pending FdsReminderData refactor.");
}
public async Task<byte[]> StoreReminderDocumentFileAsync(FdsReminderData reminder, bool draft,
string userAccountId, DatabaseSecurity dbSec)
{
throw new NotImplementedException("ReminderService.StoreReminderDocumentFileAsync pending FdsReminderData refactor.");
}
public async Task<byte[]> GetReminderFileAsync(FdsReminderData reminder, bool draft,
fds.IFdsMfr mfr, string userAccountId, DatabaseSecurity dbSec)
{
throw new NotImplementedException("ReminderService.GetReminderFileAsync pending FdsReminderData refactor.");
}
public async Task<(FileInfo? file, byte[]? content)> GetStoredFileAsync(string reminderId,
string userAccountId, DatabaseSecurity dbSec)
{
throw new NotImplementedException("ReminderService.GetStoredFileAsync pending FdsReminderData refactor.");
}
}
+46
View File
@@ -0,0 +1,46 @@
using Newtonsoft.Json;
namespace Fuchs.Services;
/// <summary>
/// SMTP account configuration for a single email account.
/// Property names match the OCORE EmailServerSettings JSON format (lowercase).
/// </summary>
public class SmtpAccountSettings
{
[JsonProperty("alias")]
public string Alias { get; set; } = "";
[JsonProperty("to")]
public string To { get; set; } = "";
[JsonProperty("from")]
public string From { get; set; } = "";
[JsonProperty("bcc")]
public string Bcc { get; set; } = "";
[JsonProperty("host")]
public string Host { get; set; } = "";
[JsonProperty("port")]
public int Port { get; set; } = 587;
[JsonProperty("security")]
public string Security { get; set; } = "StartTls";
[JsonProperty("username")]
public string Username { get; set; } = "";
[JsonProperty("password")]
public string Password { get; set; } = "";
[JsonProperty("type")]
public string Type { get; set; } = "smtp";
/// <summary>
/// Serializes this instance back to the JSON string format expected by
/// <c>OCORE.email.EmailServerSettings(type, raw)</c>.
/// </summary>
public string ToOcoreJson() => JsonConvert.SerializeObject(this);
}