/** * Zammad API service – create/close tickets, manage users, post articles. */ const axios = require('axios'); const { ZAMMAD } = require('../config'); const zammadHeaders = () => ZAMMAD.URL && ZAMMAD.TOKEN ? { Authorization: `Token token=${ZAMMAD.TOKEN}`, 'Content-Type': 'application/json' } : null; function baseUrl() { return ZAMMAD.URL ? ZAMMAD.URL.replace(/\/+$/, '') : ''; } async function createZammadTicket({ subject, body, email, name, gameName, gameKey, group, discordUsername }) { if (!ZAMMAD.URL || !ZAMMAD.TOKEN) { console.warn('Zammad not configured; skipping ticket create.'); return null; } const url = `${baseUrl()}/api/v1/tickets`; const isDiscordTicket = Boolean(discordUsername); const firstname = isDiscordTicket ? '' : (name || 'Email Customer').split(' ')[0]; const lastname = isDiscordTicket ? '' : (name || 'Email Customer').split(' ').slice(1).join(' ') || 'Customer'; const payload = { title: subject || 'Support', group, customer: { email, firstname: firstname || '–', lastname: lastname || '–', role_ids: [3] }, state: 'new', priority: '2 normal', article: { subject: subject || 'Support', body: `Email: ${email}\nGame: ${gameName}\n\nMessage:\n${body}`, type: 'note', internal: false, sender: 'Customer', from: isDiscordTicket ? `${discordUsername} <${email}>` : `${name} <${email}>` } }; if (gameKey) payload.gameid = gameKey; if (discordUsername) payload.discordusername = String(discordUsername).slice(0, 120); const res = await axios.post(url, payload, { headers: { Authorization: `Token token=${ZAMMAD.TOKEN}`, 'Content-Type': 'application/json' } }); return res.data; } async function closeZammadTicket(zammadTicketId) { if (!ZAMMAD.URL || !ZAMMAD.TOKEN || !zammadTicketId) return; const url = `${baseUrl()}/api/v1/tickets/${zammadTicketId}`; await axios.patch(url, { state: 'closed' }, { headers: { Authorization: `Token token=${ZAMMAD.TOKEN}`, 'Content-Type': 'application/json' } }); } async function updateZammadUserDiscordId(zammadUserId, discordId) { if (!ZAMMAD.URL || !ZAMMAD.TOKEN || !zammadUserId || !discordId) return; const url = `${baseUrl()}/api/v1/users/${zammadUserId}`; await axios.patch(url, { discord_id: String(discordId) }, { headers: { Authorization: `Token token=${ZAMMAD.TOKEN}`, 'Content-Type': 'application/json' } }); } async function updateZammadUser(zammadUserId, attrs) { if (!ZAMMAD.URL || !ZAMMAD.TOKEN || !zammadUserId || !attrs || Object.keys(attrs).length === 0) return; const url = `${baseUrl()}/api/v1/users/${zammadUserId}`; const body = {}; if (attrs.discord_id != null) body.discord_id = String(attrs.discord_id); if (attrs.discord_username != null) body.discord_username = String(attrs.discord_username).slice(0, 120); if (Object.keys(body).length === 0) return; await axios.patch(url, body, { headers: { Authorization: `Token token=${ZAMMAD.TOKEN}`, 'Content-Type': 'application/json' } }); } async function searchZammadUsers(query) { if (!ZAMMAD.URL || !ZAMMAD.TOKEN || !query) return []; try { const { data } = await axios.get(`${baseUrl()}/api/v1/users/search`, { params: { query: String(query).trim() }, headers: zammadHeaders() }); return Array.isArray(data) ? data : data?.users || []; } catch (e) { if (e.response?.status === 404) return []; throw e; } } async function createZammadUser({ email, firstname, lastname, login, discordId, discordUsername }) { if (!ZAMMAD.URL || !ZAMMAD.TOKEN || !email) return null; const isDiscord = Boolean(discordUsername); const payload = { login: login || email, email: email.trim(), firstname: (firstname || '').trim() || (isDiscord ? '–' : 'Customer'), lastname: (lastname || '').trim() || (isDiscord ? '–' : 'User'), role_ids: [3] }; if (discordId) payload.discord_id = String(discordId); if (discordUsername) payload.discord_username = String(discordUsername).slice(0, 120); const { data } = await axios.post(`${baseUrl()}/api/v1/users`, payload, { headers: zammadHeaders() }); return data; } async function ensureZammadUserForDiscordUser(websiteUser, opts = {}) { if (!websiteUser?.email || !ZAMMAD.URL || !ZAMMAD.TOKEN) return null; const email = String(websiteUser.email).trim().toLowerCase(); const discordId = websiteUser.discordID ? String(websiteUser.discordID) : null; const discordUsername = opts.discordUsername ? String(opts.discordUsername).slice(0, 120) : null; const firstname = (websiteUser.firstname || '').trim(); const lastname = (websiteUser.lastname || '').trim(); let zammadUser = null; try { const list = await searchZammadUsers(email); zammadUser = list.find((u) => (u.email || '').toLowerCase() === email) || null; } catch (e) { console.error('Zammad user search failed:', e.response?.data || e.message); return null; } if (!zammadUser) { try { zammadUser = await createZammadUser({ email, firstname: firstname || (discordUsername ? '–' : 'Customer'), lastname: lastname || (discordUsername ? '–' : 'User'), login: email, discordId, discordUsername }); } catch (e) { console.error('Zammad user create failed:', e.response?.data || e.message); return null; } } if (zammadUser?.id && (discordId || discordUsername)) { try { await updateZammadUser(zammadUser.id, { ...(discordId && { discord_id: discordId }), ...(discordUsername && { discord_username: discordUsername }) }); } catch (_) { /* custom attributes may not exist in Zammad */ } } return zammadUser?.id ?? null; } async function getZammadTicketArticles(zammadTicketId) { if (!ZAMMAD.URL || !ZAMMAD.TOKEN || !zammadTicketId) return []; const url = `${baseUrl()}/api/v1/ticket_articles/by_ticket/${zammadTicketId}`; const res = await axios.get(url, { headers: { Authorization: `Token token=${ZAMMAD.TOKEN}` } }); return Array.isArray(res.data) ? res.data : []; } async function addZammadArticle(zammadTicketId, body, { from: fromDisplay } = {}) { if (!ZAMMAD.URL || !ZAMMAD.TOKEN || !zammadTicketId) return; const url = `${baseUrl()}/api/v1/ticket_articles`; const payload = { ticket_id: zammadTicketId, body: body || '', content_type: 'text/plain', type: 'note', internal: false, sender: 'Agent' }; if (fromDisplay) { payload.subject = `Discord reply from ${fromDisplay}`; } await axios.post(url, payload, { headers: { Authorization: `Token token=${ZAMMAD.TOKEN}`, 'Content-Type': 'application/json' } }); } module.exports = { createZammadTicket, closeZammadTicket, updateZammadUserDiscordId, updateZammadUser, searchZammadUsers, createZammadUser, ensureZammadUserForDiscordUser, getZammadTicketArticles, addZammadArticle, zammadHeaders };