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:
@@ -15,9 +15,10 @@ public interface IComService
|
||||
/// <param name="email">Recipient email address.</param>
|
||||
/// <param name="name">Recipient display name.</param>
|
||||
/// <param name="attachments">Optional file attachments (filename → content).</param>
|
||||
/// <param name="cc">Optional Cc recipient (e.g. a customer's "Zusatzempfänger").</param>
|
||||
/// <returns><c>true</c> when the email was accepted successfully.</returns>
|
||||
Task<bool> SendEmailAsync(string reference, string subject, string html,
|
||||
string email, string name, Dictionary<string, byte[]>? attachments = null);
|
||||
string email, string name, Dictionary<string, byte[]>? attachments = null, string? cc = null);
|
||||
|
||||
/// <summary>
|
||||
/// Sends an SMS message.
|
||||
|
||||
@@ -135,13 +135,13 @@ public class InvoiceService : IInvoiceService
|
||||
var sqlParts = new List<string> { "DECLARE @Id varchar(10);" };
|
||||
if (!change)
|
||||
{
|
||||
sqlParts.Add("EXECUTE [dbo].[fds__createInvoice] @InvoiceType, @InvoiceTitle, @InvoiceBalance, @InvoiceBalance_net, @InvoiceVAT_net1, @InvoiceVAT_1, @PaymentTerm, @CustomerId, @SendToAddress, @SendToEmail, @ProvisionPeriod, @CustomValues, @authuser, @Id OUTPUT, @SendToAddressJson;");
|
||||
sqlParts.Add("EXECUTE [dbo].[fds__createInvoice] @InvoiceType, @InvoiceTitle, @InvoiceBalance, @InvoiceBalance_net, @InvoiceVAT_net1, @InvoiceVAT_1, @PaymentTerm, @CustomerId, @SendToAddress, @SendToEmail, @SendToEmailCc, @ProvisionPeriod, @CustomValues, @authuser, @Id OUTPUT, @SendToAddressJson;");
|
||||
sqlParts.Add("EXECUTE [dbo].[fds__createInvoice_Details] @Id, @InvoiceService_net, @InvoiceService_VAT, @InvoiceOptions, @authuser;");
|
||||
}
|
||||
else
|
||||
{
|
||||
pl.Add(SQL_VarChar("@InvId", invId));
|
||||
sqlParts.Add("EXECUTE [dbo].[fds__setInvoice] @InvId, @InvoiceType, @InvoiceTitle, @InvoiceBalance, @InvoiceBalance_net, @InvoiceVAT_net1, @InvoiceVAT_1, @PaymentTerm, @CustomerId, @SendToAddress, @SendToEmail, @ProvisionPeriod, @CustomValues, @authuser, @Id OUTPUT, @SendToAddressJson;");
|
||||
sqlParts.Add("EXECUTE [dbo].[fds__setInvoice] @InvId, @InvoiceType, @InvoiceTitle, @InvoiceBalance, @InvoiceBalance_net, @InvoiceVAT_net1, @InvoiceVAT_1, @PaymentTerm, @CustomerId, @SendToAddress, @SendToEmail, @SendToEmailCc, @ProvisionPeriod, @CustomValues, @authuser, @Id OUTPUT, @SendToAddressJson;");
|
||||
sqlParts.Add("EXECUTE [dbo].[fds__createInvoice_Details] @Id, @InvoiceService_net, @InvoiceService_VAT, @InvoiceOptions, @authuser;");
|
||||
}
|
||||
if (invoice.RawProvisionLocation.Length > 0)
|
||||
|
||||
@@ -46,10 +46,11 @@ public class ProcessWebComService : IComService
|
||||
}
|
||||
|
||||
public async Task<bool> SendEmailAsync(string reference, string subject, string html,
|
||||
string email, string name, Dictionary<string, byte[]>? attachments = null)
|
||||
string email, string name, Dictionary<string, byte[]>? attachments = null, string? cc = null)
|
||||
{
|
||||
using var act = FuchsTelemetry.StartActivity("email.send");
|
||||
act?.SetTag("fuchs.email.ref", reference);
|
||||
cc = string.IsNullOrWhiteSpace(cc) ? null : cc.Trim();
|
||||
|
||||
string overrideRecipient = _emailSettings.OverrideRecipient ?? "";
|
||||
if (!string.IsNullOrWhiteSpace(overrideRecipient))
|
||||
@@ -58,12 +59,13 @@ public class ProcessWebComService : IComService
|
||||
// redirect every outbound email to a single controlled inbox, so a locally
|
||||
// enabled mailer can never reach a real tenant-owner or end-customer.
|
||||
_logger.LogWarning(
|
||||
"SendEmailAsync: recipient override active for ref {Reference} – redirecting from '{OriginalEmail}' to '{OverrideRecipient}'",
|
||||
reference, email, overrideRecipient);
|
||||
"SendEmailAsync: recipient override active for ref {Reference} – redirecting from '{OriginalEmail}' (cc='{OriginalCc}') to '{OverrideRecipient}'",
|
||||
reference, email, cc ?? "", overrideRecipient);
|
||||
act?.SetTag("fuchs.email.overridden", true);
|
||||
act?.SetTag("fuchs.email.original_recipient", email);
|
||||
subject = $"[DEV \u2192 {email}] {subject}";
|
||||
email = overrideRecipient;
|
||||
cc = null;
|
||||
}
|
||||
|
||||
if (!IsValidEmail(email))
|
||||
@@ -111,13 +113,14 @@ public class ProcessWebComService : IComService
|
||||
var communication = new
|
||||
{
|
||||
to = email,
|
||||
cc,
|
||||
subject,
|
||||
body,
|
||||
attachments = attachmentPayload
|
||||
};
|
||||
|
||||
_logger.LogDebug("SendEmailAsync ref={Reference} to={Email} attachments={Count}",
|
||||
reference, email, attachmentPayload.Length);
|
||||
_logger.LogDebug("SendEmailAsync ref={Reference} to={Email} cc={Cc} attachments={Count}",
|
||||
reference, email, cc ?? "", attachmentPayload.Length);
|
||||
|
||||
var (ok, status, responseBody) = await PostToApiAsync(reference, "e", communication);
|
||||
if (ok)
|
||||
|
||||
Reference in New Issue
Block a user