fix(audit-wave-11): BullMQ jobId plumbing for natural dedup
concurrency-auditor C-2: every queue.add(...) site previously enqueued without a stable jobId, so a double-dispatch (webhook retry, double- click on Send, scheduler tick collision) would create two queue jobs and the downstream worker would deliver twice. BullMQ rejects a duplicate jobId while the original is still queued or active, so a stable per-entity key gives at-most-once semantics naturally. Added jobIds across all 10 enqueue sites: - email send-invoice → `send-invoice:<invoiceId>` - notifications invoice-overdue-notify → keyed per UTC day so dupes collapse intra-day but tomorrow's run can re-notify if unpaid - export gdpr-export → keyed on the exportId (unique per request) - webhooks deliver (3 sites: dispatch, retry, test) → keyed on the webhook_deliveries row UUID - maintenance expense-dedup-scan → keyed on expenseId - notifications send-notification-email → keyed on notification id - email send-inquiry-confirmation → keyed on interestId (1 per submission) - email send-inquiry-sales-notification → keyed on interestId+email (1 per recipient per submission) - reports generate-report → keyed on the generated_reports row id Pure refactor — no UX impact. Closes the BullMQ dedup gap that was the second half of the concurrency-auditor's CRITICAL-tier findings. Test fixture update: gdpr-export integration test now asserts the jobId option on the queue.add call. Tests 1315/1315. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,14 +48,18 @@ export async function sendInquiryNotifications(params: InquiryNotificationParams
|
||||
: 'sales@portnimara.com';
|
||||
|
||||
const emailQueue = getQueue('email');
|
||||
await emailQueue.add('send-inquiry-confirmation', {
|
||||
to: clientEmail,
|
||||
firstName,
|
||||
mooringNumber,
|
||||
contactEmail,
|
||||
portId,
|
||||
portName: 'Port Nimara', // future: resolve from getPortBrandingConfig
|
||||
});
|
||||
await emailQueue.add(
|
||||
'send-inquiry-confirmation',
|
||||
{
|
||||
to: clientEmail,
|
||||
firstName,
|
||||
mooringNumber,
|
||||
contactEmail,
|
||||
portId,
|
||||
portName: 'Port Nimara', // future: resolve from getPortBrandingConfig
|
||||
},
|
||||
{ jobId: `send-inquiry-confirmation:${interestId}` },
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error({ err, interestId }, 'Failed to queue client confirmation email');
|
||||
}
|
||||
@@ -115,16 +119,22 @@ export async function sendInquiryNotifications(params: InquiryNotificationParams
|
||||
|
||||
await Promise.all(
|
||||
externalEmails.map((externalEmail) =>
|
||||
emailQueue.add('send-inquiry-sales-notification', {
|
||||
to: externalEmail,
|
||||
fullName: clientFullName,
|
||||
email: clientEmail,
|
||||
phone: clientPhone,
|
||||
mooringNumber,
|
||||
crmUrl,
|
||||
portId,
|
||||
portName: 'Port Nimara',
|
||||
}),
|
||||
emailQueue.add(
|
||||
'send-inquiry-sales-notification',
|
||||
{
|
||||
to: externalEmail,
|
||||
fullName: clientFullName,
|
||||
email: clientEmail,
|
||||
phone: clientPhone,
|
||||
mooringNumber,
|
||||
crmUrl,
|
||||
portId,
|
||||
portName: 'Port Nimara',
|
||||
},
|
||||
// Per-recipient per-interest jobId so a public-form retry
|
||||
// doesn't fan out duplicate sales notifications.
|
||||
{ jobId: `send-inquiry-sales-notification:${interestId}:${externalEmail}` },
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user