first commit
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s

This commit is contained in:
John
2026-09-23 17:31:02 +05:30
commit 69b4e68baf
51657 changed files with 3864077 additions and 0 deletions
+400
View File
@@ -0,0 +1,400 @@
// 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;