const db = require('../../../database/database'); const PersonalUser = require('../../../models/PersonalUser'); const User = require('../../../models/User'); const { logger } = require('../../../middleware/logger'); class PersonalUserRepository { constructor(unitOfWork) { this.unitOfWork = unitOfWork; } async create({ email, password, firstName, lastName, phone, referralEmail }) { logger.info('PersonalUserRepository.create:start', { email, firstName, lastName }); try { console.log('📊 PersonalUserRepository: Creating personal user in database...'); const hashedPassword = await User.hashPassword(password); const conn = this.unitOfWork.connection; // 1. Insert into users table const userQuery = ` INSERT INTO users (email, password, user_type, role) VALUES (?, ?, 'personal', 'user') `; const [userResult] = await conn.query(userQuery, [email, hashedPassword]); const userId = userResult.insertId; logger.info('PersonalUserRepository.create:user_created', { userId }); console.log('✅ User record created with ID:', userId); // 2. Insert into personal_profiles table const profileQuery = ` INSERT INTO personal_profiles (user_id, first_name, last_name, phone) VALUES (?, ?, ?, ?) `; await conn.query(profileQuery, [userId, firstName, lastName, phone]); logger.info('PersonalUserRepository.create:profile_created', { userId }); console.log('✅ Personal profile created'); logger.info('PersonalUserRepository.create:success', { userId }); return new PersonalUser( userId, email, hashedPassword, firstName, lastName, phone, null, referralEmail, new Date() ); } catch (error) { logger.error('PersonalUserRepository.create:error', { email, error: error.message }); throw error; } } async findByEmail(email) { logger.info('PersonalUserRepository.findByEmail:start', { email }); try { console.log('🔍 PersonalUserRepository: Querying database for personal user...'); const conn = this.unitOfWork.connection; const query = ` SELECT u.*, pp.first_name, pp.last_name, pp.phone, pp.date_of_birth FROM users u LEFT JOIN personal_profiles pp ON u.id = pp.user_id WHERE u.email = ? AND u.user_type = 'personal' `; const [rows] = await conn.query(query, [email]); if (rows.length > 0) { logger.info('PersonalUserRepository.findByEmail:found', { email, userId: rows[0].id }); const row = rows[0]; return new PersonalUser( row.id, row.email, row.password, row.first_name, row.last_name, row.phone, row.date_of_birth, null, row.created_at, row.updated_at ); } logger.info('PersonalUserRepository.findByEmail:not_found', { email }); return null; } catch (error) { logger.error('PersonalUserRepository.findByEmail:error', { email, error: error.message }); throw error; } } async updateProfileAndMarkCompleted(userId, profileData) { logger.info('PersonalUserRepository.updateProfileAndMarkCompleted:start', { userId }); try { const conn = this.unitOfWork.connection; const { dateOfBirth, nationality, address, zip_code, city, // Added city country, phoneSecondary, emergencyContactName, emergencyContactPhone, accountHolderName, iban // Added field } = profileData; await conn.query( `UPDATE personal_profiles SET date_of_birth = ?, nationality = ?, address = ?, zip_code = ?, city = ?, country = ?, phone_secondary = ?, emergency_contact_name = ?, emergency_contact_phone = ?, account_holder_name = ? WHERE user_id = ?`, [ dateOfBirth, nationality, address, zip_code, city, // Added city country, phoneSecondary, emergencyContactName, emergencyContactPhone, accountHolderName, userId ] ); logger.info('PersonalUserRepository.updateProfileAndMarkCompleted:profile_updated', { userId }); // Update IBAN in users table if provided if (iban) { await conn.query( `UPDATE users SET iban = ? WHERE id = ?`, [iban, userId] ); logger.info('PersonalUserRepository.updateProfileAndMarkCompleted:iban_updated', { userId }); } await conn.query( `UPDATE user_status SET profile_completed = 1, profile_completed_at = NOW() WHERE user_id = ?`, [userId] ); logger.info('PersonalUserRepository.updateProfileAndMarkCompleted:profile_completed', { userId }); } catch (error) { logger.error('PersonalUserRepository.updateProfileAndMarkCompleted:error', { userId, error: error.message }); throw error; } } async findById(userId) { logger.info('PersonalUserRepository.findById:start', { userId }); try { const conn = this.unitOfWork.connection; const query = ` SELECT u.*, pp.first_name, pp.last_name, pp.phone, pp.date_of_birth FROM users u LEFT JOIN personal_profiles pp ON u.id = pp.user_id WHERE u.id = ? AND u.user_type = 'personal' `; const [rows] = await conn.query(query, [userId]); if (rows.length > 0) { logger.info('PersonalUserRepository.findById:found', { userId }); const row = rows[0]; return new PersonalUser( row.id, row.email, row.password, row.first_name, row.last_name, row.phone, row.date_of_birth, null, row.created_at, row.updated_at ); } logger.info('PersonalUserRepository.findById:not_found', { userId }); return null; } catch (error) { logger.error('PersonalUserRepository.findById:error', { userId, error: error.message }); throw error; } } // Returns merged profile shape used by "GET /api/me"-like responses async getMergedProfileById(userId) { const conn = this.unitOfWork.connection; const [rows] = await conn.query( ` SELECT u.id, u.email, u.user_type, u.role, u.iban, u.created_at, u.updated_at, pp.first_name, pp.last_name, pp.phone, pp.address, pp.date_of_birth, pp.account_holder_name FROM users u LEFT JOIN personal_profiles pp ON u.id = pp.user_id WHERE u.id = ? `, [userId] ); if (!rows.length) return null; const r = rows[0]; return { id: r.id, email: r.email, user_type: r.user_type, role: r.role, firstName: r.first_name, lastName: r.last_name, phone: r.phone, address: r.address, dateOfBirth: r.date_of_birth, accountHolderName: r.account_holder_name, iban: r.iban, createdAt: r.created_at, updatedAt: r.updated_at }; } async updateBasicInfo(userId, payload) { logger.info('PersonalUserRepository.updateBasicInfo:start', { userId }); const conn = this.unitOfWork.connection; // Lock current rows to compute diffs safely const [[currentUser]] = await conn.query( `SELECT id, email FROM users WHERE id = ? FOR UPDATE`, [userId] ); const [[currentProfile]] = await conn.query( `SELECT first_name, last_name, phone, address FROM personal_profiles WHERE user_id = ? FOR UPDATE`, [userId] ); if (!currentUser) { logger.warn('PersonalUserRepository.updateBasicInfo:not_found', { userId }); return null; } const { firstName, lastName, email, phone, address } = payload; // Compute diffs const changes = {}; const setProfile = {}; const setUser = {}; if (email !== undefined && email !== currentUser.email) { // Uniqueness check const [conflict] = await conn.query( `SELECT id FROM users WHERE email = ? AND id <> ? LIMIT 1`, [email, userId] ); if (conflict.length) { const err = new Error('Email already in use'); err.code = 'EMAIL_CONFLICT'; throw err; } setUser.email = email; changes.email = { from: currentUser.email, to: email }; } if (firstName !== undefined && firstName !== currentProfile?.first_name) { setProfile.first_name = firstName; changes.firstName = { from: currentProfile?.first_name ?? null, to: firstName }; } if (lastName !== undefined && lastName !== currentProfile?.last_name) { setProfile.last_name = lastName; changes.lastName = { from: currentProfile?.last_name ?? null, to: lastName }; } if (phone !== undefined && phone !== currentProfile?.phone) { setProfile.phone = phone; changes.phone = { from: currentProfile?.phone ?? null, to: phone }; } if (address !== undefined && address !== currentProfile?.address) { setProfile.address = address; changes.address = { from: currentProfile?.address ?? null, to: address }; } // Persist updates if (Object.keys(setUser).length) { await conn.query( `UPDATE users SET ${Object.keys(setUser).map(k => `${k} = ?`).join(', ')} WHERE id = ?`, [...Object.values(setUser), userId] ); // Email changed: reset verification flags if (setUser.email) { await conn.query( `UPDATE user_status SET email_verified = 0, email_verified_at = NULL WHERE user_id = ?`, [userId] ); } } if (Object.keys(setProfile).length) { await conn.query( `UPDATE personal_profiles SET ${Object.keys(setProfile).map(k => `${k} = ?`).join(', ')} WHERE user_id = ?`, [...Object.values(setProfile), userId] ); } // Audit log if (Object.keys(changes).length) { await conn.query( `INSERT INTO user_action_logs (user_id, action, details) VALUES (?, 'profile_update_basic', ?)`, [userId, JSON.stringify({ changes })] ); } logger.info('PersonalUserRepository.updateBasicInfo:success', { userId, changedKeys: Object.keys(changes) }); return this.getMergedProfileById(userId); } async updateBankInfo(userId, payload) { logger.info('PersonalUserRepository.updateBankInfo:start', { userId, payloadPreview: { ...payload, ...(payload?.iban ? { iban: `****${String(payload.iban).slice(-4)}` } : {}) } }); const conn = this.unitOfWork.connection; // Lock current rows to compute diffs safely const [[currentUser]] = await conn.query( `SELECT id, iban FROM users WHERE id = ? FOR UPDATE`, [userId] ); const [[currentProfile]] = await conn.query( `SELECT account_holder_name FROM personal_profiles WHERE user_id = ? FOR UPDATE`, [userId] ); if (!currentUser) { logger.warn('PersonalUserRepository.updateBankInfo:not_found', { userId }); return null; } const { accountHolderName, iban } = payload; const changes = {}; const setUser = {}; const setProfile = {}; if (iban !== undefined && iban !== currentUser.iban) { setUser.iban = iban; changes.iban = { from: currentUser.iban ? `****${String(currentUser.iban).slice(-4)}` : null, to: `****${String(iban).slice(-4)}` }; } if (accountHolderName !== undefined && accountHolderName !== currentProfile?.account_holder_name) { setProfile.account_holder_name = accountHolderName; changes.accountHolderName = { from: currentProfile?.account_holder_name ?? null, to: accountHolderName }; } logger.info('PersonalUserRepository.updateBankInfo:diff', { userId, willUpdateUsers: Object.keys(setUser), willUpdateProfile: Object.keys(setProfile), changes }); if (Object.keys(setUser).length) { await conn.query( `UPDATE users SET ${Object.keys(setUser).map(k => `${k} = ?`).join(', ')} WHERE id = ?`, [...Object.values(setUser), userId] ); } if (Object.keys(setProfile).length) { await conn.query( `UPDATE personal_profiles SET ${Object.keys(setProfile).map(k => `${k} = ?`).join(', ')} WHERE user_id = ?`, [...Object.values(setProfile), userId] ); } if (Object.keys(changes).length) { await conn.query( `INSERT INTO user_action_logs (user_id, action, details) VALUES (?, 'profile_update_bank', ?)`, [userId, JSON.stringify({ changes })] ); } logger.info('PersonalUserRepository.updateBankInfo:success', { userId, changedKeys: Object.keys(changes) }); return this.getMergedProfileById(userId); } } module.exports = PersonalUserRepository;