feat(usage): distinguish real edits and template delivery with v5 consent (#1339)

* feat(usage): distinguish real edits and template delivery with v5 consent

* fix(usage): exclude queued test messages and count reorders as edits

- queueEmail carries usageEligible: false into email_data and the queue
  processor passes it on, so the dev tools' send-test-email no longer
  records email_template_delivery once the worker sends it.
- event-types/reorder and categories/reorder-global compare the persisted
  order before and after and record the v5 edit markers only when it
  changed, matching the display_order edit already counted on PUT.
- normalized() builds arrays with Array.from so a row array from the sqlite
  binding compares equal under Jest's separate realm.

* fix(usage): cover per-gallery category order and workflow test runs

- categories/reorder records category_editing when an event's override
  changes; reorder/:eventId records it when an override was actually
  removed.
- send_email and the collections handoff pass usageEligible: false for a
  workflow test run (engine.testRun sets __test), so a non-dry test send is
  not counted as template delivery.

---------

Co-authored-by: Paul Nothaft <[email protected]>
This commit is contained in:
Paul Nothaft
2026-09-07 20:03:15 +02:00
committed by GitHub
co-authored by Paul Nothaft
parent 69754f8a2c
commit 5c1e38d921
31 changed files with 5398 additions and 292 deletions
+17 -2
View File
@@ -906,7 +906,7 @@ async function buildSignatureTextFor(language) {
}
}
async function sendTemplateEmail(to, templateKey, variables) {
async function sendTemplateEmail(to, templateKey, variables, { usageEligible = true } = {}) {
try {
// Webhook transport (#1225) replaces SMTP entirely when configured, so an
// instance using it has no SMTP settings to initialise and must not be
@@ -987,6 +987,17 @@ async function sendTemplateEmail(to, templateKey, variables) {
? await emailWebhookTransport.send(mail)
: await transporter.sendMail(mail);
// Transport acceptance is the measured event, not rendering or inbox
// delivery. The service accepts only the fixed bit under confirmed v5
// consent; marker failure must never retry an already-sent message.
if (usageEligible && (viaWebhook || info.accepted?.length > 0)) {
try {
await require('./productUsageService').markUsed(['email_template_delivery']);
} catch {
logger.warn('Product usage mail marker could not be recorded');
}
}
logger.info(`Email sent successfully: ${info.messageId} (${language})`);
// Return the rendered HTML so the queue processor can persist the ACTUAL
// sent body (email_queue.rendered_html) for the Project Overview preview.
@@ -1275,7 +1286,8 @@ async function processEmailQueue({ ignoreSchedule = false, limit = 10, onlyId =
sendResult = await sendTemplateEmail(
email.recipient_email,
email.email_type,
emailData
emailData,
{ usageEligible: emailData.__usageEligible !== false }
);
}
@@ -1436,6 +1448,9 @@ async function queueEmail(eventId, recipientEmail, emailType, emailData, options
try {
// Add eventId to emailData for language detection
emailData.eventId = eventId;
// An explicit test message (dev tools' send-test-email) must not count as
// template delivery when the queue processor sends it later.
if (options.usageEligible === false) emailData.__usageEligible = false;
const row = {
event_id: eventId,
recipient_email: recipientEmail,