fix: reconcile totalRecipients in CampaignService to prevent stuck campaigns

closes #348
This commit is contained in:
Dries Augustyns
2026-04-20 20:15:56 +02:00
parent bfdfde68fb
commit 4322de929a
+42
View File
@@ -505,6 +505,48 @@ export class CampaignService {
limit,
cursor: nextCursor,
});
} else {
// Last batch: reconcile totalRecipients to the actual number of emails created.
// Dynamic segments re-evaluate on each batch query, so contacts that left the
// segment after totalRecipients was calculated are silently skipped. Without this
// reconciliation, sentCount can never reach the original totalRecipients and the
// campaign remains stuck in SENDING forever.
const [actualEmailCount, alreadySentCount] = await Promise.all([
prisma.email.count({where: {campaignId}}),
prisma.email.count({where: {campaignId, sentAt: {not: null}}}),
]);
await prisma.campaign.update({
where: {id: campaignId},
data: {totalRecipients: actualEmailCount},
});
// If all emails were already processed by the time the last batch finished
// (race: worker was faster than the batch chain), finalize the campaign now.
if (actualEmailCount === 0 || alreadySentCount >= actualEmailCount) {
const finalCampaign = await prisma.campaign.findUnique({
where: {id: campaignId},
include: {project: {select: {name: true}}},
});
if (finalCampaign && finalCampaign.status === CampaignStatus.SENDING) {
await prisma.campaign.update({
where: {id: campaignId},
data: {status: CampaignStatus.SENT, sentCount: alreadySentCount},
});
signale.success(
`[CAMPAIGN] Campaign ${finalCampaign.name} finalized after last batch: ${alreadySentCount}/${actualEmailCount} emails sent`,
);
await NtfyService.notifyCampaignSendCompleted(
finalCampaign.name,
finalCampaign.project.name,
finalCampaign.projectId,
alreadySentCount,
);
}
}
}
}