@@ -0,0 +1,305 @@
|
||||
// services/sales/masters.js
|
||||
// Settings, divisions, product catalog, Sales User Types (permission
|
||||
// templates), per-employee sales profiles, sales-person ↔ customer mapping
|
||||
// and customer portal accounts for the Sales Order module.
|
||||
'use strict';
|
||||
const crypto = require('crypto');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { query, one, json, csv } = require('./db');
|
||||
const { DEFAULT_SETTINGS } = require('./constants');
|
||||
const { DEFAULT_COMPANY } = require('../companyConfig');
|
||||
|
||||
// ── Settings (cached 30s) ───────────────────────────────────────────────────
|
||||
let _settings = null, _settingsAt = 0;
|
||||
async function getSettings(force = false) {
|
||||
if (!force && _settings && Date.now() - _settingsAt < 30000) return _settings;
|
||||
const rows = await query(`SELECT SKEY, SVALUE FROM dbo.ZSO_SETTINGS`);
|
||||
const s = { ...DEFAULT_SETTINGS };
|
||||
rows.forEach(r => { s[r.SKEY] = json(r.SVALUE, r.SVALUE); });
|
||||
if (!s.company) s.company = DEFAULT_COMPANY;
|
||||
_settings = s; _settingsAt = Date.now();
|
||||
return s;
|
||||
}
|
||||
async function saveSettings(patch, by) {
|
||||
for (const [k, v] of Object.entries(patch || {})) {
|
||||
if (!(k in DEFAULT_SETTINGS)) continue;
|
||||
await query(`MERGE dbo.ZSO_SETTINGS AS t USING (SELECT ? AS K) s ON t.SKEY=s.K
|
||||
WHEN MATCHED THEN UPDATE SET SVALUE=?, UPDATED_AT=SYSDATETIME(), UPDATED_BY=?
|
||||
WHEN NOT MATCHED THEN INSERT (SKEY,SVALUE,UPDATED_BY) VALUES (?,?,?);`, [k, JSON.stringify(v), by, k, JSON.stringify(v), by]);
|
||||
}
|
||||
return getSettings(true);
|
||||
}
|
||||
// Settings safe to show to every logged-in employee/customer (no API keys).
|
||||
function publicSettings(s) { const { bankApiKey, ...rest } = s; return rest; }
|
||||
|
||||
// ── Divisions ───────────────────────────────────────────────────────────────
|
||||
function divFromRow(r) { return { id: r.ID, name: r.NAME, code: r.CODE || '', itemGroups: r.ITEM_GROUPS || '', warehouse: r.WAREHOUSE || '', sort: r.SORT || 0, active: !!r.ACTIVE }; }
|
||||
async function listDivisions(includeInactive = false) {
|
||||
const rows = await query(`SELECT * FROM dbo.ZSO_DIVISIONS ${includeInactive ? '' : 'WHERE ACTIVE=1'} ORDER BY SORT, ID`);
|
||||
return rows.map(divFromRow);
|
||||
}
|
||||
async function saveDivision(d) {
|
||||
const id = parseInt(d.id);
|
||||
if (!id) throw new Error('Division ID is required');
|
||||
await query(`MERGE dbo.ZSO_DIVISIONS AS t USING (SELECT ? AS ID) s ON t.ID=s.ID
|
||||
WHEN MATCHED THEN UPDATE SET NAME=?, CODE=?, ITEM_GROUPS=?, WAREHOUSE=?, SORT=?, ACTIVE=?
|
||||
WHEN NOT MATCHED THEN INSERT (ID,NAME,CODE,ITEM_GROUPS,WAREHOUSE,SORT,ACTIVE) VALUES (?,?,?,?,?,?,?);`,
|
||||
[id, d.name, d.code || '', d.itemGroups || '', d.warehouse || '', parseInt(d.sort) || 0, d.active !== false,
|
||||
id, d.name, d.code || '', d.itemGroups || '', d.warehouse || '', parseInt(d.sort) || 0, d.active !== false]);
|
||||
}
|
||||
async function getDivision(id) { const r = await one(`SELECT * FROM dbo.ZSO_DIVISIONS WHERE ID=?`, [parseInt(id)]); return r ? divFromRow(r) : null; }
|
||||
|
||||
// ── Products ────────────────────────────────────────────────────────────────
|
||||
// Two sources (Admin → Settings → Product source):
|
||||
// catalog — ZSO_PRODUCTS, maintained in the Sales admin screen (msale's
|
||||
// product + price_list tables, migrated)
|
||||
// sap — live OITM items of the division's SAP item groups, priced from
|
||||
// the configured SAP price list
|
||||
// Either way callers get the same shape, keyed by `key` (P:<id> / S:<code>).
|
||||
function prodFromRow(r) {
|
||||
return { key: `P:${r.ID}`, id: r.ID, catalog: r.CATALOG, divisionId: r.DIVISION_ID, itemCode: r.ITEM_CODE, displayCode: r.DISPLAY_CODE || r.ITEM_CODE,
|
||||
description: r.DESCRIPTION || '', shortDesc: r.SHORT_DESC || '', packSize: r.PACK_SIZE || 1, hsn: r.HSN || '', price: Number(r.UNIT_PRICE) || 0,
|
||||
isInstrument: !!r.IS_INSTRUMENT, active: !!r.ACTIVE };
|
||||
}
|
||||
async function listCatalog({ catalog = 'domestic', divisionId, includeInactive = false } = {}) {
|
||||
const w = ['CATALOG=?']; const p = [catalog];
|
||||
if (divisionId) { w.push('DIVISION_ID=?'); p.push(parseInt(divisionId)); }
|
||||
if (!includeInactive) w.push('ACTIVE=1');
|
||||
const rows = await query(`SELECT * FROM dbo.ZSO_PRODUCTS WHERE ${w.join(' AND ')} ORDER BY DIVISION_ID, DISPLAY_CODE`, p);
|
||||
return rows.map(prodFromRow);
|
||||
}
|
||||
async function saveProduct(d, by) {
|
||||
const vals = [d.catalog || 'domestic', parseInt(d.divisionId), String(d.itemCode || '').trim(), d.displayCode || d.itemCode, d.description || '',
|
||||
d.shortDesc || '', parseInt(d.packSize) || 1, d.hsn || '', Number(d.price) || 0, !!d.isInstrument, d.active !== false, by];
|
||||
if (!vals[1] || !vals[2]) throw new Error('Division and SAP item code are required');
|
||||
if (d.id) {
|
||||
await query(`UPDATE dbo.ZSO_PRODUCTS SET CATALOG=?, DIVISION_ID=?, ITEM_CODE=?, DISPLAY_CODE=?, DESCRIPTION=?, SHORT_DESC=?, PACK_SIZE=?, HSN=?,
|
||||
UNIT_PRICE=?, IS_INSTRUMENT=?, ACTIVE=?, UPDATED_BY=?, UPDATED_AT=SYSDATETIME() WHERE ID=?`, [...vals, parseInt(d.id)]);
|
||||
return parseInt(d.id);
|
||||
}
|
||||
const r = await one(`INSERT INTO dbo.ZSO_PRODUCTS (CATALOG,DIVISION_ID,ITEM_CODE,DISPLAY_CODE,DESCRIPTION,SHORT_DESC,PACK_SIZE,HSN,UNIT_PRICE,IS_INSTRUMENT,ACTIVE,UPDATED_BY)
|
||||
OUTPUT INSERTED.ID VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`, vals);
|
||||
return r.ID;
|
||||
}
|
||||
// Products orderable in a division, from whichever source is configured.
|
||||
async function listOrderableProducts(divisionId, catalog = 'domestic') {
|
||||
const s = await getSettings();
|
||||
if (s.productSource === 'sap' && catalog === 'domestic') {
|
||||
const div = await getDivision(divisionId);
|
||||
return require('./sap').listSapProducts(s.company, csv(div && div.itemGroups), s.sapPriceList, divisionId);
|
||||
}
|
||||
return listCatalog({ catalog, divisionId });
|
||||
}
|
||||
// Resolve one product key → product (used to price order lines server-side;
|
||||
// the client's price is never trusted).
|
||||
async function resolveProduct(key, divisionId, catalog = 'domestic') {
|
||||
if (String(key).startsWith('P:')) {
|
||||
const r = await one(`SELECT * FROM dbo.ZSO_PRODUCTS WHERE ID=? AND ACTIVE=1`, [parseInt(String(key).slice(2))]);
|
||||
return r ? prodFromRow(r) : null;
|
||||
}
|
||||
if (String(key).startsWith('S:')) {
|
||||
const list = await listOrderableProducts(divisionId, catalog);
|
||||
return list.find(p => p.key === key) || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Sales User Types (permission templates) ─────────────────────────────────
|
||||
function utFromRow(r) {
|
||||
return { id: r.ID, name: r.NAME, scope: r.SCOPE, listStatuses: r.LIST_STATUSES || '', defaultSteps: json(r.DEFAULT_STEPS, []),
|
||||
defaultModules: json(r.DEFAULT_MODULES, []), active: !!r.ACTIVE };
|
||||
}
|
||||
async function listUserTypes() { return (await query(`SELECT * FROM dbo.ZSO_USER_TYPES ORDER BY ID`)).map(utFromRow); }
|
||||
async function getUserType(id) { const r = await one(`SELECT * FROM dbo.ZSO_USER_TYPES WHERE ID=?`, [parseInt(id)]); return r ? utFromRow(r) : null; }
|
||||
async function saveUserType(d) {
|
||||
const id = parseInt(d.id);
|
||||
if (!id || !d.name) throw new Error('ID and name are required');
|
||||
if (!['all', 'mapped', 'own'].includes(d.scope)) throw new Error('Scope must be all / mapped / own');
|
||||
const steps = JSON.stringify(Array.isArray(d.defaultSteps) ? d.defaultSteps : []);
|
||||
const mods = JSON.stringify(Array.isArray(d.defaultModules) ? d.defaultModules : []);
|
||||
await query(`MERGE dbo.ZSO_USER_TYPES AS t USING (SELECT ? AS ID) s ON t.ID=s.ID
|
||||
WHEN MATCHED THEN UPDATE SET NAME=?, SCOPE=?, LIST_STATUSES=?, DEFAULT_STEPS=?, DEFAULT_MODULES=?, ACTIVE=?
|
||||
WHEN NOT MATCHED THEN INSERT (ID,NAME,SCOPE,LIST_STATUSES,DEFAULT_STEPS,DEFAULT_MODULES,ACTIVE) VALUES (?,?,?,?,?,?,?);`,
|
||||
[id, d.name, d.scope, d.listStatuses || '', steps, mods, d.active !== false, id, d.name, d.scope, d.listStatuses || '', steps, mods, d.active !== false]);
|
||||
}
|
||||
|
||||
// ── Employee sales profiles ─────────────────────────────────────────────────
|
||||
function profFromRow(r) {
|
||||
return { userId: r.USER_ID, userTypeId: r.USER_TYPE_ID, divisions: csv(r.DIVISIONS).map(Number), empCode: r.EMP_CODE || '',
|
||||
territory: r.TERRITORY || '', ccEmails: r.CC_EMAILS || '' };
|
||||
}
|
||||
// Profile + resolved user type for an ERP user. ERP admins with no profile
|
||||
// act as Superuser across every division.
|
||||
const _profCache = new Map();
|
||||
async function getProfile(user) {
|
||||
const uid = parseInt(user && user.id);
|
||||
const hit = _profCache.get(uid);
|
||||
if (hit && Date.now() - hit.at < 20000) return hit.v;
|
||||
const r = uid ? await one(`SELECT * FROM dbo.ZSO_USER_PROFILES WHERE USER_ID=?`, [uid]) : null;
|
||||
let prof = r ? profFromRow(r) : null;
|
||||
const ut = prof && prof.userTypeId ? await getUserType(prof.userTypeId) : null;
|
||||
// All categories incl. disabled — disabling only stops NEW orders; existing
|
||||
// ones in it must stay visible. (Creation checks division.active itself.)
|
||||
const allDivs = (await listDivisions(true)).map(d => d.id);
|
||||
const isAdmin = user && user.role === 'admin';
|
||||
const v = {
|
||||
userId: uid,
|
||||
configured: !!prof,
|
||||
userTypeId: prof ? prof.userTypeId : null,
|
||||
userTypeName: ut ? ut.name : (isAdmin ? 'Administrator' : null),
|
||||
scope: isAdmin ? 'all' : (ut ? ut.scope : 'all'),
|
||||
listStatuses: ut ? csv(ut.listStatuses).map(Number) : [],
|
||||
divisions: isAdmin ? allDivs : (prof && prof.divisions.length ? prof.divisions : []),
|
||||
empCode: prof ? prof.empCode : '',
|
||||
ccEmails: prof ? prof.ccEmails : '',
|
||||
};
|
||||
_profCache.set(uid, { at: Date.now(), v });
|
||||
return v;
|
||||
}
|
||||
async function listProfiles() {
|
||||
return (await query(`SELECT * FROM dbo.ZSO_USER_PROFILES`)).map(profFromRow);
|
||||
}
|
||||
// Save an employee's sales profile. With applyTemplate=true, the user type's
|
||||
// default steps/modules are written onto the ERP user (replacing any existing
|
||||
// sales_* steps and sales-* modules, leaving everything else untouched).
|
||||
async function saveProfile(d, by) {
|
||||
const uid = parseInt(d.userId);
|
||||
if (!uid) throw new Error('userId is required');
|
||||
const divs = (Array.isArray(d.divisions) ? d.divisions : csv(d.divisions)).map(Number).filter(Boolean).join(',');
|
||||
await query(`MERGE dbo.ZSO_USER_PROFILES AS t USING (SELECT ? AS U) s ON t.USER_ID=s.U
|
||||
WHEN MATCHED THEN UPDATE SET USER_TYPE_ID=?, DIVISIONS=?, EMP_CODE=?, TERRITORY=?, CC_EMAILS=?, UPDATED_AT=SYSDATETIME(), UPDATED_BY=?
|
||||
WHEN NOT MATCHED THEN INSERT (USER_ID,USER_TYPE_ID,DIVISIONS,EMP_CODE,TERRITORY,CC_EMAILS,UPDATED_BY) VALUES (?,?,?,?,?,?,?);`,
|
||||
[uid, parseInt(d.userTypeId) || null, divs, d.empCode || '', d.territory || '', d.ccEmails || '', by,
|
||||
uid, parseInt(d.userTypeId) || null, divs, d.empCode || '', d.territory || '', d.ccEmails || '', by]);
|
||||
_profCache.delete(uid);
|
||||
if (d.applyTemplate && d.userTypeId) await applyUserTypeTemplate(uid, parseInt(d.userTypeId));
|
||||
}
|
||||
async function applyUserTypeTemplate(userId, userTypeId) {
|
||||
const users = require('../hanaUsers');
|
||||
const u = await users.findById(userId);
|
||||
const ut = await getUserType(userTypeId);
|
||||
if (!u || !ut) throw new Error('User or user type not found');
|
||||
const isSalesStep = s => /^sales_/.test(typeof s === 'string' ? s : (s && s.step) || '');
|
||||
const steps = (u.approvalSteps || []).filter(s => !isSalesStep(s)).concat(ut.defaultSteps || []);
|
||||
const mods = Array.isArray(u.modules) ? u.modules.filter(m => !/^sales-/.test(m)) : [];
|
||||
await users.updateUser(userId, { approvalSteps: steps, modules: mods.concat(ut.defaultModules || []) });
|
||||
}
|
||||
|
||||
// ── Sales persons & customer mapping (msale sales_person_master) ────────────
|
||||
async function listSalesPersons() {
|
||||
const rows = await query(`SELECT p.*, (SELECT COUNT(*) FROM dbo.ZSO_SP_CUSTOMER_MAP m WHERE m.SALES_PERSON_ID=p.ID AND m.ACTIVE=1) AS CUST_COUNT
|
||||
FROM dbo.ZSO_SALES_PERSONS p ORDER BY p.ACTIVE DESC, p.NAME`);
|
||||
return rows.map(r => ({ id: r.ID, name: r.NAME, email: r.EMAIL || '', designation: r.DESIGNATION || '', divisions: csv(r.DIVISIONS).map(Number),
|
||||
userId: r.USER_ID, active: !!r.ACTIVE, customerCount: r.CUST_COUNT }));
|
||||
}
|
||||
async function saveSalesPerson(d) {
|
||||
const divs = (Array.isArray(d.divisions) ? d.divisions : csv(d.divisions)).join(',');
|
||||
if (!d.name) throw new Error('Name is required');
|
||||
if (d.id) {
|
||||
await query(`UPDATE dbo.ZSO_SALES_PERSONS SET NAME=?, EMAIL=?, DESIGNATION=?, DIVISIONS=?, USER_ID=?, ACTIVE=? WHERE ID=?`,
|
||||
[d.name, d.email || '', d.designation || '', divs, parseInt(d.userId) || null, d.active !== false, parseInt(d.id)]);
|
||||
return parseInt(d.id);
|
||||
}
|
||||
const r = await one(`INSERT INTO dbo.ZSO_SALES_PERSONS (NAME,EMAIL,DESIGNATION,DIVISIONS,USER_ID,ACTIVE) OUTPUT INSERTED.ID VALUES (?,?,?,?,?,1)`,
|
||||
[d.name, d.email || '', d.designation || '', divs, parseInt(d.userId) || null]);
|
||||
return r.ID;
|
||||
}
|
||||
async function listMappedCustomers(salesPersonId) {
|
||||
return (await query(`SELECT ID, CARD_CODE FROM dbo.ZSO_SP_CUSTOMER_MAP WHERE SALES_PERSON_ID=? AND ACTIVE=1 ORDER BY CARD_CODE`, [parseInt(salesPersonId)]))
|
||||
.map(r => ({ id: r.ID, cardCode: r.CARD_CODE }));
|
||||
}
|
||||
async function mapCustomers(salesPersonId, cardCodes, by) {
|
||||
for (const cc of cardCodes) {
|
||||
await query(`IF NOT EXISTS (SELECT 1 FROM dbo.ZSO_SP_CUSTOMER_MAP WHERE SALES_PERSON_ID=? AND CARD_CODE=? AND ACTIVE=1)
|
||||
INSERT INTO dbo.ZSO_SP_CUSTOMER_MAP (SALES_PERSON_ID,CARD_CODE,ACTIVE,CREATED_BY) VALUES (?,?,1,?)`,
|
||||
[parseInt(salesPersonId), cc, parseInt(salesPersonId), cc, by]);
|
||||
}
|
||||
}
|
||||
async function unmapCustomer(mapId) { await query(`UPDATE dbo.ZSO_SP_CUSTOMER_MAP SET ACTIVE=0 WHERE ID=?`, [parseInt(mapId)]); }
|
||||
// Customers visible to a 'mapped'-scope employee: those mapped to any sales
|
||||
// person linked to this user (by USER_ID, or by matching email — msale keyed on email).
|
||||
async function mappedCardCodesForUser(userId, email) {
|
||||
const rows = await query(`SELECT DISTINCT m.CARD_CODE FROM dbo.ZSO_SP_CUSTOMER_MAP m JOIN dbo.ZSO_SALES_PERSONS p ON p.ID=m.SALES_PERSON_ID
|
||||
WHERE m.ACTIVE=1 AND p.ACTIVE=1 AND (p.USER_ID=? OR (?<>'' AND LOWER(p.EMAIL)=LOWER(?)))`, [parseInt(userId) || 0, email || '', email || '']);
|
||||
return rows.map(r => r.CARD_CODE);
|
||||
}
|
||||
// Sales-person emails for a customer in a division (order email CCs — msale get_zsm_email_by_dealer_id).
|
||||
async function salesPersonEmailsFor(cardCode, divisionId) {
|
||||
const rows = await query(`SELECT DISTINCT p.EMAIL, p.DIVISIONS FROM dbo.ZSO_SP_CUSTOMER_MAP m JOIN dbo.ZSO_SALES_PERSONS p ON p.ID=m.SALES_PERSON_ID
|
||||
WHERE m.ACTIVE=1 AND p.ACTIVE=1 AND m.CARD_CODE=?`, [cardCode]);
|
||||
return rows.filter(r => r.EMAIL && (!divisionId || csv(r.DIVISIONS).map(Number).includes(Number(divisionId)))).map(r => r.EMAIL);
|
||||
}
|
||||
|
||||
// ── Customer portal accounts ────────────────────────────────────────────────
|
||||
// Passwords migrated from msale are unsalted MD5 — stored as "md5:<hex>" and
|
||||
// transparently upgraded to bcrypt on the customer's first successful login.
|
||||
async function hashPassword(p) { return bcrypt.hash(String(p), 10); }
|
||||
async function verifyPassword(plain, stored) {
|
||||
if (!stored) return false;
|
||||
if (stored.startsWith('md5:')) return crypto.createHash('md5').update(String(plain)).digest('hex') === stored.slice(4).toLowerCase();
|
||||
return bcrypt.compare(String(plain), stored);
|
||||
}
|
||||
function custFromRow(r) {
|
||||
return { cardCode: r.CARD_CODE, cardName: r.CARD_NAME || '', email: r.EMAIL || '', active: !!r.ACTIVE, locked: !!r.LOCKED,
|
||||
loginAttempts: r.LOGIN_ATTEMPTS || 0, mustChangePwd: !!r.MUST_CHANGE_PWD, lastLogin: r.LAST_LOGIN, creditLimit: Number(r.CREDIT_LIMIT) || 0,
|
||||
hasPassword: !!r.PASSWORD_HASH };
|
||||
}
|
||||
async function getCustomerAccount(cardCode) {
|
||||
const r = await one(`SELECT * FROM dbo.ZSO_CUSTOMERS WHERE CARD_CODE=?`, [cardCode]);
|
||||
return r ? { ...custFromRow(r), _hash: r.PASSWORD_HASH } : null;
|
||||
}
|
||||
async function listCustomerAccounts(search) {
|
||||
const s = `%${String(search || '').trim()}%`;
|
||||
const rows = await query(`SELECT TOP 500 * FROM dbo.ZSO_CUSTOMERS WHERE (?='%%' OR CARD_CODE LIKE ? OR CARD_NAME LIKE ?) ORDER BY CARD_CODE`, [s, s, s]);
|
||||
return rows.map(custFromRow);
|
||||
}
|
||||
async function upsertCustomerAccount(d, by) {
|
||||
const cc = String(d.cardCode || '').trim();
|
||||
if (!cc) throw new Error('Customer code is required');
|
||||
const existing = await one(`SELECT CARD_CODE FROM dbo.ZSO_CUSTOMERS WHERE CARD_CODE=?`, [cc]);
|
||||
if (d.password && String(d.password).length < 8) throw new Error('Password must be at least 8 characters');
|
||||
const hash = d.password ? await hashPassword(d.password) : null;
|
||||
if (existing) {
|
||||
await query(`UPDATE dbo.ZSO_CUSTOMERS SET CARD_NAME=COALESCE(?,CARD_NAME), EMAIL=?, ACTIVE=?, LOCKED=?, CREDIT_LIMIT=?,
|
||||
PASSWORD_HASH=COALESCE(?,PASSWORD_HASH), MUST_CHANGE_PWD=CASE WHEN ? IS NULL THEN MUST_CHANGE_PWD ELSE 1 END,
|
||||
LOGIN_ATTEMPTS=CASE WHEN ?=0 THEN 0 ELSE LOGIN_ATTEMPTS END, UPDATED_AT=SYSDATETIME(), UPDATED_BY=? WHERE CARD_CODE=?`,
|
||||
[d.cardName || null, d.email || '', d.active !== false, !!d.locked, Number(d.creditLimit) || 0, hash, hash, d.locked ? 1 : 0, by, cc]);
|
||||
return { created: false, passwordReset: !!hash };
|
||||
} else {
|
||||
if (!hash) throw new Error('An initial password is required for a new customer login');
|
||||
await query(`INSERT INTO dbo.ZSO_CUSTOMERS (CARD_CODE,CARD_NAME,EMAIL,PASSWORD_HASH,ACTIVE,LOCKED,CREDIT_LIMIT,MUST_CHANGE_PWD,UPDATED_BY) VALUES (?,?,?,?,?,0,?,1,?)`,
|
||||
[cc, d.cardName || '', d.email || '', hash, d.active !== false, Number(d.creditLimit) || 0, by]);
|
||||
return { created: true, passwordReset: false };
|
||||
}
|
||||
}
|
||||
const MAX_LOGIN_ATTEMPTS = 5;
|
||||
async function customerLogin(cardCode, password) {
|
||||
const acc = await getCustomerAccount(String(cardCode || '').trim());
|
||||
if (!acc || !acc.active) return { ok: false, message: 'Invalid customer code or password' };
|
||||
if (acc.locked) return { ok: false, message: 'This login is locked after too many failed attempts. Please contact MIPL to unlock it.' };
|
||||
const ok = await verifyPassword(password, acc._hash);
|
||||
if (!ok) {
|
||||
const n = acc.loginAttempts + 1;
|
||||
await query(`UPDATE dbo.ZSO_CUSTOMERS SET LOGIN_ATTEMPTS=?, LOCKED=? WHERE CARD_CODE=?`, [n, n >= MAX_LOGIN_ATTEMPTS, acc.cardCode]);
|
||||
return { ok: false, message: n >= MAX_LOGIN_ATTEMPTS ? 'Too many failed attempts — this login is now locked. Contact MIPL.' : 'Invalid customer code or password' };
|
||||
}
|
||||
const upgrade = acc._hash.startsWith('md5:') ? await hashPassword(password) : null;
|
||||
await query(`UPDATE dbo.ZSO_CUSTOMERS SET LOGIN_ATTEMPTS=0, LAST_LOGIN=SYSDATETIME(), PASSWORD_HASH=COALESCE(?,PASSWORD_HASH) WHERE CARD_CODE=?`, [upgrade, acc.cardCode]);
|
||||
return { ok: true, account: acc };
|
||||
}
|
||||
async function changeCustomerPassword(cardCode, oldPwd, newPwd) {
|
||||
const acc = await getCustomerAccount(cardCode);
|
||||
if (!acc || !(await verifyPassword(oldPwd, acc._hash))) throw new Error('Current password is incorrect');
|
||||
if (String(newPwd || '').length < 8) throw new Error('New password must be at least 8 characters');
|
||||
await query(`UPDATE dbo.ZSO_CUSTOMERS SET PASSWORD_HASH=?, MUST_CHANGE_PWD=0, UPDATED_AT=SYSDATETIME() WHERE CARD_CODE=?`, [await hashPassword(newPwd), cardCode]);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSettings, saveSettings, publicSettings,
|
||||
listDivisions, saveDivision, getDivision,
|
||||
listCatalog, saveProduct, listOrderableProducts, resolveProduct,
|
||||
listUserTypes, getUserType, saveUserType,
|
||||
getProfile, listProfiles, saveProfile, applyUserTypeTemplate,
|
||||
listSalesPersons, saveSalesPerson, listMappedCustomers, mapCustomers, unmapCustomer, mappedCardCodesForUser, salesPersonEmailsFor,
|
||||
getCustomerAccount, listCustomerAccounts, upsertCustomerAccount, customerLogin, changeCustomerPassword, verifyPassword, hashPassword,
|
||||
};
|
||||
Reference in New Issue
Block a user