'use strict' const { log, json, parseBody, erpRequest } = require('./helpers') const { broadcast } = require('./sse') let oktopus try { oktopus = require('./oktopus') } catch (e) { log('Oktopus module not loaded:', e.message) } async function handle (req, res, method, path) { try { const action = path.replace('/provision/', '').split('/').filter(Boolean)[0] if (action === 'pre-authorize' && method === 'POST') { const body = await parseBody(req) const { gpon_serial, olt_ip, frame, slot, port, ont_id, equipment_name } = body const vlans = body.vlans || {} if (!gpon_serial || !olt_ip || frame === undefined || slot === undefined || port === undefined) { return json(res, 400, { error: 'Missing required fields: gpon_serial, olt_ip, frame, slot, port' }) } const vlanCmd = (vlan, gemport) => vlan ? `service-port auto vlan ${vlan} gemport ${gemport} gpon ${frame}/${slot}/${port} ont ${ont_id} transparent` : `# no vlan for gemport ${gemport}` const commands = [ `# OLT: ${olt_ip} — Pre-authorize ONT`, `interface gpon ${frame}/${slot}`, `ont add ${port} sn-auth "${gpon_serial}" omci ont-lineprofile-id ${body.line_profile || 10} ont-srvprofile-id ${body.service_profile || 10} desc "pre-auth-${equipment_name || 'unknown'}"`, `ont confirm ${port} ont ${ont_id}`, `quit`, `# Service ports (VLANs)`, vlanCmd(vlans.internet, 1), vlanCmd(vlans.manage, 2), vlanCmd(vlans.telephone, 3), vlanCmd(vlans.tv, 4), ] if (equipment_name) { try { await erpRequest('PUT', `/api/resource/Service Equipment/${encodeURIComponent(equipment_name)}`, { gpon_serial, status: 'Actif' }) } catch (e) { log('ERPNext update error:', e.message) } } log(`[provision] Pre-authorize ${gpon_serial} on ${olt_ip} ${frame}/${slot}/${port} ONT:${ont_id}`) return json(res, 200, { status: 'commands_generated', gpon_serial, olt_ip, port: `${frame}/${slot}/${port}`, ont_id, commands, note: 'Execute via n8n SSH node or manual SSH to OLT' }) } if (action === 'on-scan' && method === 'POST') { const body = await parseBody(req) const { serial, equipment_name, service_location } = body if (!serial) return json(res, 400, { error: 'Missing serial' }) const result = { serial, actions: [] } if (equipment_name) { try { await erpRequest('PUT', `/api/resource/Service Equipment/${encodeURIComponent(equipment_name)}`, { serial_number: serial, mac_address: body.mac || null, status: 'Actif' }) result.actions.push({ action: 'equipment_updated', equipment_name, serial }) } catch (e) { result.actions.push({ action: 'equipment_update_failed', error: e.message }) } } if (service_location && (body.equipment_type === 'ONT' || serial.startsWith('TPLG') || serial.startsWith('RCMG'))) { try { const locRes = await erpRequest('GET', `/api/resource/Service Location/${encodeURIComponent(service_location)}?fields=["olt_ip","olt_port","ont_id","vlan_internet","vlan_manage","vlan_telephone","vlan_tv"]`) const loc = locRes.data || {} if (loc.olt_ip && loc.olt_port) { const m = (loc.olt_port || '').match(/(\d+)\/(\d+)\/(\d+)/) if (m) { result.olt_pre_auth = { gpon_serial: serial, olt_ip: loc.olt_ip, frame: +m[1], slot: +m[2], port: +m[3], ont_id: loc.ont_id || 0, vlans: { internet: loc.vlan_internet, manage: loc.vlan_manage, telephone: loc.vlan_telephone, tv: loc.vlan_tv } } result.actions.push({ action: 'olt_pre_auth_ready', message: 'OLT commands generated, send to n8n for SSH execution' }) } } } catch (e) { result.actions.push({ action: 'location_lookup_failed', error: e.message }) } } if (oktopus && body.mac && (body.equipment_type === 'ONT' || serial.startsWith('TPLG') || serial.startsWith('RCMG'))) { try { const oktRes = await oktopus.provisionDevice({ mac: body.mac, serial, service_location: body.service_location, equipment_name: body.equipment_name, customer: body.customer, }) result.oktopus = oktRes result.actions.push(...(oktRes.actions || [])) } catch (e) { result.actions.push({ action: 'oktopus_provision_failed', error: e.message }) } } if (body.customer) broadcast('customer:' + body.customer, 'equipment-scanned', { serial, equipment_type: body.equipment_type, job: body.job_name }) log(`[provision] Scan: ${serial} → ${result.actions.length} actions`) return json(res, 200, result) } if (action === 'swap' && method === 'POST') { const body = await parseBody(req) const { old_equipment_name, new_serial, equipment_type, customer, service_location } = body const swapType = body.swap_type || 'permanent' if (!old_equipment_name || !new_serial) return json(res, 400, { error: 'Missing old_equipment_name and new_serial' }) const result = { old_equipment: old_equipment_name, new_serial, swap_type: swapType, actions: [] } const oldStatus = { permanent: 'Défectueux', diagnostic: 'En diagnostic', upgrade: 'Retourné' }[swapType] || 'Défectueux' let oldEquip = {} try { const r = await erpRequest('GET', `/api/resource/Service Equipment/${encodeURIComponent(old_equipment_name)}`) oldEquip = r.data?.data || r.data || {} } catch { return json(res, 404, { error: 'Old equipment not found: ' + old_equipment_name }) } try { await erpRequest('PUT', `/api/resource/Service Equipment/${encodeURIComponent(old_equipment_name)}`, { status: oldStatus, notes: `${oldEquip.notes || ''}\n[${new Date().toISOString()}] ${swapType === 'diagnostic' ? 'Swap diagnostic' : 'Remplacé'} par ${new_serial}. Raison: ${body.reason || swapType}`.trim(), }) result.actions.push({ action: 'old_status_updated', name: old_equipment_name, status: oldStatus }) } catch (e) { result.actions.push({ action: 'status_update_failed', error: e.message }) } const newName = 'EQ-SWAP-' + new_serial.substring(0, 10).replace(/[^A-Za-z0-9]/g, '') try { await erpRequest('POST', '/api/resource/Service Equipment', { name: newName, equipment_type: equipment_type || oldEquip.equipment_type, serial_number: new_serial, mac_address: body.new_mac || null, brand: oldEquip.brand, model: oldEquip.model, customer: customer || oldEquip.customer, service_location: service_location || oldEquip.service_location, subscription: oldEquip.subscription, status: 'Actif', ownership: 'Gigafibre', wifi_ssid: oldEquip.wifi_ssid, wifi_password: oldEquip.wifi_password, sip_username: oldEquip.sip_username, sip_password: oldEquip.sip_password, fibre_line_profile: oldEquip.fibre_line_profile, fibre_service_profile: oldEquip.fibre_service_profile, parent_device: oldEquip.parent_device, notes: `Remplacement de ${old_equipment_name} (${oldEquip.serial_number || 'N/A'}). Raison: ${body.reason || 'défectueux'}`, }) result.actions.push({ action: 'new_equipment_created', name: newName, serial: new_serial }) } catch (e) { result.actions.push({ action: 'create_new_failed', error: e.message }) } if ((equipment_type || oldEquip.equipment_type) === 'ONT' && (service_location || oldEquip.service_location)) { try { const locRes = await erpRequest('GET', `/api/resource/Service Location/${encodeURIComponent(service_location || oldEquip.service_location)}?fields=["olt_ip","olt_port","ont_id","vlan_internet","vlan_manage","vlan_telephone","vlan_tv"]`) const loc = locRes.data || {} const m = (loc.olt_port || '').match(/(\d+)\/(\d+)\/(\d+)/) if (loc.olt_ip && m) { result.olt_swap_commands = [ `# OLT: ${loc.olt_ip} — Swap ONT on ${m[0]}`, `interface gpon ${m[1]}/${m[2]}`, `ont delete ${m[3]} ${loc.ont_id || 0}`, `ont add ${m[3]} sn-auth "${new_serial}" omci ont-lineprofile-id ${oldEquip.fibre_line_profile || 10} ont-srvprofile-id ${oldEquip.fibre_service_profile || 10} desc "swap-${newName}"`, `ont confirm ${m[3]} ont ${loc.ont_id || 0}`, `quit`, loc.vlan_internet ? `service-port auto vlan ${loc.vlan_internet} gemport 1 gpon ${m[1]}/${m[2]}/${m[3]} ont ${loc.ont_id || 0} transparent` : '', loc.vlan_manage ? `service-port auto vlan ${loc.vlan_manage} gemport 2 gpon ${m[1]}/${m[2]}/${m[3]} ont ${loc.ont_id || 0} transparent` : '', ].filter(Boolean) result.actions.push({ action: 'olt_swap_commands_ready', message: 'Execute via n8n SSH node' }) } } catch (e) { result.actions.push({ action: 'olt_swap_lookup_failed', error: e.message }) } } if (customer || oldEquip.customer) { broadcast('customer:' + (customer || oldEquip.customer), 'equipment-swapped', { old: old_equipment_name, new_serial, reason: body.reason }) } log(`[provision] Swap: ${old_equipment_name} → ${new_serial} (${result.actions.length} actions)`) return json(res, 200, result) } if (action === 'equipment' && method === 'GET') { const serial = decodeURIComponent(path.replace('/provision/', '').split('/').filter(Boolean)[1] || '') if (!serial) return json(res, 400, { error: 'Missing serial' }) try { const r = await erpRequest('GET', `/api/resource/Service Equipment?filters=[["serial_number","=","${serial}"]]&fields=["name","serial_number","mac_address","wifi_ssid","wifi_password","sip_username","sip_password","fibre_line_profile","fibre_service_profile","customer","service_location","equipment_type"]&limit_page_length=1`) const equips = r.data || [] if (!equips.length) return json(res, 404, { error: 'Equipment not found for serial: ' + serial }) return json(res, 200, equips[0]) } catch (e) { return json(res, 502, { error: 'ERPNext lookup failed: ' + e.message }) } } return json(res, 400, { error: 'Unknown provision endpoint: ' + action }) } catch (e) { log('Provisioning error:', e.message) return json(res, 500, { error: 'Provisioning error: ' + e.message }) } } module.exports = { handle }