fix: use existing email transport for phone summaries
This commit is contained in:
parent
0d23693642
commit
74a23ae2af
4 changed files with 56 additions and 53 deletions
|
|
@ -33,7 +33,6 @@ ADMIN_EMAIL=
|
||||||
# Direct phone-call visibility
|
# Direct phone-call visibility
|
||||||
PHONE_AGENT_INTERNAL_TOKEN=
|
PHONE_AGENT_INTERNAL_TOKEN=
|
||||||
PHONE_CALL_SUMMARY_FROM_EMAIL=
|
PHONE_CALL_SUMMARY_FROM_EMAIL=
|
||||||
RESEND_API_KEY=
|
|
||||||
|
|
||||||
# Placeholder for a later LiveKit rollout
|
# Placeholder for a later LiveKit rollout
|
||||||
LIVEKIT_URL=
|
LIVEKIT_URL=
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ ADMIN_EMAIL=
|
||||||
|
|
||||||
PHONE_AGENT_INTERNAL_TOKEN=
|
PHONE_AGENT_INTERNAL_TOKEN=
|
||||||
PHONE_CALL_SUMMARY_FROM_EMAIL=
|
PHONE_CALL_SUMMARY_FROM_EMAIL=
|
||||||
RESEND_API_KEY=
|
|
||||||
|
|
||||||
USESEND_API_KEY=
|
USESEND_API_KEY=
|
||||||
USESEND_BASE_URL=
|
USESEND_BASE_URL=
|
||||||
|
|
|
||||||
|
|
@ -46,15 +46,19 @@ export async function sendTransactionalEmail({
|
||||||
subject,
|
subject,
|
||||||
html,
|
html,
|
||||||
replyTo,
|
replyTo,
|
||||||
|
from,
|
||||||
}: {
|
}: {
|
||||||
to: string;
|
to: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
html: string;
|
html: string;
|
||||||
replyTo?: string;
|
replyTo?: string;
|
||||||
|
from?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const sender = from || FROM_EMAIL;
|
||||||
|
|
||||||
if (usesend) {
|
if (usesend) {
|
||||||
return usesend.emails.send({
|
return usesend.emails.send({
|
||||||
from: FROM_EMAIL,
|
from: sender,
|
||||||
to,
|
to,
|
||||||
subject,
|
subject,
|
||||||
html,
|
html,
|
||||||
|
|
@ -65,7 +69,7 @@ export async function sendTransactionalEmail({
|
||||||
if (sesClient) {
|
if (sesClient) {
|
||||||
return sesClient.send(
|
return sesClient.send(
|
||||||
new SendEmailCommand({
|
new SendEmailCommand({
|
||||||
FromEmailAddress: FROM_EMAIL,
|
FromEmailAddress: sender,
|
||||||
Destination: {
|
Destination: {
|
||||||
ToAddresses: [to],
|
ToAddresses: [to],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { businessConfig } from "@/lib/seo-config";
|
import { businessConfig } from "@/lib/seo-config";
|
||||||
|
import { isEmailConfigured, sendTransactionalEmail } from "@/lib/email";
|
||||||
|
|
||||||
export type AdminPhoneCallTurn = {
|
export type AdminPhoneCallTurn = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -132,15 +133,14 @@ export async function sendPhoneCallSummaryEmail(args: {
|
||||||
detail: AdminPhoneCallDetail;
|
detail: AdminPhoneCallDetail;
|
||||||
adminUrl: string;
|
adminUrl: string;
|
||||||
}) {
|
}) {
|
||||||
const resendApiKey = String(process.env.RESEND_API_KEY || "").trim();
|
|
||||||
const adminEmail = String(process.env.ADMIN_EMAIL || "").trim().toLowerCase();
|
const adminEmail = String(process.env.ADMIN_EMAIL || "").trim().toLowerCase();
|
||||||
const fromEmail = String(process.env.PHONE_CALL_SUMMARY_FROM_EMAIL || "").trim();
|
const fromEmail = String(process.env.PHONE_CALL_SUMMARY_FROM_EMAIL || "").trim();
|
||||||
|
|
||||||
if (!adminEmail || !resendApiKey || !fromEmail) {
|
if (!adminEmail || !fromEmail || !isEmailConfigured()) {
|
||||||
const missing = [
|
const missing = [
|
||||||
!adminEmail ? "ADMIN_EMAIL" : null,
|
!adminEmail ? "ADMIN_EMAIL" : null,
|
||||||
!resendApiKey ? "RESEND_API_KEY" : null,
|
|
||||||
!fromEmail ? "PHONE_CALL_SUMMARY_FROM_EMAIL" : null,
|
!fromEmail ? "PHONE_CALL_SUMMARY_FROM_EMAIL" : null,
|
||||||
|
!isEmailConfigured() ? "email transport" : null,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -154,60 +154,61 @@ export async function sendPhoneCallSummaryEmail(args: {
|
||||||
const callerNumber = normalizePhoneFromIdentity(args.detail.call.participantIdentity) || "Unknown caller";
|
const callerNumber = normalizePhoneFromIdentity(args.detail.call.participantIdentity) || "Unknown caller";
|
||||||
const statusLabel = args.detail.call.callStatus.toUpperCase();
|
const statusLabel = args.detail.call.callStatus.toUpperCase();
|
||||||
|
|
||||||
const text = [
|
const transcriptHtml = args.detail.turns
|
||||||
`Rocky Mountain Vending phone call summary`,
|
.slice(-6)
|
||||||
``,
|
.map((turn) => {
|
||||||
`Caller: ${callerNumber}`,
|
const role = turn.role.replace(/[<>&"]/g, "");
|
||||||
`Started: ${formatPhoneCallTimestamp(args.detail.call.startedAt)}`,
|
const text = turn.text
|
||||||
`Duration: ${formatPhoneCallDuration(args.detail.call.durationMs)}`,
|
.replace(/&/g, "&")
|
||||||
`Call status: ${statusLabel}`,
|
.replace(/</g, "<")
|
||||||
`Jessica answered: ${args.detail.call.answered ? "Yes" : "No"}`,
|
.replace(/>/g, ">");
|
||||||
`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 response = await fetch("https://api.resend.com/emails", {
|
return `<li><strong>${role}:</strong> ${text}</li>`;
|
||||||
method: "POST",
|
})
|
||||||
headers: {
|
.join("");
|
||||||
Authorization: `Bearer ${resendApiKey}`,
|
|
||||||
"Content-Type": "application/json",
|
const html = `
|
||||||
},
|
<div style="font-family: Arial, sans-serif; color: #111827; line-height: 1.6;">
|
||||||
body: JSON.stringify({
|
<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,
|
from: fromEmail,
|
||||||
to: [adminEmail],
|
to: adminEmail,
|
||||||
subject: `[RMV Phone] ${statusLabel} call from ${callerNumber}`,
|
subject: `[RMV Phone] ${statusLabel} call from ${callerNumber}`,
|
||||||
text,
|
html,
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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}.`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: "failed" as const,
|
|
||||||
error: errorText,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: "sent" as const,
|
status: "sent" as const,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: "failed" as const,
|
||||||
|
error: error instanceof Error ? error.message : "Failed to send phone call summary email.",
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildFallbackPhoneCallUrl(callId: string) {
|
export function buildFallbackPhoneCallUrl(callId: string) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue