require('dotenv').config({ path: require('path').resolve(__dirname, '../.env') }); const axios = require('axios'); const WORKFLOW_URL_SCRAPE = process.env.WORKFLOW_URL_SCRAPE; const WORKFLOW_URL_TEMPLATE = process.env.WORKFLOW_URL_TEMPLATE; const WORKFLOW_URL_CHECK_CURL = process.env.WORKFLOW_URL_CHECK_CURL; const WORKFLOW_VALIDATE_FIELDS = process.env.WORKFLOW_VALIDATE_FIELDS; if (!WORKFLOW_URL_SCRAPE) throw new Error('Missing WORKFLOW_URL_SCRAPE environment variable'); if (!WORKFLOW_URL_TEMPLATE) throw new Error('Missing WORKFLOW_URL_TEMPLATE environment variable'); if (!WORKFLOW_URL_CHECK_CURL) throw new Error('Missing WORKFLOW_URL_CHECK_CURL environment variable'); if (!WORKFLOW_VALIDATE_FIELDS) throw new Error('Missing WORKFLOW_VALIDATE_FIELDS environment variable'); const TRAI_RULES_TEXT = '1) Max 160 chars. 2) Dynamic vars use {#var#}. 3) Transactional: no promo/URLs unless required. 4) Sender ID DLT-compliant. 5) Allowed punctuation only. 6) Must match event type. 7) Avoid URLs unless explicitly needed. 8) Start with event/order context.'; const EVENT_DESCRIPTIONS = { placed: 'The customer has successfully placed an order', confirmed: 'The order has been confirmed by the seller/warehouse', dp_assigned: 'A delivery partner has been assigned to deliver the order', pack: 'The order has been packed and is ready for dispatch', cancelled: 'The order has been cancelled', delivery_done: 'The order has been successfully delivered to the customer', }; function requestId(prefix) { return `${prefix}_${Date.now()}`; } function parseJsonField(value, fallback) { if (typeof value !== 'string') return value ?? fallback; try { return JSON.parse(value); } catch { return fallback; } } async function postWorkflow(url, payload) { try { const response = await axios.post(url, payload, { headers: { 'Content-Type': 'application/json' }, maxBodyLength: Infinity, timeout: 60000, }); return response.data; } catch (error) { const details = error.response?.data ? ` | response: ${JSON.stringify(error.response.data)}` : ''; throw new Error(`Workflow API error (${url}): ${error.message}${details}`); } } async function parseBrandContext(scrapedData = {}) { const representativePages = Array.isArray(scrapedData.representativePages) ? scrapedData.representativePages.slice(0, 20) : []; const representativeTextBlocks = Array.isArray(scrapedData.representativeTextBlocks) ? scrapedData.representativeTextBlocks.slice(0, 20) : []; const productPages = Array.isArray(scrapedData.productPages) ? scrapedData.productPages.slice(0, 5) : []; const contentDigest = representativeTextBlocks .map((block) => { const title = String(block?.title || '').trim(); const pageType = String(block?.pageType || '').trim(); const text = String(block?.text || '').trim(); return [title, pageType, text].filter(Boolean).join(' | '); }) .filter(Boolean) .join('\n\n') .slice(0, 14000); const payload = { task: 'parse_brand_context', request_id: requestId('parse_brand_context'), start_url: String(scrapedData.startUrl || ''), domain: String(scrapedData.domain || ''), site_stats_json: JSON.stringify(scrapedData.siteStats || {}), homepage_json: JSON.stringify(scrapedData.homepage || {}), about_page_json: JSON.stringify(scrapedData.aboutPage || {}), product_pages_json: JSON.stringify(productPages), contact_page_json: JSON.stringify(scrapedData.contactPage || {}), representative_pages_json: JSON.stringify(representativePages), representative_text_blocks_json: JSON.stringify(representativeTextBlocks), navigation_json: JSON.stringify(scrapedData.navigation || []), policy_pages_json: JSON.stringify(scrapedData.policyPages || []), links_json: JSON.stringify(scrapedData.links || []), top_images_json: JSON.stringify(scrapedData.topImages || []), screenshots_json: JSON.stringify(scrapedData.screenshots || []), branding_json: JSON.stringify(scrapedData.branding || {}), crawl_summary_json: JSON.stringify(scrapedData || {}), content_digest: contentDigest, output_schema_text: 'You are given homepage, about-page, product-page, branding, and image evidence for a storefront. Use that evidence to infer brand identity and product language. Return ONLY valid JSON object with exactly these keys: brandName (string), tone (one of: friendly, professional, formal, casual, energetic), taglines (array of strings, max 3), colors (array of hex color strings, or empty array), relevantImageUrls (array of 3-5 absolute image URLs for logo/hero/product images only; no icons/tracking/data URLs), aboutSummary (string, 2-4 sentences, concise customer-facing brand summary that explains what the brand is about, what it sells, and its vibe; do not copy the About Us page verbatim). No markdown, no prose, no extra keys.', must_return_json_only: 'true', }; const data = await postWorkflow(WORKFLOW_URL_SCRAPE, payload); const output = typeof data === 'string' ? parseJsonField(data, {}) : (data || {}); return { brandName: String(output.brandName || '').trim() || 'Unknown Brand', tone: ['friendly', 'professional', 'formal', 'casual', 'energetic'].includes(String(output.tone || '').toLowerCase()) ? String(output.tone).toLowerCase() : 'professional', taglines: Array.isArray(output.taglines) ? output.taglines.slice(0, 3).map(String) : [], colors: Array.isArray(output.colors) ? output.colors.map(String) : [], relevantImageUrls: Array.isArray(output.relevantImageUrls) ? output.relevantImageUrls.map(String) : [], aboutSummary: String(output.aboutSummary || '').trim(), }; } async function generateTemplates(brandContext = {}, eventSlug, eventLabel) { const eventDesc = EVENT_DESCRIPTIONS[eventSlug] || `A "${eventLabel}" event in the order lifecycle`; const payload = { task: 'generate_sms_templates', request_id: requestId('generate_sms_templates'), brand_name: String(brandContext.brandName || ''), tone: String(brandContext.tone || ''), taglines_json: JSON.stringify(Array.isArray(brandContext.taglines) ? brandContext.taglines : []), event_slug: String(eventSlug || ''), event_label: String(eventLabel || ''), event_description: eventDesc, trai_rules_text: TRAI_RULES_TEXT, templates_count: '3', max_chars: '160', variable_format: '{#var#}', output_schema_text: 'Return ONLY valid JSON object with exactly one key: templates (array of exactly 3 strings). No extra keys.', must_return_json_only: 'true', }; const data = await postWorkflow(WORKFLOW_URL_TEMPLATE, payload); const output = typeof data === 'string' ? parseJsonField(data, {}) : (data || {}); const templates = Array.isArray(output.templates) ? output.templates : parseJsonField(output.templates_json, []); return Array.isArray(templates) ? templates.map(String).slice(0, 3) : []; } async function processCurl(rawCurl, approvedTemplate, eventSlug) { const payload = { task: 'process_provider_curl', request_id: requestId('process_provider_curl'), raw_curl: String(rawCurl || ''), approved_template: String(approvedTemplate || ''), event_slug: String(eventSlug || ''), instructions_text: 'Identify placeholders, map to semantic field names, normalize placeholders in curl to camelCase, and build positional mapping for DLT placeholder tokens in approved_template. Supported token types include {#var#}, {#numeric#}, {#url#}, and {#cbn#}. Preserve the actual token text in variableMap keys using the format "[index]" based on appearance order within approved_template.', output_schema_text: 'Return ONLY valid JSON object with exactly these keys: processedCurl (string), variableMap (object where keys preserve the actual DLT token text in approved_template, such as {#var#}[0], {#numeric#}[1], {#url#}[2], and values are field names in camelCase). No extra keys.', must_return_json_only: 'true', }; const data = await postWorkflow(WORKFLOW_URL_CHECK_CURL, payload); const output = typeof data === 'string' ? parseJsonField(data, {}) : (data || {}); const variableMap = typeof output.variableMap === 'object' && output.variableMap !== null ? output.variableMap : parseJsonField(output.variable_map_json, {}); return { processedCurl: String(output.processedCurl || ''), variableMap: variableMap && typeof variableMap === 'object' ? variableMap : {}, }; } async function validateCurlFields(rawCurl) { const payload = { curl_b64: Buffer.from(String(rawCurl || ''), 'utf8').toString('base64'), }; const data = await postWorkflow(WORKFLOW_VALIDATE_FIELDS, payload); const output = typeof data === 'string' ? parseJsonField(data, {}) : (data || {}); const isValidCurl = output.is_valid_curl === true || String(output.is_valid_curl).toLowerCase() === 'true'; return { isValidCurl, provider: { providerName: String(output.provider_name || '').trim(), senderId: String(output.dlt_sender_id || '').trim().toUpperCase(), dltEntityId: String(output.dlt_entity_id || '').trim(), authKey: String(output.api_auth_key || '').trim(), }, reason: String(output.reason || '').trim(), }; } module.exports = { parseBrandContext, generateTemplates, processCurl, validateCurlFields };