/** * Resolve the LAST SENT OFFER (quote PDF) of a lead/partner so it can ride * along as "Anlage 1" when the contract is sent / signed. * * 1. Preferred: the exact PDF that was e-mailed — every quote send attaches it * to the record via Strapi (Lead_User / C_BPartner) as * `Angebot_LogYou__.pdf`; we pick the newest such file. * 2. Fallback: re-render from the record's saved `offer_conditions.quote` * (same generator the send used; date = the saved timestamp). */ import strapiHelper from '../strapiHelper' import { resolveTableId, fetchStrapiFileBytes } from '../inbox/strapiAttach' import { readOfferConditions } from './offerConditions' import { generateQuotePdf } from './quotePdf' export interface LastOfferInfo { available: boolean filename?: string date?: string via?: 'attachment' | 'conditions' fileUrl?: string } const QUOTE_FILE_RE = /^(Angebot|Quote|Offer)_LogYou_.*\.pdf$/i const listRecordFiles = async (event: any, token: string, source: string, recordId: number): Promise => { const tableName = source === 'lead' ? 'Lead_User' : 'C_BPartner' const tableId = await resolveTableId(event, token, tableName) const existing: any = await strapiHelper(event, `ad-attachments?filters[AD_Table_ID][$eq]=${tableId}&filters[Record_ID][$eq]=${recordId}`, 'GET', null) const attId = existing?.data?.[0]?.documentId || existing?.data?.[0]?.id if (!attId) return [] const res: any = await strapiHelper(event, `ad-attachments/${attId}?populate=attachment`, 'GET', null) const files = res?.data?.attachment || [] return Array.isArray(files) ? files : [] } const newestQuoteFile = (files: any[]) => files .filter((f: any) => QUOTE_FILE_RE.test(String(f?.name || ''))) .sort((a: any, b: any) => new Date(b?.createdAt || 0).getTime() - new Date(a?.createdAt || 0).getTime())[0] || null /** Cheap check for the modal: is there a last offer to attach, and which? */ export const describeLastOffer = async (event: any, token: string, source: string, recordId: number): Promise => { try { const f = newestQuoteFile(await listRecordFiles(event, token, source, recordId)) if (f) return { available: true, filename: f.name, date: String(f.createdAt || '').slice(0, 10), via: 'attachment', fileUrl: f.url } } catch (err: any) { console.warn('[Offers] last-offer attachment lookup failed:', err?.message || err) } try { const cond = await readOfferConditions(event, token, source, recordId) if (cond?.quote && typeof cond.quote === 'object') { return { available: true, date: String(cond.quote.savedAt || '').slice(0, 10), via: 'conditions' } } } catch {} return { available: false } } /** The bytes of the last offer (exact sent file first, regenerated second). */ export const resolveLastOfferPdf = async (event: any, token: string, source: string, recordId: number): Promise<{ buffer: Buffer; filename: string; via: 'attachment' | 'conditions' } | null> => { try { const f = newestQuoteFile(await listRecordFiles(event, token, source, recordId)) if (f?.url) { const buffer = await fetchStrapiFileBytes(event, f.url) if (buffer?.length) return { buffer, filename: f.name, via: 'attachment' } } } catch (err: any) { console.warn('[Offers] last-offer attachment fetch failed:', err?.message || err) } try { const cond = await readOfferConditions(event, token, source, recordId) const q = cond?.quote if (q && typeof q === 'object' && q.customer) { const payload = { docType: 'quote', source, recordId, customer: q.customer, assumptions: q.assumptions || {}, pricing: q.pricing || {}, inbound: q.inbound || { mode: 'included' }, services: q.services || {}, customPositions: q.customPositions || [], sections: q.sections || {}, customTexts: q.customTexts || [], volumeMatrix: q.volumeMatrix || {}, visuals: { includeScreenshot: false, includeLogo: false }, forecast: { includeInPdf: false, lines: [] }, meta: { validDays: Number(q.validDays) || 7, language: q.language === 'en' ? 'en' : 'de', date: String(q.savedAt || '').slice(0, 10) || new Date().toISOString().slice(0, 10) } } const { buffer, filename } = await generateQuotePdf(payload) return { buffer, filename, via: 'conditions' } } } catch (err: any) { console.warn('[Offers] last-offer regeneration failed:', err?.message || err) } return null }