only unsub contacts on permanent bounces (#156)

* only unsub on permanent counces

* add hard bounce to email usage

* add hard bounce for campaign

* fix
This commit is contained in:
KM Koushik
2025-04-26 09:05:54 +10:00
committed by GitHub
parent 759e438863
commit 6dc6b4d213
5 changed files with 57 additions and 16 deletions

View File

@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "DailyEmailUsage" ADD COLUMN "hardBounced" INTEGER NOT NULL DEFAULT 0;
-- AlterTable
ALTER TABLE "EmailEvent" ALTER COLUMN "status" DROP DEFAULT;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Campaign" ADD COLUMN "hardBounced" INTEGER NOT NULL DEFAULT 0;

View File

@@ -325,6 +325,7 @@ model Campaign {
clicked Int @default(0) clicked Int @default(0)
unsubscribed Int @default(0) unsubscribed Int @default(0)
bounced Int @default(0) bounced Int @default(0)
hardBounced Int @default(0)
complained Int @default(0) complained Int @default(0)
status CampaignStatus @default(DRAFT) status CampaignStatus @default(DRAFT)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -356,18 +357,19 @@ enum EmailUsageType {
} }
model DailyEmailUsage { model DailyEmailUsage {
teamId Int teamId Int
date String date String
type EmailUsageType type EmailUsageType
domainId Int domainId Int
sent Int @default(0) sent Int @default(0)
delivered Int @default(0) delivered Int @default(0)
opened Int @default(0) opened Int @default(0)
clicked Int @default(0) clicked Int @default(0)
bounced Int @default(0) bounced Int @default(0)
complained Int @default(0) complained Int @default(0)
createdAt DateTime @default(now()) hardBounced Int @default(0)
updatedAt DateTime @updatedAt createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)

View File

@@ -334,7 +334,8 @@ export async function sendCampaignEmail(
export async function updateCampaignAnalytics( export async function updateCampaignAnalytics(
campaignId: string, campaignId: string,
emailStatus: EmailStatus emailStatus: EmailStatus,
hardBounce: boolean = false
) { ) {
const campaign = await db.campaign.findUnique({ const campaign = await db.campaign.findUnique({
where: { id: campaignId }, where: { id: campaignId },
@@ -361,6 +362,9 @@ export async function updateCampaignAnalytics(
break; break;
case EmailStatus.BOUNCED: case EmailStatus.BOUNCED:
updateData.bounced = { increment: 1 }; updateData.bounced = { increment: 1 };
if (hardBounce) {
updateData.hardBounced = { increment: 1 };
}
break; break;
case EmailStatus.COMPLAINED: case EmailStatus.COMPLAINED:
updateData.complained = { increment: 1 }; updateData.complained = { increment: 1 };

View File

@@ -1,5 +1,10 @@
import { EmailStatus, Prisma, UnsubscribeReason } from "@prisma/client"; import { EmailStatus, Prisma, UnsubscribeReason } from "@prisma/client";
import { SesClick, SesEvent, SesEventDataKey } from "~/types/aws-types"; import {
SesBounce,
SesClick,
SesEvent,
SesEventDataKey,
} from "~/types/aws-types";
import { db } from "../db"; import { db } from "../db";
import { import {
unsubscribeContact, unsubscribeContact,
@@ -57,6 +62,12 @@ export async function parseSesHook(data: SesEvent) {
// Update daily email usage statistics // Update daily email usage statistics
const today = new Date().toISOString().split("T")[0] as string; // Format: YYYY-MM-DD const today = new Date().toISOString().split("T")[0] as string; // Format: YYYY-MM-DD
const isHardBounced =
mailStatus === EmailStatus.BOUNCED &&
(mailData as SesBounce).bounceType === "Permanent";
console.log("mailStatus", mailStatus, "isHardBounced", isHardBounced);
if ( if (
[ [
"DELIVERED", "DELIVERED",
@@ -89,11 +100,13 @@ export async function parseSesHook(data: SesEvent) {
bounced: updateField === "bounced" ? 1 : 0, bounced: updateField === "bounced" ? 1 : 0,
complained: updateField === "complained" ? 1 : 0, complained: updateField === "complained" ? 1 : 0,
sent: updateField === "sent" ? 1 : 0, sent: updateField === "sent" ? 1 : 0,
hardBounced: isHardBounced ? 1 : 0,
}, },
update: { update: {
[updateField]: { [updateField]: {
increment: 1, increment: 1,
}, },
...(isHardBounced ? { hardBounced: { increment: 1 } } : {}),
}, },
}); });
} }
@@ -108,6 +121,7 @@ export async function parseSesHook(data: SesEvent) {
campaignId: email.campaignId, campaignId: email.campaignId,
teamId: email.teamId, teamId: email.teamId,
event: mailStatus, event: mailStatus,
mailData: data,
}); });
const mailEvent = await db.emailEvent.findFirst({ const mailEvent = await db.emailEvent.findFirst({
@@ -118,7 +132,11 @@ export async function parseSesHook(data: SesEvent) {
}); });
if (!mailEvent) { if (!mailEvent) {
await updateCampaignAnalytics(email.campaignId, mailStatus); await updateCampaignAnalytics(
email.campaignId,
mailStatus,
isHardBounced
);
} }
} }
} }
@@ -139,13 +157,23 @@ async function checkUnsubscribe({
campaignId, campaignId,
teamId, teamId,
event, event,
mailData,
}: { }: {
contactId: string; contactId: string;
campaignId: string; campaignId: string;
teamId: number; teamId: number;
event: EmailStatus; event: EmailStatus;
mailData: SesEvent;
}) { }) {
if (event === EmailStatus.BOUNCED || event === EmailStatus.COMPLAINED) { /**
* If the email is bounced and the bounce type is permanent, we need to unsubscribe the contact
* If the email is complained, we need to unsubscribe the contact
*/
if (
(event === EmailStatus.BOUNCED &&
mailData.bounce?.bounceType === "Permanent") ||
event === EmailStatus.COMPLAINED
) {
const contact = await db.contact.findUnique({ const contact = await db.contact.findUnique({
where: { where: {
id: contactId, id: contactId,