1// Email-to-Ticket AI Worker: Smart ticket creation, update, or reopen
2const Redis = require("ioredis");
3const { v4: uuidv4 } = require("uuid");
4const fetch = require("node-fetch");
6const redisConfig = require('./config/redis');
7const sub = new Redis(redisConfig);
8const pub = new Redis(redisConfig);
10sub.subscribe("emails:ai:analysis");
16async function findRelatedTicket(email) {
17 // Call backend API to find open/recent tickets for this customer/email
18 const res = await fetch(`http://localhost:3000/api/tickets/find-related?email=${encodeURIComponent(email.from)}&subject=${encodeURIComponent(email.subject)}`);
19 if (!res.ok) return null;
20 return await res.json();
30async function addNoteToTicket(ticketId, note, isReopen = false, referenceId = null) {
31 // Add note to ticket, optionally reopen and reference old ticket
32 await fetch(`http://localhost:3000/api/tickets/${ticketId}/notes`, {
34 headers: { "Content-Type": "application/json" },
35 body: JSON.stringify({
38 reference_ticket_id: referenceId || undefined,
39 reopen: isReopen || undefined
50async function createNewTicket(email, note, referenceId = null) {
51 // Create a new ticket and add the note
52 const res = await fetch(`http://localhost:3000/api/tickets`, {
54 headers: { "Content-Type": "application/json" },
55 body: JSON.stringify({
58 customer_email: email.from,
60 reference_ticket_id: referenceId || undefined
63 if (!res.ok) return null;
64 return await res.json();
74async function sendTicketEmail(tenantId, to, subject, body) {
75 // Call backend email send endpoint
76 await fetch(`http://localhost:3000/api/email/send/${tenantId}`, {
78 headers: { "Content-Type": "application/json" },
79 body: JSON.stringify({ to, subject, body })
83sub.on("message", async (_, message) => {
84 const { email, ai } = JSON.parse(message); // { email, ai: { status, reason, confidence } }
85 // Find related ticket
86 const related = await findRelatedTicket(email);
88 if (related && related.status === "open") {
89 // Add note to open ticket
90 await addNoteToTicket(related.id, ai.reason);
91 ticketId = related.id;
92 } else if (related && related.status === "closed" && !related.billed) {
93 // Reopen closed ticket and add note
94 await addNoteToTicket(related.id, ai.reason, true);
95 ticketId = related.id;
96 } else if (related && related.status === "closed" && related.billed) {
97 // Create new ticket, reference old
98 const newTicket = await createNewTicket(email, ai.reason, related.id);
100 await addNoteToTicket(newTicket.id, `Related to previous ticket #${related.id}`);
101 ticketId = newTicket.id;
104 // No related ticket, create new
105 const newTicket = await createNewTicket(email, ai.reason);
106 if (newTicket) ticketId = newTicket.id;
108 // Send notification email to customer (if ticketId and email.from)
109 if (ticketId && email.from) {
110 // Compose notification body
111 const subject = `Ticket Update: ${email.subject}`;
112 const body = `Your ticket has been updated.\n\n${ai.reason}`;
113 // Assume tenant_id is available on email or related
114 const tenantId = email.tenant_id || (related && related.tenant_id);
116 await sendTicketEmail(tenantId, email.from, subject, body);
119 // Optionally publish dashboard event
120 pub.publish("dashboard:events", JSON.stringify({ type: "ticket_update", email, ai }));
123console.log("Email-to-Ticket AI Worker running: smart ticket creation/update/reopen.");