This commit is contained in:
Executable
+100
@@ -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 };
|
||||
}
|
||||
}
|
||||
Executable
+13
@@ -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 {}
|
||||
Executable
+152
@@ -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[];
|
||||
}
|
||||
Reference in New Issue
Block a user