import { pool } from '../config/database.js';
import { User, CreateUserRequest, UpdateUserRequest, UserResponse } from '../models/User.js';
import { $log } from '@tsed/logger';
import bcrypt from 'bcryptjs';
import { randomUUID } from 'crypto';
export class UserService {
async createUser(userData: CreateUserRequest): Promise<UserResponse> {
const connection = await pool.getConnection();
try {
// Check if user already exists
const [existingUsers] = await connection.execute(
'SELECT id FROM users WHERE email = ? AND deleted_at IS NULL',
[userData.email]
);
if (Array.isArray(existingUsers) && existingUsers.length > 0) {
throw new Error('User with this email already exists');
}
// Hash password
const password_hash = await bcrypt.hash(userData.password, 10);
// Generate UUID for user ID
const userId = randomUUID();
// Insert user
const [result] = await connection.execute(
`INSERT INTO users (id, email, password_hash, first_name, last_name, role, company_name, is_active, email_verified_at, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW(), NOW())`,
[
userId,
userData.email,
password_hash,
userData.first_name,
userData.last_name,
userData.role || 'recruiter',
userData.company_name || null,
true
]
);
// Get the created user
const user = await this.getUserById(userId);
if (!user) {
throw new Error('Failed to create user');
}
return this.mapUserToResponse(user);
} catch (error) {
$log.error('Error creating user:', error);
throw error;
} finally {
connection.release();
}
}
async getUserByEmail(email: string): Promise<User | null> {
const connection = await pool.getConnection();
try {
const [rows] = await connection.execute(
'SELECT * FROM users WHERE email = ? AND deleted_at IS NULL',
[email]
);
if (Array.isArray(rows) && rows.length > 0) {
return rows[0] as User;
}
return null;
} catch (error) {
$log.error('Error getting user by email:', error);
throw error;
} finally {
connection.release();
}
}
async getUserById(id: string): Promise<User | null> {
const connection = await pool.getConnection();
try {
const [rows] = await connection.execute(
'SELECT * FROM users WHERE id = ? AND deleted_at IS NULL',
[id]
);
if (Array.isArray(rows) && rows.length > 0) {
return rows[0] as User;
}
return null;
} catch (error) {
$log.error('Error getting user by ID:', error);
throw error;
} finally {
connection.release();
}
}
async updateUser(id: string, userData: UpdateUserRequest): Promise<UserResponse | null> {
const connection = await pool.getConnection();
try {
const updateFields = [];
const values = [];
if (userData.first_name) {
updateFields.push('first_name = ?');
values.push(userData.first_name);
}
if (userData.last_name) {
updateFields.push('last_name = ?');
values.push(userData.last_name);
}
if (userData.company_name !== undefined) {
updateFields.push('company_name = ?');
values.push(userData.company_name);
}
if (userData.avatar_url !== undefined) {
updateFields.push('avatar_url = ?');
values.push(userData.avatar_url);
}
if (userData.is_active !== undefined) {
updateFields.push('is_active = ?');
values.push(userData.is_active);
}
if (updateFields.length === 0) {
throw new Error('No fields to update');
}
updateFields.push('updated_at = NOW()');
values.push(id);
await connection.execute(
`UPDATE users SET ${updateFields.join(', ')} WHERE id = ? AND deleted_at IS NULL`,
values
);
const user = await this.getUserById(id);
return user ? this.mapUserToResponse(user) : null;
} catch (error) {
$log.error('Error updating user:', error);
throw error;
} finally {
connection.release();
}
}
async updateLastLogin(id: string): Promise<void> {
const connection = await pool.getConnection();
try {
await connection.execute(
'UPDATE users SET last_login_at = NOW(), updated_at = NOW() WHERE id = ? AND deleted_at IS NULL',
[id]
);
} catch (error) {
$log.error('Error updating last login:', error);
throw error;
} finally {
connection.release();
}
}
async verifyPassword(user: User, password: string): Promise<boolean> {
return await bcrypt.compare(password, user.password_hash);
}
async changePassword(id: string, newPassword: string): Promise<void> {
const connection = await pool.getConnection();
try {
const password_hash = await bcrypt.hash(newPassword, 10);
await connection.execute(
'UPDATE users SET password_hash = ?, updated_at = NOW() WHERE id = ? AND deleted_at IS NULL',
[password_hash, id]
);
} catch (error) {
$log.error('Error changing password:', error);
throw error;
} finally {
connection.release();
}
}
async softDeleteUser(id: string): Promise<void> {
const connection = await pool.getConnection();
try {
await connection.execute(
'UPDATE users SET deleted_at = NOW(), is_active = FALSE, updated_at = NOW() WHERE id = ? AND deleted_at IS NULL',
[id]
);
} catch (error) {
$log.error('Error soft deleting user:', error);
throw error;
} finally {
connection.release();
}
}
/**
* Get user payment history
*/
async getUserPaymentHistory(userId: string): Promise<any[]> {
const connection = await pool.getConnection();
try {
const [rows] = await connection.execute(`
SELECT
pr.*,
tp.name as package_name
FROM payment_records pr
LEFT JOIN token_packages tp ON pr.token_package_id = tp.id
WHERE pr.user_id = ?
ORDER BY pr.created_at DESC
`, [userId]);
return Array.isArray(rows) ? rows : [];
} catch (error) {
$log.error('Error getting user payment history:', error);
throw error;
} finally {
connection.release();
}
}
/**
* Get user by Stripe customer ID
*/
async getUserByStripeCustomerId(stripeCustomerId: string): Promise<User | null> {
const connection = await pool.getConnection();
try {
const [rows] = await connection.execute(
'SELECT * FROM users WHERE stripe_customer_id = ? AND deleted_at IS NULL',
[stripeCustomerId]
);
if (Array.isArray(rows) && rows.length > 0) {
return rows[0] as User;
}
return null;
} catch (error) {
$log.error('Error getting user by Stripe customer ID:', error);
throw error;
} finally {
connection.release();
}
}
/**
* Update user's Stripe customer ID
*/
async updateUserStripeCustomerId(userId: string, stripeCustomerId: string): Promise<void> {
const connection = await pool.getConnection();
try {
await connection.execute(
'UPDATE users SET stripe_customer_id = ?, updated_at = NOW() WHERE id = ?',
[stripeCustomerId, userId]
);
$log.info(`Updated Stripe customer ID for user: ${userId}`);
} catch (error) {
$log.error('Error updating user Stripe customer ID:', error);
throw error;
} finally {
connection.release();
}
}
/**
* Get user payment statistics
*/
async getUserPaymentStatistics(userId: string): Promise<{
totalSpent: number;
totalPayments: number;
averagePaymentValue: number;
lastPaymentDate: string | null;
}> {
const connection = await pool.getConnection();
try {
const [rows] = await connection.execute(`
SELECT
COUNT(*) as total_payments,
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) as total_spent,
AVG(CASE WHEN status = 'paid' THEN amount ELSE NULL END) as avg_payment_value,
MAX(CASE WHEN status = 'paid' THEN paid_at ELSE NULL END) as last_payment_date
FROM payment_records
WHERE user_id = ?
`, [userId]);
const stats = Array.isArray(rows) ? rows[0] as any : {};
return {
totalSpent: stats.total_spent || 0,
totalPayments: stats.total_payments || 0,
averagePaymentValue: stats.avg_payment_value || 0,
lastPaymentDate: stats.last_payment_date || null,
};
} catch (error) {
$log.error('Error getting user payment statistics:', error);
throw error;
} finally {
connection.release();
}
}
private mapUserToResponse(user: User): UserResponse {
return {
id: user.id,
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
role: user.role,
company_name: user.company_name,
avatar_url: user.avatar_url,
is_active: user.is_active,
last_login_at: user.last_login_at,
email_verified_at: user.email_verified_at,
created_at: user.created_at,
updated_at: user.updated_at
};
}
}