Files
mintel.me/apps/web/src/payload/endpoints/bulkMailEndpoint.ts
Marc Mintel 3f6fa36f9b
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
feat(crm): implement bulk mail action for contacts
- 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
2026-03-30 19:27:56 +02:00

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 });
}
};