Files
sap-erp/services/hanaStore.js
T
John 69b4e68baf
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s
first commit
2026-09-23 17:31:02 +05:30

559 lines
22 KiB
JavaScript

// backend/services/hanaStore.js
// ─────────────────────────────────────────────────────────────────────────────
// Persistent storage for the Customer Registration Portal using SAP SQL Server.
// All customer submissions are stored in the JIVO_OIL_HANADB database in a
// custom table: ZCUST_PORTAL
//
// TABLE STRUCTURE (auto-created on first run):
// ZCUST_PORTAL (
// ID INT IDENTITY(1,1) PRIMARY KEY, -- auto-increment
// CARD_NAME NVARCHAR(200),
// FOREIGN_NAME NVARCHAR(200),
// CUSTOMER_TYPE NVARCHAR(10), -- B2B / B2C
// TYPE_OF_BUSINESS NVARCHAR(50),
// INDUSTRY NVARCHAR(100),
// MOBILE NVARCHAR(30),
// EMAIL NVARCHAR(150),
// WEBSITE NVARCHAR(200),
// CONTACT_FIRST NVARCHAR(100),
// CONTACT_LAST NVARCHAR(100),
// CONTACT_TITLE NVARCHAR(100),
// CONTACT_MOBILE NVARCHAR(30),
// CONTACT_EMAIL NVARCHAR(150),
// GSTIN NVARCHAR(20),
// PAN NVARCHAR(15),
// CURRENCY NVARCHAR(50),
// REMARKS NCLOB,
// -- MSME
// HAS_MSME TINYINT DEFAULT 0,
// MSME_NO NVARCHAR(30),
// MSME_TYPE NVARCHAR(20),
// MSME_BTYPE NVARCHAR(20),
// -- Status & workflow
// STATUS NVARCHAR(20) DEFAULT 'PENDING',
// SUBMITTED_AT TIMESTAMP,
// VERIFIED_AT TIMESTAMP,
// APPROVED_AT TIMESTAMP,
// APPROVED_BY NVARCHAR(100),
// SAP_CARD_CODE NVARCHAR(50),
// SAP_ATT_ENTRY INTEGER,
// -- Billing address (primary)
// BILL_ADDR_NAME NVARCHAR(100),
// BILL_STREET NVARCHAR(200),
// BILL_BLOCK NVARCHAR(200),
// BILL_CITY NVARCHAR(100),
// BILL_ZIP NVARCHAR(20),
// BILL_STATE NVARCHAR(10),
// BILL_COUNTRY NVARCHAR(100),
// -- Shipping address (primary)
// SHIP_ADDR_NAME NVARCHAR(100),
// SHIP_STREET NVARCHAR(200),
// SHIP_BLOCK NVARCHAR(200),
// SHIP_CITY NVARCHAR(100),
// SHIP_ZIP NVARCHAR(20),
// SHIP_STATE NVARCHAR(10),
// SHIP_COUNTRY NVARCHAR(100),
// SAME_AS_BILL TINYINT DEFAULT 0,
// -- Multi-address JSON blobs
// ALL_BILL_ADDRS NCLOB, -- JSON array of billing address objects
// ALL_SHIP_ADDRS NCLOB, -- JSON array of shipping address objects
// -- Attachments (base64 JSON blob — cleared after SAP upload)
// ATTACHMENTS NCLOB,
// -- Manager fields
// MGR_PREFIX NVARCHAR(20) DEFAULT 'CUSTA',
// MGR_GROUP_CODE NVARCHAR(20),
// MGR_GROUP NVARCHAR(100),
// MGR_CURRENCY NVARCHAR(50),
// MGR_CHAIN NVARCHAR(50),
// MGR_MAIN_GROUP NVARCHAR(50),
// MGR_BRANCH NVARCHAR(50),
// MGR_COUNTRY NVARCHAR(10),
// MGR_CITY NVARCHAR(100),
// MGR_ZONE NVARCHAR(100),
// MGR_AREA NVARCHAR(100),
// MGR_SUBAREA NVARCHAR(100),
// MGR_COUNTRY_HD NVARCHAR(100),
// MGR_RSM NVARCHAR(100),
// MGR_ASM NVARCHAR(100),
// MGR_SO NVARCHAR(100),
// MGR_SR NVARCHAR(100),
// MGR_PROMOTER NVARCHAR(100),
// MGR_SALES_EMP NVARCHAR(100),
// MGR_SLP_CODE NVARCHAR(20),
// MGR_SCHEME_TYPE NVARCHAR(100),
// MGR_TERRITORY NVARCHAR(100),
// MGR_NOTES NCLOB,
// MGR_CREDIT_LMT DECIMAL(18,2) DEFAULT 0,
// MGR_PAY_TERMS NVARCHAR(100),
// MGR_PAY_CODE NVARCHAR(20),
// MGR_AR_ACCOUNT NVARCHAR(30),
// MGR_AR_ACC_NAME NVARCHAR(200),
// MGR_LANGUAGE NVARCHAR(50)
// )
// ─────────────────────────────────────────────────────────────────────────────
const { getPool } = require('./appSqlPool');
const { DEFAULT_COMPANY } = require('./companyConfig');
const DEFAULT_SCHEMA = DEFAULT_COMPANY;
const tbl = (s) => `[dbo].[ZCUST_PORTAL]`;
const seq = (s) => `[dbo].[ZCUST_PORTAL_SEQ]`;
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 || [];
}
// ── Check if a SQL error means "object already exists" ─────────────────────
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: create table if not exists ────────────────────────
async function bootstrap(schema) {
console.log('[SQL-STORE] Checking table', tbl(schema), '...');
try {
// 1. Try to create full table
let tableExisted = false;
await exec(`
CREATE TABLE ${tbl(schema)} (
ID INT IDENTITY(1,1) PRIMARY KEY,
USER_ID NVARCHAR(50),
USER_TYPE NVARCHAR(50),
USER_DEPT NVARCHAR(100),
CARD_NAME NVARCHAR(200),
FOREIGN_NAME NVARCHAR(200),
CUSTOMER_TYPE NVARCHAR(10),
TYPE_OF_BUSINESS NVARCHAR(50),
INDUSTRY NVARCHAR(100),
MOBILE NVARCHAR(30),
EMAIL NVARCHAR(150),
WEBSITE NVARCHAR(200),
CONTACT_FIRST NVARCHAR(100),
CONTACT_LAST NVARCHAR(100),
CONTACT_TITLE NVARCHAR(100),
CONTACT_MOBILE NVARCHAR(30),
CONTACT_EMAIL NVARCHAR(150),
GSTIN NVARCHAR(20),
PAN NVARCHAR(15),
CURRENCY NVARCHAR(50),
REMARKS NVARCHAR(MAX),
HAS_MSME TINYINT DEFAULT 0,
MSME_NO NVARCHAR(30),
MSME_TYPE NVARCHAR(20),
MSME_BTYPE NVARCHAR(20),
STATUS NVARCHAR(20) DEFAULT 'PENDING',
SUBMITTED_AT DATETIME2,
VERIFIED_AT DATETIME2,
APPROVED_AT DATETIME2,
APPROVED_BY NVARCHAR(100),
SAP_CARD_CODE NVARCHAR(50),
SAP_ATT_ENTRY INTEGER,
BILL_ADDR_NAME NVARCHAR(100),
BILL_STREET NVARCHAR(200),
BILL_BLOCK NVARCHAR(200),
BILL_CITY NVARCHAR(100),
BILL_ZIP NVARCHAR(20),
BILL_STATE NVARCHAR(10),
BILL_COUNTRY NVARCHAR(100),
SHIP_ADDR_NAME NVARCHAR(100),
SHIP_STREET NVARCHAR(200),
SHIP_BLOCK NVARCHAR(200),
SHIP_CITY NVARCHAR(100),
SHIP_ZIP NVARCHAR(20),
SHIP_STATE NVARCHAR(10),
SHIP_COUNTRY NVARCHAR(100),
SAME_AS_BILL TINYINT DEFAULT 0,
ALL_BILL_ADDRS NVARCHAR(MAX),
ALL_SHIP_ADDRS NVARCHAR(MAX),
ATTACHMENTS NVARCHAR(MAX),
MGR_PREFIX NVARCHAR(20) DEFAULT 'CUSTA',
MGR_GROUP_CODE NVARCHAR(20),
MGR_GROUP NVARCHAR(100),
MGR_CURRENCY NVARCHAR(50),
MGR_CHAIN NVARCHAR(50),
MGR_MAIN_GROUP NVARCHAR(50),
MGR_BRANCH NVARCHAR(50),
MGR_COUNTRY NVARCHAR(10),
MGR_CITY NVARCHAR(100),
MGR_ZONE NVARCHAR(100),
MGR_AREA NVARCHAR(100),
MGR_SUBAREA NVARCHAR(100),
MGR_COUNTRY_HD NVARCHAR(100),
MGR_RSM NVARCHAR(100),
MGR_ASM NVARCHAR(100),
MGR_SO NVARCHAR(100),
MGR_SR NVARCHAR(100),
MGR_PROMOTER NVARCHAR(100),
MGR_SALES_EMP NVARCHAR(100),
MGR_SLP_CODE NVARCHAR(20),
MGR_SCHEME_TYPE NVARCHAR(100),
MGR_TERRITORY NVARCHAR(100),
MGR_NOTES NVARCHAR(MAX),
MGR_CREDIT_LMT DECIMAL(18,2) DEFAULT 0,
MGR_PAY_TERMS NVARCHAR(100),
MGR_PAY_CODE NVARCHAR(20),
MGR_AR_ACCOUNT NVARCHAR(30),
MGR_AR_ACC_NAME NVARCHAR(200),
MGR_LANGUAGE NVARCHAR(50),
COMPANY NVARCHAR(50) DEFAULT '${DEFAULT_SCHEMA}'
)
`).catch(e => {
if (isAlreadyExists(e)) {
tableExisted = true;
console.log('[SQL-STORE] Table already exists — checking for missing columns...');
} else {
throw e;
}
});
// 3. If table existed, safely add any new columns (migration)
if (tableExisted) {
const migrations = [
`ALTER TABLE ${tbl(schema)} ADD [USER_ID] NVARCHAR(50)`,
`ALTER TABLE ${tbl(schema)} ADD [USER_TYPE] NVARCHAR(50)`,
`ALTER TABLE ${tbl(schema)} ADD [USER_DEPT] NVARCHAR(100)`,
`ALTER TABLE ${tbl(schema)} ADD [COMPANY] NVARCHAR(50) DEFAULT '${DEFAULT_SCHEMA}'`,
];
for (const sql of migrations) {
await exec(sql).catch(e => {
// Ignore "column already exists" errors
const em = (e.message || '').toLowerCase();
if (!em.includes('column') && !em.includes('duplicate') && !isAlreadyExists(e)) {
console.warn('[SQL-STORE] Migration warning:', e.message);
}
});
}
console.log('[SQL-STORE] ✅ Column migration check complete');
}
console.log('[SQL-STORE] ✅ Table ready:', tbl(schema));
} catch (err) {
console.error('[HANA-STORE] ❌ Bootstrap failed:', err.message);
throw err;
}
}
// ── Helpers ──────────────────────────────────────────────────────────────────
function toTs(isoStr) {
if (!isoStr) return null;
return isoStr.replace('T', ' ').replace('Z', '').substring(0, 23);
}
function fromRow(row) {
if (!row) return null;
return {
id: row.ID,
userId: row.USER_ID || '',
userType: row.USER_TYPE || '',
userDept: row.USER_DEPT || '',
cardName: row.CARD_NAME || '',
foreignName: row.FOREIGN_NAME || '',
customerType: row.CUSTOMER_TYPE || 'B2B',
typeOfBusiness: row.TYPE_OF_BUSINESS || '',
industry: row.INDUSTRY || '',
mobile: row.MOBILE || '',
email: row.EMAIL || '',
website: row.WEBSITE || '',
contactFirst: row.CONTACT_FIRST || '',
contactLast: row.CONTACT_LAST || '',
contactTitle: row.CONTACT_TITLE || '',
contactMobile: row.CONTACT_MOBILE || '',
contactEmail: row.CONTACT_EMAIL || '',
gstin: row.GSTIN || '',
pan: row.PAN || '',
currency: row.CURRENCY || 'Indian Rupee',
remarks: row.REMARKS || '',
hasMsme: row.HAS_MSME === 1,
msmeNo: row.MSME_NO || '',
msmeType: row.MSME_TYPE || '',
msmeBType: row.MSME_BTYPE || '',
status: row.STATUS || 'PENDING',
submittedAt: row.SUBMITTED_AT ? new Date(row.SUBMITTED_AT).toISOString() : null,
verifiedAt: row.VERIFIED_AT ? new Date(row.VERIFIED_AT).toISOString() : null,
approvedAt: row.APPROVED_AT ? new Date(row.APPROVED_AT).toISOString() : null,
approvedBy: row.APPROVED_BY || null,
sapCardCode: row.SAP_CARD_CODE || null,
sapAttachmentEntry: row.SAP_ATT_ENTRY || null,
// Bill
billAddressName: row.BILL_ADDR_NAME || '',
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',
// Ship
shipAddressName: row.SHIP_ADDR_NAME || '',
shipStreet: row.SHIP_STREET || '',
shipBlock: row.SHIP_BLOCK || '',
shipCity: row.SHIP_CITY || '',
shipZip: row.SHIP_ZIP || '',
shipState: row.SHIP_STATE || '',
shipCountry: row.SHIP_COUNTRY || 'India',
sameAsBill: row.SAME_AS_BILL === 1,
allBillAddresses: safeJson(row.ALL_BILL_ADDRS, []),
allShipAddresses: safeJson(row.ALL_SHIP_ADDRS, []),
attachments: safeJson(row.ATTACHMENTS, {}),
// Manager
mgrCardCodePrefix: row.MGR_PREFIX || 'CUSTA',
mgrGroupCode: row.MGR_GROUP_CODE || '',
mgrGroup: row.MGR_GROUP || '',
mgrCurrency: row.MGR_CURRENCY || 'Indian Rupee',
mgrChain: row.MGR_CHAIN || '',
mgrMainGroup: row.MGR_MAIN_GROUP || '',
mgrBranch: row.MGR_BRANCH || '',
mgrCountry: row.MGR_COUNTRY || 'IN',
mgrCity: row.MGR_CITY || '',
mgrZone: row.MGR_ZONE || '',
mgrArea: row.MGR_AREA || '',
mgrSubarea: row.MGR_SUBAREA || '',
mgrCountryHead: row.MGR_COUNTRY_HD || '',
mgrRsm: row.MGR_RSM || '',
mgrAsm: row.MGR_ASM || '',
mgrSo: row.MGR_SO || '',
mgrSr: row.MGR_SR || '',
mgrPromoter: row.MGR_PROMOTER || '',
mgrSalesEmployee: row.MGR_SALES_EMP || '',
mgrSalesPersonCode: row.MGR_SLP_CODE || '',
mgrSchemeType: row.MGR_SCHEME_TYPE || '',
mgrTerritory: row.MGR_TERRITORY || '',
mgrNotes: row.MGR_NOTES || '',
mgrCreditLimit: row.MGR_CREDIT_LMT || 0,
mgrPayTerms: row.MGR_PAY_TERMS || '',
mgrPayTermsCode: row.MGR_PAY_CODE || '',
mgrArAccount: row.MGR_AR_ACCOUNT || '1101001',
mgrArAccountName: row.MGR_AR_ACC_NAME || 'SUNDRY DEBTORS GT',
mgrLanguage: row.MGR_LANGUAGE || 'English (UK)',
company: row.COMPANY || '',
};
}
function safeJson(val, fallback) {
if (!val) return fallback;
try { return JSON.parse(val); } catch { return fallback; }
}
function s(v, max) {
if (v === null || v === undefined) return null;
const str = String(v);
return max ? str.substring(0, max) : str;
}
// ── CRUD ─────────────────────────────────────────────────────────────────────
// INSERT — returns new customer object with ID
async function insertCustomer(b, _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()} (
USER_ID, USER_TYPE, USER_DEPT,
CARD_NAME, FOREIGN_NAME, CUSTOMER_TYPE, TYPE_OF_BUSINESS, INDUSTRY,
MOBILE, EMAIL, WEBSITE,
CONTACT_FIRST, CONTACT_LAST, CONTACT_TITLE, CONTACT_MOBILE, CONTACT_EMAIL,
GSTIN, PAN, CURRENCY, REMARKS,
HAS_MSME, MSME_NO, MSME_TYPE, MSME_BTYPE,
STATUS, SUBMITTED_AT,
BILL_ADDR_NAME, BILL_STREET, BILL_BLOCK, BILL_CITY, BILL_ZIP, BILL_STATE, BILL_COUNTRY,
SHIP_ADDR_NAME, SHIP_STREET, SHIP_BLOCK, SHIP_CITY, SHIP_ZIP, SHIP_STATE, SHIP_COUNTRY,
SAME_AS_BILL, ALL_BILL_ADDRS, ALL_SHIP_ADDRS, ATTACHMENTS,
MGR_PREFIX, MGR_AR_ACCOUNT, MGR_AR_ACC_NAME, MGR_CURRENCY, MGR_LANGUAGE,
COMPANY
) VALUES (
?,?,?,?,
?,?,?,?,?,
?,?,?,
?,?,?,?,?,
?,?,?,?,
?,?,?,?,
?,?,
?,?,?,?,?,?,?,
?,?,?,?,?,?,?,
?,?,?,?,
?,?,?,?,?,
?
);
SELECT SCOPE_IDENTITY() AS ID;
`, [
s(b.userId, 50), s(b.userType, 50), s(b.userDept, 100),
s(b.cardName, 200), s(b.foreignName, 200), s(b.customerType || 'B2B', 10),
s(b.typeOfBusiness, 50), s(b.industry, 100),
s(b.mobile, 30), s(b.email, 150), s(b.website, 200),
s(b.contactFirst, 100), s(b.contactLast, 100), s(b.contactTitle, 100),
s(b.contactMobile, 30), s(b.contactEmail, 150),
s(b.gstin, 20), s(b.pan, 15), s(b.currency || 'Indian Rupee', 50), b.remarks || '',
b.hasMsme ? 1 : 0, s(b.msmeNo, 30), s(b.msmeType, 20), s(b.msmeBType, 20),
'PENDING', now,
s(b.billAddressName, 100), s(b.billStreet, 200), s(b.billBlock, 200),
s(b.billCity, 100), s(b.billZip, 20), s(b.billState, 10), s(b.billCountry, 100),
s(b.shipAddressName, 100), s(b.shipStreet, 200), s(b.shipBlock, 200),
s(b.shipCity, 100), s(b.shipZip, 20), s(b.shipState, 10), s(b.shipCountry, 100),
b.sameAsBill ? 1 : 0,
JSON.stringify(b.allBillAddresses || []),
JSON.stringify(b.allShipAddresses || []),
JSON.stringify(b.attachments || {}),
s(b.mgrCardCodePrefix || 'CUSTA', 20),
s(b.mgrArAccount || '1101001', 30),
s(b.mgrArAccountName || 'SUNDRY DEBTORS GT', 200),
s(b.mgrCurrency || 'Indian Rupee', 50),
s(b.mgrLanguage || 'English (UK)', 50),
s(b.company || '', 50),
]);
const id = idRows[0].ID;
console.log(`[SQL-STORE] ✅ Inserted ID=${id} — ${b.cardName}`);
return { id, ...b, status: 'PENDING', submittedAt: new Date().toISOString() };
}
// SELECT ALL by status — always query default schema table, filter by COMPANY
async function listByStatus(status, companyDB) {
const rows = await exec(
`SELECT * FROM ${tbl()} WHERE STATUS = ? ORDER BY SUBMITTED_AT DESC`,
[status.toUpperCase()]
);
let result = rows.map(fromRow);
if (companyDB) result = result.filter(r => r.company === companyDB);
return result;
}
// SELECT ONE by id — always query default schema table
async function findById(id, companyDB) {
const rows = await exec(
`SELECT * FROM ${tbl()} WHERE ID = ?`,
[parseInt(id)]
);
return rows.length ? fromRow(rows[0]) : null;
}
// UPDATE — apply a partial patch object
async function updateCustomer(id, patch, _schema) {
// Build SET clause dynamically from only provided fields
const FIELD_MAP = {
userId: 'USER_ID',
userType: 'USER_TYPE',
userDept: 'USER_DEPT',
cardName: 'CARD_NAME',
foreignName: 'FOREIGN_NAME',
customerType: 'CUSTOMER_TYPE',
typeOfBusiness: 'TYPE_OF_BUSINESS',
industry: 'INDUSTRY',
mobile: 'MOBILE',
email: 'EMAIL',
website: 'WEBSITE',
contactFirst: 'CONTACT_FIRST',
contactLast: 'CONTACT_LAST',
contactTitle: 'CONTACT_TITLE',
contactMobile: 'CONTACT_MOBILE',
contactEmail: 'CONTACT_EMAIL',
gstin: 'GSTIN',
pan: 'PAN',
currency: 'CURRENCY',
remarks: 'REMARKS',
hasMsme: 'HAS_MSME',
msmeNo: 'MSME_NO',
msmeType: 'MSME_TYPE',
msmeBType: 'MSME_BTYPE',
status: 'STATUS',
verifiedAt: 'VERIFIED_AT',
approvedAt: 'APPROVED_AT',
approvedBy: 'APPROVED_BY',
sapCardCode: 'SAP_CARD_CODE',
sapAttachmentEntry: 'SAP_ATT_ENTRY',
billAddressName: 'BILL_ADDR_NAME',
billStreet: 'BILL_STREET',
billBlock: 'BILL_BLOCK',
billCity: 'BILL_CITY',
billZip: 'BILL_ZIP',
billState: 'BILL_STATE',
billCountry: 'BILL_COUNTRY',
shipAddressName: 'SHIP_ADDR_NAME',
shipStreet: 'SHIP_STREET',
shipBlock: 'SHIP_BLOCK',
shipCity: 'SHIP_CITY',
shipZip: 'SHIP_ZIP',
shipState: 'SHIP_STATE',
shipCountry: 'SHIP_COUNTRY',
sameAsBill: 'SAME_AS_BILL',
allBillAddresses: 'ALL_BILL_ADDRS',
allShipAddresses: 'ALL_SHIP_ADDRS',
attachments: 'ATTACHMENTS',
mgrCardCodePrefix: 'MGR_PREFIX',
mgrGroupCode: 'MGR_GROUP_CODE',
mgrGroup: 'MGR_GROUP',
mgrCurrency: 'MGR_CURRENCY',
mgrChain: 'MGR_CHAIN',
mgrMainGroup: 'MGR_MAIN_GROUP',
mgrBranch: 'MGR_BRANCH',
mgrCountry: 'MGR_COUNTRY',
mgrCity: 'MGR_CITY',
mgrZone: 'MGR_ZONE',
mgrArea: 'MGR_AREA',
mgrSubarea: 'MGR_SUBAREA',
mgrCountryHead: 'MGR_COUNTRY_HD',
mgrRsm: 'MGR_RSM',
mgrAsm: 'MGR_ASM',
mgrSo: 'MGR_SO',
mgrSr: 'MGR_SR',
mgrPromoter: 'MGR_PROMOTER',
mgrSalesEmployee: 'MGR_SALES_EMP',
mgrSalesPersonCode: 'MGR_SLP_CODE',
mgrSchemeType: 'MGR_SCHEME_TYPE',
mgrTerritory: 'MGR_TERRITORY',
mgrNotes: 'MGR_NOTES',
mgrCreditLimit: 'MGR_CREDIT_LMT',
mgrPayTerms: 'MGR_PAY_TERMS',
mgrPayTermsCode: 'MGR_PAY_CODE',
mgrArAccount: 'MGR_AR_ACCOUNT',
mgrArAccountName: 'MGR_AR_ACC_NAME',
mgrLanguage: 'MGR_LANGUAGE',
company: 'COMPANY',
};
// JSON / boolean / timestamp transforms
function prepareValue(key, val) {
if (key === 'hasMsme' || key === 'sameAsBill') return val ? 1 : 0;
if (key === 'allBillAddresses' || key === 'allShipAddresses' || key === 'attachments')
return JSON.stringify(val || (key === 'attachments' ? {} : []));
if (key === 'verifiedAt' || key === 'approvedAt') return toTs(val);
if (key === 'mgrCreditLimit') return parseFloat(val) || 0;
if (key === 'sapAttachmentEntry') return parseInt(val) || null;
return val;
}
const sets = [];
const vals = [];
for (const [jsKey, colName] of Object.entries(FIELD_MAP)) {
if (patch[jsKey] !== undefined) {
sets.push(`${colName} = ?`);
vals.push(prepareValue(jsKey, patch[jsKey]));
}
}
if (!sets.length) return;
vals.push(parseInt(id));
await exec(
`UPDATE ${tbl()} SET ${sets.join(', ')} WHERE ID = ?`,
vals
);
console.log(`[HANA-STORE] ✅ Updated ID=${id} (${sets.length} fields)`);
}
module.exports = { bootstrap, insertCustomer, listByStatus, findById, updateCustomer };