Add Cc (Zusatzempfänger) recipient support for invoice emails

Lets an invoice carry an additional Cc recipient alongside the primary
SendToEmail, sent via the ProcessWeb mailer API. Honors the
Fuchs:Email:OverrideRecipient dev safety net: when the override is
active, Cc is dropped entirely (only the single override inbox is
ever reached), never leaked to it.

- IComService.SendEmailAsync / ProcessWebComService: new optional
  `cc` parameter, included in the mailer payload only when non-empty.
- FdsInvoiceData.RawInvoiceEmailCc + BuildInvoiceParams: reads
  `new.invoiceemailcc`, posts @SendToEmailCc.
- IntranetController.Requests: reads SendToEmailCc from the loaded
  invoice row and passes it through on send/resend.
- Fuchs_Database: SendToEmailCc column on fds__invoices, threaded
  through fds__createInvoice/fds__setInvoice/fds__getInvoice and the
  fds__tt__invoice_base/_core table types.
- fis.req_txt_de.js: "Zusatzempfänger (CC)" field label.
- Tests: ProcessWebComServiceTests (Cc included/omitted/dropped under
  override); SystemStatusServiceTests mocks updated for the new
  IComService.SendEmailAsync parameter.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 09:09:29 +02:00
co-authored by Claude Sonnet 5
parent ecc74cf3c6
commit 94aa7cbfe4
14 changed files with 82 additions and 18 deletions
+40
View File
@@ -322,6 +322,46 @@ public class ProcessWebComServiceTests
Assert.Equal("realcustomer@example.de", json["communication"]!["to"]!.ToString());
}
// ── Cc recipient ("Zusatzempfänger") ─────────────────────────────────────────
[Fact]
public async Task SendEmailAsync_CcProvided_IsIncludedInPayload()
{
var handler = new StubHandler(HttpStatusCode.OK);
var svc = CreateService(handler);
await svc.SendEmailAsync("inv_cc1", "Subject", "<p>hi</p>", "kunde@example.de", "Kunde",
cc: "zusatz@example.de");
var json = JObject.Parse(handler.LastRequestBody!);
Assert.Equal("zusatz@example.de", json["communication"]!["cc"]!.ToString());
}
[Fact]
public async Task SendEmailAsync_CcOmitted_IsNullInPayload()
{
var handler = new StubHandler(HttpStatusCode.OK);
var svc = CreateService(handler);
await svc.SendEmailAsync("inv_cc2", "Subject", "<p>hi</p>", "kunde@example.de", "Kunde");
var json = JObject.Parse(handler.LastRequestBody!);
Assert.Equal(JTokenType.Null, json["communication"]!["cc"]!.Type);
}
[Fact]
public async Task SendEmailAsync_OverrideRecipientSet_DropsCcEntirely()
{
var handler = new StubHandler(HttpStatusCode.OK);
var svc = CreateService(handler, overrideRecipient: "dev-inbox@example.test");
await svc.SendEmailAsync("inv_cc3", "Subject", "<p>hi</p>", "realcustomer@example.de", "Kunde",
cc: "zusatz@example.de");
var json = JObject.Parse(handler.LastRequestBody!);
Assert.Equal("dev-inbox@example.test", json["communication"]!["to"]!.ToString());
Assert.Equal(JTokenType.Null, json["communication"]!["cc"]!.Type);
}
// ── Override enforcement across every real appsettings*.json environment ───
// Loads the ACTUAL Fuchs/appsettings*.json files the same way ASP.NET Core
// layers them (base file + optional environment-specific overlay). These files
+6 -6
View File
@@ -404,7 +404,7 @@ public class SystemStatusServiceTests
Assert.False(r.Sent);
com.Verify(c => c.SendEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Dictionary<string, byte[]>>()), Times.Never);
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Dictionary<string, byte[]>>(), It.IsAny<string?>()), Times.Never);
}
[Fact]
@@ -413,9 +413,9 @@ public class SystemStatusServiceTests
string? capturedHtml = null, capturedTo = null, capturedSubject = null;
var com = new Mock<IComService>();
com.Setup(c => c.SendEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Dictionary<string, byte[]>?>()))
.Callback<string, string, string, string, string, Dictionary<string, byte[]>?>(
(_, subj, html, to, _, _) => { capturedSubject = subj; capturedHtml = html; capturedTo = to; })
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Dictionary<string, byte[]>?>(), It.IsAny<string?>()))
.Callback<string, string, string, string, string, Dictionary<string, byte[]>?, string?>(
(_, subj, html, to, _, _, _) => { capturedSubject = subj; capturedHtml = html; capturedTo = to; })
.ReturnsAsync(true);
var svc = Build(com: com);
@@ -436,7 +436,7 @@ public class SystemStatusServiceTests
{
var com = new Mock<IComService>();
com.Setup(c => c.SendEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Dictionary<string, byte[]>?>()))
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Dictionary<string, byte[]>?>(), It.IsAny<string?>()))
.ReturnsAsync(true);
var svc = Build(com: com, email: new FuchsEmailSettings { OverrideRecipient = "safety@example.test" });
@@ -453,7 +453,7 @@ public class SystemStatusServiceTests
{
var com = new Mock<IComService>();
com.Setup(c => c.SendEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Dictionary<string, byte[]>?>()))
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Dictionary<string, byte[]>?>(), It.IsAny<string?>()))
.ReturnsAsync(false);
var svc = Build(com: com);