first (part 3: src directory)
Deploy Iddaai Backend / build-and-deploy (push) Successful in 33s

This commit is contained in:
2026-04-16 15:12:27 +03:00
parent 2f0b85a0c7
commit 182f4aae16
125 changed files with 22552 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
import {
Controller,
Post,
Get,
Body,
HttpCode,
HttpStatus,
ForbiddenException,
} from '@nestjs/common';
import {
ApiTags,
ApiBearerAuth,
ApiOperation,
ApiResponse,
} from '@nestjs/swagger';
import { AnalysisService } from './analysis.service';
import { AnalyzeMatchesDto } from './dto/analysis-request.dto';
import { CurrentUser } from '../../common/decorators';
@ApiTags('Analysis')
@ApiBearerAuth()
@Controller('analysis')
export class AnalysisController {
constructor(private readonly analysisService: AnalysisService) {}
/**
* POST /analysis/analyze-matches
* Analyze multiple matches (coupon generation)
*/
@Post('analyze-matches')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Analyze multiple matches for coupon' })
@ApiResponse({ status: 200, description: 'Analysis successful' })
@ApiResponse({ status: 400, description: 'Invalid input' })
@ApiResponse({ status: 429, description: 'Usage limit exceeded' })
async analyzeMatches(
@CurrentUser() user: any,
@Body() dto: AnalyzeMatchesDto,
) {
const { matchIds } = dto;
// Check usage limit
const isCoupon = matchIds.length > 1;
const canProceed = await this.analysisService.checkUsageLimit(
user.id,
isCoupon,
matchIds.length,
);
if (!canProceed) {
throw new ForbiddenException('You have exceeded your daily usage limit');
}
// Run analysis
const result = await this.analysisService.analyzeCoupon(matchIds, user.id);
if (!result) {
return {
success: false,
message: 'None of the provided matches could be analyzed successfully',
};
}
// Record usage
await this.analysisService.recordUsage(user.id, isCoupon);
return {
success: true,
data: result,
};
}
/**
* POST /analysis/analyze (alias for /analyze-matches - frontend compatibility)
*/
@Post('analyze')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Analyze multiple matches for coupon (alias)',
deprecated: true,
})
async analyzeMatchesAlias(
@CurrentUser() user: any,
@Body() dto: AnalyzeMatchesDto,
) {
return this.analyzeMatches(user, dto);
}
/**
* GET /analysis/history
* Get user's analysis history
*/
@Get('history')
@ApiOperation({ summary: 'Get analysis history' })
@ApiResponse({ status: 200, description: 'History retrieved' })
async getHistory(@CurrentUser() user: any) {
const history = await this.analysisService.getAnalysisHistory(user.id);
return { success: true, data: history };
}
}
+13
View File
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { AnalysisController } from './analysis.controller';
import { AnalysisService } from './analysis.service';
import { DatabaseModule } from '../../database/database.module';
import { ServicesModule } from '../../services/services.module';
@Module({
imports: [DatabaseModule, ServicesModule],
controllers: [AnalysisController],
providers: [AnalysisService],
exports: [AnalysisService],
})
export class AnalysisModule {}
+152
View File
@@ -0,0 +1,152 @@
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../database/prisma.service';
import {
MatchAnalysisService,
AnalysisResult,
} from '../../services/match-analysis.service';
@Injectable()
export class AnalysisService {
private readonly logger = new Logger(AnalysisService.name);
constructor(
private readonly prisma: PrismaService,
private readonly matchAnalysisService: MatchAnalysisService,
) {}
/**
* Analyze multiple matches (coupon)
*/
async analyzeCoupon(matchIds: string[], userId: string): Promise<any> {
this.logger.log(`Analyzing ${matchIds.length} matches for coupon`);
const results: AnalysisResult[] = [];
for (const matchId of matchIds) {
try {
// Get match from DB
const match = await this.prisma.match.findFirst({
where: {
OR: [{ id: matchId }],
},
include: {
league: true,
homeTeam: true,
awayTeam: true,
},
});
// Try live match if not found
const liveMatch = !match
? await this.prisma.liveMatch.findUnique({
where: { id: matchId },
})
: null;
const targetMatch = match || liveMatch;
if (!targetMatch) {
this.logger.warn(`Match not found: ${matchId}`);
continue;
}
// Build URL for analysis
const sport = (targetMatch as any).sport || 'football';
const slug = (targetMatch as any).matchSlug || matchId;
const url = `https://www.mackolik.com/${sport === 'basketball' ? 'basketbol/mac' : 'mac'}/${slug}/${matchId}`;
// Run analysis
const result = await this.matchAnalysisService.analyzeMatch(
url,
userId,
);
results.push(result);
} catch (err: any) {
this.logger.warn(`Analysis failed for ${matchId}: ${err.message}`);
}
}
if (results.length === 0) {
return null;
}
// Combine results into coupon format
return {
totalMatches: matchIds.length,
analyzedMatches: results.length,
matches: results.map((r) => ({
matchDetails: r.matchDetails,
predictions: r.aiAnalysis?.predictions || [],
recommendedBets: r.aiAnalysis?.recommendedBets || [],
confidence: r.aiAnalysis?.confidenceScore || 0,
})),
generatedAt: new Date().toISOString(),
};
}
/**
* Check user usage limit
*/
async checkUsageLimit(
userId: string,
isCoupon: boolean,
matchCount: number,
): Promise<boolean> {
const usageLimit = await this.prisma.usageLimit.findUnique({
where: { userId },
});
if (!usageLimit) {
// Create default limit
await this.prisma.usageLimit.create({
data: {
userId,
analysisCount: 0,
couponCount: 0,
lastResetDate: new Date(),
},
});
return true;
}
// Check limits (default: 10 analyses, 3 coupons per day)
const user = await this.prisma.user.findUnique({ where: { id: userId } });
const isPremium = user?.subscriptionStatus === 'active';
const maxAnalyses = isPremium ? 50 : 10;
const maxCoupons = isPremium ? 10 : 3;
if (isCoupon) {
return usageLimit.couponCount < maxCoupons;
}
return usageLimit.analysisCount + matchCount <= maxAnalyses;
}
/**
* Record usage
*/
async recordUsage(userId: string, isCoupon: boolean): Promise<void> {
if (isCoupon) {
await this.prisma.usageLimit.update({
where: { userId },
data: { couponCount: { increment: 1 } },
});
} else {
await this.prisma.usageLimit.update({
where: { userId },
data: { analysisCount: { increment: 1 } },
});
}
}
/**
* Get user analysis history
*/
async getAnalysisHistory(userId: string, limit: number = 20) {
return this.prisma.analysis.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
take: limit,
});
}
}
@@ -0,0 +1,16 @@
import { IsArray, IsString, ArrayMinSize, ArrayMaxSize } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class AnalyzeMatchesDto {
@ApiProperty({
description: 'List of match IDs to analyze',
example: ['match-1', 'match-2'],
minItems: 1,
maxItems: 20,
})
@IsArray()
@IsString({ each: true })
@ArrayMinSize(1)
@ArrayMaxSize(20)
matchIds: string[];
}