import nodemailer from 'nodemailer'
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib'
/*
* Phone-IVR return-instructions email.
* Called by the FreePBX IVR middleware (localhost service on the PBX box) AFTER it has
* created the M_RMA in iDempiere. Email-only: no iDempiere, no cookie/session. Gated by
* the shared secret (X-IVR-Secret) from runtimeConfig.ivr.sharedSecret. The recipient is
* resolved by the middleware from the order's own ship-to user (never caller input).
* Generates + attaches a return-label PDF (return address with the RMA number).
*
* Body: { to, cc?, lang?, rmaNos: string[], orderNo?, customerName?, items: [{name,value,qty}] }
*/
interface Item { name?: string; value?: string; qty?: number }
// Static return-address lines, pipe-separated in IVR_RETURN_ADDRESS (runtimeConfig.ivr.returnAddress).
// The RMA number is inserted as line 2 so the warehouse can match the parcel.
const DEFAULT_RETURN_ADDRESS = 'LogYou GmbH|Mühlenweg 4|35110 Butzbach'
const addressLines = (baseRaw: string, rmaNos: string[]) => {
const base = String(baseRaw || DEFAULT_RETURN_ADDRESS).split('|').map(s => s.trim()).filter(Boolean)
return [base[0] || 'LogYou GmbH', 'RMA ' + rmaNos.join(' / '), ...base.slice(1)]
}
const esc = (s: any) => String(s == null ? '' : s)
.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"')
// keep only chars the PDF standard font (WinAnsi) can encode
const san = (s: any) => String(s == null ? '' : s).replace(/[^\x20-\x7E\xA0-\xFF]/g, ' ')
const buildHtmlDE = (rmaNos: string[], orderNo: string, customerName: string, items: Item[], addr: string[]) => {
const rmaList = rmaNos.map(esc).join(', ')
const rows = items.map(i => `
| ${esc(i.value)} |
${esc(i.name)} |
${esc(i.qty)} |
`).join('')
const addrHtml = addr.map((l, idx) => idx === 0 ? `${esc(l)}` : esc(l)).join('
')
return `
Rücksendeanweisungen
RMA ${rmaList}
|
|
Guten Tag${customerName ? ' ' + esc(customerName) : ''},
vielen Dank für Ihren Anruf. Ihre Rückgabe wurde unter der/den folgenden RMA-Nummer(n) registriert:
${rmaList}${orderNo ? ' (Bestellung ' + esc(orderNo) + ')' : ''}.
Folgende Artikel werden zurückgesendet:
| Artikelnr. |
Bezeichnung |
Menge |
${rows}
So senden Sie zurück:
- Verpacken Sie die Artikel sicher.
- Drucken Sie das beigefügte Rücksendeetikett (PDF) und kleben Sie es gut sichtbar auf das Paket.
- Senden Sie das Paket an die unten stehende Adresse. Die RMA-Nummer ${rmaList} ist bereits Teil der Adresse.
Rücksendeadresse: ${addrHtml} |
|
|
Diese E-Mail wurde automatisch versendet. Bitte antworten Sie nicht direkt auf diese E-Mail.
|
|
`
}
const buildText = (rmaNos: string[], orderNo: string, items: Item[], addr: string[]) =>
`Ihre Rückgabe wurde registriert.\n\nRMA-Nummer(n): ${rmaNos.join(', ')}${orderNo ? '\nBestellung: ' + orderNo : ''}\n\n` +
`Artikel:\n${items.map(i => `- ${i.qty}x ${i.name || ''} (${i.value || ''})`).join('\n')}\n\n` +
`So senden Sie zurück:\n1. Artikel sicher verpacken.\n2. Beigefügtes Rücksendeetikett (PDF) ausdrucken und auf das Paket kleben.\n3. Paket senden an:\n\n${addr.join('\n')}\n`
// Return-label PDF: a printable A4 sheet showing the return address (incl. RMA no), to be
// stuck on the parcel. Uses the standard Helvetica font (WinAnsi) — dynamic text sanitized.
async function buildLabelPdf (rmaNos: string[], orderNo: string, items: Item[], addr: string[]): Promise {
const doc = await PDFDocument.create()
const page = doc.addPage([595.28, 841.89]) // A4
const font = await doc.embedFont(StandardFonts.Helvetica)
const bold = await doc.embedFont(StandardFonts.HelveticaBold)
const { width, height } = page.getSize()
const navy = rgb(0.12, 0.22, 0.5)
const black = rgb(0, 0, 0)
const gray = rgb(0.42, 0.42, 0.42)
const lightBlue = rgb(0.92, 0.95, 1)
let y = height - 60
page.drawText(san('Rücksendeetikett'), { x: 60, y, size: 24, font: bold, color: navy })
y -= 16
page.drawText('Return label', { x: 60, y, size: 11, font, color: gray })
// RMA highlight box
y -= 44
page.drawRectangle({ x: 60, y: y - 6, width: width - 120, height: 42, color: lightBlue, borderColor: navy, borderWidth: 1 })
page.drawText(san('RMA ' + rmaNos.join(' / ')), { x: 76, y: y + 8, size: 20, font: bold, color: navy })
y -= 44
page.drawText(san('Bitte dieses Etikett gut sichtbar außen auf das Paket kleben.'), { x: 60, y, size: 11, font, color: black })
// Address box (shipping-label style)
y -= 28
const boxH = 150
const boxTop = y
page.drawRectangle({ x: 60, y: boxTop - boxH, width: 330, height: boxH, borderColor: black, borderWidth: 1.2 })
let ay = boxTop - 26
page.drawText(san('Empfänger / An:'), { x: 78, y: ay, size: 10, font, color: gray })
ay -= 28
addr.forEach((ln, idx) => {
page.drawText(san(ln), { x: 78, y: ay, size: 15, font: idx === 0 ? bold : font, color: black })
ay -= 24
})
// Order + items
y = boxTop - boxH - 36
if (orderNo) { page.drawText(san('Bestellung: ' + orderNo), { x: 60, y, size: 11, font, color: black }); y -= 22 }
if (items && items.length) {
page.drawText(san('Artikel:'), { x: 60, y, size: 11, font: bold, color: black }); y -= 18
for (const it of items) {
const line = '- ' + (it.qty != null ? it.qty + 'x ' : '') + (it.name || '') + (it.value ? ' (' + it.value + ')' : '')
page.drawText(san(line).substring(0, 90), { x: 70, y, size: 10, font, color: black })
y -= 16
if (y < 70) break
}
}
page.drawText(san((addr[0] || 'LogYou GmbH') + ' · automatisch erstellt'), { x: 60, y: 40, size: 8, font, color: gray })
const bytes = await doc.save()
return Buffer.from(bytes)
}
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const secret = config.ivr?.sharedSecret
const provided = getHeader(event, 'x-ivr-secret')
if (!secret || !provided || provided !== secret) {
setResponseStatus(event, 401)
return { status: 401, message: 'Unauthorized' }
}
const body = await readBody(event)
const rmaNos: string[] = Array.isArray(body?.rmaNos) ? body.rmaNos.map((x: any) => String(x)) : []
const items: Item[] = Array.isArray(body?.items) ? body.items : []
const orderNo = String(body?.orderNo || '')
const customerName = String(body?.customerName || '')
let to = String(body?.to || '').trim()
const cc = String(body?.cc || '').trim()
if (!rmaNos.length) {
setResponseStatus(event, 400)
return { status: 400, message: 'Missing rmaNos' }
}
const addr = addressLines(config.ivr?.returnAddress, rmaNos)
// No customer email → send to staff (cc) only so the return is still actioned.
const ccList: string[] = []
if (!to) { to = cc || 'info@logyou.de' } else if (cc) { ccList.push(cc) }
const html = buildHtmlDE(rmaNos, orderNo, customerName, items, addr)
const text = buildText(rmaNos, orderNo, items, addr)
const subject = `Rücksendeanweisungen – RMA ${rmaNos.join(', ')}`
let pdf: Buffer | null = null
try { pdf = await buildLabelPdf(rmaNos, orderNo, items, addr) } catch (e: any) { console.error('[ivr return email] PDF build failed:', e?.message || e) }
const transporter = nodemailer.createTransport({
host: 'localhost', port: 25, secure: false, tls: { rejectUnauthorized: false }
})
const mail: any = { from: 'no-reply@logyou.de', to, subject, text, html }
if (ccList.length) mail.cc = ccList.join(', ')
if (pdf) mail.attachments = [{ filename: `Ruecksendeetikett-RMA-${rmaNos.join('_')}.pdf`, content: pdf, contentType: 'application/pdf' }]
try {
await transporter.sendMail(mail)
} catch (err: any) {
console.error('[ivr return email] send error:', err?.message || err)
setResponseStatus(event, 500)
return { status: 500, message: 'Failed to send return instructions email' }
}
return { status: 200, message: 'Return instructions email sent', to, attachedPdf: !!pdf }
})