gigafibre-fsm/services/targo-hub/lib/otp.js
louispaulb 01bb99857f refactor(targo-hub): add erp.js wrapper + migrate 7 lib files to it
Replaces hand-rolled `erpFetch` + `encodeURIComponent(JSON.stringify(...))`
URL building with a structured wrapper: erp.get/list/listRaw/create/update/
remove/getMany/hydrateLabels/raw.

Key wins:
- erp.list auto-retries up to 5 times when v16 rejects a fetched/linked
  field with "Field not permitted in query" — the field is dropped and the
  call continues, so callers don't have to know which fields v16 allows.
- erp.hydrateLabels batches link-label resolution (customer_name,
  service_location_name, …) in one query per link field — no N+1, no
  v16 breakage.
- Consistent {ok, error, status} shape for mutations.

Migrated call sites:
- otp.js: Customer email lookup + verifyOTP customer detail fetch +
  Contact email fallback + Service Location listing
- referral.js: Referral Credit fetch / update / generate
- tech-absence-sms.js: lookupTechByPhone, set/clear absence
- conversation.js: Issue archive create
- magic-link.js: Tech lookup for /refresh
- ical.js: Tech lookup + jobs listing for iCal feed
- tech-mobile.js: 13 erpFetch sites → erp wrapper

Remaining erpFetch callers (dispatch.js, acceptance.js, payments.js,
contracts.js, checkout.js, …) deliberately left untouched this pass —
they each have 10+ sites and need individual smoke-tests.

Live-tested against production ERPNext: tech-mobile page renders 54K
bytes, no runtime errors in targo-hub logs post-restart.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 23:01:27 -04:00

101 lines
3.3 KiB
JavaScript

'use strict'
const { log } = require('./helpers')
const erp = require('./erp')
const { otpEmailHtml } = require('./email-templates')
const otpStore = new Map()
function generateOTP () {
return String(Math.floor(100000 + Math.random() * 900000))
}
async function sendOTP (identifier) {
const isEmail = identifier.includes('@')
const code = generateOTP()
const expires = Date.now() + 10 * 60 * 1000
let customer = null
if (isEmail) {
const rows = await erp.list('Customer', {
fields: ['name', 'customer_name', 'cell_phone', 'email_id'],
filters: [['email_id', '=', identifier]], limit: 1,
})
if (rows.length) customer = rows[0]
} else {
const { lookupCustomerByPhone } = require('./helpers')
customer = await lookupCustomerByPhone(identifier)
}
if (!customer) return { found: false }
otpStore.set(identifier, { code, expires, customerId: customer.name, customerName: customer.customer_name })
if (isEmail) {
const { sendEmail } = require('./email')
await sendEmail({
to: identifier,
subject: 'Code de vérification Gigafibre',
html: otpEmailHtml(code),
})
} else {
try {
const { sendSmsInternal } = require('./twilio')
await sendSmsInternal(identifier, `Gigafibre — Votre code de vérification : ${code}\nExpire dans 10 min.`)
} catch (e) { log('OTP SMS failed:', e.message) }
}
log(`OTP sent to ${identifier} for customer ${customer.name}`)
return { found: true, sent: true, channel: isEmail ? 'email' : 'sms' }
}
async function verifyOTP (identifier, code) {
const entry = otpStore.get(identifier)
if (!entry) return { valid: false, reason: 'no_otp' }
if (Date.now() > entry.expires) { otpStore.delete(identifier); return { valid: false, reason: 'expired' } }
if (entry.code !== code) return { valid: false, reason: 'wrong_code' }
otpStore.delete(identifier)
const result = { valid: true, customer_id: entry.customerId, customer_name: entry.customerName }
try {
const c = await erp.get('Customer', entry.customerId, {
fields: ['name', 'customer_name', 'cell_phone', 'email_id', 'tel_home'],
})
if (c) {
result.phone = c.cell_phone || c.tel_home || ''
result.email = c.email_id || ''
}
if (!result.email) {
try {
const contacts = await erp.list('Contact', {
fields: ['email_id'],
filters: [['Dynamic Link', 'link_doctype', '=', 'Customer'], ['Dynamic Link', 'link_name', '=', entry.customerId]],
limit: 1,
})
if (contacts[0]?.email_id) result.email = contacts[0].email_id
} catch (e) { log('OTP - Contact email fallback error:', e.message) }
}
const locs = await erp.list('Service Location', {
fields: ['name', 'address_line', 'city', 'postal_code', 'location_name', 'latitude', 'longitude'],
filters: [['customer', '=', entry.customerId]], limit: 20,
})
if (locs.length) {
result.addresses = locs.map(l => ({
name: l.name,
address: l.address_line || l.location_name || l.name,
city: l.city || '',
postal_code: l.postal_code || '',
latitude: l.latitude || null,
longitude: l.longitude || null,
}))
}
} catch (e) {
log('OTP verify - customer details fetch error:', e.message)
}
return result
}
module.exports = { otpStore, generateOTP, sendOTP, verifyOTP }