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>
101 lines
3.3 KiB
JavaScript
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 }
|