cr
This commit is contained in:
@@ -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[];
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user