Initial Commit after switching from SVN to git
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<FuchsEmailSettings></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> </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; }
|
||||
}
|
||||
}
|
||||
@@ -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; } = "";
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user