/** * Per-record offer/contract conditions persistence. * The last-sent quote and contract conditions are stored as ONE JSON blob in the * custom column `offer_conditions` (Text) on AD_User (leads) / C_BPartner * (partners): { quote: {...}, contract: {...} } — each section stamped with * `savedAt`. Written on every successful send, read on every modal open, and * copied lead → partner by merchant onboarding. * * All readers/writers are FAIL-SOFT: a missing column (not yet created in * iDempiere) or malformed JSON degrades to "no saved conditions" and must never * break the send/onboarding flows. */ import fetchHelper from '../fetchHelper' const modelFor = (source: string): string => (source === 'lead' ? 'ad_user' : 'c_bpartner') export const readOfferConditions = async (event: any, token: string, source: string, recordId: number): Promise => { let res: any = null try { res = await fetchHelper(event, `models/${modelFor(source)}/${recordId}`, 'GET', token, null) } catch (e: any) { const s = Number(e?.statusCode ?? e?.status ?? e?.response?.status) if (s === 401) throw e // let callers run the token-refresh path return {} } try { const raw = res?.offer_conditions ?? res?.Offer_Conditions ?? '' if (!raw || String(raw).trim() === '') return {} const parsed = JSON.parse(String(raw)) return parsed && typeof parsed === 'object' ? parsed : {} } catch { return {} } } /** Read-merge-write so the quote and contract sections never clobber each other. */ export const saveOfferConditions = async ( event: any, token: string, source: string, recordId: number, docType: 'quote' | 'contract', conditions: any ): Promise => { const current = await readOfferConditions(event, token, source, recordId) const merged = { ...current, [docType]: { ...conditions, savedAt: new Date().toISOString() } } await fetchHelper(event, `models/${modelFor(source)}/${recordId}`, 'PUT', token, { offer_conditions: JSON.stringify(merged) }) } /** Extract the persistable conditions from a quote send payload (email etc. stripped). */ export const conditionsFromQuotePayload = (p: any) => ({ language: p?.meta?.language === 'en' ? 'en' : 'de', customer: p?.customer || {}, assumptions: p?.assumptions || {}, pricing: p?.pricing || {}, inbound: p?.inbound || {}, services: p?.services || {}, customPositions: Array.isArray(p?.customPositions) ? p.customPositions : [], sections: p?.sections && typeof p.sections === 'object' ? p.sections : {}, customTexts: Array.isArray(p?.customTexts) ? p.customTexts : [], volumeMatrix: p?.volumeMatrix || {}, validDays: Number(p?.meta?.validDays) || 7, confirmation: { enabled: p?.confirmation?.enabled !== false, attachPdf: p?.confirmation?.attachPdf === true } }) /** Extract the persistable conditions from a contract send payload. */ export const conditionsFromContractPayload = (p: any) => ({ language: p?.meta?.language === 'en' ? 'en' : 'de', customer: p?.customer || {}, assumptions: p?.assumptions || {}, contract: p?.contract || {}, // incl. `sections` (deselected parts) attachments: p?.attachments && typeof p.attachments === 'object' ? p.attachments : {}, signing: p?.signing && typeof p.signing === 'object' ? { enabled: p.signing.enabled !== false, expiryDays: Number(p.signing.expiryDays) || 7, attachDocuments: p.signing.attachDocuments === true } : {} }) /** * Merge a partial patch into ONE section (e.g. `contract.signature = {...}`) * without touching the section's other keys or its `savedAt`. Used by the * public signing flow, which must not look like a "new send". */ export const patchOfferConditionsSection = async ( event: any, token: string, source: string, recordId: number, docType: 'quote' | 'contract', patch: any ): Promise => { const current = await readOfferConditions(event, token, source, recordId) const merged = { ...current, [docType]: { ...(current?.[docType] || {}), ...patch } } await fetchHelper(event, `models/${modelFor(source)}/${recordId}`, 'PUT', token, { offer_conditions: JSON.stringify(merged) }) }