'use strict' const { log, json, parseBody, erpFetch } = require('./helpers') const { searchAddresses } = require('./address-search') const { sendOTP, verifyOTP } = require('./otp') const { getTemplateSteps } = require('./project-templates') const { orderConfirmationHtml } = require('./email-templates') function erpQuery (doctype, filters, fields, limit, orderBy) { let url = `/api/resource/${encodeURIComponent(doctype)}?filters=${encodeURIComponent(JSON.stringify(filters))}&fields=${encodeURIComponent(JSON.stringify(fields))}` if (orderBy) url += `&order_by=${encodeURIComponent(orderBy)}` if (limit) url += `&limit_page_length=${limit}` return erpFetch(url) } // ── Product Catalog API ───────────────────────────────────────────────────── async function getCatalog () { const fields = [ 'name', 'item_code', 'item_name', 'item_group', 'standard_rate', 'description', 'image', 'project_template_id', 'requires_visit', 'delivery_method', 'is_bundle_parent', 'billing_type', 'service_category', ] const res = await erpQuery('Item', [['is_sales_item', '=', 1], ['disabled', '=', 0]], fields, 100, 'service_category,standard_rate') if (res.status !== 200) { log('Catalog fetch failed:', res.status) return [] } return (res.data.data || []).map(item => ({ item_code: item.item_code, item_name: item.item_name, item_group: item.item_group, rate: item.standard_rate || 0, description: item.description || '', image: item.image || '', project_template_id: item.project_template_id || '', requires_visit: !!item.requires_visit, delivery_method: item.delivery_method || '', is_bundle: !!item.is_bundle_parent, billing_type: item.billing_type || '', service_category: item.service_category || '', })) } // ── Checkout / Order Processing ───────────────────────────────────────────── async function processCheckout (body) { const { contact = {}, installation = {} } = body const items = body.items || [] const customer_name = body.customer_name || contact.name || '' const phone = body.phone || contact.phone || '' const email = body.email || contact.email || '' const address = body.address || contact.address || '' const city = body.city || contact.city || '' const postal_code = body.postal_code || contact.postal_code || '' const preferred_date = body.preferred_date || installation.preferred_date || '' const preferred_slot = body.preferred_slot || installation.preferred_slot || '' const notes = body.notes || '' const mode = body.mode || 'postpaid' log('Checkout request:', { customer_name, phone, email, items: items.length, mode }) if (!items?.length) throw new Error('Aucun article dans le panier') if (!customer_name) throw new Error('Nom du client requis') if (!phone && !email) throw new Error('Téléphone ou courriel requis') const today = new Date().toISOString().split('T')[0] const result = { ok: true, items_count: items.length } // ── 1. Find or create Customer ── let customerName = '' const providedCustomerId = body.customer_id || '' try { if (providedCustomerId) { customerName = providedCustomerId result.customer_existing = true log('Using OTP-verified customer:', customerName) } const { lookupCustomerByPhone } = require('./helpers') if (!customerName && phone) { const existing = await lookupCustomerByPhone(phone) if (existing) { customerName = existing.name result.customer_existing = true } } if (!customerName) { const custPayload = { customer_name, customer_type: 'Individual', customer_group: 'Individual', territory: 'Canada', cell_phone: phone || '', email_id: email || '', } log('Creating customer:', custPayload) const custRes = await erpFetch('/api/resource/Customer', { method: 'POST', body: custPayload }) log('Customer creation result:', custRes.status, custRes.data?.data?.name || JSON.stringify(custRes.data).substring(0, 200)) if (custRes.status === 200 && custRes.data?.data) { customerName = custRes.data.data.name result.customer_created = true } else { customerName = customer_name } } } catch (e) { log('Customer lookup/create failed:', e.message) customerName = customer_name } // ── 2. Create Sales Order ── const recurringItems = items.filter(i => i.billing_type === 'Mensuel' || i.billing_type === 'Annuel') const allItems = items.map(i => ({ item_code: i.item_code, item_name: i.item_name, qty: i.qty || 1, rate: i.rate, description: i.billing_type === 'Mensuel' ? `${i.item_name} — ${i.rate}$/mois` : i.billing_type === 'Annuel' ? `${i.item_name} — ${i.rate}$/an` : i.item_name, })) let orderName = '' try { const soPayload = { customer: customerName, company: 'TARGO', currency: 'CAD', selling_price_list: 'Standard Selling', transaction_date: today, delivery_date: preferred_date || today, items: allItems, terms: notes || '', } const soRes = await erpFetch('/api/resource/Sales%20Order', { method: 'POST', body: JSON.stringify(soPayload) }) if (soRes.status === 200 && soRes.data?.data) { orderName = soRes.data.data.name result.sales_order = orderName log(`Sales Order created: ${orderName} for ${customerName}`) } else { log('Sales Order creation failed:', soRes.status, JSON.stringify(soRes.data).substring(0, 300)) result.sales_order_error = `ERPNext ${soRes.status}` } } catch (e) { log('Sales Order creation failed:', e.message) result.sales_order_error = e.message } // ── 3. Create Subscriptions for recurring items ── for (const item of recurringItems) { try { let planName = null try { const findRes = await erpFetch(`/api/resource/Subscription%20Plan/${encodeURIComponent(item.item_code)}`) if (findRes.status === 200) planName = findRes.data?.data?.name } catch {} if (!planName) { const planRes = await erpFetch('/api/resource/Subscription%20Plan', { method: 'POST', body: JSON.stringify({ plan_name: item.item_code, item: item.item_code, currency: 'CAD', price_determination: 'Fixed Rate', cost: item.rate, billing_interval: item.billing_type === 'Annuel' ? 'Year' : 'Month', billing_interval_count: 1, }), }) if (planRes.status === 200) planName = planRes.data?.data?.name } if (planName) { await erpFetch('/api/resource/Subscription', { method: 'POST', body: JSON.stringify({ party_type: 'Customer', party: customerName, company: 'TARGO', start_date: today, generate_invoice_at: 'Beginning of the current subscription period', days_until_due: 30, follow_calendar_months: 1, plans: [{ plan: planName, qty: item.qty || 1 }], }), }) log(`Subscription created for ${item.item_code}`) } } catch (e) { log(`Subscription failed for ${item.item_code}:`, e.message) } } // ── 4. Create Dispatch Jobs from project templates ── const templatesNeeded = new Set() for (const item of items) { if (item.project_template_id) templatesNeeded.add(item.project_template_id) } log('Templates needed:', [...templatesNeeded], 'from items:', items.map(i => `${i.item_code}:${i.project_template_id || 'none'}`)) const createdJobs = [] if (templatesNeeded.size > 0) { const fullAddress = [address, city, postal_code].filter(Boolean).join(', ') for (const templateId of templatesNeeded) { const steps = getTemplateSteps(templateId) if (!steps.length) continue for (let i = 0; i < steps.length; i++) { const step = steps[i] const ticketId = 'DJ-' + Date.now().toString(36).toUpperCase() + '-' + i let dependsOn = '' if (step.depends_on_step != null) { const depJob = createdJobs[step.depends_on_step] if (depJob) dependsOn = depJob.name } const parentJob = createdJobs.length > 0 ? createdJobs[0].name : '' const jobPayload = { ticket_id: ticketId, subject: step.subject, address: fullAddress, duration_h: step.duration_h || 1, priority: step.priority || 'medium', status: 'open', job_type: step.job_type || 'Autre', customer: customerName, depends_on: dependsOn, parent_job: parentJob, step_order: i + 1, on_open_webhook: step.on_open_webhook || '', on_close_webhook: step.on_close_webhook || '', notes: [ step.assigned_group ? `Groupe: ${step.assigned_group}` : '', orderName ? `Sales Order: ${orderName}` : '', preferred_date ? `Date souhaitée: ${preferred_date} ${preferred_slot || ''}` : '', 'Commande en ligne', ].filter(Boolean).join(' | '), scheduled_date: preferred_date || '', } try { const jobRes = await erpFetch('/api/resource/Dispatch%20Job', { method: 'POST', body: JSON.stringify(jobPayload) }) if (jobRes.status === 200 && jobRes.data?.data) { createdJobs.push(jobRes.data.data) } } catch (e) { log(`Job creation failed: ${step.subject} — ${e.message}`) } } } result.jobs_created = createdJobs.length } // ── 5. Send confirmation SMS/email ── if (phone) { try { const { sendSmsInternal } = require('./twilio') const itemList = items.map(i => `• ${i.item_name}`).join('\n') const msg = `Gigafibre — Commande confirmée!\n\n${itemList}\n\n${orderName ? `Réf: ${orderName}\n` : ''}${preferred_date ? `Date souhaitée: ${preferred_date}\n` : ''}Nous vous contacterons pour confirmer les détails.\n\nMerci!` await sendSmsInternal(phone, msg) result.sms_sent = true } catch (e) { log('Checkout confirmation SMS failed:', e.message) } } if (email) { try { const { sendEmail } = require('./email') const itemRows = items.map(i => `