/**
* POST /api/offers/forecast/send
* Body: forecast payload incl. email { to, cc?, subject, message }.
* Builds the custom-forecast PDF ONCE, e-mails it (from info@logyou.de, like the
* quote), attaches the same PDF to the triggering record (partner → C_BPartner,
* lead → Lead_User), stores the editor state as offer_conditions.forecast and
* logs an activity. Every step after the send is fail-soft.
*/
import refreshTokenHelper from '../../../utils/refreshTokenHelper'
import errorHandlingHelper from '../../../utils/errorHandlingHelper'
import forceLogoutHelper from '../../../utils/forceLogoutHelper'
import { BRAND, LEGAL_FOOTER_LINES } from '../../../utils/offers/quotePdf'
import { generateForecastPdf, validateForecastPayload, conditionsFromForecastPayload } from '../../../utils/offers/forecastPdf'
import { attachToStrapi } from '../../../utils/inbox/strapiAttach'
import { saveOfferConditions } from '../../../utils/offers/offerConditions'
import { logContactActivity, strapiPublicFileUrl } from '../../../utils/offers/contactActivity'
import { mailTransport } from '../../../utils/offers/contractSigning'
const EMAIL_TEXTS: any = {
de: {
headline: 'Ihre individuelle Kostenprognose',
attachmentBox: 'Die Prognose liegt dieser E-Mail als PDF-Anhang bei. Unverbindliche Beispielrechnung — abgerechnet wird nach tatsächlichem Verbrauch.',
auto: 'Diese E-Mail wurde automatisch versendet.'
},
en: {
headline: 'Your individual cost forecast',
attachmentBox: 'The forecast is attached to this email as a PDF. Non-binding example calculation — billing is based on actual consumption.',
auto: 'This email was sent automatically.'
}
}
const generateForecastEmailHtml = (message: string, language: string) => {
const T = EMAIL_TEXTS[language === 'en' ? 'en' : 'de']
const messageHtml = String(message || '')
.replace(/&/g, '&').replace(//g, '>')
.replace(/\n/g, '
')
return `
|
${T.headline}
LogYou GmbH · logyou.de
|
|
${messageHtml}
|
|
|
${LEGAL_FOOTER_LINES.join(' ')}
${T.auto}
|
|
`
}
const handleFunc = async (event: any, authToken: string | null = null) => {
const token = authToken ?? await getTokenHelper(event)
if (!token) return { status: 401, message: 'Not authenticated' }
const body = await readBody(event)
const invalid = validateForecastPayload(body)
if (invalid) return { status: 400, message: invalid }
const toEmail = String(body?.email?.to || '').trim()
if (!/.+@.+\..+/.test(toEmail)) {
return { status: 400, message: 'A valid recipient email is required' }
}
const language = body?.meta?.language === 'en' ? 'en' : 'de'
const en = language === 'en'
const source = body.source === 'lead' ? 'lead' : 'partner'
const recordId = Number(body.recordId)
// Build the PDF once — reused for the email attachment AND the record attachment.
const { buffer, filename } = await generateForecastPdf(body)
const ccList = String(body?.email?.cc || '')
.split(/[,;]/)
.map((s: string) => s.trim())
.filter((s: string) => s.length > 0)
try {
await mailTransport().sendMail({
from: 'info@logyou.de',
to: toEmail,
cc: ccList.length > 0 ? ccList : undefined,
subject: body?.email?.subject || filename.replace(/\.pdf$/, ''),
text: body?.email?.message || '',
html: generateForecastEmailHtml(body?.email?.message || '', language),
attachments: [{ filename, content: buffer, contentType: 'application/pdf' }]
})
} catch (err: any) {
console.error('Forecast email send error:', err)
return { status: 500, message: `Failed to send email: ${err.message}` }
}
// Attach to the record — own try/catch so a failure never reverts the send.
let attached = false
let attachWarning = ''
let attachedUrl = ''
try {
if (recordId > 0) {
const up = await attachToStrapi(event, token, {
tableName: source === 'lead' ? 'Lead_User' : 'C_BPartner',
recordId,
recordUu: body.recordUu || undefined,
buffer,
filename,
mimeType: 'application/pdf'
})
attached = true
attachedUrl = strapiPublicFileUrl(up.fileUrl)
} else {
attachWarning = 'No record id — attachment skipped'
}
} catch (err: any) {
console.error('Forecast attachment error:', err)
attachWarning = 'Email sent, but attaching the PDF to the record failed'
}
// Remember the editor state for the next open (fail-soft).
let conditionsSaved = false
try {
if (recordId > 0) {
await saveOfferConditions(event, token, source, recordId, 'forecast', {
...conditionsFromForecastPayload(body),
lastSend: { at: new Date().toISOString(), to: toEmail, filename }
})
conditionsSaved = true
}
} catch (err: any) {
console.error('Forecast conditions save skipped:', err?.message || err)
}
// Activity on the record with the PDF as a document button (fail-soft).
let activityLogged = false
try {
if (recordId > 0) {
const ccText = ccList.length ? ` (CC: ${ccList.join(', ')})` : ''
const scenarioText = (Array.isArray(body.scenarios) ? body.scenarios : [])
.map((s: any, i: number) => `${s?.heading || s?.orders}: ${new Intl.NumberFormat(en ? 'en-IE' : 'de-DE', { style: 'currency', currency: 'EUR' }).format(Number(body?.totals?.total?.[i]) || 0)}`)
.join(' · ')
const created = await logContactActivity(event, token, {
source,
recordId,
type: 'EM',
contactEmail: toEmail,
description: en ? `Cost forecast sent: ${filename}` : `Kostenprognose gesendet: ${filename}`,
comments: [
en ? `Individual cost forecast ${filename} e-mailed to ${toEmail}${ccText}.` : `Individuelle Kostenprognose ${filename} per E-Mail an ${toEmail}${ccText} gesendet.`,
scenarioText ? (en ? `Monthly total — ${scenarioText}` : `Gesamt pro Monat — ${scenarioText}`) : ''
].filter(Boolean).join('\n'),
documents: attachedUrl ? [{ title: en ? 'Forecast (PDF)' : 'Prognose (PDF)', url: attachedUrl }] : []
})
activityLogged = !!created
}
} catch (err: any) {
console.error('Forecast activity skipped:', err?.message || err)
}
return {
status: 200,
message: 'Forecast sent successfully',
filename,
attached,
conditionsSaved,
activityLogged,
warning: attachWarning || undefined
}
}
export default defineEventHandler(async (event) => {
let data: any = {}
try {
data = await handleFunc(event)
} catch (err: any) {
try {
let authToken: any = await refreshTokenHelper(event)
data = await handleFunc(event, authToken)
} catch (error: any) {
data = errorHandlingHelper(err?.data ?? err, error?.data ?? error)
forceLogoutHelper(event, data)
}
}
return data
})