904 lines
40 KiB
JavaScript
904 lines
40 KiB
JavaScript
// 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,
|
|
}; |