/** * POST /api/offers/sepa/send — standalone SEPA mandate: render the fillable * mandate, create a signing request (kind 'sepa', link /sepa/), mail * the customer, attach the blank mandate to the record, log an activity and * remember the setup on offer_conditions.sepa. * * Body: { source: 'lead'|'partner', recordId, recordUu?, language: 'de'|'en', * scheme: 'B2B'|'CORE', recurring, prenotificationDays, mandateReference?, * customer: { company, contactName, street, zip, city, country }, * signing: { expiryDays, expiresAt }, email: { to, cc, subject, message } } */ import refreshTokenHelper from '../../../utils/refreshTokenHelper' import errorHandlingHelper from '../../../utils/errorHandlingHelper' import forceLogoutHelper from '../../../utils/forceLogoutHelper' import { buildSepaMandatePdf, sepaFilename } from '../../../utils/offers/sepaPdf' import { buildMandateData, sepaLink, NOTIFY_EMAIL } from '../../../utils/offers/sepaMandate' import { createSigningRequest, isSigningAvailable, writeSigningFile, sha256Hex } from '../../../utils/offers/contractSigningDb' import { mailTransport, resolveSigningExpiry, brandedEmail, escapeHtml, formatDateHuman } from '../../../utils/offers/contractSigning' import { attachToStrapi } from '../../../utils/inbox/strapiAttach' import { logContactActivity, strapiPublicFileUrl } from '../../../utils/offers/contactActivity' import { saveOfferConditions } from '../../../utils/offers/offerConditions' const handleFunc = async (event: any, authToken: string | null = null) => { const token = authToken ?? await getTokenHelper(event) if (!token) return { status: 401, message: 'Not authenticated' } if (!isSigningAvailable()) return { status: 503, message: 'Signing store not available' } const body: any = await readBody(event) const language = body?.language === 'en' ? 'en' : 'de' const en = language === 'en' const source = body?.source === 'lead' ? 'lead' : 'partner' const recordId = Number(body?.recordId) || 0 const c = body?.customer || {} const toEmail = String(body?.email?.to || '').trim() if (!/.+@.+\..+/.test(toEmail)) return { status: 400, message: en ? 'Recipient e-mail is required' : 'Empfänger-E-Mail ist erforderlich' } if (!String(c.company || '').trim()) return { status: 400, message: en ? 'Company is required' : 'Firma ist erforderlich' } const ccList = String(body?.email?.cc || '').split(/[;,]/).map((s: string) => s.trim()).filter((s: string) => /.+@.+\..+/.test(s)) const debtor = { company: c.company || '', accountHolder: c.company || '', street: c.street || '', zipCity: [c.zip, c.city].filter(Boolean).join(' '), country: c.country || '' } const mandate = buildMandateData(source, recordId, { scheme: body?.scheme, recurring: body?.recurring !== false, prenotificationDays: Number(body?.prenotificationDays) || undefined, mandateReference: body?.mandateReference }, debtor) const buffer = await buildSepaMandatePdf({ data: mandate, language, filled: debtor }) const filename = sepaFilename(language, mandate.mandateReference) const expiresAt = resolveSigningExpiry(body?.signing || {}) const req = createSigningRequest({ kind: 'sepa', source, recordId: recordId > 0 ? recordId : 0, recordUu: body?.recordUu || undefined, company: String(c.company), contactName: String(c.contactName || ''), email: toEmail, language, payload: { source, recordId, customer: c, mandate }, documents: [{ key: 'sepa', title: en ? 'SEPA Direct Debit Mandate' : 'SEPA-Lastschriftmandat', filename, sha256: sha256Hex(buffer), size: buffer.length }], createdBy: Number(getCookie(event, 'logship_user_id')) || null, expiresAt }) writeSigningFile(req.token, filename, buffer) const link = sepaLink(req.token) const warnings: string[] = [] // mail const greeting = en ? `Dear ${escapeHtml(c.contactName || c.company)},` : `Guten Tag ${escapeHtml(c.contactName || c.company)},` const msg = String(body?.email?.message || '').trim() const intro = msg ? escapeHtml(msg).replace(/\n/g, '
') : (en ? `please grant us the SEPA direct debit mandate for the fees of our fulfilment service. Enter the account holder and IBAN online and sign — it only takes a minute.` : `bitte erteilen Sie uns das SEPA-Lastschriftmandat für die Entgelte unserer Fulfillment-Leistungen. Tragen Sie Kontoinhaber und IBAN online ein und unterschreiben Sie – das dauert nur eine Minute.`) const cta = `

${en ? 'Complete SEPA mandate online' : 'SEPA-Mandat online erteilen'}

${en ? 'Link valid until' : 'Link gültig bis'} ${escapeHtml(formatDateHuman(expiresAt, language))} · ${en ? 'Mandate reference' : 'Mandatsreferenz'} ${escapeHtml(mandate.mandateReference)}

` try { await mailTransport().sendMail({ from: NOTIFY_EMAIL, to: toEmail, cc: ccList.length ? ccList.join(', ') : undefined, subject: String(body?.email?.subject || '').trim() || (en ? `SEPA direct debit mandate – ${c.company}` : `SEPA-Lastschriftmandat – ${c.company}`), text: `${msg || (en ? 'Please complete the SEPA mandate online:' : 'Bitte erteilen Sie das SEPA-Mandat online:')}\n${link}`, html: brandedEmail(en ? 'SEPA direct debit mandate' : 'SEPA-Lastschriftmandat', `

${greeting}

${intro}

${cta}`, en ? 'The mandate is attached as a fillable PDF as well.' : 'Das Mandat liegt zusätzlich als ausfüllbares PDF bei.'), attachments: [{ filename, content: buffer, contentType: 'application/pdf' }] }) } catch (err: any) { console.error('[SEPA] send mail failed:', err?.message || err) warnings.push(en ? 'E-mail could not be sent (link created)' : 'E-Mail konnte nicht gesendet werden (Link wurde erstellt)') } // attach blank mandate + conditions + activity (fail-soft) 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' }) attachedUrl = strapiPublicFileUrl(up.fileUrl) } } catch (err: any) { console.error('[SEPA] attach blank failed:', err?.message || err) } try { if (recordId > 0) await saveOfferConditions(event, token, source, recordId, 'sepa', { language, scheme: mandate.scheme, recurring: mandate.recurring, prenotificationDays: mandate.creditor.prenotificationDays, mandateReference: mandate.mandateReference, lastSend: { at: new Date().toISOString(), to: toEmail, token: req.token, expiresAt } }) } catch (err: any) { console.error('[SEPA] conditions save failed:', err?.message || err) } try { await logContactActivity(event, token, { source, recordId, type: 'EM', contactEmail: toEmail, description: en ? `SEPA mandate sent: ${mandate.mandateReference}` : `SEPA-Mandat gesendet: ${mandate.mandateReference}`, comments: [ en ? `SEPA ${mandate.scheme} direct debit mandate ${filename} e-mailed to ${toEmail}${ccList.length ? ` (CC: ${ccList.join(', ')})` : ''}.` : `SEPA-${mandate.scheme === 'B2B' ? 'Firmenlastschrift' : 'Basislastschrift'}mandat ${filename} per E-Mail an ${toEmail}${ccList.length ? ` (CC: ${ccList.join(', ')})` : ''} gesendet.`, en ? `Mandate reference: ${mandate.mandateReference}` : `Mandatsreferenz: ${mandate.mandateReference}`, en ? `Online mandate link (valid until ${formatDateHuman(expiresAt, 'en')}): ${link}` : `Online-Mandats-Link (gültig bis ${formatDateHuman(expiresAt, 'de')}): ${link}` ].join('\n'), documents: attachedUrl ? [{ title: en ? 'SEPA mandate (blank, PDF)' : 'SEPA-Mandat (Vorlage, PDF)', url: attachedUrl }] : [] }) } catch (err: any) { console.error('[SEPA] activity failed:', err?.message || err) } return { status: 200, message: en ? 'SEPA mandate sent' : 'SEPA-Mandat gesendet', filename, mandateReference: mandate.mandateReference, link, expiresAt, token: req.token, warning: warnings.join(' · ') || undefined } } export default defineEventHandler(async (event) => { let data: any = {} try { data = await handleFunc(event) } catch (err: any) { try { const authToken = await refreshTokenHelper(event) data = await handleFunc(event, authToken) } catch (error: any) { data = errorHandlingHelper(err?.data ?? err, error?.data ?? error) forceLogoutHelper(event, data) } } return data })