import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; import { and, eq, or } from 'drizzle-orm'; import { ConfigService } from '@nestjs/config'; import { DrizzleService } from '../../database/drizzle.service'; import { chat, like, match, profile, user } from '../../database/schema'; import { NotificationsService } from '../../notifications/notifications.service'; import { RedisService } from '../../redis/redis.service'; import { ChatGateway } from '../../gateways/chat.gateway'; import { CreateLikeDto } from './dto/create-like.dto'; @Injectable() export class LikesService { constructor( private readonly drizzleService: DrizzleService, private readonly notificationsService: NotificationsService, private readonly redisService: RedisService, private readonly configService: ConfigService, private readonly chatGateway: ChatGateway, ) {} async createLike(userId: string, dto: CreateLikeDto) { await this.assertProfileOwnership(userId, dto.sourceProfileId); if (dto.sourceProfileId === dto.targetProfileId) { throw new BadRequestException('Cannot like yourself'); } const [sourceProfile] = await this.drizzleService.db .select({ activeChatId: profile.activeChatId }) .from(profile) .where(eq(profile.id, dto.sourceProfileId)) .limit(1); if (sourceProfile?.activeChatId) { throw new BadRequestException( 'У вас уже есть активный матч. Завершите текущий диалог перед поиском.', ); } const existing = await this.drizzleService.db .select() .from(like) .where( and( eq(like.sourceProfileId, dto.sourceProfileId), eq(like.targetProfileId, dto.targetProfileId), ), ) .limit(1); if (existing.length > 0) throw new BadRequestException('Already reacted to this profile'); const [newLike] = await this.drizzleService.db .insert(like) .values({ sourceProfileId: dto.sourceProfileId, targetProfileId: dto.targetProfileId, type: dto.type, }) .returning(); if (dto.type === 'like') { return this.checkAndCreateMatch(dto.sourceProfileId, dto.targetProfileId, newLike); } return { like: newLike, match: null, chat: null }; } async getMyMatches(userId: string, profileId: string) { await this.assertProfileOwnership(userId, profileId); return this.drizzleService.db .select() .from(match) .where(or(eq(match.profile1Id, profileId), eq(match.profile2Id, profileId))) .orderBy(match.createdAt); } private async checkAndCreateMatch(profileId1: string, profileId2: string, newLike: any) { const reverseLike = await this.drizzleService.db .select() .from(like) .where( and( eq(like.sourceProfileId, profileId2), eq(like.targetProfileId, profileId1), eq(like.type, 'like'), ), ) .limit(1); if (reverseLike.length === 0) return { like: newLike, match: null, chat: null }; const [targetProfile] = await this.drizzleService.db .select({ activeChatId: profile.activeChatId }) .from(profile) .where(eq(profile.id, profileId2)) .limit(1); if (targetProfile?.activeChatId) { return { like: newLike, match: null, chat: null }; } const existingMatch = await this.drizzleService.db .select() .from(match) .where( or( and(eq(match.profile1Id, profileId1), eq(match.profile2Id, profileId2)), and(eq(match.profile1Id, profileId2), eq(match.profile2Id, profileId1)), ), ) .limit(1); if (existingMatch.length > 0) { const existingChat = await this.drizzleService.db .select() .from(chat) .where( or( and(eq(chat.profile1Id, profileId1), eq(chat.profile2Id, profileId2)), and(eq(chat.profile1Id, profileId2), eq(chat.profile2Id, profileId1)), ), ) .limit(1); return { like: newLike, match: existingMatch[0], chat: existingChat[0] ?? null }; } const [newMatch] = await this.drizzleService.db .insert(match) .values({ profile1Id: profileId1, profile2Id: profileId2 }) .returning(); const [newChat] = await this.drizzleService.db .insert(chat) .values({ profile1Id: profileId1, profile2Id: profileId2, status: 'active', } as any) .returning(); await this.drizzleService.db .update(profile) .set({ activeChatId: newChat.id } as any) .where(or(eq(profile.id, profileId1), eq(profile.id, profileId2))); await this.notifyMatch(profileId1, profileId2, newMatch.id, newChat.id); return { like: newLike, match: newMatch, chat: newChat }; } private async notifyMatch(profileId1: string, profileId2: string, matchId: string, chatId: string) { this.chatGateway.emitToProfile(profileId1, 'match_created', { matchId, chatId, partnerProfileId: profileId2, }); this.chatGateway.emitToProfile(profileId2, 'match_created', { matchId, chatId, partnerProfileId: profileId1, }); const profiles = await this.drizzleService.db .select({ userId: profile.userId }) .from(profile) .where(or(eq(profile.id, profileId1), eq(profile.id, profileId2))); for (const p of profiles) { const [u] = await this.drizzleService.db .select({ fcmToken: user.fcmToken }) .from(user) .where(eq(user.id, p.userId)) .limit(1); if (u?.fcmToken) { await this.notificationsService.sendPushNotification( u.fcmToken, 'Новый матч!', 'У вас новый матч. Начните общение прямо сейчас.', { matchId, chatId, type: 'match_created' }, ); } } await this.redisService.publish( 'match:created', JSON.stringify({ matchId, chatId, profileId1, profileId2 }), ); } private async assertProfileOwnership(userId: string, profileId: string) { const [found] = await this.drizzleService.db .select({ userId: profile.userId }) .from(profile) .where(eq(profile.id, profileId)) .limit(1); if (!found) throw new NotFoundException('Profile not found'); if (found.userId !== userId) throw new ForbiddenException('Profile does not belong to you'); } }