added improvements on set proces and minor other improvements

This commit is contained in:
Stefan
2026-07-13 22:40:08 +02:00
parent 5c0fdc6c1d
commit 5f85b75c22
55 changed files with 1170 additions and 357 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
/// <summary>
/// Abstraction for outbound communication: email and SMS.
/// Backed by the ProcessWeb Mailer API (POST /api/mailer?fn=push_com).
/// Backed by the ProcessWeb Mailer API (POST {BaseUrl}/mailer/push_com).
/// </summary>
public interface IComService
{
+48 -3
View File
@@ -1,4 +1,4 @@
using System.Globalization;
using System.Globalization;
using System.Text.RegularExpressions;
using Fuchs.intranet;
using Microsoft.Extensions.Logging;
@@ -247,10 +247,53 @@ public sealed class InvoiceDraftEditService : IInvoiceDraftService
block_net = session.Sums.NetByBlock
},
validation = session.ValidationMessages.Select(v => new { field = v.Field, severity = v.Severity, message = v.Message }),
historyCount = session.History.Count
historyCount = session.History.Count,
notes = BuildNotes(session),
setDisplay = BuildSetDisplay(session)
};
}
/// <summary>
/// Authoritative per-item set-pricing display flags (ADR 0006/set-pricing): for every block
/// that contains a "set" item, runs the same <see cref="InvoiceSetPricing.Build"/> the PDF
/// uses (keyed by <c>admin.setmode</c>) and returns a flat map of item id → whether its
/// price/total should be shown and whether it is a set header. The online editor applies
/// this to the rendered rows so it always matches the PDF without duplicating the pricing
/// rules client-side.
/// </summary>
private static Dictionary<string, object> BuildSetDisplay(InvoiceDraftSession session)
{
var result = new Dictionary<string, object>();
var mode = InvoiceSetPricing.ParseMode(Str(session.Admin["setmode"]));
foreach (var blockTok in session.Req)
{
if (blockTok is not JObject block || block["items"] is not JArray itemsArr) continue;
List<Dictionary<string, object?>>? items = itemsArr.ToObject<List<Dictionary<string, object?>>>();
if (items == null || !InvoiceSetPricing.ContainsSets(items)) continue;
foreach (var line in InvoiceSetPricing.Build(items, mode))
{
if (string.IsNullOrEmpty(line.Id)) continue;
result[line.Id] = new { p = line.ShowPrice, h = line.IsSetHeader };
}
}
return result;
}
/// <summary>
/// Renders the backend-authoritative notice paragraphs (see <see cref="FuchsPdf.BuildInvoiceNotes"/>)
/// for the editor's read-only preview — the same text the PDF will show, generated from the
/// same builder, so the two can never drift apart.
/// </summary>
private IEnumerable<object> BuildNotes(InvoiceDraftSession session)
{
var fds = BuildFdsData(session);
fds.InvoiceRegistration = SynthesizeRegistration(session);
bool p13b = AsBool(session.Admin["p13b"]);
return FuchsPdf.BuildInvoiceNotes(new FuchsPdf.FdsTextBlocks(), fds, p13b)
.Select(n => new { text = n.Text, style = n.Style });
}
public IReadOnlyList<ChangeHistoryEntry> GetHistory(string token) =>
_cache.Get(token)?.History ?? (IReadOnlyList<ChangeHistoryEntry>)Array.Empty<ChangeHistoryEntry>();
@@ -385,7 +428,9 @@ public sealed class InvoiceDraftEditService : IInvoiceDraftService
var tokens = new List<string>();
if (AsBool(session.Admin["p13b"])) tokens.Add("§13b");
string setmode = Str(session.Admin["setmode"]).Trim().ToLowerInvariant();
if (setmode is "itemprices" or "setonly") tokens.Add("setmode:" + setmode);
// Mirrors FdsInvoiceData.BuildInvoiceOptions: persist any explicitly-chosen mode
// (including the default "setprice") so it is distinguishable from "never touched".
if (setmode is "setprice" or "itemprices" or "setonly") tokens.Add("setmode:" + setmode);
return string.Join(",", tokens);
}
+21 -13
View File
@@ -14,7 +14,7 @@ namespace Fuchs.Services;
/// <summary>
/// Outbound communication service backed by the ProcessWeb Mailer API
/// (POST https://api.processweb.de/api/mailer?fn=push_com).
/// (POST https://api.processweb.de/mailer/push_com).
/// When <c>ProcessWebComSettings.Enabled</c> is <c>false</c> the service
/// only logs the intended communication without calling the API.
/// </summary>
@@ -105,10 +105,9 @@ public class ProcessWebComService : IComService
})
.ToArray();
var payload = new
var communication = new
{
comType = "email",
recipient = email,
to = email,
subject,
body,
attachments = attachmentPayload
@@ -117,7 +116,7 @@ public class ProcessWebComService : IComService
_logger.LogDebug("SendEmailAsync ref={Reference} to={Email} attachments={Count}",
reference, email, attachmentPayload.Length);
var (ok, responseBody) = await PostToApiAsync("push_com", payload);
var (ok, responseBody) = await PostToApiAsync(reference, "e", communication);
if (ok)
{
success = true;
@@ -166,14 +165,13 @@ public class ProcessWebComService : IComService
try
{
var payload = new
var communication = new
{
comType = "sms",
recipient = mobile,
body = message
to = mobile,
body = message
};
var (ok, responseBody) = await PostToApiAsync("push_com", payload);
var (ok, responseBody) = await PostToApiAsync(mobile, "s", communication);
if (ok)
{
FuchsTelemetry.SmsSent.Add(1);
@@ -193,10 +191,20 @@ public class ProcessWebComService : IComService
// ── Private helpers ────────────────────────────────────────────────────────
private async Task<(bool ok, string body)> PostToApiAsync(string fn, object payload)
private async Task<(bool ok, string body)> PostToApiAsync(string reference, string comtype, object communication)
{
var client = _httpClientFactory.CreateClient("ProcessWebMailer");
var json = JsonConvert.SerializeObject(payload);
// The Mailer API expects a fixed envelope: { serverid, comtype, communication, reference }.
// "communication" carries the actual channel-specific payload (to/subject/body/attachments).
var payload = new
{
serverid = _settings.ServerId,
comtype,
communication,
reference
};
var json = JsonConvert.SerializeObject(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
string credentials = Convert.ToBase64String(
@@ -204,7 +212,7 @@ public class ProcessWebComService : IComService
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", credentials);
var response = await client.PostAsync($"{_settings.BaseUrl}/api/mailer?fn={fn}", content);
var response = await client.PostAsync($"{_settings.BaseUrl}/mailer/push_com", content);
string responseBody = await response.Content.ReadAsStringAsync();
return (response.IsSuccessStatusCode, responseBody);
}
+3
View File
@@ -14,6 +14,9 @@ public class ProcessWebComSettings
/// <summary>API token used for HTTP Basic authentication.</summary>
public string Token { get; set; } = "";
/// <summary>Server identifier assigned by ProcessWeb, sent as "serverid" in every push_com call.</summary>
public string ServerId { get; set; } = "";
/// <summary>
/// When <c>false</c> (default) the service is disabled and only logs the
/// intended communication without actually calling the API.
+18 -2
View File
@@ -1,4 +1,4 @@
using System.Globalization;
using System.Globalization;
using Fuchs.intranet;
using Microsoft.Extensions.Logging;
using MigraDoc.DocumentObjectModel;
@@ -159,10 +159,24 @@ public sealed class ReminderDraftEditService : IReminderDraftService
amount_open = session.Sums.AmountOpen
},
validation = session.ValidationMessages.Select(v => new { field = v.Field, severity = v.Severity, message = v.Message }),
historyCount = session.History.Count
historyCount = session.History.Count,
notes = BuildNotes(session)
};
}
/// <summary>
/// Renders the backend-authoritative intro/closing notice paragraphs (see
/// <see cref="FuchsPdf.BuildReminderNotes"/>) for the editor's read-only preview — the same
/// text the PDF will show, generated from the same builder, so the two can never drift apart.
/// </summary>
private object BuildNotes(ReminderDraftSession session)
{
var fds = BuildReminderData(session);
fds.ReminderRegistration = SynthesizeRegistration(session);
var (intro, closing) = FuchsPdf.BuildReminderNotes(new FuchsPdf.FdsTextBlocks(), fds);
return new { intro, closing };
}
public IReadOnlyList<ChangeHistoryEntry> GetHistory(string token) =>
_cache.Get(token)?.History ?? (IReadOnlyList<ChangeHistoryEntry>)Array.Empty<ChangeHistoryEntry>();
@@ -250,6 +264,8 @@ public sealed class ReminderDraftEditService : IReminderDraftService
["SendToAddress"] = Str(session.New["invoiceaddress"]),
["SendToEmail"] = Str(session.New["invoiceemail"]),
["InvoiceId"] = invoiceId,
["amount"] = session.Sums.AmountTotal,
["amount_payed"] = session.Sums.AmountPayed,
["amount_open"] = session.Sums.AmountOpen,
["PaymentTerm"] = Str(session.Rem["paymentterm"]),
["invoices"] = invoices,