// backend/routes/customers.js — HANA-backed version // Replaces the in-memory array store with SAP HANA (TEST_OIL_15122025.ZCUST_PORTAL) const express = require('express'); const router = express.Router(); const { body, validationResult } = require('express-validator'); const { createCustomer, getNextCardCode } = require('../services/sapServiceLayer'); const { verifyToken, verifyAdmin } = require('../middleware/auth'); const store = require('../services/hanaStore'); // ← HANA persistence const { resolve: resolveCompany } = require('../services/companyConfig'); function mapCurrency(label) { return { 'Indian Rupee':'INR','US Dollar':'USD','Euro':'EUR', 'British Pound':'GBP','UAE Dirham':'AED', }[label] || 'INR'; } function mapCountryCode(name) { return { 'India':'IN','United States':'US','United Kingdom':'GB', 'UAE':'AE','Singapore':'SG','Germany':'DE','Japan':'JP','Australia':'AU', }[name] || 'IN'; } // Fields the customer form / verifier can touch const CUSTOMER_EDITABLE = [ 'cardName','foreignName','typeOfBusiness','industry','mobile','email','website', 'contactFirst','contactLast','contactMobile','contactEmail','contactTitle', 'currency','gstin','pan','remarks', 'hasMsme','msmeNo','msmeType','msmeBType','attachments', 'billAddressName','billStreet','billBlock','billCity','billZip','billState','billCountry', 'shipAddressName','shipStreet','shipBlock','shipCity','shipZip','shipState','shipCountry', 'sameAsBill','allBillAddresses','allShipAddresses', ]; const MANAGER_EDITABLE = [ 'mgrCardCodePrefix','mgrGroupCode','mgrGroup','mgrCurrency','mgrChain','mgrMainGroup', 'mgrBranch','mgrCountry','mgrCity','mgrZone','mgrArea','mgrSubarea','mgrCountryHead', 'mgrRsm','mgrAsm','mgrSo','mgrSr','mgrPromoter', 'mgrSalesEmployee','mgrSalesPersonCode', 'mgrSchemeType','mgrTerritory','mgrNotes', 'mgrCreditLimit', 'mgrPayTerms','mgrPayTermsCode', 'mgrArAccount','mgrArAccountName','mgrLanguage', ]; const ALL_EDITABLE = [...CUSTOMER_EDITABLE, ...MANAGER_EDITABLE]; // Build a patch object from request body — only keys in allowedFields function extractPatch(body, fields) { const patch = {}; fields.forEach(f => { if (body[f] !== undefined) patch[f] = body[f]; }); return patch; } // ── POST /submit ────────────────────────────────────────────────────────────── router.post('/submit', [ body('cardName').notEmpty().trim().withMessage('Company name is required'), body('email').isEmail().normalizeEmail().withMessage('Valid email is required'), body('mobile').notEmpty().trim().withMessage('Mobile is required'), body('contactFirst').notEmpty().trim().withMessage('Contact first name is required'), body('contactLast').notEmpty().trim().withMessage('Contact last name is required'), body('billStreet').notEmpty().trim().withMessage('Street is required'), body('billCity').notEmpty().trim().withMessage('City is required'), ], async (req, res) => { const errs = validationResult(req); if (!errs.isEmpty()) return res.status(400).json({ success: false, errors: errs.array() }); const b = req.body; const companyDB = resolveCompany(b.company); try { const customer = await store.insertCustomer({ customerType: b.customerType || 'B2B', cardName: b.cardName, foreignName: b.foreignName || '', typeOfBusiness: b.typeOfBusiness || 'Company', industry: b.industry || '', mobile: b.mobile, email: b.email, website: b.website || '', contactFirst: b.contactFirst, contactLast: b.contactLast, contactMobile: b.contactMobile || b.mobile, contactEmail: b.contactEmail || b.email || '', contactTitle: b.contactTitle || '', billAddressName: b.billAddressName || b.cardName, billStreet: b.billStreet, billBlock: b.billBlock || '', billCity: b.billCity, billZip: b.billZip || '', billState: b.billState || '', billCountry: b.billCountry || 'India', sameAsBill: b.sameAsBill || false, shipAddressName: b.sameAsBill ? (b.billAddressName || b.cardName) : (b.shipAddressName || b.billAddressName || b.cardName), shipStreet: b.sameAsBill ? b.billStreet : (b.shipStreet || b.billStreet), shipBlock: b.sameAsBill ? b.billBlock : (b.shipBlock || b.billBlock || ''), shipCity: b.sameAsBill ? b.billCity : (b.shipCity || b.billCity), shipZip: b.sameAsBill ? b.billZip : (b.shipZip || b.billZip || ''), shipState: b.sameAsBill ? b.billState : (b.shipState || b.billState || ''), shipCountry: b.sameAsBill ? b.billCountry : (b.shipCountry || b.billCountry || 'India'), allBillAddresses: Array.isArray(b.allBillAddresses) ? b.allBillAddresses : [], allShipAddresses: Array.isArray(b.allShipAddresses) ? b.allShipAddresses : [], currency: b.currency || 'Indian Rupee', gstin: b.gstin || '', pan: b.pan || '', remarks: b.remarks || '', hasMsme: b.hasMsme || false, msmeNo: b.msmeNo || '', msmeType: b.msmeType || '', msmeBType: b.msmeBType|| '', attachments: b.attachments || {}, // Manager defaults mgrCardCodePrefix: 'CUSTA', mgrArAccount: '1101001', mgrArAccountName: 'SUNDRY DEBTORS GT', mgrCurrency: 'Indian Rupee', mgrLanguage: 'English (UK)', }, companyDB); const attKeys = Object.keys(b.attachments || {}).filter(k => b.attachments[k]); console.log(`[APP] Submitted: ${b.cardName} (id=${customer.id}) | Attachments: ${attKeys.join(',') || 'none'}`); res.json({ success: true, message: 'Submitted', id: customer.id }); } catch (err) { console.error('[APP] submit error:', err.message); res.status(500).json({ success: false, message: err.message }); } }); // ── GET list ────────────────────────────────────────────────────────────────── router.get('/', verifyToken, async (req, res) => { const st = (req.query.status || 'PENDING').toUpperCase(); const companyDB = resolveCompany(req.query.company); try { const data = await store.listByStatus(st, companyDB); res.json({ success: true, data }); } catch (err) { console.error('[APP] list error:', err.message); res.status(500).json({ success: false, message: err.message }); } }); // ── GET single ──────────────────────────────────────────────────────────────── router.get('/:id', verifyToken, async (req, res) => { const companyDB = resolveCompany(req.query.company); try { const c = await store.findById(req.params.id, companyDB); if (!c) return res.status(404).json({ success: false, message: 'Not found' }); res.json({ success: true, data: c }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // ── PATCH /verify ───────────────────────────────────────────────────────────── router.patch('/:id/verify', verifyToken, async (req, res) => { const companyDB = resolveCompany(req.body.company); try { const c = await store.findById(req.params.id, companyDB); if (!c) return res.status(404).json({ success: false, message: 'Not found' }); if (c.status !== 'PENDING') return res.status(400).json({ success: false, message: 'Only PENDING can be verified. Current: ' + c.status }); const patch = extractPatch(req.body, ALL_EDITABLE); patch.status = req.body.approved ? 'VERIFIED' : 'REJECTED'; patch.verifiedAt = new Date().toISOString(); await store.updateCustomer(c.id, patch, companyDB); console.log(`[APP] ${c.id} (${c.cardName}) → ${patch.status}`); res.json({ success: true, message: `Customer ${patch.status.toLowerCase()}` }); } catch (err) { console.error('[APP] verify error:', err.message); res.status(500).json({ success: false, message: err.message }); } }); // ── PATCH /approve ──────────────────────────────────────────────────────────── router.patch('/:id/approve', verifyToken, verifyAdmin, async (req, res) => { const companyDB = resolveCompany(req.body.company); try { const c = await store.findById(req.params.id, companyDB); if (!c) return res.status(404).json({ success: false, message: 'Not found' }); if (!req.body.approved) { await store.updateCustomer(c.id, { status: 'REJECTED' }, companyDB); return res.json({ success: true, message: 'Customer rejected' }); } if (c.status !== 'VERIFIED') return res.status(400).json({ success: false, message: `Must be VERIFIED. Current: ${c.status}` }); // Merge incoming manager edits into the record const patch = extractPatch(req.body, ALL_EDITABLE); const merged = { ...c, ...patch }; const prefix = merged.mgrCardCodePrefix || 'CUSTA'; const cardCode = await getNextCardCode(prefix, companyDB); const payTermsGrpCode = merged.mgrPayTermsCode && !isNaN(parseInt(merged.mgrPayTermsCode)) ? parseInt(merged.mgrPayTermsCode) : null; const salesPersonCode = merged.mgrSalesPersonCode && !isNaN(parseInt(merged.mgrSalesPersonCode)) ? parseInt(merged.mgrSalesPersonCode) : null; const groupCode = merged.mgrGroupCode && !isNaN(parseInt(merged.mgrGroupCode)) ? parseInt(merged.mgrGroupCode) : null; console.log(`[APP] APPROVE ${cardCode}: PayTermsGrpCode=${payTermsGrpCode} SalesPersonCode=${salesPersonCode} GroupCode=${groupCode}`); const result = await createCustomer({ cardCode, cardName: merged.cardName, currency: mapCurrency(merged.mgrCurrency || merged.currency), phone1: merged.mobile, email: merged.email, website: merged.website, creditLimit: parseFloat(merged.mgrCreditLimit) || 0, remarks: merged.remarks, typeOfBusiness: merged.typeOfBusiness, groupCode, payTermsGrpCode, salesPersonCode, contactFirst: merged.contactFirst, contactLast: merged.contactLast, contactMobile: merged.contactMobile || merged.mobile, contactEmail: merged.contactEmail || merged.email, contactTitle: merged.contactTitle, billAddressName: merged.billAddressName, billStreet: merged.billStreet, billBlock: merged.billBlock, billCity: merged.billCity, billZip: merged.billZip, billState: merged.billState, billCountry: mapCountryCode(merged.billCountry), shipAddressName: merged.shipAddressName, shipStreet: merged.shipStreet, shipBlock: merged.shipBlock, shipCity: merged.shipCity, shipZip: merged.shipZip, shipState: merged.shipState, shipCountry: mapCountryCode(merged.shipCountry), allBillAddresses: merged.allBillAddresses || [], allShipAddresses: merged.allShipAddresses || [], mgrMainGroup: merged.mgrMainGroup, mgrChain: merged.mgrChain, mgrArAccount: merged.mgrArAccount, hasMsme: merged.hasMsme, msmeNo: merged.msmeNo || '', msmeType: merged.msmeType || '', msmeBType: merged.msmeBType|| '', gstin: merged.gstin, pan: merged.pan, attachments: merged.attachments || {}, }, companyDB); await store.updateCustomer(c.id, { ...patch, status: 'APPROVED', sapCardCode: cardCode, approvedAt: new Date().toISOString(), approvedBy: req.user.username, sapAttachmentEntry: result?.attachmentEntry || null, }, companyDB); console.log(`[APP] ✅ ${merged.cardName} → SAP B1 as ${cardCode}`); res.json({ success: true, message: 'Customer created in SAP B1!', cardCode, cardName: merged.cardName }); } catch (err) { console.error('[APP] approve error:', err.message); res.status(500).json({ success: false, message: err.message }); } }); // ── PATCH /draft ────────────────────────────────────────────────────────────── router.patch('/:id/draft', verifyToken, async (req, res) => { const companyDB = resolveCompany(req.body.company); try { const c = await store.findById(req.params.id, companyDB); if (!c) return res.status(404).json({ success: false, message: 'Not found' }); if (c.status === 'APPROVED' || c.status === 'REJECTED') return res.status(400).json({ success: false, message: 'Cannot edit ' + c.status + ' records' }); const patch = extractPatch(req.body, ALL_EDITABLE); await store.updateCustomer(c.id, patch, companyDB); console.log(`[APP] Draft saved: id=${c.id} (${c.cardName})`); res.json({ success: true, message: 'Draft saved' }); } catch (err) { console.error('[APP] draft error:', err.message); res.status(500).json({ success: false, message: err.message }); } }); module.exports = router;