/** * Assemble the document package that goes out with a contract: the contract * itself plus the selected annexes. One place for titles, order and loaders so * the send route, the signing portal and the e-mails all agree. * * Attachment flags (body.attachments): offer (Anlage 1 — last sent quote), * agb (2a — stored AGB), adsp (2b — static PDF), logistikagb (2c — static PDF), * avv (3 — stored AVV). Absent flag = included, `false` = left out. * flyer (marketing overview) is the exception: OPT-IN, only attached when `true`. */ import { resolveLastOfferPdf } from './offerAttachments' import { generateLegalDocPdf } from './agbPdf' import { loadStaticAnnex } from './staticAnnexes' import { getLegalDocumentWithFallback } from '../legalDocsDb' import { generateFlyerPdf } from './flyerPdf' export interface PackageDocument { key: 'contract' | 'offer' | 'agb' | 'adsp' | 'logistikagb' | 'avv' | 'flyer' title: string filename: string buffer: Buffer version?: number signable?: boolean } export const PACKAGE_KEYS = ['offer', 'agb', 'adsp', 'logistikagb', 'avv', 'flyer'] as const export const packageTitle = (key: string, language: string): string => { const en = language === 'en' switch (key) { case 'contract': return en ? 'Fulfillment agreement' : 'Fulfillment-Rahmenvertrag' case 'offer': return en ? 'Annex 1 – Offer' : 'Anlage 1 – Angebot' case 'agb': return en ? 'Annex 2a – Terms and Conditions (AGB)' : 'Anlage 2a – AGB' case 'adsp': return en ? 'Annex 2b – ADSp 2017' : 'Anlage 2b – ADSp 2017' case 'logistikagb': return en ? 'Annex 2c – Logistik-AGB 2019' : 'Anlage 2c – Logistik-AGB 2019' case 'avv': return en ? 'Annex 3 – Data Processing Agreement (Art. 28 GDPR)' : 'Anlage 3 – Auftragsverarbeitungsvertrag (AVV)' case 'flyer': return en ? 'LogYou & LogShip – Overview' : 'LogYou & LogShip – Überblick' default: return key } } export const wantsAttachment = (attachments: any, key: string): boolean => !(attachments && attachments[key] === false) /** Availability info for the modal (no heavy rendering). */ export const describePackageAvailability = (language: string) => { const agb = getLegalDocumentWithFallback('agb', language) const avv = getLegalDocumentWithFallback('avv', language) return { agb: { available: !!String(agb.contentHtml || '').trim(), version: agb.version, language: agb.language, updatedAt: agb.updatedAt }, avv: { available: !!String(avv.contentHtml || '').trim(), version: avv.version, language: avv.language, updatedAt: avv.updatedAt }, adsp: { available: true }, logistikagb: { available: true } } } /** * Load every selected annex. Never throws for a single missing annex — the * returned `warnings` list names what could not be included so the send route * can surface it to the user while still sending the contract. */ export const assembleContractPackage = async ( event: any, token: string, opts: { source: string; recordId: number; language: string; attachments: any; contract: { buffer: Buffer; filename: string } } ): Promise<{ documents: PackageDocument[]; warnings: string[] }> => { const { language } = opts const en = language === 'en' const documents: PackageDocument[] = [ { key: 'contract', title: packageTitle('contract', language), filename: opts.contract.filename, buffer: opts.contract.buffer, signable: true } ] const warnings: string[] = [] if (wantsAttachment(opts.attachments, 'offer')) { try { const offer = opts.recordId > 0 ? await resolveLastOfferPdf(event, token, opts.source, opts.recordId) : null if (offer) documents.push({ key: 'offer', title: packageTitle('offer', language), filename: offer.filename, buffer: offer.buffer }) else warnings.push(en ? 'No previous offer found — Annex 1 not attached' : 'Kein früheres Angebot gefunden — Anlage 1 nicht angehängt') } catch (err: any) { warnings.push(en ? 'Offer (Annex 1) could not be attached' : 'Angebot (Anlage 1) konnte nicht angehängt werden') } } if (wantsAttachment(opts.attachments, 'agb')) { try { const agb = await generateLegalDocPdf('agb', language) documents.push({ key: 'agb', title: packageTitle('agb', language), filename: agb.filename, buffer: agb.buffer, version: agb.version }) } catch (err: any) { warnings.push(en ? 'AGB (Annex 2a) could not be attached' : 'AGB (Anlage 2a) konnten nicht angehängt werden') } } for (const key of ['adsp', 'logistikagb'] as const) { if (!wantsAttachment(opts.attachments, key)) continue const st = await loadStaticAnnex(key) if (st) documents.push({ key, title: packageTitle(key, language), filename: st.filename, buffer: st.buffer }) else warnings.push(`${packageTitle(key, language)}: ${en ? 'file not available' : 'Datei nicht verfügbar'}`) } if (wantsAttachment(opts.attachments, 'avv')) { try { const avv = await generateLegalDocPdf('avv', language) documents.push({ key: 'avv', title: packageTitle('avv', language), filename: avv.filename, buffer: avv.buffer, version: avv.version }) } catch (err: any) { warnings.push(en ? 'AVV (Annex 3) could not be attached' : 'AVV (Anlage 3) konnte nicht angehängt werden') } } // Marketing flyer — OPT-IN (default off), so an explicit true is required. if (opts.attachments && opts.attachments.flyer === true) { try { const fl = await generateFlyerPdf(language) documents.push({ key: 'flyer', title: packageTitle('flyer', language), filename: fl.filename, buffer: fl.buffer }) } catch (err: any) { warnings.push(en ? 'Flyer could not be attached' : 'Flyer konnte nicht angehängt werden') } } return { documents, warnings } }