This commit is contained in:
2026-04-16 17:21:48 +03:00
parent c8fa4c442d
commit c8e7e4e927
116 changed files with 3720 additions and 4197 deletions
+12 -12
View File
@@ -8,21 +8,21 @@ import {
Max,
IsArray,
ValidateNested,
} from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
} from "class-validator";
import { Type } from "class-transformer";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
export enum Sport {
FOOTBALL = 'football',
BASKETBALL = 'basketball',
FOOTBALL = "football",
BASKETBALL = "basketball",
}
export class OddFilterDto {
@ApiProperty({ example: 'Maç Sonucu' })
@ApiProperty({ example: "Maç Sonucu" })
@IsString()
categoryName: string;
@ApiProperty({ example: '1' })
@ApiProperty({ example: "1" })
@IsString()
selectionName: string;
@@ -39,10 +39,10 @@ export class TeamFilterDto {
@IsString()
id: string;
@ApiPropertyOptional({ enum: ['home', 'away', 'any'] })
@ApiPropertyOptional({ enum: ["home", "away", "any"] })
@IsOptional()
@IsString()
role?: 'home' | 'away' | 'any';
role?: "home" | "away" | "any";
}
export class DateRangeDto {
@@ -73,13 +73,13 @@ export class MatchQueryDto {
leagueId?: string;
@ApiPropertyOptional({
description: 'Filter by status: LIVE, Finished, etc.',
description: "Filter by status: LIVE, Finished, etc.",
})
@IsOptional()
@IsString()
status?: string;
@ApiPropertyOptional({ description: 'Single date filter (YYYY-MM-DD)' })
@ApiPropertyOptional({ description: "Single date filter (YYYY-MM-DD)" })
@IsOptional()
@IsDateString()
date?: string;
@@ -153,7 +153,7 @@ export class MatchResponseDto {
@ApiPropertyOptional()
countryName?: string;
@ApiPropertyOptional({ type: 'array' })
@ApiPropertyOptional({ type: "array" })
odds?: any[];
}
+32 -32
View File
@@ -10,26 +10,26 @@ import {
NotFoundException,
BadRequestException,
UseInterceptors,
} from '@nestjs/common';
import { Public } from '../../common/decorators';
} from "@nestjs/common";
import { Public } from "../../common/decorators";
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiQuery,
ApiParam,
} from '@nestjs/swagger';
import { CacheInterceptor, CacheTTL } from '@nestjs/cache-manager';
import { MatchesService } from './matches.service';
} from "@nestjs/swagger";
import { CacheInterceptor, CacheTTL } from "@nestjs/cache-manager";
import { MatchesService } from "./matches.service";
import {
MatchQueryDto,
Sport,
LeagueWithMatchesDto,
ActiveLeagueDto,
} from './dto';
} from "./dto";
@ApiTags('Matches')
@Controller('matches')
@ApiTags("Matches")
@Controller("matches")
export class MatchesController {
constructor(private readonly matchesService: MatchesService) {}
@@ -38,9 +38,9 @@ export class MatchesController {
* Advanced match query with filters
*/
@Public()
@Post('query')
@Post("query")
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Advanced match query with filters' })
@ApiOperation({ summary: "Advanced match query with filters" })
@ApiResponse({ status: 200, type: [LeagueWithMatchesDto] })
async queryMatches(
@Body() queryDto: MatchQueryDto,
@@ -67,18 +67,18 @@ export class MatchesController {
*/
@Public()
@Get()
@ApiOperation({ summary: 'List matches with pagination' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
@ApiQuery({ name: 'sport', required: false, enum: Sport })
@ApiResponse({ status: 200, description: 'Paginated list of matches' })
@ApiOperation({ summary: "List matches with pagination" })
@ApiQuery({ name: "page", required: false, type: Number })
@ApiQuery({ name: "limit", required: false, type: Number })
@ApiQuery({ name: "sport", required: false, enum: Sport })
@ApiResponse({ status: 200, description: "Paginated list of matches" })
async listMatches(
@Query('page') page?: string,
@Query('limit') limit?: string,
@Query('sport') sport?: Sport,
@Query("page") page?: string,
@Query("limit") limit?: string,
@Query("sport") sport?: Sport,
) {
const pageNum = parseInt(page || '1', 10);
const limitNum = parseInt(limit || '20', 10);
const pageNum = parseInt(page || "1", 10);
const limitNum = parseInt(limit || "20", 10);
const sportType = sport || Sport.FOOTBALL;
return this.matchesService.listMatches(sportType, pageNum, limitNum);
@@ -89,14 +89,14 @@ export class MatchesController {
* Get active leagues with match counts
*/
@Public()
@Get('leagues/active')
@Get("leagues/active")
@UseInterceptors(CacheInterceptor)
@CacheTTL(60000) // 1 minute cache
@ApiOperation({ summary: 'Get active leagues with upcoming/live matches' })
@ApiQuery({ name: 'sport', required: false, enum: Sport })
@ApiOperation({ summary: "Get active leagues with upcoming/live matches" })
@ApiQuery({ name: "sport", required: false, enum: Sport })
@ApiResponse({ status: 200, type: [ActiveLeagueDto] })
async getActiveLeagues(
@Query('sport') sport?: Sport,
@Query("sport") sport?: Sport,
): Promise<ActiveLeagueDto[]> {
return this.matchesService.getActiveLeagues(sport || Sport.FOOTBALL);
}
@@ -106,23 +106,23 @@ export class MatchesController {
* Get full match details
*/
@Public()
@Get(':id')
@ApiOperation({ summary: 'Get full match details by ID' })
@ApiParam({ name: 'id', description: 'Match ID' })
@Get(":id")
@ApiOperation({ summary: "Get full match details by ID" })
@ApiParam({ name: "id", description: "Match ID" })
@ApiResponse({
status: 200,
description: 'Match details with lineups, stats, odds, events',
description: "Match details with lineups, stats, odds, events",
})
@ApiResponse({ status: 404, description: 'Match not found' })
async getMatchDetails(@Param('id') id: string) {
@ApiResponse({ status: 404, description: "Match not found" })
async getMatchDetails(@Param("id") id: string) {
if (!id) {
throw new BadRequestException('Match ID is required');
throw new BadRequestException("Match ID is required");
}
const match = await this.matchesService.getMatchDetailsById(id);
if (!match) {
throw new NotFoundException('Match not found');
throw new NotFoundException("Match not found");
}
return match;
+4 -4
View File
@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { MatchesController } from './matches.controller';
import { MatchesService } from './matches.service';
import { DatabaseModule } from '../../database/database.module';
import { Module } from "@nestjs/common";
import { MatchesController } from "./matches.controller";
import { MatchesService } from "./matches.service";
import { DatabaseModule } from "../../database/database.module";
@Module({
imports: [DatabaseModule],
+70 -70
View File
@@ -1,14 +1,14 @@
import { Injectable, Logger } from '@nestjs/common';
import * as fs from 'fs';
import * as path from 'path';
import { PrismaService } from '../../database/prisma.service';
import { Injectable, Logger } from "@nestjs/common";
import * as fs from "fs";
import * as path from "path";
import { PrismaService } from "../../database/prisma.service";
import {
Sport,
MatchQueryDto,
LeagueWithMatchesDto,
ActiveLeagueDto,
} from './dto';
import { Prisma } from '@prisma/client';
} from "./dto";
import { Prisma } from "@prisma/client";
@Injectable()
export class MatchesService {
@@ -21,9 +21,9 @@ export class MatchesService {
private loadTopLeagues() {
try {
const topLeaguesPath = path.join(process.cwd(), 'top_leagues.json');
const topLeaguesPath = path.join(process.cwd(), "top_leagues.json");
if (fs.existsSync(topLeaguesPath)) {
this.topLeagueIds = JSON.parse(fs.readFileSync(topLeaguesPath, 'utf8'));
this.topLeagueIds = JSON.parse(fs.readFileSync(topLeaguesPath, "utf8"));
this.logger.log(
`Loaded ${this.topLeagueIds.length} top leagues for filtering.`,
);
@@ -39,22 +39,22 @@ export class MatchesService {
{
status: {
in: [
'LIVE',
'1H',
'2H',
'HT',
'1Q',
'2Q',
'3Q',
'4Q',
'Playing',
'Half Time',
"LIVE",
"1H",
"2H",
"HT",
"1Q",
"2Q",
"3Q",
"4Q",
"Playing",
"Half Time",
],
},
},
{
state: {
in: ['live', 'firsthalf', 'secondhalf'],
in: ["live", "firsthalf", "secondhalf"],
},
},
],
@@ -66,12 +66,12 @@ export class MatchesService {
OR: [
{
status: {
in: ['Finished', 'Played', 'FT', 'AET', 'PEN', 'Ended'],
in: ["Finished", "Played", "FT", "AET", "PEN", "Ended"],
},
},
{
state: {
in: ['Finished', 'post', 'FT', 'postGame'],
in: ["Finished", "post", "FT", "postGame"],
},
},
],
@@ -134,16 +134,16 @@ export class MatchesService {
if (leagueId) {
where.leagueId = leagueId;
} else if (status === 'LIVE' && this.topLeagueIds.length > 0) {
} else if (status === "LIVE" && this.topLeagueIds.length > 0) {
// Filter live matches by top leagues by default if no leagueId is provided
where.leagueId = { in: this.topLeagueIds };
}
if (status === 'LIVE') {
if (status === "LIVE") {
andConditions.push(this.getLiveFilter());
} else if (status === 'UPCOMING' || status === 'NOT_STARTED') {
} else if (status === "UPCOMING" || status === "NOT_STARTED") {
andConditions.push(this.getUpcomingFilter(Date.now()));
} else if (status === 'FINISHED') {
} else if (status === "FINISHED") {
andConditions.push(this.getFinishedFilter());
} else if (status) {
where.status = status;
@@ -170,9 +170,9 @@ export class MatchesService {
// Team filter
if (team) {
if (team.role === 'home') {
if (team.role === "home") {
where.homeTeamId = team.id;
} else if (team.role === 'away') {
} else if (team.role === "away") {
where.awayTeamId = team.id;
} else {
andConditions.push({
@@ -197,7 +197,7 @@ export class MatchesService {
const matches = await this.prisma.liveMatch.findMany({
where,
select: { id: true },
orderBy: { mstUtc: 'asc' }, // Sort by nearest match first
orderBy: { mstUtc: "asc" }, // Sort by nearest match first
take: limit,
});
@@ -220,7 +220,7 @@ export class MatchesService {
AND: [this.getUpcomingFilter(Date.now())],
},
select: { id: true },
orderBy: { mstUtc: 'asc' },
orderBy: { mstUtc: "asc" },
take: limit,
});
console.log(
@@ -283,16 +283,16 @@ export class MatchesService {
const leaguesMap = new Map<string, LeagueWithMatchesDto>();
for (const match of matches) {
const leagueId = match.leagueId || 'unknown';
const leagueId = match.leagueId || "unknown";
if (!leaguesMap.has(leagueId)) {
leaguesMap.set(leagueId, {
id: leagueId,
name: match.league?.name || 'Unknown League',
name: match.league?.name || "Unknown League",
code: match.league?.code || undefined,
country: {
id: match.league?.country?.id || '',
name: match.league?.country?.name || '',
id: match.league?.country?.id || "",
name: match.league?.country?.name || "",
flagUrl: match.league?.country?.flagUrl || undefined,
},
sport: sport,
@@ -306,13 +306,13 @@ export class MatchesService {
const structuredOdds: any[] = [];
if (
match.odds &&
typeof match.odds === 'object' &&
typeof match.odds === "object" &&
!Array.isArray(match.odds)
) {
const oddsObj = match.odds as Record<string, Record<string, number>>;
for (const [marketName, selections] of Object.entries(oddsObj)) {
const structuredSelections: Record<string, { odd: string }> = {};
if (selections && typeof selections === 'object') {
if (selections && typeof selections === "object") {
for (const [selName, selOdd] of Object.entries(selections)) {
structuredSelections[selName] = { odd: String(selOdd) };
}
@@ -325,15 +325,15 @@ export class MatchesService {
}
// Map status for frontend
let displayStatus = match.status || 'NS';
if (match.state === 'live') {
displayStatus = 'LIVE';
let displayStatus = match.status || "NS";
if (match.state === "live") {
displayStatus = "LIVE";
} else if (
match.state === 'post' ||
match.state === 'FT' ||
match.status === 'Finished'
match.state === "post" ||
match.state === "FT" ||
match.status === "Finished"
) {
displayStatus = 'Finished';
displayStatus = "Finished";
}
league.matches.push({
@@ -349,11 +349,11 @@ export class MatchesService {
scoreAway: match.scoreAway ?? undefined,
htScoreHome: undefined, // LiveMatch table doesn't have HT scores separately usually
htScoreAway: undefined,
homeTeamName: match.homeTeam?.name || 'Unknown',
homeTeamName: match.homeTeam?.name || "Unknown",
homeTeamLogo: match.homeTeamId
? `https://file.mackolikfeeds.com/teams/${match.homeTeamId}`
: undefined,
awayTeamName: match.awayTeam?.name || 'Unknown',
awayTeamName: match.awayTeam?.name || "Unknown",
awayTeamLogo: match.awayTeamId
? `https://file.mackolikfeeds.com/teams/${match.awayTeamId}`
: undefined,
@@ -390,15 +390,15 @@ export class MatchesService {
// Priority sorting (Mackolik style)
const PRIORITY = [
'Trendyol Süper Lig',
'Süper Lig',
'Trendyol 1. Lig',
'1. Lig',
'Premier Lig',
'LaLiga',
'Serie A',
'Bundesliga',
'Ligue 1',
"Trendyol Süper Lig",
"Süper Lig",
"Trendyol 1. Lig",
"1. Lig",
"Premier Lig",
"LaLiga",
"Serie A",
"Bundesliga",
"Ligue 1",
];
return leagues
@@ -410,7 +410,7 @@ export class MatchesService {
const bPriority = bIdx === -1 ? 999 : bIdx;
if (aPriority !== bPriority) return aPriority - bPriority;
return (a.name || '').localeCompare(b.name || '');
return (a.name || "").localeCompare(b.name || "");
})
.map((l) => ({
id: l.id,
@@ -439,7 +439,7 @@ export class MatchesService {
include: { country: true },
},
},
orderBy: { mstUtc: 'desc' },
orderBy: { mstUtc: "desc" },
skip,
take: limit,
}),
@@ -482,7 +482,7 @@ export class MatchesService {
createdAt: stat.createdAt,
};
if ((sport || '').toLowerCase() === 'basketball') {
if ((sport || "").toLowerCase() === "basketball") {
return {
...base,
points: stat.points,
@@ -532,7 +532,7 @@ export class MatchesService {
basketballTeamStats: true,
playerParticipations: {
include: { player: true },
orderBy: [{ isStarting: 'desc' }, { position: 'asc' }],
orderBy: [{ isStarting: "desc" }, { position: "asc" }],
},
playerEvents: {
include: {
@@ -540,7 +540,7 @@ export class MatchesService {
assistPlayer: true,
substitutedOut: true,
},
orderBy: [{ periodId: 'asc' }, { timeMinute: 'asc' }],
orderBy: [{ periodId: "asc" }, { timeMinute: "asc" }],
},
oddCategories: {
include: { selections: true },
@@ -562,15 +562,15 @@ export class MatchesService {
if (liveMatch) {
// Map liveMatch status
let displayStatus = liveMatch.status || 'NS';
if (liveMatch.state === 'live') {
displayStatus = 'LIVE';
let displayStatus = liveMatch.status || "NS";
if (liveMatch.state === "live") {
displayStatus = "LIVE";
} else if (
liveMatch.state === 'post' ||
liveMatch.state === 'FT' ||
liveMatch.status === 'Finished'
liveMatch.state === "post" ||
liveMatch.state === "FT" ||
liveMatch.status === "Finished"
) {
displayStatus = 'Finished';
displayStatus = "Finished";
}
match = {
@@ -607,14 +607,14 @@ export class MatchesService {
if (
match.isLiveSource &&
match.odds &&
typeof match.odds === 'object' &&
typeof match.odds === "object" &&
!Array.isArray(match.odds)
) {
// Parse JSON odds from LiveMatch
const oddsObj = match.odds as Record<string, Record<string, number>>;
for (const [marketName, selections] of Object.entries(oddsObj)) {
odds[marketName] = {};
if (selections && typeof selections === 'object') {
if (selections && typeof selections === "object") {
for (const [selName, selOdd] of Object.entries(selections)) {
odds[marketName][selName] = { odd: String(selOdd) };
}
@@ -628,7 +628,7 @@ export class MatchesService {
for (const sel of cat.selections) {
if (sel.name) {
odds[cat.name][sel.name] = {
odd: sel.oddValue || '',
odd: sel.oddValue || "",
sov: sel.sov ?? undefined,
};
}
@@ -637,7 +637,7 @@ export class MatchesService {
}
const sportStats =
match.sport === 'basketball'
match.sport === "basketball"
? match.basketballTeamStats || []
: match.footballTeamStats || [];
const normalizedTeamStats = sportStats.map((s: any) =>
@@ -692,7 +692,7 @@ export class MatchesService {
// Fuzzy search
team = await this.prisma.team.findFirst({
where: {
name: { contains: trimmedName, mode: 'insensitive' },
name: { contains: trimmedName, mode: "insensitive" },
sport: sport as any,
},
select: { id: true },