// backend/services/hanaVendorStore.js // SQL persistence for vendor registrations. // Schema: uses SAP_B1_COMPANY env var (same as hanaStore.js) // Table: ZVENDOR_PORTAL (auto-created on first run) const { getPool } = require('./appSqlPool'); const { DEFAULT_COMPANY } = require('./companyConfig'); const DEFAULT_SCHEMA = DEFAULT_COMPANY; const tbl = (s) => `[dbo].[ZVENDOR_PORTAL]`; async function exec(sqlQuery, params = []) { const pool = await getPool(); const request = pool.request(); params.forEach((param, index) => { request.input(`param${index}`, param); }); const replacedSql = sqlQuery.replace(/\?/g, (match, offset, string) => { const paramIndex = (string.slice(0, offset).match(/\?/g) || []).length; return `@param${paramIndex}`; }); const result = await request.query(replacedSql); return result.recordset || []; } function isAlreadyExists(e) { const m = (e.message || '').toLowerCase(); return m.includes('already exists') || m.includes('duplicate') || m.includes('existing object') || m.includes('there is already an object'); } // ── Bootstrap ───────────────────────────────────────────────────────────────── async function bootstrap(schema) { console.log('[VENDOR STORE] Checking table', tbl(schema), '...'); let tableExisted = false; await exec(` CREATE TABLE ${tbl(schema)} ( [ID] INT IDENTITY(1,1) PRIMARY KEY, [STATUS] NVARCHAR(30) DEFAULT 'PENDING', [VENDOR_TYPE] NVARCHAR(20) DEFAULT 'SUPPLIER', [CARD_NAME] NVARCHAR(100), [FOREIGN_NAME] NVARCHAR(100), [TYPE_OF_BUSINESS] NVARCHAR(50), [INDUSTRY] NVARCHAR(100), [PRODUCTS] NVARCHAR(500), [PAYMENT_TERMS] NVARCHAR(50), [CONTACT_FIRST] NVARCHAR(60), [CONTACT_LAST] NVARCHAR(60), [CONTACT_TITLE] NVARCHAR(60), [MOBILE] NVARCHAR(20), [ALT_CONTACT] NVARCHAR(20), [EMAIL] NVARCHAR(150), [BILL_STREET] NVARCHAR(200), [BILL_BLOCK] NVARCHAR(100), [BILL_CITY] NVARCHAR(100), [BILL_ZIP] NVARCHAR(20), [BILL_STATE] NVARCHAR(100), [BILL_COUNTRY] NVARCHAR(60), [GSTIN] NVARCHAR(15), [PAN] NVARCHAR(10), [TAN] NVARCHAR(10), [CURRENCY] NVARCHAR(50), [HAS_TDS] NVARCHAR(1) DEFAULT 'N', [TDS_CATEGORY] NVARCHAR(100), [TDS_RATE] DECIMAL(5,2) DEFAULT 0, [TDS_LDC_NO] NVARCHAR(50), [HAS_MSME] NVARCHAR(1) DEFAULT 'N', [MSME_NO] NVARCHAR(30), [MSME_TYPE] NVARCHAR(20), [MSME_BTYPE] NVARCHAR(30), [FSSAI_NO] NVARCHAR(20), [REMARKS] NVARCHAR(MAX), [BANK_ACCOUNTS] NVARCHAR(MAX), [ATTACHMENTS] NVARCHAR(MAX), [ALL_BILL_ADDRS] NVARCHAR(MAX), [ALL_SHIP_ADDRS] NVARCHAR(MAX), [USER_ID] NVARCHAR(50), [USER_TYPE] NVARCHAR(50), [USER_DEPT] NVARCHAR(100), [SUBMITTED_AT] DATETIME2 DEFAULT GETDATE(), [MGR_CARD_CODE_PREFIX] NVARCHAR(20) DEFAULT 'VENDA', [MGR_GROUP_CODE] INT, [MGR_GROUP] NVARCHAR(100), [MGR_PAY_TERMS_CODE] INT, [MGR_PAY_TERMS] NVARCHAR(100), [MGR_PURCHASE_ACCOUNT] NVARCHAR(20) DEFAULT '2101001', [MGR_PURCHASE_ACCT_NAME] NVARCHAR(100), [MGR_CURRENCY] NVARCHAR(50) DEFAULT 'Indian Rupee', [MGR_LANGUAGE] NVARCHAR(50) DEFAULT 'English (UK)', [MGR_CREDIT_LIMIT] DECIMAL(18,2) DEFAULT 0, [MGR_NOTES] NVARCHAR(MAX), [MGR_BRANCH] NVARCHAR(100), [MGR_AREA] NVARCHAR(100), [MGR_TERRITORY] NVARCHAR(100), [MGR_SALES_PERSON_CODE] INT, [MGR_SALES_EMPLOYEE] NVARCHAR(100), [MGR_MAIN_GROUP] NVARCHAR(50), [MGR_CHAIN] NVARCHAR(50), [VERIFIED_BY] NVARCHAR(60), [VERIFIED_AT] DATETIME2, [APPROVED_BY] NVARCHAR(60), [APPROVED_AT] DATETIME2, [REJECTED_BY] NVARCHAR(60), [REJECTED_AT] DATETIME2, [SAP_CARD_CODE] NVARCHAR(30), [SAP_ATTACHMENT_ENTRY] INT, [COMPANY] NVARCHAR(50) DEFAULT '${DEFAULT_SCHEMA}' ) `).catch(e => { if (isAlreadyExists(e)) { tableExisted = true; console.log('[VENDOR STORE] Table already exists — checking for missing columns...'); } else { throw e; } }); // ── Safe migrations — only add columns that don't exist yet ────────────── if (tableExisted) { const migrations = [ `IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZVENDOR_PORTAL' AND COLUMN_NAME='MGR_SALES_PERSON_CODE') ALTER TABLE [dbo].[ZVENDOR_PORTAL] ADD [MGR_SALES_PERSON_CODE] INT`, `IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZVENDOR_PORTAL' AND COLUMN_NAME='MGR_SALES_EMPLOYEE') ALTER TABLE [dbo].[ZVENDOR_PORTAL] ADD [MGR_SALES_EMPLOYEE] NVARCHAR(100)`, `IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZVENDOR_PORTAL' AND COLUMN_NAME='MGR_MAIN_GROUP') ALTER TABLE [dbo].[ZVENDOR_PORTAL] ADD [MGR_MAIN_GROUP] NVARCHAR(50)`, `IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZVENDOR_PORTAL' AND COLUMN_NAME='MGR_CHAIN') ALTER TABLE [dbo].[ZVENDOR_PORTAL] ADD [MGR_CHAIN] NVARCHAR(50)`, `IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZVENDOR_PORTAL' AND COLUMN_NAME='COMPANY') ALTER TABLE [dbo].[ZVENDOR_PORTAL] ADD [COMPANY] NVARCHAR(50) DEFAULT '${DEFAULT_SCHEMA}'`, `IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZVENDOR_PORTAL' AND COLUMN_NAME='MSME_BTYPE') ALTER TABLE [dbo].[ZVENDOR_PORTAL] ADD [MSME_BTYPE] NVARCHAR(30)`, `IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZVENDOR_PORTAL' AND COLUMN_NAME='FSSAI_NO') ALTER TABLE [dbo].[ZVENDOR_PORTAL] ADD [FSSAI_NO] NVARCHAR(20)`, `IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZVENDOR_PORTAL' AND COLUMN_NAME='ALL_BILL_ADDRS') ALTER TABLE [dbo].[ZVENDOR_PORTAL] ADD [ALL_BILL_ADDRS] NVARCHAR(MAX)`, `IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZVENDOR_PORTAL' AND COLUMN_NAME='ALL_SHIP_ADDRS') ALTER TABLE [dbo].[ZVENDOR_PORTAL] ADD [ALL_SHIP_ADDRS] NVARCHAR(MAX)`, `IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZVENDOR_PORTAL' AND COLUMN_NAME='VERIFIED_BY') ALTER TABLE [dbo].[ZVENDOR_PORTAL] ADD [VERIFIED_BY] NVARCHAR(60)`, `IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZVENDOR_PORTAL' AND COLUMN_NAME='VERIFIED_AT') ALTER TABLE [dbo].[ZVENDOR_PORTAL] ADD [VERIFIED_AT] DATETIME2`, `IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZVENDOR_PORTAL' AND COLUMN_NAME='APPROVED_BY') ALTER TABLE [dbo].[ZVENDOR_PORTAL] ADD [APPROVED_BY] NVARCHAR(60)`, `IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZVENDOR_PORTAL' AND COLUMN_NAME='APPROVED_AT') ALTER TABLE [dbo].[ZVENDOR_PORTAL] ADD [APPROVED_AT] DATETIME2`, `IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZVENDOR_PORTAL' AND COLUMN_NAME='REJECTED_BY') ALTER TABLE [dbo].[ZVENDOR_PORTAL] ADD [REJECTED_BY] NVARCHAR(60)`, `IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZVENDOR_PORTAL' AND COLUMN_NAME='REJECTED_AT') ALTER TABLE [dbo].[ZVENDOR_PORTAL] ADD [REJECTED_AT] DATETIME2`, `IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZVENDOR_PORTAL' AND COLUMN_NAME='SAP_ATTACHMENT_ENTRY') ALTER TABLE [dbo].[ZVENDOR_PORTAL] ADD [SAP_ATTACHMENT_ENTRY] INT`, ]; for (const migration of migrations) { await exec(migration).catch(e => { console.warn('[VENDOR STORE] Migration warning:', e.message); }); } console.log('[VENDOR STORE] ✅ Column migration check complete'); } console.log('[VENDOR STORE] ✅ Table ready:', tbl(schema)); } // ── Row → JS object ─────────────────────────────────────────────────────────── function rowToVendor(row) { if (!row) return null; let bankAccounts = []; let attachments = {}; try { bankAccounts = JSON.parse(row.BANK_ACCOUNTS || '[]'); } catch (_) {} try { attachments = JSON.parse(row.ATTACHMENTS || '{}'); } catch (_) {} return { id: row.ID, status: row.STATUS, vendorType: row.VENDOR_TYPE || 'SUPPLIER', cardName: row.CARD_NAME || '', foreignName: row.FOREIGN_NAME || '', typeOfBusiness: row.TYPE_OF_BUSINESS || '', industry: row.INDUSTRY || '', products: row.PRODUCTS || '', paymentTerms: row.PAYMENT_TERMS || '30 Days', contactFirst: row.CONTACT_FIRST || '', contactLast: row.CONTACT_LAST || '', contactTitle: row.CONTACT_TITLE || '', mobile: row.MOBILE || '', altContact: row.ALT_CONTACT || '', email: row.EMAIL || '', billStreet: row.BILL_STREET || '', billBlock: row.BILL_BLOCK || '', billCity: row.BILL_CITY || '', billZip: row.BILL_ZIP || '', billState: row.BILL_STATE || '', billCountry: row.BILL_COUNTRY || 'India', gstin: row.GSTIN || '', pan: row.PAN || '', tan: row.TAN || '', currency: row.CURRENCY || 'Indian Rupee', hasTds: row.HAS_TDS === 'Y', tdsCategory: row.TDS_CATEGORY || '', tdsRate: Number(row.TDS_RATE) || 0, tdsLdcNo: row.TDS_LDC_NO || '', hasMsme: row.HAS_MSME === 'Y', msmeNo: row.MSME_NO || '', msmeType: row.MSME_TYPE || '', msmeBType: row.MSME_BTYPE || '', fssaiNo: row.FSSAI_NO || '', remarks: row.REMARKS || '', bankAccounts, attachments, allBillAddresses: safeJson(row.ALL_BILL_ADDRS, []), allShipAddresses: safeJson(row.ALL_SHIP_ADDRS, []), userId: row.USER_ID || '', userType: row.USER_TYPE || '', userDept: row.USER_DEPT || '', submittedAt: row.SUBMITTED_AT ? new Date(row.SUBMITTED_AT).toISOString() : null, mgrCardCodePrefix: row.MGR_CARD_CODE_PREFIX || 'VENDA', mgrGroupCode: row.MGR_GROUP_CODE, mgrGroup: row.MGR_GROUP || '', mgrPayTermsCode: row.MGR_PAY_TERMS_CODE, mgrPayTerms: row.MGR_PAY_TERMS || '', mgrPurchaseAccount: row.MGR_PURCHASE_ACCOUNT || '2101001', mgrPurchaseAcctName: row.MGR_PURCHASE_ACCT_NAME|| '', mgrCurrency: row.MGR_CURRENCY || 'Indian Rupee', mgrLanguage: row.MGR_LANGUAGE || 'English (UK)', mgrCreditLimit: Number(row.MGR_CREDIT_LIMIT) || 0, mgrNotes: row.MGR_NOTES || '', mgrBranch: row.MGR_BRANCH || '', mgrArea: row.MGR_AREA || '', mgrTerritory: row.MGR_TERRITORY || '', mgrSalesPersonCode: row.MGR_SALES_PERSON_CODE || null, mgrSalesEmployee: row.MGR_SALES_EMPLOYEE || '', mgrMainGroup: row.MGR_MAIN_GROUP || '', mgrChain: row.MGR_CHAIN || '', verifiedBy: row.VERIFIED_BY || null, verifiedAt: row.VERIFIED_AT ? new Date(row.VERIFIED_AT).toISOString() : null, approvedBy: row.APPROVED_BY || null, approvedAt: row.APPROVED_AT ? new Date(row.APPROVED_AT).toISOString() : null, rejectedBy: row.REJECTED_BY || null, rejectedAt: row.REJECTED_AT ? new Date(row.REJECTED_AT).toISOString() : null, sapCardCode: row.SAP_CARD_CODE || null, sapAttachmentEntry: row.SAP_ATTACHMENT_ENTRY || null, company: row.COMPANY || '', }; } function safeJson(v, fb) { if (!v) return fb; try { return JSON.parse(v); } catch (_) { return fb; } } // ── Helpers ─────────────────────────────────────────────────────────────────── function toTs(isoStr) { if (!isoStr) return null; return isoStr.replace('T', ' ').replace('Z', '').substring(0, 23); } function s(v, max) { if (v === null || v === undefined) return null; const str = String(v); return max ? str.substring(0, max) : str; } // ── INSERT ──────────────────────────────────────────────────────────────────── async function insertVendor(data, _schema) { const now = toTs(new Date().toISOString()); // INSERT and SELECT SCOPE_IDENTITY() must be one batch — as two separate // exec() calls, a pooled connection can route the second one to a // DIFFERENT physical connection than the one that just inserted, where // SCOPE_IDENTITY() correctly returns NULL (see workOrderStore.js's // insertWorkOrder() for the full write-up of this bug class). const idRows = await exec(` INSERT INTO ${tbl()} ( [VENDOR_TYPE],[CARD_NAME],[FOREIGN_NAME],[TYPE_OF_BUSINESS],[INDUSTRY], [PRODUCTS],[PAYMENT_TERMS], [CONTACT_FIRST],[CONTACT_LAST],[CONTACT_TITLE], [MOBILE],[ALT_CONTACT],[EMAIL], [BILL_STREET],[BILL_BLOCK],[BILL_CITY],[BILL_ZIP],[BILL_STATE],[BILL_COUNTRY], [GSTIN],[PAN],[TAN],[CURRENCY], [HAS_TDS],[TDS_CATEGORY],[TDS_RATE],[TDS_LDC_NO], [HAS_MSME],[MSME_NO],[MSME_TYPE],[MSME_BTYPE],[FSSAI_NO], [REMARKS],[BANK_ACCOUNTS],[ATTACHMENTS], [ALL_BILL_ADDRS],[ALL_SHIP_ADDRS], [USER_ID],[USER_TYPE],[USER_DEPT], [SUBMITTED_AT], [MGR_CARD_CODE_PREFIX],[MGR_PURCHASE_ACCOUNT],[MGR_CURRENCY],[MGR_LANGUAGE], [COMPANY] ) VALUES ( ?,?,?,?,?, ?,?, ?,?,?, ?,?,?, ?,?,?,?,?,?, ?,?,?,?, ?,?,?,?, ?,?,?,?,?, ?,?,?, ?,?, ?,?,?, ?, ?,?,?,?, ? ); SELECT SCOPE_IDENTITY() AS ID; `, [ s(data.vendorType || 'SUPPLIER', 20), s(data.cardName, 100), s(data.foreignName || '', 100), s(data.typeOfBusiness || 'Company', 50), s(data.industry || '', 100), s(data.products || '', 500), s(data.paymentTerms || '30 Days', 50), s(data.contactFirst, 60), s(data.contactLast, 60), s(data.contactTitle || '', 60), s(data.mobile || '', 20), s(data.altContact || '', 20), s(data.email, 150), s(data.billStreet || '', 200), s(data.billBlock || '', 100), s(data.billCity || '', 100), s(data.billZip || '', 20), s(data.billState || '', 100), s(data.billCountry || 'India', 60), s(data.gstin || '', 15), s(data.pan || '', 10), s(data.tan || '', 10), s(data.currency || 'Indian Rupee', 50), data.hasTds ? 'Y' : 'N', s(data.tdsCategory || '', 100), parseFloat(data.tdsRate) || 0, s(data.tdsLdcNo || '', 50), data.hasMsme ? 'Y' : 'N', s(data.msmeNo || '', 30), s(data.msmeType || '', 20), s(data.msmeBType || '', 30), s(data.fssaiNo || '', 20), data.remarks || '', JSON.stringify(data.bankAccounts || []), JSON.stringify(data.attachments || {}), JSON.stringify(data.allBillAddresses || []), JSON.stringify(data.allShipAddresses || []), s(data.userId || '', 50), s(data.userType || '', 50), s(data.userDept || '', 100), now, s(data.mgrCardCodePrefix || 'VENDA', 20), s(data.mgrPurchaseAccount || '2101001', 20), s(data.mgrCurrency || 'Indian Rupee', 50), s(data.mgrLanguage || 'English (UK)', 50), s(data.company || '', 50), ]); const id = idRows[0].ID; console.log(`[VENDOR STORE] ✅ Inserted ID=${id} — ${data.cardName}`); return { id }; } // ── FIND BY ID ──────────────────────────────────────────────────────────────── async function findById(id, companyDB) { const rows = await exec( `SELECT * FROM ${tbl()} WHERE [ID] = ?`, [parseInt(id)] ); return rowToVendor(rows[0]); } // ── LIST BY STATUS ──────────────────────────────────────────────────────────── async function listByStatus(status, companyDB) { let rows; if (status === 'ALL') { rows = await exec(`SELECT * FROM ${tbl()} ORDER BY [SUBMITTED_AT] DESC`); } else { rows = await exec( `SELECT * FROM ${tbl()} WHERE [STATUS] = ? ORDER BY [SUBMITTED_AT] DESC`, [status.toUpperCase()] ); } let result = rows.map(rowToVendor); if (companyDB) result = result.filter(r => r.company === companyDB); return result; } // ── UPDATE ──────────────────────────────────────────────────────────────────── async function updateVendor(id, patch, _schema) { if (!patch || Object.keys(patch).length === 0) return; const COL_MAP = { status: 'STATUS', vendorType: 'VENDOR_TYPE', cardName: 'CARD_NAME', foreignName: 'FOREIGN_NAME', typeOfBusiness: 'TYPE_OF_BUSINESS', industry: 'INDUSTRY', products: 'PRODUCTS', paymentTerms: 'PAYMENT_TERMS', contactFirst: 'CONTACT_FIRST', contactLast: 'CONTACT_LAST', contactTitle: 'CONTACT_TITLE', mobile: 'MOBILE', altContact: 'ALT_CONTACT', email: 'EMAIL', billStreet: 'BILL_STREET', billBlock: 'BILL_BLOCK', billCity: 'BILL_CITY', billZip: 'BILL_ZIP', billState: 'BILL_STATE', billCountry: 'BILL_COUNTRY', gstin: 'GSTIN', pan: 'PAN', tan: 'TAN', currency: 'CURRENCY', hasTds: 'HAS_TDS', tdsCategory: 'TDS_CATEGORY', tdsRate: 'TDS_RATE', tdsLdcNo: 'TDS_LDC_NO', hasMsme: 'HAS_MSME', msmeNo: 'MSME_NO', msmeType: 'MSME_TYPE', msmeBType: 'MSME_BTYPE', fssaiNo: 'FSSAI_NO', remarks: 'REMARKS', bankAccounts: 'BANK_ACCOUNTS', attachments: 'ATTACHMENTS', allBillAddresses: 'ALL_BILL_ADDRS', allShipAddresses: 'ALL_SHIP_ADDRS', verifiedBy: 'VERIFIED_BY', verifiedAt: 'VERIFIED_AT', approvedBy: 'APPROVED_BY', approvedAt: 'APPROVED_AT', rejectedBy: 'REJECTED_BY', rejectedAt: 'REJECTED_AT', sapCardCode: 'SAP_CARD_CODE', sapAttachmentEntry: 'SAP_ATTACHMENT_ENTRY', mgrCardCodePrefix: 'MGR_CARD_CODE_PREFIX', mgrGroupCode: 'MGR_GROUP_CODE', mgrGroup: 'MGR_GROUP', mgrPayTermsCode: 'MGR_PAY_TERMS_CODE', mgrPayTerms: 'MGR_PAY_TERMS', mgrPurchaseAccount: 'MGR_PURCHASE_ACCOUNT', mgrPurchaseAcctName: 'MGR_PURCHASE_ACCT_NAME', mgrCurrency: 'MGR_CURRENCY', mgrLanguage: 'MGR_LANGUAGE', mgrCreditLimit: 'MGR_CREDIT_LIMIT', mgrNotes: 'MGR_NOTES', mgrBranch: 'MGR_BRANCH', mgrArea: 'MGR_AREA', mgrTerritory: 'MGR_TERRITORY', mgrSalesPersonCode: 'MGR_SALES_PERSON_CODE', mgrSalesEmployee: 'MGR_SALES_EMPLOYEE', mgrMainGroup: 'MGR_MAIN_GROUP', mgrChain: 'MGR_CHAIN', company: 'COMPANY', }; const BOOL_COLS = new Set(['HAS_TDS', 'HAS_MSME']); const JSON_COLS = new Set(['BANK_ACCOUNTS', 'ATTACHMENTS', 'ALL_BILL_ADDRS', 'ALL_SHIP_ADDRS']); const NUMBER_COLS = new Set(['MGR_GROUP_CODE', 'MGR_PAY_TERMS_CODE', 'MGR_CREDIT_LIMIT', 'TDS_RATE', 'SAP_ATTACHMENT_ENTRY', 'MGR_SALES_PERSON_CODE']); const TS_COLS = new Set(['VERIFIED_AT', 'APPROVED_AT', 'REJECTED_AT']); const setClauses = []; const vals = []; Object.entries(patch).forEach(([k, v]) => { const col = COL_MAP[k]; if (!col) return; setClauses.push(`[${col}] = ?`); if (BOOL_COLS.has(col)) { vals.push(v ? 'Y' : 'N'); } else if (JSON_COLS.has(col)) { vals.push(JSON.stringify(v || (col === 'ATTACHMENTS' ? {} : []))); } else if (NUMBER_COLS.has(col)) { vals.push(v === null || v === undefined ? null : Number(v)); } else if (TS_COLS.has(col)) { vals.push(v ? toTs(v) : null); } else { vals.push(v === null || v === undefined ? null : String(v)); } }); if (!setClauses.length) return; vals.push(parseInt(id)); await exec( `UPDATE ${tbl()} SET ${setClauses.join(', ')} WHERE [ID] = ?`, vals ); console.log(`[VENDOR STORE] ✅ Updated ID=${id} (${setClauses.length} fields)`); } module.exports = { bootstrap, insertVendor, findById, listByStatus, updateVendor };