// backend/services/sapServiceLayer.js — FIXED FILE // Fixes applied: // 1. IFSC → BICSwiftCode in BPBankAccounts (OCRB) // 2. PAN → TaxId0 in BPFiscalTaxIDCollection (CRD7) // 3. Attachment FileName now includes extension to fix "File cannot be displayed" [40003-12] // 4. MSME: U_MSME_Type values → 'Micro','Small','Medium','Large' // 5. MSME: U_MSME_BType field wired // 6. U_Fssai field support added 'use strict'; const axios = require('axios'); const https = require('https'); const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const { DEFAULT_COMPANY } = require('./companyConfig'); const { AsyncLocalStorage } = require('async_hooks'); const httpsAgent = new https.Agent({ rejectUnauthorized: false }); const SAP_BASE = process.env.SAP_B1_SERVER; const DEFAULT_DB = DEFAULT_COMPANY; // ← read ONCE at startup const _sessions = {}; // companyDB → cookie (shared .env account) // ── Per-request SAP identity (AsyncLocalStorage) ───────────────────── // Every authenticated HTTP request runs inside a context (set by the // server's attachSapUser middleware) that names WHICH SAP user its SAP calls // should act as. sapRequest() reads this so documents are attributed to the // real person, not one shared login — without every route having to pass // credentials through explicitly. // ctx = { sapUser, sapPassword } → act as this SAP user // ctx = { blocked: true } → authenticated user has NO SAP login set // ctx = null/undefined → no user context (background/unauth) → shared account const _sapUserALS = new AsyncLocalStorage(); const _userSessions = {}; // `${db}::${sapUser}` → cookie (per-SAP-user; shared IDs share one) function runWithSapUser(ctx, fn) { return _sapUserALS.run(ctx || null, fn); } function currentSapCtx() { return _sapUserALS.getStore() || null; } // ════════════════════════════════════════════════════════════════ // SMB / UNC SHARE HELPERS // ════════════════════════════════════════════════════════════════ let _shareAuthenticated = false; let _lastMountedShare = ''; function sanitizeFolderName(name) { return (name || 'VENDOR') .replace(/[\\/:*?"<>|.]/g, '') .replace(/\s+/g, '_') .toUpperCase() .trim() .substring(0, 60) || 'VENDOR'; } function runCmd(cmd) { try { const stdout = execSync(cmd, { stdio: 'pipe', timeout: 15000 }).toString().trim(); return { ok: true, stdout, stderr: '' }; } catch (e) { return { ok: false, stdout: '', stderr: (e.stderr?.toString() || e.message || '').trim(), }; } } function mountShare(shareRoot) { if (_shareAuthenticated && _lastMountedShare === shareRoot) { console.log('[ATTACH] Share already authenticated — skipping net use'); return true; } const smbUser = (process.env.SMB_USER || '').trim(); const smbPass = (process.env.SMB_PASSWORD || '').trim(); const smbDomain = (process.env.SMB_DOMAIN || '').trim(); const testA = runCmd(`dir "${shareRoot}" /b`); if (testA.ok) { console.log('[ATTACH] ✅ Strategy A: share accessible without net use'); _shareAuthenticated = true; _lastMountedShare = shareRoot; return true; } runCmd(`net use "${shareRoot}" /delete /y`); if (smbUser && smbPass) { const userArg = smbDomain ? `${smbDomain}\\${smbUser}` : smbUser; const cmdB = `net use "${shareRoot}" /user:${userArg} "${smbPass}" /persistent:no`; console.log(`[ATTACH] Strategy B: net use as ${userArg}`); const b = runCmd(cmdB); if (b.ok) { console.log('[ATTACH] ✅ Strategy B succeeded'); _shareAuthenticated = true; _lastMountedShare = shareRoot; return true; } console.warn('[ATTACH] Strategy B failed:', b.stderr); runCmd(`net use "${shareRoot}" /delete /y`); const cmdC = `net use "${shareRoot}" /user:WORKGROUP\\${smbUser} "${smbPass}" /persistent:no`; console.log(`[ATTACH] Strategy C: net use as WORKGROUP\\${smbUser}`); const c = runCmd(cmdC); if (c.ok) { console.log('[ATTACH] ✅ Strategy C succeeded'); _shareAuthenticated = true; _lastMountedShare = shareRoot; return true; } console.warn('[ATTACH] Strategy C failed:', c.stderr); runCmd(`net use "${shareRoot}" /delete /y`); } else { console.warn('[ATTACH] SMB_USER / SMB_PASSWORD not set in .env — skipping B & C'); } const cmdD = `net use "${shareRoot}" /persistent:no`; console.log('[ATTACH] Strategy D: net use with current Windows session'); const d = runCmd(cmdD); if (d.ok) { console.log('[ATTACH] ✅ Strategy D succeeded (current session)'); _shareAuthenticated = true; _lastMountedShare = shareRoot; return true; } console.error('[ATTACH] ❌ All 4 mount strategies failed. Last error:', d.stderr); return false; } // ════════════════════════════════════════════════════════════════ // SAP B1 SERVICE LAYER — LOGIN // ════════════════════════════════════════════════════════════════ const _loginPromises = {}; // companyDB → promise async function loginFor(companyDB) { const db = companyDB || process.env.SAP_B1_COMPANY; if (_loginPromises[db]) { console.log(`[SAP] Login in progress for ${db} — waiting...`); return _loginPromises[db]; } _loginPromises[db] = _doLoginFor(db).finally(() => { delete _loginPromises[db]; }); return _loginPromises[db]; } // Keep legacy login() as alias for the default company (backward compat) async function login() { return loginFor(process.env.SAP_B1_COMPANY); } async function _doLoginFor(companyDB) { console.log(`\n[SAP] ══ LOGIN ══════════════════════════════`); console.log(`[SAP] Server : ${SAP_BASE}`); console.log(`[SAP] Company : ${companyDB}`); console.log(`[SAP] User : ${process.env.SAP_B1_USER}`); try { const res = await axios.post(`${SAP_BASE}/b1s/v2/Login`, { CompanyDB: companyDB, UserName: process.env.SAP_B1_USER, Password: process.env.SAP_B1_PASSWORD, }, { httpsAgent, timeout: 30000 }); const cookies = res.headers['set-cookie']; if (!cookies?.length) throw new Error('SAP B1 returned no session cookie'); _sessions[companyDB] = cookies.map(c => c.split(';')[0]).join('; '); console.log(`[SAP] ✅ Login successful for ${companyDB}\n`); return true; } catch (err) { delete _sessions[companyDB]; const detail = err.response?.data?.error?.message?.value || err.response?.data || err.message; console.error(`[SAP] ❌ Login FAILED for ${companyDB}:`, detail); throw new Error('SAP B1 Login failed: ' + JSON.stringify(detail)); } } // Log in as a specific SAP user (not the shared .env account) and cache the // cookie by `${db}::${sapUser}`, so two portal users sharing one SAP login // reuse one session. Throws with a clear message on bad credentials. async function loginUserFor(db, sapUser, sapPassword) { const key = `${db}::${sapUser}`; try { const res = await axios.post(`${SAP_BASE}/b1s/v2/Login`, { CompanyDB: db, UserName: sapUser, Password: sapPassword, }, { httpsAgent, timeout: 30000 }); const cookies = res.headers['set-cookie']; if (!cookies?.length) throw new Error('SAP returned no session cookie'); _userSessions[key] = cookies.map(c => c.split(';')[0]).join('; '); return _userSessions[key]; } catch (err) { delete _userSessions[key]; const detail = err.response?.data?.error?.message?.value || err.response?.data || err.message; throw new Error('SAP login failed for user "' + sapUser + '": ' + (typeof detail === 'string' ? detail : JSON.stringify(detail))); } } // Validate a SAP login (used when a user saves their credentials). Returns // true on success, throws a friendly error otherwise. Logs out immediately. async function testSapLogin(companyDB, sapUser, sapPassword) { const db = companyDB || process.env.SAP_B1_COMPANY; let cookie; try { const res = await axios.post(`${SAP_BASE}/b1s/v2/Login`, { CompanyDB: db, UserName: sapUser, Password: sapPassword, }, { httpsAgent, timeout: 30000 }); const cookies = res.headers['set-cookie']; if (!cookies?.length) throw new Error('SAP returned no session cookie'); cookie = cookies.map(c => c.split(';')[0]).join('; '); return true; } catch (err) { const detail = err.response?.data?.error?.message?.value || err.response?.data || err.message; throw new Error(typeof detail === 'string' ? detail : JSON.stringify(detail)); } finally { if (cookie) axios.post(`${SAP_BASE}/b1s/v2/Logout`, null, { httpsAgent, timeout: 10000, headers: { Cookie: cookie } }).catch(() => {}); } } // ════════════════════════════════════════════════════════════════ // SAP REQUEST // ════════════════════════════════════════════════════════════════ async function sapRequest(method, endpoint, data = null, companyDB = null, retry = true, extraHeaders = null, timeoutMs = 60000) { const db = companyDB || process.env.SAP_B1_COMPANY; const ctx = currentSapCtx(); const isWrite = String(method).toUpperCase() !== 'GET'; // Decide which SAP identity to use for this call. let cookie, userKey = null; if (ctx && ctx.sapUser && ctx.sapPassword) { // Authenticated user with their own SAP login → act as them. userKey = `${db}::${ctx.sapUser}`; if (!_userSessions[userKey]) await loginUserFor(db, ctx.sapUser, ctx.sapPassword); cookie = _userSessions[userKey]; } else if (ctx && ctx.blocked && isWrite) { // Authenticated user WITHOUT a SAP login trying to change SAP data — block // (read-only lookups fall through to the shared account below). throw new Error('[SAP] You have not set your SAP User ID & Password yet. Open "My SAP Account" and add them before performing SAP actions.'); } else { // No user identity (background/read-only) → shared .env account. if (!_sessions[db]) await loginFor(db); cookie = _sessions[db]; } const url = `${SAP_BASE}/b1s/v2/${endpoint}`; console.log(`[SAP:${db}${userKey ? ' as ' + ctx.sapUser : ''}] ${method.toUpperCase()} ${url}`); try { const cfg = { method, url, httpsAgent, timeout: timeoutMs, headers: { Cookie: cookie, 'Content-Type': 'application/json', 'B1S-CaseInsensitive': 'true', ...(extraHeaders || {}), }, }; if (data) cfg.data = data; const res = await axios(cfg); return res.data; } catch (err) { const status = err.response?.status; const body = err.response?.data || {}; const isSaml = JSON.stringify(body).includes('SAML') || body?.error?.code === '299' || status === 401 || status === 302; if (retry && isSaml) { console.log(`[SAP] Session expired for ${db}${userKey ? ' (user ' + ctx.sapUser + ')' : ''} — re-logging in...`); if (userKey) { delete _userSessions[userKey]; try { await loginUserFor(db, ctx.sapUser, ctx.sapPassword); } catch (le) { throw new Error('SAP re-login failed: ' + le.message); } } else { delete _sessions[db]; try { await loginFor(db); } catch (le) { throw new Error('SAP re-login failed: ' + le.message); } } return sapRequest(method, endpoint, data, db, false, extraHeaders, timeoutMs); } const sapErr = err.response?.data?.error; const sapMsg = sapErr?.message?.value || sapErr?.message || err.message; const sapCode = sapErr?.code || status; console.error(`[SAP] ❌ [${sapCode}]: ${sapMsg}`); if (data) console.error('[SAP] Payload:', JSON.stringify(data, null, 2)); throw new Error(`[SAP ${sapCode}] ${sapMsg}`); } } // ════════════════════════════════════════════════════════════════ // CARD CODE HELPERS // ════════════════════════════════════════════════════════════════ async function getNextCardCode(prefix = 'CUSTA', companyDB = null) { try { const encoded = encodeURIComponent( `startswith(CardCode,'${prefix}') and CardType eq 'cCustomer'` ); const r = await sapRequest('GET', `BusinessPartners?$filter=${encoded}&$select=CardCode&$orderby=CardCode desc&$top=1`, null, companyDB ); if (r?.value?.length) { const last = r.value[0].CardCode; const numStr = last.slice(prefix.length).replace(/\D/g, ''); const next = `${prefix}${String((parseInt(numStr) || 0) + 1).padStart(numStr.length || 6, '0')}`; console.log(`[SAP] CardCode: ${last} → ${next}`); return next; } return `${prefix}000001`; } catch (e) { const fb = `${prefix}${Date.now().toString().slice(-6)}`; console.log(`[SAP] getNextCardCode fallback: ${fb}`); return fb; } } async function getNextVendorCardCode(prefix = 'VENDA', companyDB = null) { try { const safePrefix = (prefix || 'VENDA').replace(/'/g, "''"); const encoded = encodeURIComponent( `startswith(CardCode,'${safePrefix}') and CardType eq 'cSupplier'` ); const r = await sapRequest('GET', `BusinessPartners?$filter=${encoded}&$select=CardCode&$orderby=CardCode desc&$top=1`, null, companyDB ); if (r?.value?.length) { const last = r.value[0].CardCode; const numStr = last.slice(safePrefix.length).replace(/\D/g, ''); const next = `${safePrefix}${String((parseInt(numStr) || 0) + 1).padStart(numStr.length || 6, '0')}`; console.log(`[SAP] VendorCardCode: ${last} → ${next}`); return next; } return `${safePrefix}000001`; } catch (e) { const fb = `${safePrefix}${Date.now().toString().slice(-6)}`; console.log(`[SAP] getNextVendorCardCode fallback: ${fb}`); return fb; } } // ════════════════════════════════════════════════════════════════ // BANK CODES // ════════════════════════════════════════════════════════════════ async function getBankCodes(countryCode = 'IN', companyDB = null) { try { console.log(`[SAP] Fetching bank codes for country: ${countryCode}`); const encoded = encodeURIComponent(`CountryCode eq '${countryCode}'`); let url = `Banks?$filter=${encoded}&$select=BankCode,BankName,SwiftNo,CountryCode&$orderby=BankName`; let allBanks = []; while (url) { const r = await sapRequest('GET', url, null, companyDB); const page = (r?.value || []).map(b => ({ BankCode: b.BankCode, BankName: b.BankName, SwiftNo: b.SwiftNo || '', CountryCode: b.CountryCode, })); allBanks = allBanks.concat(page); const nextLink = r?.['@odata.nextLink']; url = nextLink ? nextLink.replace(/^.*\/b1s\/v2\//, '') : null; } console.log(`[SAP] ✅ getBankCodes: ${allBanks.length} banks for ${countryCode}`); return allBanks; } catch (err) { console.error(`[SAP] ❌ getBankCodes failed: ${err.message}`); throw err; } } // ════════════════════════════════════════════════════════════════ // ADDRESS / MAP HELPERS // ════════════════════════════════════════════════════════════════ function mapCountry(name) { const m = { 'India':'IN','United States':'US','United Kingdom':'GB', 'UAE':'AE','Singapore':'SG','Germany':'DE','Japan':'JP','Australia':'AU', }; return m[name] || (name && name.length <= 3 ? name.toUpperCase() : 'IN'); } function mapStateCode(state) { if (!state) return ''; const SAP_CODES = new Set([ 'AN','AP','AR','AS','BH','CH','CT','DD','DL','DN','GA','GJ','HP', 'HR','JH','JK','KA','KL','LA','LD','MH','MN','MP','MZ','NL','OR', 'PB','PY','RJ','SK','TG','TN','TR','UP','UT','WB', ]); const upper = state.trim().toUpperCase(); if (upper.length <= 3 && SAP_CODES.has(upper)) return upper; const MAP = { 'Andaman and Nicobar Islands':'AN','Andaman & Nicobar Islands':'AN', 'Andhra Pradesh':'AP','Arunachal Pradesh':'AR','Assam':'AS','Bihar':'BH', 'Chandigarh':'CH','Chhattisgarh':'CT','Dadra & Nagar Haveli':'DN', 'Daman & Diu':'DD','Delhi':'DL','Goa':'GA','Gujarat':'GJ','Haryana':'HR', 'Himachal Pradesh':'HP','Jammu & Kashmir':'JK','Jammu and Kashmir':'JK', 'Jharkhand':'JH','Karnataka':'KA','Kerala':'KL','Ladakh':'LA', 'Lakshadweep':'LD','Madhya Pradesh':'MP','Maharashtra':'MH','Manipur':'MN', 'Meghalaya':'ME','Mizoram':'MZ','Nagaland':'NL','Odisha':'OR','Orissa':'OR', 'Puducherry':'PY','Pondicherry':'PY','Punjab':'PB','Rajasthan':'RJ', 'Sikkim':'SK','Tamil Nadu':'TN','Telangana':'TG','Tripura':'TR', 'Uttar Pradesh':'UP','Uttarakhand':'UT','Uttaranchal':'UT','West Bengal':'WB', }; const code = MAP[state] || MAP[state.trim()]; if (code) return code; console.warn(`[SAP] Unknown state "${state}"`); return ''; } function buildAddrObj(a, type) { const addrName = (a.addrName || a.addressName || '').substring(0, 50); const obj = { AddressName: addrName, AddressType: type, Street: (a.street || '').substring(0, 100), Block: (a.block || '').substring(0, 100), City: (a.city || '').substring(0, 100), ZipCode: (a.zip || '').substring(0, 20), State: mapStateCode(a.state || ''), Country: mapCountry(a.country || 'India'), }; const gstin = (a.gstin || '').trim().toUpperCase(); if (gstin && /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/.test(gstin)) { obj.GSTIN = gstin; obj.GstType = 'gstRegularTDSISD'; } console.log(`[SAP] ${type} "${addrName}" State:${obj.State} Country:${obj.Country}`); return obj; } function sanitizeMobileForSAP(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); } // ════════════════════════════════════════════════════════════════ // ATTACHMENT UPLOAD // FIX: FileName must include extension so SAP can open the file. // SAP error [40003-12] "File cannot be displayed" happens when // FileName has no extension — Windows has no program associated. // ════════════════════════════════════════════════════════════════ async function uploadAttachmentsToSAP(attachments, cardName, companyDB = null) { if (!attachments || typeof attachments !== 'object') return null; const nodePath = process.env.SAP_ATTACHMENT_PATH; const svrPath = process.env.SAP_ATTACHMENT_SERVER_PATH || nodePath; if (!nodePath) { console.log('[ATTACH] SAP_ATTACHMENT_PATH not set — skipping attachment upload'); return null; } const isUNC = nodePath.startsWith('\\\\') || nodePath.startsWith('//'); if (isUNC) { const parts = nodePath.replace(/\\/g, '/').split('/').filter(Boolean); const shareRoot = `\\\\${parts[0]}\\${parts[1]}`; const mounted = mountShare(shareRoot); if (!mounted) { console.error('[ATTACH] ❌ Cannot reach share — vendor will still be created in SAP B1'); return null; } } const folderName = sanitizeFolderName(cardName); const vendorFolder = `${nodePath}\\${folderName}`; const vendorSvrDir = `${svrPath}\\${folderName}`; try { if (!fs.existsSync(vendorFolder)) { fs.mkdirSync(vendorFolder, { recursive: true }); console.log(`[ATTACH] ✅ Created vendor folder: ${vendorFolder}`); } } catch (mkErr) { console.error(`[ATTACH] ❌ mkdir failed "${vendorFolder}": ${mkErr.message}`); _shareAuthenticated = false; return null; } const filesToUpload = []; ['pan', 'cheque', 'gst', 'msme', 'other'].forEach(key => { const val = attachments[key]; if (!val) return; const arr = Array.isArray(val) ? val : [val]; arr.forEach(a => { if (a?.data) filesToUpload.push({ key, ...a }); }); }); if (!filesToUpload.length) { console.log('[ATTACH] No attachment data — nothing to upload'); return null; } console.log(`\n[ATTACH] Uploading ${filesToUpload.length} file(s) → ${vendorFolder}`); const attachmentLines = []; try { for (const file of filesToUpload) { const base64 = file.data.includes(',') ? file.data.split(',')[1] : file.data; const buffer = Buffer.from(base64, 'base64'); // ── FIX [40003-12]: derive extension from original filename or mime type let ext = ''; if (file.name && file.name.includes('.')) { ext = file.name.split('.').pop().toLowerCase().replace(/[^a-z0-9]/g, ''); } else if (file.type) { const mimeMap = { 'application/pdf': 'pdf', 'image/jpeg': 'jpg', 'image/jpg': 'jpg', 'image/png': 'png', 'image/gif': 'gif', 'image/tiff': 'tif', 'application/msword': 'doc', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx', }; ext = mimeMap[file.type] || 'pdf'; } else { ext = 'pdf'; // safe default } const stem = `${folderName}_${file.key.toUpperCase()}_${Date.now()}`; const fileName = `${stem}.${ext}`; // ← MUST include extension const dest = path.join(vendorFolder, fileName); fs.writeFileSync(dest, buffer); console.log(`[ATTACH] ✅ ${(buffer.length / 1024).toFixed(1)} KB → ${dest}`); // ── FIX: SAP Attachments2_Lines // FileName = stem WITHOUT extension (SAP stores stem + FileExtension separately) // FileExtension = extension WITHOUT dot // SourcePath = folder path (no trailing slash) attachmentLines.push({ FileName: stem, // no extension here — SAP adds it from FileExtension FileExtension: ext, // no dot, e.g. "pdf", "jpg" SourcePath: vendorSvrDir, // folder only, no filename UserID: '1', Override: 'tYES', }); } const result = await sapRequest('POST', 'Attachments2', { Attachments2_Lines: attachmentLines }, companyDB); const absEntry = result?.AbsoluteEntry; if (absEntry) { console.log(`[ATTACH] ✅ Attachments2 AbsoluteEntry=${absEntry} folder: ${folderName}`); } else { console.warn('[ATTACH] ⚠ No AbsoluteEntry returned from SAP Attachments2'); } return absEntry || null; } catch (err) { console.error(`[ATTACH] ❌ Upload error: ${err.message}`); return null; } } // ════════════════════════════════════════════════════════════════ // CREATE CUSTOMER // ════════════════════════════════════════════════════════════════ async function createCustomer(d, companyDB = null) { console.log(`\n[SAP] ══ CREATE CUSTOMER ════════════════════`); console.log(`[SAP] CardCode : ${d.cardCode}`); console.log(`[SAP] CardName : ${d.cardName}`); const hasAttachData = d.attachments && Object.values(d.attachments).some(v => (Array.isArray(v) ? v : [v]).some(f => f?.data)); let attachmentEntry = null; if (hasAttachData) attachmentEntry = await uploadAttachmentsToSAP(d.attachments, d.cardName, companyDB); const payload = { CardCode: d.cardCode, CardName: d.cardName, CardType: 'cCustomer' }; if (d.currency) payload.Currency = d.currency; if (d.phone1) payload.Phone1 = d.phone1; if (d.mobile) payload.Cellular = d.mobile; if (d.email) payload.EmailAddress = d.email; if (d.website) payload.Website = d.website; if (d.creditLimit > 0) payload.CreditLimit = parseFloat(d.creditLimit); if (d.remarks) payload.Notes = d.remarks; if (d.groupCode != null && !isNaN(d.groupCode)) payload.GroupCode = parseInt(d.groupCode); if (d.payTermsGrpCode != null && !isNaN(d.payTermsGrpCode) && d.payTermsGrpCode >= 0) payload.PayTermsGrpCode = parseInt(d.payTermsGrpCode); if (d.salesPersonCode != null && !isNaN(d.salesPersonCode) && d.salesPersonCode > 0) payload.SalesPersonCode = parseInt(d.salesPersonCode); if (attachmentEntry) payload.AttachmentEntry = parseInt(attachmentEntry); const contactName = `${d.contactFirst || ''} ${d.contactLast || ''}`.trim(); if (contactName) { payload.ContactEmployees = [{ Name: contactName, FirstName: d.contactFirst || '', LastName: d.contactLast || '', MobilePhone: d.contactMobile || '', E_Mail: d.contactEmail || d.email || '', Active: 'tYES', }]; } payload.BPAddresses = []; if (Array.isArray(d.allBillAddresses) && d.allBillAddresses.length) { d.allBillAddresses.forEach(a => payload.BPAddresses.push(buildAddrObj(a, 'bo_BillTo'))); } else { payload.BPAddresses.push(buildAddrObj({ addrName: d.billAddressName, street: d.billStreet, block: d.billBlock, city: d.billCity, zip: d.billZip, state: d.billState, country: d.billCountry, gstin: d.gstin, }, 'bo_BillTo')); } if (Array.isArray(d.allShipAddresses) && d.allShipAddresses.length) { d.allShipAddresses.forEach(a => payload.BPAddresses.push(buildAddrObj(a, 'bo_ShipTo'))); } else { payload.BPAddresses.push(buildAddrObj({ addrName: d.shipAddressName || d.billAddressName, street: d.shipStreet || d.billStreet, block: d.shipBlock || d.billBlock, city: d.shipCity || d.billCity, zip: d.shipZip || d.billZip, state: d.shipState || d.billState, country: d.shipCountry || d.billCountry, gstin: '', }, 'bo_ShipTo')); } if (d.mgrMainGroup) payload.U_Main_Group = d.mgrMainGroup; if (d.mgrChain) payload.U_Chain = d.mgrChain; // ── MSME UDFs ───────────────────────────────────────────────── if (d.hasMsme && d.msmeNo) { payload.U_MSME = d.msmeNo; // U_MSME_Type: exact SAP UDF values — Micro, Small, Medium, Large payload.U_MSME_Type = d.msmeType || ''; payload.U_MSME_BType = d.msmeBType || ''; } // FSSAI if (d.fssaiNo && d.fssaiNo.trim()) { payload.U_Fssai = d.fssaiNo.trim().toUpperCase(); } if (d.mgrArAccount) payload.DebitorAccount = d.mgrArAccount; // ── PAN → BPFiscalTaxIDCollection (CRD7.TaxId0) ────────────── const panUpper = (d.pan || '').toUpperCase().trim(); if (panUpper && /^[A-Z]{5}[0-9]{4}[A-Z]$/.test(panUpper)) { const billAddrName = ( (Array.isArray(d.allBillAddresses) && d.allBillAddresses[0]?.addrName) || d.billAddressName || d.cardName || '' ).substring(0, 50); payload.BPFiscalTaxIDCollection = [{ Address: billAddrName, AddrType: 'bo_BillTo', TaxId0: panUpper, // ← PAN goes in TaxId0 (maps to CRD7.TaxId0) }]; console.log(`[SAP] PAN (TaxId0): ${panUpper} Address: ${billAddrName}`); } console.log('[SAP] Customer Payload:\n' + JSON.stringify(payload, null, 2)); try { const result = await sapRequest('POST', 'BusinessPartners', payload, companyDB); console.log(`\n[SAP] ✅ Customer created: ${result?.CardCode || d.cardCode}`); return { ...result, attachmentEntry }; } catch (err) { console.error(`[SAP] ❌ createCustomer failed: ${err.message}`); throw err; } } // ════════════════════════════════════════════════════════════════ // CREATE VENDOR // ════════════════════════════════════════════════════════════════ async function createVendor(d, companyDB = null) { console.log(`\n[SAP] ══ CREATE VENDOR (SUPPLIER) ══════════`); console.log(`[SAP] CardCode : ${d.cardCode}`); console.log(`[SAP] CardName : ${d.cardName}`); let attachmentEntry = null; const hasAttachData = d.attachments && Object.values(d.attachments).some(v => (Array.isArray(v) ? v : [v]).some(f => f?.data)); if (hasAttachData) { try { attachmentEntry = await uploadAttachmentsToSAP(d.attachments, d.cardName, companyDB); } catch (attErr) { console.warn('[SAP] ⚠ Attachment upload skipped (non-fatal):', attErr.message); } } const payload = { CardCode: d.cardCode, CardName: d.cardName, CardType: 'cSupplier', }; if (d.currency) payload.Currency = d.currency; if (d.phone1) payload.Phone1 = d.phone1; if (d.email) payload.EmailAddress = d.email; if (d.creditLimit > 0) payload.CreditLimit = parseFloat(d.creditLimit); if (d.remarks) payload.Notes = d.remarks; if (d.groupCode != null && !isNaN(d.groupCode)) payload.GroupCode = parseInt(d.groupCode); if (d.payTermsGrpCode != null && !isNaN(d.payTermsGrpCode) && d.payTermsGrpCode >= 0) payload.PayTermsGrpCode = parseInt(d.payTermsGrpCode); if (d.salesPersonCode != null && !isNaN(d.salesPersonCode) && d.salesPersonCode > 0) payload.SalesPersonCode = parseInt(d.salesPersonCode); if (attachmentEntry) payload.AttachmentEntry = parseInt(attachmentEntry); // AP Account payload.DebitorAccount = (d.mgrPurchaseAccount && d.mgrPurchaseAccount.trim()) ? d.mgrPurchaseAccount.trim() : '2110005'; console.log(`[SAP] DebitorAccount (AP): ${payload.DebitorAccount}`); // Contact person const contactName = `${d.contactFirst || ''} ${d.contactLast || ''}`.trim(); if (contactName) { payload.ContactEmployees = [{ Name: contactName, FirstName: d.contactFirst || '', LastName: d.contactLast || '', MobilePhone: sanitizeMobileForSAP(d.contactMobile || d.mobile || d.phone1 || ''), E_Mail: d.contactEmail || d.email || '', Active: 'tYES', }]; } // Addresses payload.BPAddresses = []; if (Array.isArray(d.allBillAddresses) && d.allBillAddresses.length) { d.allBillAddresses.forEach(a => payload.BPAddresses.push(buildAddrObj(a, 'bo_BillTo'))); } else { const billAddrName = `${d.cardName.substring(0, 25)}-${mapStateCode(d.billState)}`; payload.BPAddresses.push(buildAddrObj({ addrName: billAddrName, street: d.billStreet, block: d.billBlock, city: d.billCity, zip: d.billZip, state: d.billState, country: d.billCountry || 'India', gstin: d.gstin, }, 'bo_BillTo')); } if (Array.isArray(d.allShipAddresses) && d.allShipAddresses.length) { d.allShipAddresses.forEach(a => payload.BPAddresses.push(buildAddrObj(a, 'bo_ShipTo'))); } else { const shipAddrName = `${d.cardName.substring(0, 25)}-${mapStateCode(d.billState)}`; payload.BPAddresses.push(buildAddrObj({ addrName: shipAddrName, street: d.billStreet, block: d.billBlock, city: d.billCity, zip: d.billZip, state: d.billState, country: d.billCountry || 'India', gstin: d.gstin, }, 'bo_ShipTo')); } // ── MSME UDFs ───────────────────────────────────────────────── // U_MSME_Type valid values in SAP: 'Micro', 'Small', 'Medium', 'Large' if (d.hasMsme && d.msmeNo) { payload.U_MSME = d.msmeNo.trim(); payload.U_MSME_Type = d.msmeType || ''; // 'Micro'|'Small'|'Medium'|'Large' payload.U_MSME_BType = d.msmeBType || ''; // e.g. 'Manufacturing'|'Service' console.log(`[SAP] MSME: ${payload.U_MSME} Type: ${payload.U_MSME_Type} BType: ${payload.U_MSME_BType}`); } // ── FSSAI ───────────────────────────────────────────────────── if (d.fssaiNo && d.fssaiNo.trim()) { payload.U_Fssai = d.fssaiNo.trim().toUpperCase(); console.log(`[SAP] FSSAI: ${payload.U_Fssai}`); } // UDF fields if (d.mgrMainGroup && d.mgrMainGroup.trim()) { payload.U_Main_Group = d.mgrMainGroup.trim(); console.log(`[SAP] U_Main_Group: ${payload.U_Main_Group}`); } if (d.mgrChain && d.mgrChain.trim()) { payload.U_Chain = d.mgrChain.trim(); console.log(`[SAP] U_Chain: ${payload.U_Chain}`); } // ── PAN → BPFiscalTaxIDCollection (CRD7.TaxId0) ────────────── // TaxId0 is the PAN field in SAP B1 India localisation. // Address must match an existing BPAddress AddressName exactly. const panUpper = (d.pan || '').toUpperCase().trim(); const billAddrNameForPan = ( (Array.isArray(d.allBillAddresses) && d.allBillAddresses[0]?.addrName) || `${d.cardName.substring(0, 25)}-${mapStateCode(d.billState)}` ).substring(0, 50); if (panUpper && /^[A-Z]{5}[0-9]{4}[A-Z]$/.test(panUpper)) { payload.BPFiscalTaxIDCollection = [{ Address: billAddrNameForPan, // must match BPAddresses[0].AddressName AddrType: 'bo_BillTo', TaxId0: panUpper, // ← this is PAN in CRD7 }]; console.log(`[SAP] PAN (TaxId0): ${panUpper} AddrName: ${billAddrNameForPan}`); } // ── Bank accounts ────────────────────────────────────────────── // FIX: IFSC goes in BICSwiftCode (maps to OCRB.BICSwiftCode in SAP B1) // NOT in UserNo1. The field was previously wrong. if (Array.isArray(d.bankAccounts) && d.bankAccounts.length > 0) { const validBanks = d.bankAccounts.filter(b => b.accNo && b.accNo.trim()); if (validBanks.length > 0) { const bankAccountsForSAP = []; for (const b of validBanks) { const bankCode = b.bankCode || b.mgrBankCode || null; if (!bankCode) { console.warn(`[SAP] ⚠ Skipping bank account ${b.accNo} — BankCode not resolved`); continue; } const ifscCode = (b.ifsc || '').trim().toUpperCase(); console.log(`[SAP] Bank: ${bankCode} | A/C: ${b.accNo} | IFSC→BICSwiftCode: ${ifscCode} | Type: ${b.accountType || 'Current'}`); bankAccountsForSAP.push({ BankCode: bankCode, AccountNo: b.accNo.trim(), Branch: (b.branch || '').trim().substring(0, 50), AccountName: (b.bankName || d.cardName || '').trim().substring(0, 100), // ── FIX: IFSC → BICSwiftCode (OCRB.BICSwiftCode) ────────────── BICSwiftCode: ifscCode.substring(0, 50), // ← CORRECTED field name // UserNo1 / UserNo2 can hold additional reference info UserNo1: ifscCode.substring(0, 50), // keep for backward compat UserNo2: (b.accountType || 'Current').substring(0, 50), IBAN: (b.swiftCode || '').trim().substring(0, 34), }); } if (bankAccountsForSAP.length > 0) { payload.BPBankAccounts = bankAccountsForSAP; console.log(`[SAP] BPBankAccounts: ${bankAccountsForSAP.length} account(s) with BICSwiftCode (IFSC)`); } else { console.warn('[SAP] ⚠ No bank accounts added — BankCode unresolved for all'); } } } console.log('[SAP] Vendor Payload:\n' + JSON.stringify(payload, null, 2)); try { const result = await sapRequest('POST', 'BusinessPartners', payload, companyDB); console.log(`\n[SAP] ✅ Vendor created: ${result?.CardCode || d.cardCode}`); return { ...result, attachmentEntry }; } catch (err) { console.error(`[SAP] ❌ createVendor failed: ${err.message}`); throw err; } } // ════════════════════════════════════════════════════════════════ // SAP REQUEST AS SPECIFIC USER — one-off login, no session cache // ════════════════════════════════════════════════════════════════ async function sapRequestAs(userName, password, companyDB, method, endpoint, data = null) { const db = companyDB || process.env.SAP_B1_COMPANY; console.log(`[SAP:${db}] ── one-off login as ${userName} for ${method.toUpperCase()} ${endpoint}`); let cookie; try { const lr = await axios.post(`${SAP_BASE}/b1s/v2/Login`, { CompanyDB: db, UserName: userName, Password: password, }, { httpsAgent, timeout: 30000 }); const cookies = lr.headers['set-cookie']; if (!cookies?.length) throw new Error('no session cookie returned'); cookie = cookies.map(c => c.split(';')[0]).join('; '); } catch (err) { const detail = err.response?.data?.error?.message?.value || err.response?.data || err.message; throw new Error('SAP login as ' + userName + ' failed: ' + (typeof detail === 'string' ? detail : JSON.stringify(detail))); } try { const cfg = { method, url: `${SAP_BASE}/b1s/v2/${endpoint}`, httpsAgent, timeout: 60000, headers: { Cookie: cookie, 'Content-Type': 'application/json', 'B1S-CaseInsensitive': 'true' }, }; if (data) cfg.data = data; const res = await axios(cfg); return res.data; } catch (err) { const sapErr = err.response?.data?.error; const sapMsg = sapErr?.message?.value || sapErr?.message || err.message; const sapCode = sapErr?.code || err.response?.status; if (data) console.error('[SAP] Payload:', JSON.stringify(data, null, 2)); throw new Error(`[SAP ${sapCode}] ${sapMsg}`); } finally { axios.post(`${SAP_BASE}/b1s/v2/Logout`, null, { httpsAgent, timeout: 10000, headers: { Cookie: cookie }, }).catch(() => {}); } } // ════════════════════════════════════════════════════════════════ // EXPORTS // ════════════════════════════════════════════════════════════════ module.exports = { login, sapRequest, sapRequestAs, runWithSapUser, testSapLogin, getNextCardCode, getNextVendorCardCode, getBankCodes, createCustomer, createVendor, uploadAttachmentsToSAP, sanitizeFolderName, };