Files
FEDEO/backend/src/routes/helpdesk.inbound.ts
2026-01-06 12:07:43 +01:00

143 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// modules/helpdesk/helpdesk.inbound.routes.ts
import { FastifyPluginAsync } from 'fastify'
import { createConversation } from '../modules/helpdesk/helpdesk.conversation.service.js'
import { addMessage } from '../modules/helpdesk/helpdesk.message.service.js'
import { getOrCreateContact } from '../modules/helpdesk/helpdesk.contact.service.js'
/**
* Öffentliche Route zum Empfang eingehender Kontaktformular-Nachrichten.
* Authentifizierung: über `public_token` aus helpdesk_channel_instances
*/
function extractDomain(email) {
if (!email) return null
const parts = email.split("@")
return parts.length === 2 ? parts[1].toLowerCase() : null
}
async function findCustomerOrContactByEmailOrDomain(server,fromMail, tenantId) {
const sender = fromMail
const senderDomain = extractDomain(sender)
if (!senderDomain) return null
// 1⃣ Direkter Match über contacts
const { data: contactMatch } = await server.supabase
.from("contacts")
.select("id, customer")
.eq("email", sender)
.eq("tenant", tenantId)
.maybeSingle()
if (contactMatch?.customer_id) return {
customer: contactMatch.customer,
contact: contactMatch.id
}
// 2⃣ Kunden laden, bei denen E-Mail oder Rechnungsmail passt
const { data: customers, error } = await server.supabase
.from("customers")
.select("id, infoData")
.eq("tenant", tenantId)
if (error) {
console.error(`[Helpdesk] Fehler beim Laden der Kunden:`, error.message)
return null
}
// 3⃣ Durch Kunden iterieren und prüfen
for (const c of customers || []) {
const info = c.infoData || {}
const email = info.email?.toLowerCase()
const invoiceEmail = info.invoiceEmail?.toLowerCase()
const emailDomain = extractDomain(email)
const invoiceDomain = extractDomain(invoiceEmail)
// exakter Match oder Domain-Match
if (
sender === email ||
sender === invoiceEmail ||
senderDomain === emailDomain ||
senderDomain === invoiceDomain
) {
return {customer: c.id, contact:null}
}
}
return null
}
const helpdeskInboundRoutes: FastifyPluginAsync = async (server) => {
// Öffentliche POST-Route
server.post('/helpdesk/inbound/:public_token', async (req, res) => {
const { public_token } = req.params as { public_token: string }
const { email, phone, display_name, subject, message } = req.body as {
email: string,
phone: string,
display_name: string
subject: string
message: string
}
if (!message) {
return res.status(400).send({ error: 'Message content required' })
}
// 1⃣ Kanalinstanz anhand des Tokens ermitteln
const { data: channel, error: channelError } = await server.supabase
.from('helpdesk_channel_instances')
.select('*')
.eq('public_token', public_token)
.single()
if (channelError || !channel) {
return res.status(404).send({ error: 'Invalid channel token' })
}
const tenant_id = channel.tenant_id
const channel_instance_id = channel.id
// @ts-ignore
const {customer, contact: contactPerson} = await findCustomerOrContactByEmailOrDomain(server,email, tenant_id )
// 2⃣ Kontakt finden oder anlegen
const contact = await getOrCreateContact(server, tenant_id, {
email,
phone,
display_name,
customer_id: customer,
contact_id: contactPerson,
})
// 3⃣ Konversation erstellen
const conversation = await createConversation(server, {
tenant_id,
contact,
channel_instance_id,
subject: subject ?? 'Kontaktformular Anfrage',
customer_id: customer,
contact_person_id: contactPerson
})
// 4⃣ Erste Nachricht hinzufügen
await addMessage(server, {
tenant_id,
conversation_id: conversation.id,
direction: 'incoming',
payload: { type: 'text', text: message },
raw_meta: { source: 'contact_form' },
})
// (optional) Auto-Antwort oder Event hier ergänzen
return res.status(201).send({
success: true,
conversation_id: conversation.id,
})
})
}
export default helpdeskInboundRoutes