const ReferralToken = require('../../models/ReferralToken'); const crypto = require('crypto'); const { logger } = require('../../middleware/logger'); class ReferralTokenRepository { constructor(unitOfWork) { this.unitOfWork = unitOfWork; } async createToken({ createdByUserId, expiresAt, maxUses }) { logger.info('ReferralTokenRepository.createToken:start', { createdByUserId, expiresAt, maxUses }); try { const conn = this.unitOfWork.connection; const token = crypto.randomBytes(24).toString('hex'); const unlimited = (maxUses === -1); const usesRemaining = unlimited ? -1 : maxUses; const query = ` INSERT INTO referral_tokens (token, created_by_user_id, expires_at, max_uses, uses_remaining, status) VALUES (?, ?, ?, ?, ?, 'active') `; const [result] = await conn.query(query, [ token, createdByUserId, expiresAt, maxUses, usesRemaining ]); logger.info('ReferralTokenRepository.createToken:success', { id: result.insertId, token, unlimited, usesRemaining }); return new ReferralToken({ id: result.insertId, token, createdByUserId, expiresAt, maxUses, usesRemaining, status: 'active' }); } catch (error) { logger.error('ReferralTokenRepository.createToken:error', { createdByUserId, error: error.message }); throw error; } } async findByToken(token) { logger.info('ReferralTokenRepository.findByToken:start', { token }); try { const conn = this.unitOfWork.connection; const [rows] = await conn.query( `SELECT * FROM referral_tokens WHERE token = ? LIMIT 1`, [token] ); logger.info('ReferralTokenRepository.findByToken:success', { token, found: !!rows.length }); return rows.length ? new ReferralToken(rows[0]) : null; } catch (error) { logger.error('ReferralTokenRepository.findByToken:error', { token, error: error.message }); throw error; } } async getTokensByUser(userId) { logger.info('ReferralTokenRepository.getTokensByUser:start', { userId }); try { const conn = this.unitOfWork.connection; const sql = ` SELECT rt.id, rt.token, rt.created_by_user_id, rt.expires_at, rt.max_uses AS max_uses, rt.uses_remaining AS uses_remaining, rt.status, rt.created_at, rt.updated_at, rt.max_uses_label AS max_uses_label, rt.uses_remaining_label AS uses_remaining_label, (SELECT COUNT(*) FROM referral_token_usage rtu WHERE rtu.referral_token_id = rt.id) AS usage_count, CASE WHEN rt.max_uses = -1 THEN 0 WHEN rt.max_uses IS NULL OR rt.uses_remaining IS NULL THEN 0 ELSE GREATEST(rt.max_uses - rt.uses_remaining, 0) END AS used_count FROM referral_tokens rt WHERE rt.created_by_user_id = ? ORDER BY rt.created_at DESC `; logger.debug('ReferralTokenRepository.getTokensByUser:sql', { sql, params: [userId] }); const [rows] = await conn.query(sql, [userId]); rows.slice(0, 10).forEach(r => { logger.debug('ReferralTokenRepository.getTokensByUser:row', { id: r.id, max_uses: r.max_uses, uses_remaining: r.uses_remaining, max_uses_label: r.max_uses_label, uses_remaining_label: r.uses_remaining_label, used_count: r.used_count }); }); logger.info('ReferralTokenRepository.getTokensByUser:success', { userId, count: rows.length }); return rows; } catch (error) { logger.error('ReferralTokenRepository.getTokensByUser:error', { userId, error: error.message }); throw error; } } async getStatsByUser(userId) { logger.info('ReferralTokenRepository.getStatsByUser:start', { userId }); try { const conn = this.unitOfWork.connection; // Total links generated const [[{ totalLinks }]] = await conn.query( `SELECT COUNT(*) AS totalLinks FROM referral_tokens WHERE created_by_user_id = ?`, [userId] ); // Total active links const [[{ activeLinks }]] = await conn.query( `SELECT COUNT(*) AS activeLinks FROM referral_tokens WHERE created_by_user_id = ? AND status = 'active' AND expires_at > NOW()`, [userId] ); // Total links used (sum of all usages) const [[{ linksUsed }]] = await conn.query( `SELECT COUNT(*) AS linksUsed FROM referral_token_usage rtu JOIN referral_tokens rt ON rtu.referral_token_id = rt.id WHERE rt.created_by_user_id = ?`, [userId] ); // Count personal users referred (from users table) const [[{ personalUsersReferred }]] = await conn.query( `SELECT COUNT(*) AS personalUsersReferred FROM referral_token_usage rtu JOIN referral_tokens rt ON rtu.referral_token_id = rt.id JOIN users u ON rtu.used_by_user_id = u.id WHERE rt.created_by_user_id = ? AND u.user_type = 'personal'`, [userId] ); // Count company users referred (from users table) const [[{ companyUsersReferred }]] = await conn.query( `SELECT COUNT(*) AS companyUsersReferred FROM referral_token_usage rtu JOIN referral_tokens rt ON rtu.referral_token_id = rt.id JOIN users u ON rtu.used_by_user_id = u.id WHERE rt.created_by_user_id = ? AND u.user_type = 'company'`, [userId] ); logger.info('ReferralTokenRepository.getStatsByUser:success', { userId, totalLinks, activeLinks, linksUsed, personalUsersReferred, companyUsersReferred }); return { totalLinks, activeLinks, linksUsed, personalUsersReferred, companyUsersReferred }; } catch (error) { logger.error('ReferralTokenRepository.getStatsByUser:error', { userId, error: error.message }); throw error; } } async deactivateToken(tokenId, userId) { logger.info('ReferralTokenRepository.deactivateToken:start', { tokenId, userId }); try { const conn = this.unitOfWork.connection; // Only allow deactivation if the token belongs to the user const [result] = await conn.query( `UPDATE referral_tokens SET status = 'inactive', deactivation_reason = 'user_deactivated', updated_at = NOW() WHERE id = ? AND created_by_user_id = ? AND status = 'active'`, [tokenId, userId] ); logger.info('ReferralTokenRepository.deactivateToken:success', { tokenId, userId, deactivated: result.affectedRows > 0 }); return result.affectedRows > 0; } catch (error) { logger.error('ReferralTokenRepository.deactivateToken:error', { tokenId, userId, error: error.message }); throw error; } } async getReferrerInfoByToken(token) { logger.info('ReferralTokenRepository.getReferrerInfoByToken:start', { token }); try { const conn = this.unitOfWork.connection; const sql = ` SELECT rt.id AS token_id, rt.token, rt.status, rt.expires_at, rt.max_uses AS max_uses, rt.uses_remaining AS uses_remaining, rt.max_uses_label AS max_uses_label, rt.uses_remaining_label AS uses_remaining_label, CASE WHEN rt.max_uses = -1 THEN 0 WHEN rt.max_uses IS NULL OR rt.uses_remaining IS NULL THEN 0 ELSE GREATEST(rt.max_uses - rt.uses_remaining, 0) END AS used_count, (SELECT COUNT(*) FROM referral_token_usage rtu WHERE rtu.referral_token_id = rt.id) AS usage_count, u.id AS referrer_id, u.email AS referrer_email, u.user_type AS referrer_user_type FROM referral_tokens rt JOIN users u ON rt.created_by_user_id = u.id WHERE rt.token = ? LIMIT 1 `; logger.debug('ReferralTokenRepository.getReferrerInfoByToken:sql', { sql, params: [token] }); const [rows] = await conn.query(sql, [token]); if (rows.length) { const r = rows[0]; logger.debug('ReferralTokenRepository.getReferrerInfoByToken:row', { token: r.token, max_uses: r.max_uses, uses_remaining: r.uses_remaining, max_uses_label: r.max_uses_label, uses_remaining_label: r.uses_remaining_label, used_count: r.used_count }); logger.info('ReferralTokenRepository.getReferrerInfoByToken:success', { token }); } else { logger.warn('ReferralTokenRepository.getReferrerInfoByToken:not_found', { token }); } return rows.length ? rows[0] : null; } catch (error) { logger.error('ReferralTokenRepository.getReferrerInfoByToken:error', { token, error: error.message }); throw error; } } async markReferralTokenUsed(tokenId, usedByUserId, unitOfWork) { logger.info('ReferralTokenRepository.markReferralTokenUsed:start', { tokenId, usedByUserId }); try { const conn = this.unitOfWork.connection; await conn.query( `INSERT INTO referral_token_usage (referral_token_id, used_by_user_id) VALUES (?, ?)`, [tokenId, usedByUserId] ); // Decrement only for limited tokens await conn.query( `UPDATE referral_tokens SET uses_remaining = uses_remaining - 1 WHERE id = ? AND uses_remaining > 0 AND max_uses <> -1 AND uses_remaining <> -1`, [tokenId] ); // Exhaust only when reaches 0 and not unlimited await conn.query( `UPDATE referral_tokens SET status = 'exhausted' WHERE id = ? AND uses_remaining = 0 AND max_uses <> -1`, [tokenId] ); logger.info('ReferralTokenRepository.markReferralTokenUsed:success', { tokenId, usedByUserId }); } catch (error) { logger.error('ReferralTokenRepository.markReferralTokenUsed:error', { tokenId, usedByUserId, error: error.message }); throw error; } } async countActiveTokensByUser(userId) { logger.info('ReferralTokenRepository.countActiveTokensByUser:start', { userId }); try { const conn = this.unitOfWork.connection; const [[{ count }]] = await conn.query( `SELECT COUNT(*) AS count FROM referral_tokens WHERE created_by_user_id = ? AND status = 'active' AND expires_at > NOW()`, [userId] ); logger.info('ReferralTokenRepository.countActiveTokensByUser:success', { userId, count }); return count; } catch (error) { logger.error('ReferralTokenRepository.countActiveTokensByUser:error', { userId, error: error.message }); throw error; } } // ...add more methods as needed (e.g., update, usage)... } module.exports = ReferralTokenRepository;