Refactor code structure for improved readability and maintainability
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
2026-07-08 19:33:23 +02:00
parent 4abf81cd7d
commit 59a2b86c09
23 changed files with 676 additions and 60 deletions
+104 -4
View File
@@ -6,6 +6,7 @@ using Fuchs.Observability;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using MigraDoc.DocumentObjectModel;
using Newtonsoft.Json.Linq;
using OCORE.security;
using OCORE.SQL;
using static OCORE.commons;
@@ -48,11 +49,13 @@ public class InvoiceService : IInvoiceService
var pl = new List<SqlParameter>
{
SQL_VarChar("@authuser", userAccountId),
SQL_VarChar("@Id", id),
SQL_Bit("@includefile", false)
SQL_VarChar("@Id", id)
};
// fds__getInvoice only takes @Id and @authuser (no @includefile) - passing an extra
// parameter makes SQL Server throw "too many arguments", which previously left
// InvoiceRegistration empty (Id "") and silently broke downstream file storage/emailing.
var dset = await getSQLDataSet_async(
"EXECUTE [dbo].[fds__getInvoice] @Id, @includefile, @authuser;",
"EXECUTE [dbo].[fds__getInvoice] @Id, @authuser;",
Conn, pl, tablenames: new[] { "admin", "inv", "req", "itm" },
Security: dbSec, options: new FIS_SQLOptions());
if (!string.IsNullOrEmpty(dset.Exception))
@@ -93,16 +96,113 @@ public class InvoiceService : IInvoiceService
}
var invdset = await getSQLDataSet_async(string.Join("\n", sqlParts),
Conn, pl, tablenames: new[] { "inv", "det", "req", "itm" },
Conn, pl, tablenames: new[] { "inv", "det" },
Security: dbSec, options: new FIS_SQLOptions());
if (!string.IsNullOrEmpty(invdset.Exception))
_logger.LogError("RegisterInvoiceAsync sql exception: {Ex}", invdset.Exception);
invoice.InvoiceRegistration = new GenericObjectDictionary(invdset.Table("inv").FirstRow.toObjectDictionary());
_logger.LogInformation("RegisterInvoiceAsync registered id={Id} (change={Change})", invoice.Id, change);
if (!string.IsNullOrEmpty(invoice.Id) && invoice.Req != null)
await PersistInvoiceLineItemsAsync(invoice, userAccountId, dbSec);
return invoice;
}
/// <summary>
/// Persists the invoice's service-request groupings and their line items
/// (<c>fds__invoice_servicerequests</c> / <c>fds__invoice_items</c>). These are what
/// <c>fds__getInvoice</c> reads back when a draft is reloaded (e.g. after "Zwischenstand
/// speichern" or reopening a draft from the invoice list) — the preview/PDF itself renders
/// straight from the posted JSON, so without this the preview looked fine right after
/// posting but a reloaded draft showed no steps/positions at all (nothing had ever been
/// written for the service requests/items, only the invoice header row).
/// The whole set is cleared and rewritten on every call — simpler and safer than diffing —
/// mirroring the legacy <c>fds__invoice_data.RegisterInvoice_V1</c> (VB) behaviour.
/// </summary>
private async Task PersistInvoiceLineItemsAsync(FdsInvoiceData invoice, string userAccountId, DatabaseSecurity dbSec)
{
string invoiceId = invoice.Id;
var clearReqPl = new List<SqlParameter> { SQL_VarChar("@authuser", userAccountId), SQL_VarChar("@Id", invoiceId) };
await setSQLValue_async("EXECUTE [dbo].[fds__remInvoice_ServiceRequests] @Id, @authuser;",
Conn, clearReqPl, Security: dbSec, options: new FIS_SQLOptions());
var itemsTable = (await getSQLDatatable_async(
"SELECT TOP(0) * FROM [dbo].[fds__invoice_items];",
Conn, Security: dbSec, options: new FIS_SQLOptions())).DataTable;
int sortOrder = -1;
foreach (var reqEntry in invoice.Req!)
{
sortOrder++; // 0-based, matching legacy fds__invoice_data.RegisterInvoice_V1 (VB)'s "ri" loop index
var rdic = new GenericObjectDictionary(reqEntry);
float vnet = 0;
var valueNetParam = TryCastSingle(rdic.getItem("netval"), ref vnet)
? SQL_Float("@value_net", vnet)
: SQL_Float("@value_net", stringvalue: rdic.nz("netval"));
var reqPl = new List<SqlParameter>
{
SQL_VarChar("@authuser", userAccountId),
SQL_VarChar("@InvId", invoiceId),
SQL_BigInt("@mfr__servicerequest", rdic.nz("Id")),
SQL_NVarChar("@title", rdic.nz("text").ne(rdic.nz("nme"))),
valueNetParam,
SQL_Int("@SortOrder", sortOrder)
};
var reqDt = await getSQLDatatable_async(
"EXECUTE [dbo].[fds__createInvoice_ServiceRequest] @InvId, @mfr__servicerequest, @title, @value_net, @SortOrder;",
Conn, reqPl, Security: dbSec, options: new FIS_SQLOptions());
if (!string.IsNullOrEmpty(reqDt.Exception))
_logger.LogError("PersistInvoiceLineItemsAsync: fds__createInvoice_ServiceRequest failed for invoice {Id}: {Ex}", invoiceId, reqDt.Exception);
string invRqId = reqDt.FirstRow.nz("Id");
if (string.IsNullOrEmpty(invRqId)) continue;
if (!rdic.TryGetValue("itm", out var itmObj) || itmObj is not JArray itmArray) continue;
int itemSort = 0;
foreach (var tok in itmArray)
{
itemSort++;
if (tok is not JObject itmJson) continue;
var itmDic = new GenericObjectDictionary(itmJson.ToObject<Dictionary<string, object>>()!);
if ((itmDic.nz("id") ?? "") == "" && (itmDic.nz("typ") ?? "") == "" && (itmDic.nz("t") ?? "") == "") continue;
var row = itemsTable.NewRow();
row["InvId"] = invoiceId;
row["InvRqId"] = invRqId;
if (long.TryParse(itmDic.nz("id"), out long mfrItem)) row["mfr__item"] = mfrItem;
row["Type"] = itmDic.nz("typ");
row["Position"] = itmDic.nz("p");
row["Quantity"] = itmDic.nz("q");
row["Text"] = itmDic.nz("t");
float v = 0;
if (TryCastSingle(itmDic.getItem("v"), ref v)) row["value"] = (decimal)v;
float vt = 0;
if (TryCastSingle(itmDic.getItem("vt"), ref vt)) row["value_total"] = (decimal)vt;
float vat = 0;
if (TryCastSingle(itmDic.nz("vat").Replace("%", ""), ref vat)) row["vat"] = (decimal)vat;
float vs = 0;
if (TryCastSingle(itmDic.getItem("vs"), ref vs)) row["value_service"] = (decimal)vs;
row["det"] = itmDic.no("det", false) is true;
row["SortOrder"] = (byte)Math.Clamp(itemSort, 0, 255);
itemsTable.Rows.Add(row);
}
}
await setSQLValue_async("EXECUTE [dbo].[fds__remInvoice_Items] @Id, @authuser;",
Conn, new List<SqlParameter> { SQL_VarChar("@authuser", userAccountId), SQL_VarChar("@Id", invoiceId) },
Security: dbSec, options: new FIS_SQLOptions());
if (itemsTable.Rows.Count > 0)
{
var dtw = new DatatableWriterAsync(itemsTable, Conn, "[dbo].[fds__invoice_items]");
dtw.DoSubmit();
if (dtw.SubmitException != null)
_logger.LogError(dtw.SubmitException, "PersistInvoiceLineItemsAsync: bulk-copy of invoice items failed for invoice {Id}", invoiceId);
}
}
public Document GenerateInvoicePdf(FdsInvoiceData invoice, bool draft)
{
using var act = FuchsTelemetry.StartActivity("invoice.render");
+2 -1
View File
@@ -253,7 +253,8 @@ public class StartupSelfTestService : BackgroundService
using var check = new Spire.Pdf.PdfDocument();
check.LoadFromStream(ms);
string text = check.Pages[0].ExtractText();
var extractor = new Spire.Pdf.Texts.PdfTextExtractor(check.Pages[0]);
string text = extractor.ExtractText(new Spire.Pdf.Texts.PdfTextExtractOptions());
return !text.Contains("Evaluation Warning", StringComparison.OrdinalIgnoreCase)
&& !text.Contains("created with Spire.PDF", StringComparison.OrdinalIgnoreCase);