fix: use existing email transport for phone summaries

This commit is contained in:
DMleadgen 2026-04-01 14:32:51 -06:00
parent 0d23693642
commit 74a23ae2af
Signed by: matt
GPG key ID: C2720CF8CD701894
4 changed files with 56 additions and 53 deletions

View file

@ -33,7 +33,6 @@ ADMIN_EMAIL=
# Direct phone-call visibility
PHONE_AGENT_INTERNAL_TOKEN=
PHONE_CALL_SUMMARY_FROM_EMAIL=
RESEND_API_KEY=
# Placeholder for a later LiveKit rollout
LIVEKIT_URL=

View file

@ -13,7 +13,6 @@ ADMIN_EMAIL=
PHONE_AGENT_INTERNAL_TOKEN=
PHONE_CALL_SUMMARY_FROM_EMAIL=
RESEND_API_KEY=
USESEND_API_KEY=
USESEND_BASE_URL=

View file

@ -46,15 +46,19 @@ export async function sendTransactionalEmail({
subject,
html,
replyTo,
from,
}: {
to: string;
subject: string;
html: string;
replyTo?: string;
from?: string;
}) {
const sender = from || FROM_EMAIL;
if (usesend) {
return usesend.emails.send({
from: FROM_EMAIL,
from: sender,
to,
subject,
html,
@ -65,7 +69,7 @@ export async function sendTransactionalEmail({
if (sesClient) {
return sesClient.send(
new SendEmailCommand({
FromEmailAddress: FROM_EMAIL,
FromEmailAddress: sender,
Destination: {
ToAddresses: [to],
},

View file

@ -1,4 +1,5 @@
import { businessConfig } from "@/lib/seo-config";
import { isEmailConfigured, sendTransactionalEmail } from "@/lib/email";
export type AdminPhoneCallTurn = {
id: string;
@ -132,15 +133,14 @@ export async function sendPhoneCallSummaryEmail(args: {
detail: AdminPhoneCallDetail;
adminUrl: string;
}) {
const resendApiKey = String(process.env.RESEND_API_KEY || "").trim();
const adminEmail = String(process.env.ADMIN_EMAIL || "").trim().toLowerCase();
const fromEmail = String(process.env.PHONE_CALL_SUMMARY_FROM_EMAIL || "").trim();
if (!adminEmail || !resendApiKey || !fromEmail) {
if (!adminEmail || !fromEmail || !isEmailConfigured()) {
const missing = [
!adminEmail ? "ADMIN_EMAIL" : null,
!resendApiKey ? "RESEND_API_KEY" : null,
!fromEmail ? "PHONE_CALL_SUMMARY_FROM_EMAIL" : null,
!isEmailConfigured() ? "email transport" : null,
].filter(Boolean);
return {
@ -154,60 +154,61 @@ export async function sendPhoneCallSummaryEmail(args: {
const callerNumber = normalizePhoneFromIdentity(args.detail.call.participantIdentity) || "Unknown caller";
const statusLabel = args.detail.call.callStatus.toUpperCase();
const text = [
`Rocky Mountain Vending phone call summary`,
``,
`Caller: ${callerNumber}`,
`Started: ${formatPhoneCallTimestamp(args.detail.call.startedAt)}`,
`Duration: ${formatPhoneCallDuration(args.detail.call.durationMs)}`,
`Call status: ${statusLabel}`,
`Jessica answered: ${args.detail.call.answered ? "Yes" : "No"}`,
`Lead outcome: ${args.detail.call.leadOutcome}`,
`Handoff requested: ${args.detail.call.handoffRequested ? "Yes" : "No"}`,
`Recording status: ${args.detail.call.recordingStatus || "Unavailable"}`,
args.detail.call.recordingUrl ? `Recording URL: ${args.detail.call.recordingUrl}` : `Recording URL: Not available in RMV admin`,
`Summary: ${summaryText}`,
`Admin call detail: ${callUrl}`,
args.detail.linkedLead?.id ? `Linked lead: ${args.detail.linkedLead.id}` : `Linked lead: None`,
``,
`Recent transcript:`,
...args.detail.turns.slice(-6).map((turn) => `${turn.role}: ${turn.text}`),
].join("\n");
const transcriptHtml = args.detail.turns
.slice(-6)
.map((turn) => {
const role = turn.role.replace(/[<>&"]/g, "");
const text = turn.text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
const response = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${resendApiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
return `<li><strong>${role}:</strong> ${text}</li>`;
})
.join("");
const html = `
<div style="font-family: Arial, sans-serif; color: #111827; line-height: 1.6;">
<h1 style="font-size: 20px; margin-bottom: 16px;">Rocky Mountain Vending phone call summary</h1>
<p><strong>Caller:</strong> ${callerNumber}</p>
<p><strong>Started:</strong> ${formatPhoneCallTimestamp(args.detail.call.startedAt)}</p>
<p><strong>Duration:</strong> ${formatPhoneCallDuration(args.detail.call.durationMs)}</p>
<p><strong>Call status:</strong> ${statusLabel}</p>
<p><strong>Jessica answered:</strong> ${args.detail.call.answered ? "Yes" : "No"}</p>
<p><strong>Lead outcome:</strong> ${args.detail.call.leadOutcome}</p>
<p><strong>Handoff requested:</strong> ${args.detail.call.handoffRequested ? "Yes" : "No"}</p>
<p><strong>Recording status:</strong> ${args.detail.call.recordingStatus || "Unavailable"}</p>
<p><strong>Recording URL:</strong> ${
args.detail.call.recordingUrl
? `<a href="${args.detail.call.recordingUrl}">${args.detail.call.recordingUrl}</a>`
: "Not available in RMV admin"
}</p>
<p><strong>Summary:</strong> ${summaryText}</p>
<p><strong>Admin call detail:</strong> <a href="${callUrl}">${callUrl}</a></p>
<p><strong>Linked lead:</strong> ${args.detail.linkedLead?.id || "None"}</p>
<h2 style="font-size: 16px; margin-top: 24px;">Recent transcript</h2>
<ul>${transcriptHtml || "<li>No transcript turns were captured.</li>"}</ul>
</div>
`;
try {
await sendTransactionalEmail({
from: fromEmail,
to: [adminEmail],
to: adminEmail,
subject: `[RMV Phone] ${statusLabel} call from ${callerNumber}`,
text,
}),
});
const body = (await response.json().catch(() => ({}))) as { message?: string; error?: unknown };
if (!response.ok) {
const errorText =
typeof body?.message === "string"
? body.message
: typeof body?.error === "string"
? body.error
: `Resend request failed with status ${response.status}.`;
html,
});
return {
status: "sent" as const,
error: undefined,
};
} catch (error) {
return {
status: "failed" as const,
error: errorText,
error: error instanceof Error ? error.message : "Failed to send phone call summary email.",
};
}
return {
status: "sent" as const,
error: undefined,
};
}
export function buildFallbackPhoneCallUrl(callId: string) {