// backend/routes/vendors.js — FIXED v3 // Fixes: // 1. /lookup/sales-employees added (same OSLP table as customers) // 2. mgrSalesPersonCode added to MANAGER_EDITABLE // 3. mgrChain added to MANAGER_EDITABLE (was missing → chain never saved/sent to SAP) // 4. allBillAddresses / allShipAddresses added to VENDOR_EDITABLE // 5. doApproveVendor passes salesPersonCode, allBillAddresses, allShipAddresses to createVendor // 6. getBankCodes now works (exported from sapServiceLayer) 'use strict'; const express = require('express'); const router = express.Router(); const { body, validationResult } = require('express-validator'); const { verifyToken, verifyAdmin, requireApprovalStep } = require('../middleware/auth'); const store = require('../services/hanaVendorStore'); const { resolve: resolveCompany } = require('../services/companyConfig'); function getSap() { return require('../services/sapServiceLayer'); } // SQL for lookups const sql = require('mssql'); let _hanaConn = null, _hanaConnecting = false; async function getHanaConn() { if (_hanaConn) return _hanaConn; if (_hanaConnecting) { for (let i = 0; i < 10; i++) { await new Promise(r => setTimeout(r, 500)); if (_hanaConn) return _hanaConn; } throw new Error('SQL timeout'); } _hanaConnecting = true; try { const config = { server: process.env.SQL_HOST, port: parseInt(process.env.SQL_PORT), user: process.env.SQL_USER, password: process.env.SQL_PASSWORD, database: process.env.SQL_DATABASE, options: { encrypt: true, trustServerCertificate: true, }, }; _hanaConn = await sql.connect(config); console.log('[VENDOR SQL] ✅ Connected'); } catch (err) { console.error('[VENDOR SQL] ❌', err.message); throw err; } finally { _hanaConnecting = false; } return _hanaConn; } async function hq(sqlQuery) { const conn = await getHanaConn(); const result = await conn.request().query(sqlQuery); return result.recordset || []; } const DB = () => `[dbo]`; async function safeLookup(res, sql, mapFn) { try { const rows = await hq(sql); return res.json({ success: true, data: rows.map(mapFn) }); } catch (err) { console.warn('[VENDOR LOOKUP] fallback:', err.message); return res.json({ success: true, data: [], warning: err.message }); } } // Utils function mapCurrency(label) { return { 'Indian Rupee':'INR','US Dollar':'USD','Euro':'EUR','British Pound':'GBP','UAE Dirham':'AED' }[label] || 'INR'; } function mapCountryCode(name) { return { 'India':'IN','United States':'US','United Kingdom':'GB','UAE':'AE','Singapore':'SG','Germany':'DE','Japan':'JP','Australia':'AU' }[name] || 'IN'; } function sanitizeMobile(raw) { if (!raw) return ''; const digits = String(raw).replace(/\D/g, ''); if (digits.length === 12 && digits.startsWith('91')) return digits.slice(2); if (digits.length === 13 && digits.startsWith('091')) return digits.slice(3); return digits.slice(-10); } // ── EDITABLE FIELDS ─────────────────────────────────────────────────────────── const VENDOR_EDITABLE = [ 'cardName','foreignName','typeOfBusiness','industry','products','paymentTerms', 'mobile','altContact','email','currency','contactFirst','contactLast','contactTitle', 'billStreet','billBlock','billCity','billZip','billState','billCountry', // FIX: multiple addresses support 'allBillAddresses','allShipAddresses','sameAsBill', 'shipStreet','shipBlock','shipCity','shipZip','shipState','shipCountry', 'gstin','pan','tan','remarks','hasMsme','msmeNo','msmeType','msmeBType', 'fssaiNo','bankAccounts','attachments', ]; const MANAGER_EDITABLE = [ 'mgrCardCodePrefix','mgrGroupCode','mgrGroup','mgrCurrency','mgrPayTerms', 'mgrPayTermsCode','mgrPurchaseAccount','mgrLanguage','mgrCreditLimit', 'mgrNotes','mgrTerritory', 'mgrMainGroup', // → U_Main_Group in SAP 'mgrChain', // FIX: was missing → chain never persisted or sent to SAP 'mgrSalesPersonCode', // FIX: new field for sales person ]; const ALL_EDITABLE = [...VENDOR_EDITABLE, ...MANAGER_EDITABLE]; function extractPatch(body, fields) { const p = {}; fields.forEach(f => { if (body[f] !== undefined) p[f] = body[f]; }); return p; } // ══ LOOKUP ROUTES ════════════════════════════════════════════════════════════ // Vendor BP Groups router.get('/lookup/bp-groups', verifyToken, async (req, res) => { try { const result = await getSap().sapRequest('GET', `BusinessPartnerGroups?$filter=Type eq 'bbpgt_VendorGroup'&$select=Code,Name&$orderby=Name` ); res.json({ success: true, data: (result?.value || []).map(r => ({ GroupCode: r.Code, GroupName: r.Name })) }); } catch (err) { res.json({ success: true, data: [], warning: err.message }); } }); // Payment Terms router.get('/lookup/payment-terms', verifyToken, (req, res) => safeLookup(res, `SELECT "GroupNum","PymntGroup" FROM ${DB()}."OCTG" ORDER BY "PymntGroup"`, r => ({ Code: r.GroupNum, Name: r.PymntGroup }) ) ); // AP Accounts router.get('/lookup/ap-accounts', verifyToken, (req, res) => safeLookup(res, `SELECT "AcctCode","AcctName" FROM ${DB()}."OACT" WHERE ("FatherNum" = '2101000' OR "AcctCode" LIKE '211%') AND "Finanse" = 'N' ORDER BY "AcctCode"`, r => ({ AcctCode: r.AcctCode, AcctName: r.AcctName }) ) ); // ── FIX 1: Sales Employees — was missing from vendor lookups ───────────────── router.get('/lookup/sales-employees', verifyToken, (req, res) => safeLookup(res, `SELECT "SlpCode","SlpName" FROM ${DB()}."OSLP" WHERE "SlpCode" > 0 AND "Locked" = 'N' ORDER BY "SlpName"`, r => ({ SlpCode: r.SlpCode, SlpName: r.SlpName }) ) ); // ── Bank Codes from ODSC (Bank Master) ──────────────────────────────────────── // FIX: getBankCodes is now exported from sapServiceLayer — this will work router.get('/lookup/banks', verifyToken, async (req, res) => { const country = (req.query.country || 'IN').toUpperCase(); try { const banks = await getSap().getBankCodes(country); res.json({ success: true, data: banks }); } catch (err) { // Fallback: try HANA ODSC table directly try { const rows = await hq( `SELECT "BankCode","BankName","SwiftNo","CountryCode" FROM ${DB()}."ODSC" ORDER BY "BankName"` ); res.json({ success: true, data: rows.map(r => ({ BankCode: r.BankCode, BankName: r.BankName, SwiftNo: r.SwiftNo || '', CountryCode: r.CountryCode, })) }); } catch (e2) { res.json({ success: true, data: [], warning: e2.message }); } } }); // Main Group router.get('/lookup/main-group', verifyToken, (req, res) => safeLookup(res, `SELECT "Code","Name" FROM ${DB()}."@MAIN_GROUP" ORDER BY "Code"`, r => ({ Code: r.Code, Name: r.Name || r.Code }) ) ); // ── FIX 2: Chain — query was correct but field wasn't in MANAGER_EDITABLE ─── router.get('/lookup/chain', verifyToken, (req, res) => safeLookup(res, `SELECT "Code","Name" FROM ${DB()}."@CHAIN" ORDER BY "Code"`, r => ({ Code: r.Code, Name: r.Name || r.Code }) ) ); // Next Vendor CardCode router.get('/next-cardcode', verifyToken, async (req, res) => { const prefix = (req.query.prefix || 'VENDA').trim(); try { res.json({ success: true, cardCode: await getSap().getNextVendorCardCode(prefix) }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // ══ CRUD ROUTES ══════════════════════════════════════════════════════════════ router.post('/submit', [ body('cardName').notEmpty().trim(), body('email').isEmail().normalizeEmail(), body('mobile').notEmpty().trim(), body('contactFirst').notEmpty().trim(), body('contactLast').notEmpty().trim(), body('billStreet').notEmpty().trim(), body('billCity').notEmpty().trim(), body('gstin').notEmpty().trim(), body('pan').notEmpty().trim(), ], async (req, res) => { const errs = validationResult(req); if (!errs.isEmpty()) return res.status(400).json({ success: false, errors: errs.array() }); const b = req.body; const companyDB = resolveCompany(b.company); try { const vendor = await store.insertVendor({ userId: b.userId||'', userType: b.userType||'', userDept: b.userDept||'', vendorType: b.vendorType||'SUPPLIER', cardName: b.cardName, foreignName: b.foreignName||'', typeOfBusiness: b.typeOfBusiness||'Company', industry: b.industry||'', products: b.products||'', paymentTerms: b.paymentTerms||'30 Days', contactFirst: b.contactFirst, contactLast: b.contactLast, contactTitle: b.contactTitle||'', mobile: sanitizeMobile(b.mobile), altContact: b.altContact ? sanitizeMobile(b.altContact) : '', email: b.email, billStreet: b.billStreet, billBlock: b.billBlock||'', billCity: b.billCity, billZip: b.billZip||'', billState: b.billState||'', billCountry: b.billCountry||'India', // Multiple addresses allBillAddresses: Array.isArray(b.allBillAddresses) ? b.allBillAddresses : [], allShipAddresses: Array.isArray(b.allShipAddresses) ? b.allShipAddresses : [], sameAsBill: b.sameAsBill || false, gstin: (b.gstin||'').toUpperCase(), pan: (b.pan||'').toUpperCase(), tan: (b.tan||'').toUpperCase(), currency: b.currency||'Indian Rupee', hasMsme: b.hasMsme||false, msmeNo: b.msmeNo||'', msmeType: b.msmeType||'', msmeBType: b.msmeBType||'', fssaiNo: b.fssaiNo||'', bankAccounts: Array.isArray(b.bankAccounts) ? b.bankAccounts : [], attachments: b.attachments||{}, remarks: b.remarks||'', mgrCardCodePrefix: 'VENDA', mgrPurchaseAccount: '2110005', mgrCurrency: 'Indian Rupee', mgrLanguage: 'English (UK)', company: companyDB, }, companyDB); res.json({ success: true, message: 'Vendor registration submitted', id: vendor.id }); } catch (err) { console.error('[VENDOR] submit error:', err.message); res.status(500).json({ success: false, message: err.message }); } }); router.get('/', verifyToken, async (req, res) => { const st = (req.query.status || 'PENDING').toUpperCase(); const companyDB = resolveCompany(req.query.company); try { res.json({ success: true, data: await store.listByStatus(st, companyDB) }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); router.get('/:id', verifyToken, async (req, res) => { const companyDB = resolveCompany(req.query.company); try { const v = await store.findById(req.params.id, companyDB); if (!v) return res.status(404).json({ success: false, message: 'Not found' }); res.json({ success: true, data: v }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); router.patch('/:id/verify', verifyToken, requireApprovalStep('customer_vendor:verify', 'approve'), async (req, res) => { const companyDB = resolveCompany(req.body.company); try { const v = await store.findById(req.params.id, companyDB); if (!v) return res.status(404).json({ success: false, message: 'Not found' }); if (v.status !== 'PENDING') return res.status(400).json({ success: false, message: 'Only PENDING can be verified. Current: ' + v.status }); const patch = extractPatch(req.body, ALL_EDITABLE); patch.status = req.body.approved ? 'VERIFIED' : 'REJECTED'; patch.verifiedAt = new Date().toISOString(); patch.verifiedBy = req.user.username; await store.updateVendor(v.id, patch, companyDB); console.log(`[VENDOR] ${v.id} (${v.cardName}) → ${patch.status} by ${req.user.username}`); res.json({ success: true, message: `Vendor ${patch.status.toLowerCase()}` }); } catch (err) { console.error('[VENDOR] verify error:', err.message); res.status(500).json({ success: false, message: err.message }); } }); // ── Core approval logic ─────────────────────────────────────────────────────── async function doApproveVendor(v, req, patch, companyDB) { const { createVendor, getNextVendorCardCode } = getSap(); const merged = { ...v, ...patch }; const cardCode = await getNextVendorCardCode(merged.mgrCardCodePrefix || 'VENDA', companyDB); const groupCode = merged.mgrGroupCode && !isNaN(parseInt(merged.mgrGroupCode)) ? parseInt(merged.mgrGroupCode) : null; const payTermsGrpCode = merged.mgrPayTermsCode && !isNaN(parseInt(merged.mgrPayTermsCode)) ? parseInt(merged.mgrPayTermsCode) : null; // FIX: sales person now passed through const salesPersonCode = merged.mgrSalesPersonCode && !isNaN(parseInt(merged.mgrSalesPersonCode)) ? parseInt(merged.mgrSalesPersonCode) : null; console.log(`[VENDOR] APPROVE ${cardCode}: GroupCode=${groupCode} PayTermsGrpCode=${payTermsGrpCode} SalesPersonCode=${salesPersonCode}`); console.log(`[VENDOR] UDFs: U_Main_Group=${merged.mgrMainGroup || '(none)'} U_Chain=${merged.mgrChain || '(none)'}`); // Bank accounts: manager-selected bankCode takes priority const bankAccounts = (merged.bankAccounts || []).map(b => ({ ...b, bankCode: b.bankCode || b.mgrBankCode || null, })); const result = await createVendor({ cardCode, cardName: merged.cardName, currency: mapCurrency(merged.mgrCurrency || merged.currency), phone1: sanitizeMobile(merged.mobile), email: merged.email, creditLimit: parseFloat(merged.mgrCreditLimit) || 0, remarks: merged.remarks, typeOfBusiness: merged.typeOfBusiness, groupCode, payTermsGrpCode, salesPersonCode, // FIX: now passed contactFirst: merged.contactFirst, contactLast: merged.contactLast, contactMobile: sanitizeMobile(merged.mobile), contactEmail: merged.email, contactTitle: merged.contactTitle, billStreet: merged.billStreet, billBlock: merged.billBlock, billCity: merged.billCity, billZip: merged.billZip, billState: merged.billState, billCountry: mapCountryCode(merged.billCountry), // FIX: multiple addresses now passed allBillAddresses: merged.allBillAddresses || [], allShipAddresses: merged.allShipAddresses || [], mgrPurchaseAccount: merged.mgrPurchaseAccount || '2110005', hasMsme: merged.hasMsme, msmeNo: merged.msmeNo || '', msmeType: merged.msmeType || '', msmeBType: merged.msmeBType || '', fssaiNo: merged.fssaiNo || '', gstin: merged.gstin, pan: merged.pan, bankAccounts, attachments: merged.attachments || {}, mgrMainGroup: merged.mgrMainGroup || '', // → U_Main_Group in SAP B1 mgrChain: merged.mgrChain || '', // FIX: now in MANAGER_EDITABLE → U_Chain in SAP B1 }, companyDB); return { cardCode, cardName: merged.cardName, attachmentEntry: result?.attachmentEntry || null, }; } router.patch('/:id/approve', verifyToken, requireApprovalStep('customer_vendor:approve', 'approve'), async (req, res) => { const companyDB = resolveCompany(req.body.company); try { const v = await store.findById(req.params.id, companyDB); if (!v) return res.status(404).json({ success: false, message: 'Not found' }); if (!req.body.approved) { await store.updateVendor(v.id, { status: 'REJECTED', rejectedBy: req.user.username, rejectedAt: new Date().toISOString() }, companyDB); return res.json({ success: true, message: 'Vendor rejected' }); } if (v.status !== 'VERIFIED') return res.status(400).json({ success: false, message: `Must be VERIFIED. Current: ${v.status}` }); const patch = extractPatch(req.body, ALL_EDITABLE); const { cardCode, cardName, attachmentEntry } = await doApproveVendor(v, req, patch, companyDB); await store.updateVendor(v.id, { ...patch, status: 'APPROVED', sapCardCode: cardCode, approvedAt: new Date().toISOString(), approvedBy: req.user.username, sapAttachmentEntry: attachmentEntry, }, companyDB); console.log(`[VENDOR] ✅ ${cardName} → SAP B1 as ${cardCode}`); res.json({ success: true, message: 'Vendor created in SAP B1!', cardCode, cardName }); } catch (err) { console.error('[VENDOR] approve error:', err.message); res.status(500).json({ success: false, message: err.message }); } }); router.patch('/:id/admin-approve', verifyToken, verifyAdmin, async (req, res) => { const companyDB = resolveCompany(req.body.company); try { const v = await store.findById(req.params.id, companyDB); if (!v) return res.status(404).json({ success: false, message: 'Not found' }); if (v.status === 'APPROVED') return res.status(400).json({ success: false, message: 'Already approved' }); if (v.status === 'REJECTED') return res.status(400).json({ success: false, message: 'Cannot approve rejected vendor' }); await store.updateVendor(v.id, { status: 'VERIFIED' }, companyDB); const refreshed = await store.findById(v.id, companyDB); const patch = extractPatch(req.body, ALL_EDITABLE); const { cardCode, cardName, attachmentEntry } = await doApproveVendor(refreshed, req, patch, companyDB); await store.updateVendor(v.id, { ...patch, status: 'APPROVED', sapCardCode: cardCode, approvedAt: new Date().toISOString(), approvedBy: req.user.username, sapAttachmentEntry: attachmentEntry, }, companyDB); console.log(`[VENDOR] ⚡ Admin pushed ${cardName} → SAP B1 as ${cardCode}`); res.json({ success: true, message: 'Vendor pushed to SAP B1!', cardCode, cardName }); } catch (err) { console.error('[VENDOR] admin-approve error:', err.message); res.status(500).json({ success: false, message: err.message }); } }); router.patch('/:id/draft', verifyToken, async (req, res) => { const companyDB = resolveCompany(req.body.company); try { const v = await store.findById(req.params.id, companyDB); if (!v) return res.status(404).json({ success: false, message: 'Not found' }); if (v.status === 'APPROVED' || v.status === 'REJECTED') return res.status(400).json({ success: false, message: 'Cannot edit ' + v.status + ' records' }); await store.updateVendor(v.id, extractPatch(req.body, ALL_EDITABLE), companyDB); res.json({ success: true, message: 'Draft saved' }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); module.exports = router;