Optimisation sûre (vérifiée, 0 régression) : - helpers.js : `cors()` partagé (en-têtes CORS génériques) au lieu de 2 copies locales. - address-conformity.js : réutilise `pool` (address-db) + `cors` (helpers) au lieu de redéfinir un Pool + cors → 1 seul client pg local partagé pour rqa_addresses/fiber. - address-validate.js : utilise helpers.cors. docs/architecture/VISION.md (NOUVEAU) — vision + plan de modularisation + roadmap d'optimisation, fondé sur un audit chiffré (hub 58 modules/23k lignes, Ops 45k lignes, god-files identifiés). Découpe en 9 domaines (bounded contexts), principe « source de vérité + validation à la saisie + lien stable » (modèle Adresses généralisé à Client/Device/Service), optimisations P0/P1/P2, métriques de succès. Complète les docs architecture existants + ENGINEERING_PRACTICES. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
149 lines
5.3 KiB
JavaScript
149 lines
5.3 KiB
JavaScript
'use strict'
|
|
const http = require('http')
|
|
const https = require('https')
|
|
const { URL } = require('url')
|
|
const cfg = require('./config')
|
|
|
|
function log (...args) { console.log(`[${new Date().toISOString().slice(11, 19)}]`, ...args) }
|
|
|
|
function json (res, status, data) {
|
|
res.writeHead(status, { 'Content-Type': 'application/json' })
|
|
res.end(JSON.stringify(data))
|
|
}
|
|
|
|
function parseBody (req) {
|
|
return new Promise((resolve, reject) => {
|
|
const chunks = []
|
|
req.on('data', c => chunks.push(c))
|
|
req.on('end', () => {
|
|
const raw = Buffer.concat(chunks).toString()
|
|
const ct = (req.headers['content-type'] || '').toLowerCase()
|
|
if (ct.includes('application/json')) {
|
|
try { resolve(JSON.parse(raw)) } catch { resolve({}) }
|
|
} else if (ct.includes('urlencoded')) {
|
|
resolve(Object.fromEntries(new URLSearchParams(raw)))
|
|
} else {
|
|
resolve(raw)
|
|
}
|
|
})
|
|
req.on('error', reject)
|
|
})
|
|
}
|
|
|
|
function httpRequest (baseUrl, path, { method = 'GET', body, headers = {}, timeout = 15000 } = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const u = new URL(path, baseUrl)
|
|
const proto = u.protocol === 'https:' ? https : http
|
|
const req = proto.request({
|
|
hostname: u.hostname, port: u.port || (u.protocol === 'https:' ? 443 : 80),
|
|
path: u.pathname + u.search, method,
|
|
headers: { 'Content-Type': 'application/json', ...headers }, timeout,
|
|
}, (resp) => {
|
|
let data = ''
|
|
resp.on('data', c => { data += c })
|
|
resp.on('end', () => {
|
|
try { resolve({ status: resp.statusCode, data: data ? JSON.parse(data) : null }) }
|
|
catch { resolve({ status: resp.statusCode, data }) }
|
|
})
|
|
})
|
|
req.on('error', reject)
|
|
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout: ' + path)) })
|
|
if (body) req.write(typeof body === 'string' ? body : JSON.stringify(body))
|
|
req.end()
|
|
})
|
|
}
|
|
|
|
function erpFetch (path, opts = {}) {
|
|
const parsed = new URL(cfg.ERP_URL + path)
|
|
return new Promise((resolve, reject) => {
|
|
const req = http.request({
|
|
hostname: parsed.hostname, port: parsed.port || 8000,
|
|
path: parsed.pathname + parsed.search, method: opts.method || 'GET',
|
|
headers: { Host: cfg.ERP_SITE, Authorization: 'token ' + cfg.ERP_TOKEN, 'Content-Type': 'application/json', ...opts.headers },
|
|
}, (res) => {
|
|
let body = ''
|
|
res.on('data', c => body += c)
|
|
res.on('end', () => {
|
|
try { resolve({ status: res.statusCode, data: JSON.parse(body) }) }
|
|
catch { resolve({ status: res.statusCode, data: body }) }
|
|
})
|
|
})
|
|
req.on('error', reject)
|
|
if (opts.body) req.write(typeof opts.body === 'string' ? opts.body : JSON.stringify(opts.body))
|
|
req.end()
|
|
})
|
|
}
|
|
|
|
function erpRequest (method, path, body) {
|
|
return erpFetch(path, { method, ...(body && { body }) })
|
|
}
|
|
|
|
async function lookupCustomerByPhone (phone) {
|
|
const digits = phone.replace(/\D/g, '').slice(-10)
|
|
const fields = JSON.stringify(['name', 'customer_name', 'cell_phone', 'tel_home', 'tel_office'])
|
|
for (const field of ['cell_phone', 'tel_home', 'tel_office']) {
|
|
const filters = JSON.stringify([[field, 'like', '%' + digits]])
|
|
const path = `/api/resource/Customer?filters=${encodeURIComponent(filters)}&fields=${encodeURIComponent(fields)}&limit_page_length=1`
|
|
try {
|
|
const res = await erpFetch(path)
|
|
if (res.status === 200 && res.data?.data?.length > 0) return res.data.data[0]
|
|
} catch (e) { log('lookupCustomerByPhone error on ' + field + ':', e.message) }
|
|
}
|
|
return null
|
|
}
|
|
|
|
function createCommunication (fields) {
|
|
return erpFetch('/api/resource/Communication', { method: 'POST', body: JSON.stringify(fields) })
|
|
}
|
|
|
|
// --- GenieACS NBI rate limiter ---
|
|
// Prevents overwhelming GenieACS with concurrent requests
|
|
const NBI_MAX_CONCURRENT = parseInt(process.env.NBI_MAX_CONCURRENT || '3')
|
|
const NBI_MIN_INTERVAL_MS = parseInt(process.env.NBI_MIN_INTERVAL_MS || '200')
|
|
let nbiActive = 0
|
|
let nbiLastRequest = 0
|
|
const nbiQueue = []
|
|
|
|
function nbiRequest (path, method = 'GET', body = null) {
|
|
return new Promise((resolve, reject) => {
|
|
function execute () {
|
|
nbiActive++
|
|
const now = Date.now()
|
|
const wait = Math.max(0, NBI_MIN_INTERVAL_MS - (now - nbiLastRequest))
|
|
setTimeout(() => {
|
|
nbiLastRequest = Date.now()
|
|
httpRequest(cfg.GENIEACS_NBI_URL, path, { method, body, timeout: 30000 })
|
|
.then(resolve).catch(reject)
|
|
.finally(() => {
|
|
nbiActive--
|
|
if (nbiQueue.length > 0) nbiQueue.shift()()
|
|
})
|
|
}, wait)
|
|
}
|
|
if (nbiActive >= NBI_MAX_CONCURRENT) {
|
|
nbiQueue.push(execute)
|
|
} else {
|
|
execute()
|
|
}
|
|
})
|
|
}
|
|
|
|
function deepGetValue (obj, path) {
|
|
const node = path.split('.').reduce((o, k) => o?.[k], obj)
|
|
return node?._value !== undefined ? node._value : null
|
|
}
|
|
|
|
// CORS partagé pour les endpoints PUBLICS (données publiques, lecture seule + écritures internes).
|
|
// Pose les en-têtes via setHeader (fusionnés par writeHead de json()). methods optionnel.
|
|
function cors (res, methods = 'GET, POST, OPTIONS') {
|
|
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
res.setHeader('Access-Control-Allow-Methods', methods)
|
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
|
|
}
|
|
|
|
module.exports = {
|
|
log, json, parseBody, httpRequest, cors,
|
|
erpFetch, erpRequest, lookupCustomerByPhone, createCommunication,
|
|
nbiRequest, deepGetValue,
|
|
}
|