/** * 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). * * When the saved quote conditions are NEWER than the newest sent PDF (the rep * revised prices after the last send), the re-render wins — otherwise the contract * would cite the newer offer date while attaching the stale PDF (happened 2026-10: * contract "Angebot vom 21.10." with the 28.09. PDF attached). The contract modal * mirrors the chosen date into "Angebot vom (Anlage 1)". */ 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 const savedQuoteDate = (cond: any): string => { const q = cond?.quote return q && typeof q === 'object' && q.customer ? String(q.savedAt || '').slice(0, 10) : '' } /** true when the saved quote conditions are newer than the newest sent PDF. */ const conditionsNewerThanFile = (condDate: string, f: any): boolean => !!condDate && condDate > String(f?.createdAt || '').slice(0, 10) /** 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 => { let f: any = null try { f = newestQuoteFile(await listRecordFiles(event, token, source, recordId)) } catch (err: any) { console.warn('[Offers] last-offer attachment lookup failed:', err?.message || err) } let condDate = '' try { condDate = savedQuoteDate(await readOfferConditions(event, token, source, recordId)) } catch {} if (f && !conditionsNewerThanFile(condDate, f)) { return { available: true, filename: f.name, date: String(f.createdAt || '').slice(0, 10), via: 'attachment', fileUrl: f.url } } if (condDate) return { available: true, date: condDate, via: 'conditions' } if (f) return { available: true, filename: f.name, date: String(f.createdAt || '').slice(0, 10), via: 'attachment', fileUrl: f.url } 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> => { let cond: any = null try { cond = await readOfferConditions(event, token, source, recordId) } catch {} const condDate = savedQuoteDate(cond) let f: any = null try { f = newestQuoteFile(await listRecordFiles(event, token, source, recordId)) if (f?.url && !conditionsNewerThanFile(condDate, f)) { 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 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: [] }, // Keep the regenerated Anlage 1 identical in substance to what the customer got: // the cut-off (meta card + Hinweise) and the validity the quote was saved with. cutoff: q.cutoff && typeof q.cutoff === 'object' ? { show: q.cutoff.show === true, time: String(q.cutoff.time || '14:00') } : { show: false, time: '14:00' }, meta: { validDays: Number(q.validDays) || 7, ...(q.validUntil ? { validUntil: String(q.validUntil) } : {}), 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) } // Regeneration failed although the conditions were newer — the sent PDF is still better than nothing. try { if (f?.url) { const buffer = await fetchStrapiFileBytes(event, f.url) if (buffer?.length) return { buffer, filename: f.name, via: 'attachment' } } } catch {} return null }