const UnitOfWork = require('../../database/UnitOfWork'); const PersonalProfileService = require('../../services/profile/personal/PersonalProfileService'); const PersonalUserRepository = require('../../repositories/user/personal/PersonalUserRepository'); const { logger } = require('../../middleware/logger'); // helpers const MAX = { name: 100, email: 255, phone: 32, address: 255, accountHolder: 140, iban: 34 }; const trim = v => (typeof v === 'string' ? v.trim() : v); const isEmail = v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); const isPhone = v => v ? /^\+?[1-9]\d{6,14}$/.test(v) : true; const normalizeIban = v => (v || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase(); // sanitize only, no validation const truncate = (s, n) => (typeof s === 'string' && s.length > n ? s.slice(0, n) : s); const maskIban = v => { const s = normalizeIban(v || ''); if (!s) return null; const last4 = s.slice(-4); return `**** **** **** **** **** **** **** ${last4}`.replace(/\s{2,}/g, ' '); }; class PersonalProfileController { static async completeProfile(req, res) { const userId = req.user.userId; const profileData = req.body; const unitOfWork = new UnitOfWork(); await unitOfWork.start(); logger.info('PersonalProfileController:completeProfile:start', { userId, profileData }); try { const repo = new PersonalUserRepository(unitOfWork); await repo.updateProfileAndMarkCompleted(userId, profileData); // Correct method name await unitOfWork.commit(); logger.info('PersonalProfileController:completeProfile:success', { userId }); res.json({ success: true, message: 'Personal profile completed successfully' }); } catch (error) { await unitOfWork.rollback(error); logger.error('PersonalProfileController:completeProfile:error', { userId, error }); res.status(400).json({ success: false, message: error.message }); } } static async updateBasic(req, res) { if (!req.user) return res.status(401).json({ success: false, message: 'Unauthenticated' }); res.setHeader('X-Controller', 'PersonalProfileController:updateBasic'); // prove controller is hit // prefer camelCase, then snake_case, then infer from route namespace const inferredFromRoute = req.baseUrl?.includes('/profile/personal') || req.originalUrl?.includes('/profile/personal') ? 'personal' : undefined; const userType = req.user.userType ?? req.user.user_type ?? inferredFromRoute; const authDebugBasic = { derivedUserType: userType, reqUserKeys: Object.keys(req.user || {}), id: req.user?.id, userId: req.user?.userId }; logger.info(`PersonalProfileController:updateBasic:auth userType=${userType} keys=${authDebugBasic.reqUserKeys.join(',')}`); console.log('[PersonalProfileController:updateBasic] auth', authDebugBasic); if (userType !== 'personal') { const message = 'Personal user required'; const debug = process.env.NODE_ENV !== 'production' ? { expected: 'personal', got: userType, reqUserKeys: authDebugBasic.reqUserKeys } : undefined; return res.status(400).json({ success: false, message, ...(debug && { debug }) }); } const userId = req.user.id || req.user.userId; // Only take fields that are provided and non-empty after trim let { firstName, lastName, email, phone, address } = req.body || {}; const updates = {}; if (firstName !== undefined) { const v = truncate(trim(firstName), MAX.name); if (v !== '') updates.firstName = v; } if (lastName !== undefined) { const v = truncate(trim(lastName), MAX.name); if (v !== '') updates.lastName = v; } if (email !== undefined) { const v = truncate(trim(email), MAX.email); if (v !== '') updates.email = v; } if (phone !== undefined) { const v = truncate(trim(phone), MAX.phone); if (v !== '') updates.phone = v; } if (address !== undefined) { const v = truncate(trim(address), MAX.address); if (v !== '') updates.address = v; } // Validate only provided non-empty fields if (updates.email && !isEmail(updates.email)) { return res.status(400).json({ success: false, message: 'Invalid email' }); } if (updates.phone && !isPhone(updates.phone)) { return res.status(400).json({ success: false, message: 'Invalid phone' }); } const unitOfWork = new UnitOfWork(); await unitOfWork.start(); logger.info('PersonalProfileController:updateBasic:start', { userId }); try { const updated = await PersonalProfileService.updateBasic(userId, updates, unitOfWork); await unitOfWork.commit(); if (!updated) return res.status(404).json({ success: false, message: 'User not found' }); const response = { ...updated, iban: maskIban(updated.iban) }; logger.info('PersonalProfileController:updateBasic:success', { userId }); return res.status(200).json({ success: true, profile: response }); } catch (err) { await unitOfWork.rollback(err); // add explicit debug for missing user_id column if (err?.message?.includes("Unknown column 'user_id'")) { logger.error('PersonalProfileController:updateBasic:schemaMismatch', { userId, error: err.message, sql: err.sql, sqlState: err.sqlState, code: err.code }); res.setHeader('X-Error-Code', 'SCHEMA_MISMATCH_USER_ID'); return res.status(500).json({ success: false, message: 'Database schema mismatch: column user_id is missing.', debug: process.env.NODE_ENV !== 'production' ? { hint: 'Update the table to include user_id or change the repository SQL to use the correct column name.', repoFile: 'repositories/user/personal/PersonalUserRepository.js', repoLine: 302 } : undefined }); } logger.error('PersonalProfileController:updateBasic:error', { userId, error: err.message }); if (err.code === 'EMAIL_CONFLICT') { return res.status(409).json({ success: false, message: 'Email already in use' }); } return res.status(400).json({ success: false, message: err.message || 'Invalid input' }); } } static async updateBank(req, res) { if (!req.user) return res.status(401).json({ success: false, message: 'Unauthenticated' }); res.setHeader('X-Controller', 'PersonalProfileController:updateBank'); // prove controller is hit // prefer camelCase, then snake_case, then infer from route namespace const inferredFromRoute = req.baseUrl?.includes('/profile/personal') || req.originalUrl?.includes('/profile/personal') ? 'personal' : undefined; const userType = req.user.userType ?? req.user.user_type ?? inferredFromRoute; const authDebugBank = { derivedUserType: userType, reqUserKeys: Object.keys(req.user || {}), id: req.user?.id, userId: req.user?.userId }; logger.info(`PersonalProfileController:updateBank:auth userType=${userType} keys=${authDebugBank.reqUserKeys.join(',')}`); console.log('[PersonalProfileController:updateBank] auth', authDebugBank); if (userType !== 'personal') { const message = 'Personal user required'; const debug = process.env.NODE_ENV !== 'production' ? { expected: 'personal', got: userType, reqUserKeys: authDebugBank.reqUserKeys } : undefined; return res.status(400).json({ success: false, message, ...(debug && { debug }) }); } const userId = req.user.id || req.user.userId; let { accountHolder, iban } = req.body || {}; const payload = {}; // IBAN debug (pre-sanitize) const rawIban = iban ?? ''; const normalizedIban = truncate(normalizeIban(rawIban), MAX.iban); // Log what we received vs what we will use const ibanDebug = { provided: iban !== undefined, raw: rawIban, rawLength: String(rawIban).length, normalized: normalizedIban, normalizedMasked: maskIban(normalizedIban), normalizedLength: normalizedIban.length, truncatedTo: MAX.iban }; logger.info('PersonalProfileController:updateBank:iban_debug', { userId, ...ibanDebug }); console.log('[PersonalProfileController:updateBank] iban_debug', ibanDebug); // Small headers for quick check in browser Network panel (avoid large payload) res.setHeader('X-IBAN-Len', String(ibanDebug.normalizedLength)); if (ibanDebug.normalizedMasked) res.setHeader('X-IBAN-Masked', ibanDebug.normalizedMasked); if (accountHolder !== undefined) { const v = truncate(trim(accountHolder), MAX.accountHolder); if (v !== '') payload.accountHolderName = v; } if (iban !== undefined) { // sanitize only; do not validate IBAN format if (normalizedIban !== '') { payload.iban = normalizedIban; } } // Log final payload forwarded to service (mask IBAN) const payloadLog = { ...payload, ...(payload.iban ? { iban: maskIban(payload.iban) } : {}) }; logger.info('PersonalProfileController:updateBank:payload', { userId, payload: payloadLog }); console.log('[PersonalProfileController:updateBank] payload', payloadLog); const unitOfWork = new UnitOfWork(); await unitOfWork.start(); logger.info('PersonalProfileController:updateBank:start', { userId }); try { const updated = await PersonalProfileService.updateBank(userId, payload, unitOfWork); await unitOfWork.commit(); if (!updated) return res.status(404).json({ success: false, message: 'User not found' }); const response = { ...updated, iban: maskIban(updated.iban) }; logger.info('PersonalProfileController:updateBank:success', { userId, responsePreview: { iban: response.iban, accountHolderName: response.accountHolderName } }); return res.status(200).json({ success: true, profile: response }); } catch (err) { await unitOfWork.rollback(err); logger.error('PersonalProfileController:updateBank:error', { userId, error: err.message }); res.setHeader('X-IBAN-Error', 'controller_catch'); return res.status(400).json({ success: false, message: err.message || 'Invalid input' }); } } } module.exports = PersonalProfileController;