Initial commit

This commit is contained in:
Lorenzo Sanesi
2026-03-26 22:46:03 +01:00
commit 18e0e04700
80 changed files with 13114 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
const { ValidationError } = require('../shared/errors');
const config = require('../config');
class BetsService {
constructor({ betsRepository, usersRepository, dataStitchingService }) {
this.betsRepository = betsRepository;
this.usersRepository = usersRepository;
this.dataStitchingService = dataStitchingService;
}
async getAllBets() {
const bets = await this.betsRepository.findAll();
return await this.dataStitchingService.populateBetsWithUsers(bets);
}
async getBetById(betId) {
const bet = await this.betsRepository.findById(betId);
const populated = await this.dataStitchingService.populateBetsWithUsers([bet]);
return populated[0];
}
async getUserBets(userId) {
const bets = await this.betsRepository.findByUser(userId);
return await this.dataStitchingService.populateBetsWithUsers(bets);
}
async getGroupBets(groupId) {
const bets = await this.betsRepository.findByGroup(groupId);
return await this.dataStitchingService.populateBetsWithUsers(bets);
}
async getPublicBetsExcludingUser(userId) {
const bets = await this.betsRepository.findPublicBetsExcludingUser(userId);
return await this.dataStitchingService.populateBetsWithUsers(bets);
}
async createBet(betData) {
const { title, description, stake, participants, isPublic, groupId } = betData;
if (!title || title.trim() === '') {
throw new ValidationError('Bet title is required');
}
if (!stake || stake.trim() === '') {
throw new ValidationError('Bet stake is required');
}
if (!participants || !Array.isArray(participants) || participants.length === 0) {
throw new ValidationError('At least one participant is required');
}
const normalizedParticipants = participants.map((p) => ({
...p,
user: parseInt(p.user, 10),
}));
const userIds = normalizedParticipants.map((p) => p.user);
for (const userId of userIds) {
await this.usersRepository.findById(userId);
}
const bet = await this.betsRepository.create({
title,
description,
stake,
participants: normalizedParticipants,
isPublic: isPublic || false,
groupId: groupId || null,
});
const populated = await this.dataStitchingService.populateBetsWithUsers([bet]);
return populated[0];
}
async updateBet(betId, updateData) {
if (updateData.title && updateData.title.trim() === '') {
throw new ValidationError('Bet title cannot be empty');
}
const bet = await this.betsRepository.update(betId, updateData);
const populated = await this.dataStitchingService.populateBetsWithUsers([bet]);
return populated[0];
}
async deleteBet(betId) {
return await this.betsRepository.delete(betId);
}
async declareWinner(betId, winnerId) {
await this.usersRepository.findById(winnerId);
const bet = await this.betsRepository.update(betId, {
winner: winnerId,
status: config.BET_STATUS.COMPLETED,
endDate: new Date(),
});
const populated = await this.dataStitchingService.populateBetsWithUsers([bet]);
return populated[0];
}
}
module.exports = BetsService;
@@ -0,0 +1,41 @@
class DataStitchingService {
constructor({ usersRepository }) {
this.usersRepository = usersRepository;
}
async populateBetsWithUsers(bets) {
if (!bets || bets.length === 0) return [];
const userIds = new Set();
bets.forEach((bet) => {
bet.participants.forEach((p) => userIds.add(p.user));
if (bet.winner) {
userIds.add(bet.winner);
}
});
if (userIds.size === 0)
return bets.map((b) => (typeof b.toJSON === 'function' ? b.toJSON() : b));
const users = await this.usersRepository.findAll({
where: { id: Array.from(userIds) },
});
const userMap = new Map(
users.map((u) => [u.id, typeof u.toJSON === 'function' ? u.toJSON() : u])
);
return bets.map((bet) => {
const betJson = typeof bet.toJSON === 'function' ? bet.toJSON() : bet;
betJson.participants = betJson.participants.map((p) => ({
...p,
user: userMap.get(p.user) || null,
}));
if (betJson.winner) {
betJson.winner = userMap.get(betJson.winner) || null;
}
return betJson;
});
}
}
module.exports = DataStitchingService;
+102
View File
@@ -0,0 +1,102 @@
const { ValidationError, ConflictError } = require('../shared/errors');
const { Op } = require('sequelize');
class FriendsService {
constructor({
friendshipsRepository,
friendRequestsRepository,
usersRepository,
sequelize,
}) {
this.friendshipsRepository = friendshipsRepository;
this.friendRequestsRepository = friendRequestsRepository;
this.usersRepository = usersRepository;
this.sequelize = sequelize;
}
async getFriendsByUserId(userId) {
return await this.friendshipsRepository.findByUserId(userId);
}
async getPendingRequests(userId) {
return await this.friendRequestsRepository.findPendingByUserId(userId);
}
async sendFriendRequest(fromUserId, identifier) {
let toUser;
if (typeof identifier === 'number' || !isNaN(identifier)) {
toUser = await this.usersRepository.findById(parseInt(identifier, 10));
} else {
toUser = await this.usersRepository.findOne({
[Op.or]: [{ name: identifier }, { email: identifier }],
});
if (!toUser) {
throw new ValidationError(
'User not found with provided name or email'
);
}
}
if (toUser.id === fromUserId) {
throw new ValidationError('Cannot send friend request to yourself');
}
const friendshipExists = await this.friendshipsRepository.exists(
fromUserId,
toUser.id
);
if (friendshipExists) {
throw new ConflictError('You are already friends with this user');
}
const existingRequest = await this.friendRequestsRepository.findExisting(
fromUserId,
toUser.id
);
if (existingRequest) {
throw new ConflictError('Friend request already exists');
}
return await this.friendRequestsRepository.create(fromUserId, toUser.id);
}
async acceptFriendRequest(requestId, userId) {
const request = await this.friendRequestsRepository.findById(requestId);
if (request.toUserId !== userId) {
throw new ValidationError('You can only accept requests sent to you');
}
if (request.status !== 'PENDING') {
throw new ValidationError('This request has already been processed');
}
const t = await this.sequelize.transaction();
try {
await this.friendRequestsRepository.updateStatus(requestId, 'ACCEPTED');
await this.friendshipsRepository.create(request.fromUserId, request.toUserId);
await this.friendRequestsRepository.delete(requestId);
await t.commit();
return { message: 'Friend request accepted successfully' };
} catch (error) {
await t.rollback();
throw error;
}
}
async rejectFriendRequest(requestId, userId) {
const request = await this.friendRequestsRepository.findById(requestId);
if (request.toUserId !== userId && request.fromUserId !== userId) {
throw new ValidationError('You can only manage your own requests');
}
await this.friendRequestsRepository.delete(requestId);
return { message: 'Friend request rejected successfully' };
}
}
module.exports = FriendsService;
+76
View File
@@ -0,0 +1,76 @@
const { ValidationError } = require('../shared/errors');
class GroupsService {
constructor({ groupsRepository, usersRepository }) {
this.groupsRepository = groupsRepository;
this.usersRepository = usersRepository;
}
async getAllGroups() {
return await this.groupsRepository.findAll({
include: [
{
model: this.usersRepository.User,
as: 'members',
through: { attributes: [] },
},
],
});
}
async getGroupById(groupId) {
return await this.groupsRepository.findById(groupId, true);
}
async createGroup(groupData) {
const { name, members } = groupData;
if (!name || name.trim() === '') {
throw new ValidationError('Group name is required');
}
if (!members || !Array.isArray(members) || members.length === 0) {
throw new ValidationError('At least one member is required');
}
for (const userId of members) {
await this.usersRepository.findById(userId);
}
const group = await this.groupsRepository.create({ name });
return await this.groupsRepository.addMembers(group.id, members);
}
async updateGroup(groupId, updateData) {
const { name } = updateData;
if (name && name.trim() === '') {
throw new ValidationError('Group name cannot be empty');
}
return await this.groupsRepository.update(groupId, { name });
}
async deleteGroup(groupId) {
return await this.groupsRepository.delete(groupId);
}
async addMembersToGroup(groupId, userIds) {
if (!userIds || !Array.isArray(userIds) || userIds.length === 0) {
throw new ValidationError('User IDs are required');
}
for (const userId of userIds) {
await this.usersRepository.findById(userId);
}
return await this.groupsRepository.addMembers(groupId, userIds);
}
async removeMemberFromGroup(groupId, userId) {
await this.usersRepository.findById(userId);
return await this.groupsRepository.removeMember(groupId, userId);
}
}
module.exports = GroupsService;
+36
View File
@@ -0,0 +1,36 @@
const { ValidationError } = require('../shared/errors');
class UsersService {
constructor({ usersRepository }) {
this.usersRepository = usersRepository;
}
async getAllUsers() {
return await this.usersRepository.findAll();
}
async getUserById(id) {
return await this.usersRepository.findById(id);
}
async updateUserProfile(id, updateData) {
const { name, email, avatarUrl } = updateData;
if (name && name.trim() === '') {
throw new ValidationError('Name cannot be empty');
}
if (email && email.trim() === '') {
throw new ValidationError('Email cannot be empty');
}
const dataToUpdate = {};
if (name !== undefined) dataToUpdate.name = name;
if (email !== undefined) dataToUpdate.email = email;
if (avatarUrl !== undefined) dataToUpdate.avatarUrl = avatarUrl;
return await this.usersRepository.update(id, dataToUpdate);
}
}
module.exports = UsersService;