|
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
|
|
};
|
|
}
|
|
}
|