/** * 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}
📎  ${T.attachmentBox}
${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 })