Skip to content

Commit 5cbace0

Browse files
authored
v1.2.7 - Outreach tracked per campaign.
v1.2.7 - Outreach tracked per campaign.
2 parents 45ccada + 2f6c5a3 commit 5cbace0

File tree

5 files changed

+46
-9
lines changed

5 files changed

+46
-9
lines changed

app/(routes)/crm/accounts/lists/[listId]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ export default function AccountListDetailsPage() {
441441
variant="ghost"
442442
size="icon"
443443
className="text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors"
444-
onClick={() => setLeadToDelete(lead)}
444+
onClick={(e) => { e.stopPropagation(); setLeadToDelete(lead); }}
445445
disabled={deleting === lead.id}
446446
>
447447
{deleting === lead.id ? (

app/api/campaigns/route.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,11 @@ export async function POST(req: Request) {
235235
},
236236
});
237237

238+
// NOTE: Lead-level outreach_status is NOT reset here. The send route
239+
// (POST /api/outreach/send) overwrites outreach_status to "SENT" for each
240+
// lead as emails are dispatched. Old campaign data in crm_Outreach_Items
241+
// remains untouched — each campaign tracks replies/opens via its own items.
242+
238243
// Register CRON job for auto follow-ups if enabled
239244
if (followupConfig?.enabled && user?.team_id) {
240245
try {

app/api/email/inbound/route.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,21 +62,21 @@ export async function POST(req: Request) {
6262
const normalizeId = (id?: string) => id?.replace(/[<>]/g, "").trim();
6363
const inReplyTo = normalizeId(inReplyToHeader);
6464

65-
let matchedOutreachItem = null;
65+
let matchedOutreachItem: any = null;
6666

6767
// Match via In-Reply-To — try both normalized and raw formats
6868
if (inReplyTo) {
6969
// First try without angle brackets (normalized)
7070
matchedOutreachItem = await prismadb.crm_Outreach_Items.findFirst({
7171
where: { message_id: inReplyTo },
72-
select: { id: true, campaign: true, lead: true, account_id: true, subject: true },
72+
select: { id: true, campaign: true, lead: true, account_id: true, subject: true, sender_email: true },
7373
});
7474

7575
// Also try with angle brackets (some providers store them that way)
7676
if (!matchedOutreachItem && inReplyToHeader) {
7777
matchedOutreachItem = await prismadb.crm_Outreach_Items.findFirst({
7878
where: { message_id: inReplyToHeader.trim() },
79-
select: { id: true, campaign: true, lead: true, account_id: true, subject: true },
79+
select: { id: true, campaign: true, lead: true, account_id: true, subject: true, sender_email: true },
8080
});
8181
}
8282
}
@@ -89,7 +89,7 @@ export async function POST(req: Request) {
8989
if (allRefs.length > 0) {
9090
matchedOutreachItem = await prismadb.crm_Outreach_Items.findFirst({
9191
where: { message_id: { in: allRefs } },
92-
select: { id: true, campaign: true, lead: true, account_id: true, subject: true },
92+
select: { id: true, campaign: true, lead: true, account_id: true, subject: true, sender_email: true },
9393
});
9494
}
9595
}
@@ -106,6 +106,18 @@ export async function POST(req: Request) {
106106
});
107107
}
108108

109+
// Resolve the correct user for routing: prefer sender_email lookup over campaign owner
110+
let resolvedUserId = campaignRecord?.user || undefined;
111+
if (matchedOutreachItem?.sender_email) {
112+
try {
113+
const senderUser = await prismadb.users.findFirst({
114+
where: { email: matchedOutreachItem.sender_email },
115+
select: { id: true },
116+
});
117+
if (senderUser) resolvedUserId = senderUser.id;
118+
} catch { /* fall back to campaign owner */ }
119+
}
120+
109121
let finalLeadId = matchedOutreachItem?.lead || undefined;
110122
let sentimentResult: any = null;
111123

@@ -119,7 +131,7 @@ export async function POST(req: Request) {
119131
originalSubject: matchedOutreachItem.subject || subject,
120132
leadName: isAccountOnly ? "Account Contact" : undefined,
121133
},
122-
campaignRecord?.user || "sysadm"
134+
resolvedUserId || "sysadm"
123135
);
124136

125137
if (isAccountOnly && sentimentResult?.extractedContact) {
@@ -155,7 +167,7 @@ export async function POST(req: Request) {
155167
const newThread = await prismadb.crm_Email_Thread.create({
156168
data: {
157169
team_id: campaignRecord?.team_id || "600000000000000000000000",
158-
user: campaignRecord?.user || undefined,
170+
user: resolvedUserId,
159171
lead: finalLeadId,
160172
campaign: matchedOutreachItem?.campaign || undefined,
161173
outreach_item: matchedOutreachItem?.id || undefined,

app/api/outreach/send/route.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -587,19 +587,32 @@ export async function POST(req: Request) {
587587
const resolvedSenderName = senderName || session.user.name || "Outreach";
588588
const replyToAddress = `"${resolvedSenderName}" <${userEmailPrefix}@${inboundDomain}>`;
589589

590+
// Track the actual sender email for inbound routing
591+
let actualSenderEmail = session.user.email || '';
592+
590593
if (senderMode === "personal") {
591594
// Personal mode: send via Amazon SES using the user's identity
595+
actualSenderEmail = session.user.email || process.env.SES_FROM_ADDRESS || 'noreply@basalthq.com';
592596
await sendSystemEmail({
593597
to: toEmail,
594598
subject,
595599
text,
596600
html,
597-
from: `"${resolvedSenderName}" <${session.user.email || process.env.SES_FROM_ADDRESS || 'noreply@basalthq.com'}>`,
601+
from: `"${resolvedSenderName}" <${actualSenderEmail}>`,
598602
replyTo: replyToAddress,
599603
}).then(id => { messageId = id || `ses_personal_${Date.now()}`; });
600604
} else {
601605
// Company mode: use team email config
602606
try {
607+
// Resolve the team's outbound email for tracking
608+
try {
609+
const teamOutbound = await prismadb.teamEmailConfig.findUnique({
610+
where: { team_id_purpose: { team_id: user.team_id!, purpose: "OUTREACH" } },
611+
select: { smtp_user: true },
612+
});
613+
if (teamOutbound?.smtp_user) actualSenderEmail = teamOutbound.smtp_user;
614+
} catch { /* keep session email */ }
615+
603616
const teamMsgId = await sendTeamEmail(user.team_id, {
604617
to: toEmail,
605618
subject,
@@ -613,12 +626,13 @@ export async function POST(req: Request) {
613626
// Team email not configured — for test mode, fall back to system email
614627
if (testMode) {
615628
systemLogger.warn("[OUTREACH_SEND] Team email failed in test mode, falling back to system email:", teamErr?.message);
629+
actualSenderEmail = process.env.SES_FROM_ADDRESS || 'noreply@basalthq.com';
616630
const fallbackMsgId = await sendSystemEmail({
617631
to: toEmail,
618632
subject,
619633
text,
620634
html,
621-
from: process.env.SES_FROM_ADDRESS || 'noreply@basalthq.com',
635+
from: actualSenderEmail,
622636
replyTo: replyToAddress,
623637
});
624638
messageId = fallbackMsgId || `test_system_sent_${Date.now()}`;
@@ -673,6 +687,7 @@ export async function POST(req: Request) {
673687
candidate_job_title: lead.jobTitle || undefined,
674688
account_id: pipelineResult.accountId,
675689
contact_id: pipelineResult.contactId,
690+
sender_email: actualSenderEmail,
676691
},
677692
});
678693

@@ -744,6 +759,7 @@ export async function POST(req: Request) {
744759
candidate_name: [lead.firstName, lead.lastName].filter(Boolean).join(" ") || null,
745760
candidate_company: lead.company || null,
746761
candidate_job_title: lead.jobTitle || null,
762+
sender_email: actualSenderEmail,
747763
} as any,
748764
});
749765
} catch (itemErr: any) {
@@ -778,6 +794,7 @@ export async function POST(req: Request) {
778794
candidate_company: lead.company || null,
779795
candidate_job_title: lead.jobTitle || null,
780796
account_id: lead.id,
797+
sender_email: actualSenderEmail,
781798
} as any,
782799
}).catch((e: any) => systemLogger.warn(`[OUTREACH_SEND] Failed to create outreach item for account ${lead.id}: ${e?.message}`));
783800
}

prisma/schema.prisma

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,9 @@ model crm_Outreach_Items {
551551
error_message String?
552552
retry_count Int @default(0)
553553
554+
// Sender tracking (which email address was used to send this item)
555+
sender_email String?
556+
554557
// Relations
555558
assigned_campaign crm_Outreach_Campaigns? @relation(fields: [campaign], references: [id])
556559
assigned_lead crm_Leads? @relation(name: "OutreachItemsLead", fields: [lead], references: [id])

0 commit comments

Comments
 (0)