Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🏗️ Build (push) Failing after 6m25s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
- Add BulkMailButton component for Payload list view - Add bulkMailEndpoint handler for processing prompts - Integrate bulk mail action into CrmContacts collection - Update dev:clean script to skip interactive prompts
192 lines
5.6 KiB
TypeScript
192 lines
5.6 KiB
TypeScript
import { PayloadRequest } from "payload";
|
|
|
|
export const bulkMailEndpointHandler = async (req: PayloadRequest) => {
|
|
try {
|
|
let body: any = {};
|
|
if (req.body) {
|
|
try {
|
|
body = (await req.json?.()) || {};
|
|
} catch (_e) {
|
|
body = req.body;
|
|
}
|
|
}
|
|
|
|
const { contactIds, instructions, isTest } = body;
|
|
|
|
if (!req.user) {
|
|
return Response.json(
|
|
{ success: false, error: "Unauthorized" },
|
|
{ status: 401 },
|
|
);
|
|
}
|
|
|
|
if (!Array.isArray(contactIds) || contactIds.length === 0) {
|
|
return Response.json(
|
|
{ success: false, error: "No contacts selected" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const OPENROUTER_KEY =
|
|
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
|
if (!OPENROUTER_KEY) {
|
|
return Response.json(
|
|
{ success: false, error: "Missing OPENROUTER_API_KEY" },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
|
|
const sentEmails = [];
|
|
const interactions = [];
|
|
|
|
for (const contactId of contactIds) {
|
|
// Fetch contact with account populated
|
|
const contact = await req.payload.findByID({
|
|
collection: "crm-contacts",
|
|
id: contactId,
|
|
depth: 1,
|
|
});
|
|
|
|
if (!contact || !contact.email) continue;
|
|
|
|
const account = contact.account as any;
|
|
const accountInfo = account
|
|
? `
|
|
Company Name: ${account.name || "Unknown"}
|
|
Website: ${account.website || "Unknown"}
|
|
Industry: ${account.industry || "Unknown"}
|
|
Website Status: ${account.websiteStatus || "Unknown"}
|
|
Internal Notes: ${account.notes || "None"}
|
|
`
|
|
: "No company information available.";
|
|
|
|
const prompt = `You are an expert sales/business development AI assistant. Write a professional, personalized German B2B outreach email ("Anschreiben").
|
|
|
|
CONTEXT ABOUT THE CONTACT:
|
|
Name: ${contact.fullName || contact.firstName || "Unknown"}
|
|
Role: ${contact.role || "Unknown"}
|
|
|
|
CONTEXT ABOUT THEIR COMPANY:
|
|
${accountInfo}
|
|
|
|
USER INSTRUCTIONS / GOAL OF THE EMAIL:
|
|
${instructions}
|
|
|
|
CRITICAL INSTRUCTIONS:
|
|
1. Write the email subject on the FIRST line as: "SUBJECT: <Your Subject>"
|
|
2. The rest of the message should be the email body.
|
|
3. Keep it professional and natural in German (Sie-form unless you think Du is appropriate based on the industry, but prefer Sie).
|
|
4. Output only the email text, no markdown code blocks around it. No extra chit-chat.
|
|
`;
|
|
|
|
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${OPENROUTER_KEY}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
model: "google/gemini-3-flash-preview",
|
|
messages: [{ role: "user", content: prompt }],
|
|
}),
|
|
});
|
|
|
|
const data = await res.json();
|
|
const generatedText = data.choices?.[0]?.message?.content?.trim() || "";
|
|
|
|
if (!generatedText) {
|
|
console.error("AI Generation failed for contact", contactId);
|
|
continue;
|
|
}
|
|
|
|
// Parse subject and body
|
|
const lines = generatedText.split("\n");
|
|
let subject = "Information von Mintel";
|
|
let bodyText = generatedText;
|
|
|
|
if (lines[0].startsWith("SUBJECT:")) {
|
|
subject = lines[0].replace("SUBJECT:", "").trim();
|
|
bodyText = lines.slice(1).join("\n").trim();
|
|
}
|
|
|
|
// Use a simple HTML wrapper to preserve line breaks
|
|
const htmlBody = `<p>${bodyText.replace(/\n/g, "<br/>")}</p>`;
|
|
|
|
const targetEmail = isTest ? req.user.email : contact.email;
|
|
|
|
// Send the email
|
|
await req.payload.sendEmail({
|
|
to: targetEmail,
|
|
subject: (isTest ? "[TEST] " : "") + subject,
|
|
html: htmlBody,
|
|
});
|
|
|
|
sentEmails.push({ contactId, email: targetEmail });
|
|
|
|
if (!isTest) {
|
|
// Log interaction
|
|
const interaction = await req.payload.create({
|
|
collection: "crm-interactions",
|
|
data: {
|
|
type: "email",
|
|
subject: `Outbound AI Mail: ${subject}`,
|
|
date: new Date().toISOString(),
|
|
contact: contact.id,
|
|
account: account?.id,
|
|
content: {
|
|
root: {
|
|
type: "root",
|
|
format: "",
|
|
indent: 0,
|
|
version: 1,
|
|
direction: "ltr",
|
|
children: [
|
|
{
|
|
type: "paragraph",
|
|
format: "",
|
|
indent: 0,
|
|
version: 1,
|
|
direction: "ltr",
|
|
children: [
|
|
{
|
|
type: "text",
|
|
mode: "normal",
|
|
style: "",
|
|
detail: 0,
|
|
format: 0,
|
|
text: bodyText,
|
|
version: 1,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
interactions.push(interaction.id);
|
|
|
|
// Update lead temperature to "warm" if it's currently cold and the status is lead
|
|
if (
|
|
account &&
|
|
account.status === "lead" &&
|
|
account.leadTemperature === "cold"
|
|
) {
|
|
await req.payload.update({
|
|
collection: "crm-accounts",
|
|
id: account.id,
|
|
data: {
|
|
leadTemperature: "warm",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return Response.json({ success: true, sentEmails, testMode: isTest });
|
|
} catch (e: any) {
|
|
console.error("Bulk Mail error", e);
|
|
return Response.json({ success: false, error: e.message }, { status: 500 });
|
|
}
|
|
};
|