This commit is contained in:
Executable
+703
@@ -0,0 +1,703 @@
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class MatchesService {
|
||||
private readonly logger = new Logger(MatchesService.name);
|
||||
private topLeagueIds: string[] = [];
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {
|
||||
this.loadTopLeagues();
|
||||
}
|
||||
|
||||
private loadTopLeagues() {
|
||||
try {
|
||||
const topLeaguesPath = path.join(process.cwd(), 'top_leagues.json');
|
||||
if (fs.existsSync(topLeaguesPath)) {
|
||||
this.topLeagueIds = JSON.parse(fs.readFileSync(topLeaguesPath, 'utf8'));
|
||||
this.logger.log(
|
||||
`Loaded ${this.topLeagueIds.length} top leagues for filtering.`,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.warn(`Failed to load top_leagues.json: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private getLiveFilter(): Prisma.LiveMatchWhereInput {
|
||||
return {
|
||||
OR: [
|
||||
{
|
||||
status: {
|
||||
in: [
|
||||
'LIVE',
|
||||
'1H',
|
||||
'2H',
|
||||
'HT',
|
||||
'1Q',
|
||||
'2Q',
|
||||
'3Q',
|
||||
'4Q',
|
||||
'Playing',
|
||||
'Half Time',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: {
|
||||
in: ['live', 'firsthalf', 'secondhalf'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private getFinishedFilter(): Prisma.LiveMatchWhereInput {
|
||||
return {
|
||||
OR: [
|
||||
{
|
||||
status: {
|
||||
in: ['Finished', 'Played', 'FT', 'AET', 'PEN', 'Ended'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: {
|
||||
in: ['Finished', 'post', 'FT', 'postGame'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private getUpcomingFilter(
|
||||
fromTimestampMs: number,
|
||||
): Prisma.LiveMatchWhereInput {
|
||||
return {
|
||||
AND: [
|
||||
{
|
||||
mstUtc: {
|
||||
gte: BigInt(fromTimestampMs),
|
||||
},
|
||||
},
|
||||
{
|
||||
NOT: {
|
||||
OR: [this.getLiveFilter(), this.getFinishedFilter()],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private getBrowseFilter(fromTimestampMs: number): Prisma.LiveMatchWhereInput {
|
||||
return {
|
||||
AND: [
|
||||
{
|
||||
mstUtc: {
|
||||
gte: BigInt(fromTimestampMs),
|
||||
},
|
||||
},
|
||||
{
|
||||
NOT: this.getFinishedFilter(),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find matches by query criteria
|
||||
*/
|
||||
async findMatches(options: MatchQueryDto): Promise<string[]> {
|
||||
const {
|
||||
sport,
|
||||
limit = 50,
|
||||
leagueId,
|
||||
status,
|
||||
date,
|
||||
team,
|
||||
dateRange,
|
||||
} = options;
|
||||
|
||||
// Build where conditions
|
||||
const where: Prisma.LiveMatchWhereInput = {
|
||||
sport: sport as any,
|
||||
};
|
||||
const andConditions: Prisma.LiveMatchWhereInput[] = [];
|
||||
|
||||
if (leagueId) {
|
||||
where.leagueId = leagueId;
|
||||
} 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') {
|
||||
andConditions.push(this.getLiveFilter());
|
||||
} else if (status === 'UPCOMING' || status === 'NOT_STARTED') {
|
||||
andConditions.push(this.getUpcomingFilter(Date.now()));
|
||||
} else if (status === 'FINISHED') {
|
||||
andConditions.push(this.getFinishedFilter());
|
||||
} else if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
// Date filter
|
||||
if (date) {
|
||||
const d = new Date(date);
|
||||
const startOfDay = new Date(d);
|
||||
startOfDay.setUTCHours(0, 0, 0, 0);
|
||||
const endOfDay = new Date(d);
|
||||
endOfDay.setUTCHours(23, 59, 59, 999);
|
||||
|
||||
where.mstUtc = {
|
||||
gte: BigInt(startOfDay.getTime()),
|
||||
lte: BigInt(endOfDay.getTime()),
|
||||
};
|
||||
} else if (dateRange) {
|
||||
where.mstUtc = {
|
||||
gte: BigInt(new Date(dateRange.from).getTime()),
|
||||
lte: BigInt(new Date(dateRange.to).getTime()),
|
||||
};
|
||||
}
|
||||
|
||||
// Team filter
|
||||
if (team) {
|
||||
if (team.role === 'home') {
|
||||
where.homeTeamId = team.id;
|
||||
} else if (team.role === 'away') {
|
||||
where.awayTeamId = team.id;
|
||||
} else {
|
||||
andConditions.push({
|
||||
OR: [{ homeTeamId: team.id }, { awayTeamId: team.id }],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Default date filter: From today onwards if no specific filter
|
||||
if (!date && !dateRange && !status) {
|
||||
const today = new Date();
|
||||
today.setUTCHours(0, 0, 0, 0); // Start of today in UTC
|
||||
|
||||
andConditions.push(this.getBrowseFilter(today.getTime()));
|
||||
}
|
||||
|
||||
if (andConditions.length > 0) {
|
||||
where.AND = andConditions;
|
||||
}
|
||||
|
||||
// Switch to live_matches table
|
||||
const matches = await this.prisma.liveMatch.findMany({
|
||||
where,
|
||||
select: { id: true },
|
||||
orderBy: { mstUtc: 'asc' }, // Sort by nearest match first
|
||||
take: limit,
|
||||
});
|
||||
|
||||
return matches.map((m) => m.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find upcoming matches from the live matches table
|
||||
* Used for Coupon Generator when no specific matches are selected
|
||||
*/
|
||||
async findUpcomingMatches(
|
||||
sport: Sport,
|
||||
limit: number = 50,
|
||||
): Promise<string[]> {
|
||||
console.log(`[MatchesService] Finding upcoming matches for ${sport}`);
|
||||
|
||||
const matches = await this.prisma.liveMatch.findMany({
|
||||
where: {
|
||||
sport: sport as any,
|
||||
AND: [this.getUpcomingFilter(Date.now())],
|
||||
},
|
||||
select: { id: true },
|
||||
orderBy: { mstUtc: 'asc' },
|
||||
take: limit,
|
||||
});
|
||||
console.log(
|
||||
`[MatchesService] Found ${matches.length} upcoming matches from live_matches`,
|
||||
);
|
||||
|
||||
return matches.map((m) => m.id);
|
||||
}
|
||||
|
||||
async filterUpcomingMatchIds(
|
||||
matchIds: string[],
|
||||
sport: Sport,
|
||||
): Promise<string[]> {
|
||||
const uniqueIds = [...new Set(matchIds.filter((id) => !!id))];
|
||||
|
||||
if (uniqueIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const matches = await this.prisma.liveMatch.findMany({
|
||||
where: {
|
||||
id: { in: uniqueIds },
|
||||
sport: sport as any,
|
||||
AND: [this.getUpcomingFilter(Date.now())],
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
return matches.map((match) => match.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get matches structured by league (from live_matches table)
|
||||
*/
|
||||
async getMatchesAndStructureByIds(
|
||||
matchIds: string[],
|
||||
sport: Sport,
|
||||
): Promise<LeagueWithMatchesDto[]> {
|
||||
if (!matchIds.length) return [];
|
||||
|
||||
const matches = await this.prisma.liveMatch.findMany({
|
||||
where: { id: { in: matchIds } },
|
||||
include: {
|
||||
league: {
|
||||
include: {
|
||||
country: true,
|
||||
},
|
||||
},
|
||||
homeTeam: true,
|
||||
awayTeam: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Sort matches by time (ASC) before grouping to ensure correct order
|
||||
matches.sort((a, b) =>
|
||||
Number(BigInt(a.mstUtc || 0) - BigInt(b.mstUtc || 0)),
|
||||
);
|
||||
|
||||
// Group by league
|
||||
const leaguesMap = new Map<string, LeagueWithMatchesDto>();
|
||||
|
||||
for (const match of matches) {
|
||||
const leagueId = match.leagueId || 'unknown';
|
||||
|
||||
if (!leaguesMap.has(leagueId)) {
|
||||
leaguesMap.set(leagueId, {
|
||||
id: leagueId,
|
||||
name: match.league?.name || 'Unknown League',
|
||||
code: match.league?.code || undefined,
|
||||
country: {
|
||||
id: match.league?.country?.id || '',
|
||||
name: match.league?.country?.name || '',
|
||||
flagUrl: match.league?.country?.flagUrl || undefined,
|
||||
},
|
||||
sport: sport,
|
||||
matches: [],
|
||||
});
|
||||
}
|
||||
|
||||
const league = leaguesMap.get(leagueId)!;
|
||||
|
||||
// Structure odds from JSON
|
||||
const structuredOdds: any[] = [];
|
||||
if (
|
||||
match.odds &&
|
||||
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') {
|
||||
for (const [selName, selOdd] of Object.entries(selections)) {
|
||||
structuredSelections[selName] = { odd: String(selOdd) };
|
||||
}
|
||||
structuredOdds.push({
|
||||
category_name: marketName,
|
||||
selections: structuredSelections,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Map status for frontend
|
||||
let displayStatus = match.status || 'NS';
|
||||
if (match.state === 'live') {
|
||||
displayStatus = 'LIVE';
|
||||
} else if (
|
||||
match.state === 'post' ||
|
||||
match.state === 'FT' ||
|
||||
match.status === 'Finished'
|
||||
) {
|
||||
displayStatus = 'Finished';
|
||||
}
|
||||
|
||||
league.matches.push({
|
||||
id: match.id,
|
||||
matchName:
|
||||
match.matchName ||
|
||||
`${match.homeTeam?.name} vs ${match.awayTeam?.name}`,
|
||||
matchSlug: match.matchSlug || undefined,
|
||||
mstUtc: Number(match.mstUtc),
|
||||
status: displayStatus,
|
||||
state: match.state || undefined,
|
||||
scoreHome: match.scoreHome ?? undefined,
|
||||
scoreAway: match.scoreAway ?? undefined,
|
||||
htScoreHome: undefined, // LiveMatch table doesn't have HT scores separately usually
|
||||
htScoreAway: undefined,
|
||||
homeTeamName: match.homeTeam?.name || 'Unknown',
|
||||
homeTeamLogo: match.homeTeamId
|
||||
? `https://file.mackolikfeeds.com/teams/${match.homeTeamId}`
|
||||
: undefined,
|
||||
awayTeamName: match.awayTeam?.name || 'Unknown',
|
||||
awayTeamLogo: match.awayTeamId
|
||||
? `https://file.mackolikfeeds.com/teams/${match.awayTeamId}`
|
||||
: undefined,
|
||||
leagueName: match.league?.name,
|
||||
countryName: match.league?.country?.name,
|
||||
odds: structuredOdds,
|
||||
});
|
||||
}
|
||||
|
||||
return Array.from(leaguesMap.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active leagues with match counts
|
||||
*/
|
||||
async getActiveLeagues(sport: Sport): Promise<ActiveLeagueDto[]> {
|
||||
// Use raw query for complex aggregation
|
||||
const leagues = await this.prisma.$queryRaw<any[]>`
|
||||
SELECT
|
||||
l.id, l.name, l.code,
|
||||
c.name as country_name,
|
||||
c.flag_url as country_flag,
|
||||
COUNT(lm.id)::int as match_count,
|
||||
COUNT(CASE WHEN lm.status IN ('LIVE', '1H', '2H', 'HT', '1Q', '2Q', '3Q', '4Q', 'Playing', 'Half Time')
|
||||
OR lm.state IN ('live', 'firsthalf', 'secondhalf') THEN 1 END)::int as live_count
|
||||
FROM live_matches lm
|
||||
JOIN leagues l ON lm.league_id = l.id
|
||||
LEFT JOIN countries c ON l.country_id = c.id
|
||||
WHERE lm.sport = ${sport}
|
||||
${this.topLeagueIds.length > 0 ? Prisma.sql`AND l.id IN (${Prisma.join(this.topLeagueIds)})` : Prisma.empty}
|
||||
GROUP BY l.id, l.name, l.code, c.name, c.flag_url
|
||||
ORDER BY l.name ASC
|
||||
`;
|
||||
|
||||
// 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',
|
||||
];
|
||||
|
||||
return leagues
|
||||
.sort((a, b) => {
|
||||
const aIdx = PRIORITY.findIndex((p) => a.name?.includes(p));
|
||||
const bIdx = PRIORITY.findIndex((p) => b.name?.includes(p));
|
||||
|
||||
const aPriority = aIdx === -1 ? 999 : aIdx;
|
||||
const bPriority = bIdx === -1 ? 999 : bIdx;
|
||||
|
||||
if (aPriority !== bPriority) return aPriority - bPriority;
|
||||
return (a.name || '').localeCompare(b.name || '');
|
||||
})
|
||||
.map((l) => ({
|
||||
id: l.id,
|
||||
name: l.name,
|
||||
code: l.code,
|
||||
countryName: l.country_name,
|
||||
countryFlag: l.country_flag,
|
||||
matchCount: l.match_count,
|
||||
liveCount: l.live_count,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* List matches with pagination
|
||||
*/
|
||||
async listMatches(sport: Sport, page: number = 1, limit: number = 20) {
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const [matches, total] = await Promise.all([
|
||||
this.prisma.match.findMany({
|
||||
where: { sport: sport as any },
|
||||
include: {
|
||||
homeTeam: true,
|
||||
awayTeam: true,
|
||||
league: {
|
||||
include: { country: true },
|
||||
},
|
||||
},
|
||||
orderBy: { mstUtc: 'desc' },
|
||||
skip,
|
||||
take: limit,
|
||||
}),
|
||||
this.prisma.match.count({ where: { sport: sport as any } }),
|
||||
]);
|
||||
|
||||
return {
|
||||
matches: matches.map((m) => ({
|
||||
id: m.id,
|
||||
matchName: m.matchName,
|
||||
matchSlug: m.matchSlug,
|
||||
mstUtc: Number(m.mstUtc),
|
||||
scoreHome: m.scoreHome,
|
||||
scoreAway: m.scoreAway,
|
||||
status: m.status,
|
||||
homeTeamName: m.homeTeam?.name,
|
||||
homeTeamLogo: m.homeTeamId
|
||||
? `https://file.mackolikfeeds.com/teams/${m.homeTeamId}`
|
||||
: null,
|
||||
awayTeamName: m.awayTeam?.name,
|
||||
awayTeamLogo: m.awayTeamId
|
||||
? `https://file.mackolikfeeds.com/teams/${m.awayTeamId}`
|
||||
: null,
|
||||
leagueName: m.league?.name,
|
||||
countryName: m.league?.country?.name,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
private normalizeTeamStat(stat: any, sport?: string) {
|
||||
if (!stat) return null;
|
||||
|
||||
const base = {
|
||||
id: stat.id,
|
||||
matchId: stat.matchId,
|
||||
teamId: stat.teamId,
|
||||
createdAt: stat.createdAt,
|
||||
};
|
||||
|
||||
if ((sport || '').toLowerCase() === 'basketball') {
|
||||
return {
|
||||
...base,
|
||||
points: stat.points,
|
||||
rebounds: stat.rebounds,
|
||||
assists: stat.assists,
|
||||
fgMade: stat.fgMade,
|
||||
fgAttempted: stat.fgAttempted,
|
||||
threePtMade: stat.threePtMade,
|
||||
threePtAttempted: stat.threePtAttempted,
|
||||
ftMade: stat.ftMade,
|
||||
ftAttempted: stat.ftAttempted,
|
||||
steals: stat.steals,
|
||||
blocks: stat.blocks,
|
||||
turnovers: stat.turnovers,
|
||||
q1Score: stat.q1Score,
|
||||
q2Score: stat.q2Score,
|
||||
q3Score: stat.q3Score,
|
||||
q4Score: stat.q4Score,
|
||||
otScore: stat.otScore,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
possessionPercentage: stat.possessionPercentage,
|
||||
shotsOnTarget: stat.shotsOnTarget,
|
||||
shotsOffTarget: stat.shotsOffTarget,
|
||||
totalShots: stat.totalShots,
|
||||
totalPasses: stat.totalPasses,
|
||||
corners: stat.corners,
|
||||
fouls: stat.fouls,
|
||||
offsides: stat.offsides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full match details by ID
|
||||
*/
|
||||
async getMatchDetailsById(matchId: string) {
|
||||
let match: any = await this.prisma.match.findUnique({
|
||||
where: { id: matchId },
|
||||
include: {
|
||||
league: { include: { country: true } },
|
||||
homeTeam: true,
|
||||
awayTeam: true,
|
||||
footballTeamStats: true,
|
||||
basketballTeamStats: true,
|
||||
playerParticipations: {
|
||||
include: { player: true },
|
||||
orderBy: [{ isStarting: 'desc' }, { position: 'asc' }],
|
||||
},
|
||||
playerEvents: {
|
||||
include: {
|
||||
player: true,
|
||||
assistPlayer: true,
|
||||
substitutedOut: true,
|
||||
},
|
||||
orderBy: [{ periodId: 'asc' }, { timeMinute: 'asc' }],
|
||||
},
|
||||
oddCategories: {
|
||||
include: { selections: true },
|
||||
},
|
||||
officials: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!match) {
|
||||
// Try to find in LiveMatch table
|
||||
const liveMatch = await this.prisma.liveMatch.findUnique({
|
||||
where: { id: matchId },
|
||||
include: {
|
||||
league: { include: { country: true } },
|
||||
homeTeam: true,
|
||||
awayTeam: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (liveMatch) {
|
||||
// Map liveMatch status
|
||||
let displayStatus = liveMatch.status || 'NS';
|
||||
if (liveMatch.state === 'live') {
|
||||
displayStatus = 'LIVE';
|
||||
} else if (
|
||||
liveMatch.state === 'post' ||
|
||||
liveMatch.state === 'FT' ||
|
||||
liveMatch.status === 'Finished'
|
||||
) {
|
||||
displayStatus = 'Finished';
|
||||
}
|
||||
|
||||
match = {
|
||||
...liveMatch,
|
||||
matchName:
|
||||
liveMatch.matchName ||
|
||||
`${liveMatch.homeTeam?.name} vs ${liveMatch.awayTeam?.name}`,
|
||||
status: displayStatus,
|
||||
mstUtc: liveMatch.mstUtc,
|
||||
score: {
|
||||
home: liveMatch.scoreHome,
|
||||
away: liveMatch.scoreAway,
|
||||
},
|
||||
date: new Date(Number(liveMatch.mstUtc)),
|
||||
// Fill missing relations with empty arrays
|
||||
teamStats: [],
|
||||
playerParticipations: [],
|
||||
playerEvents: [],
|
||||
oddCategories: [], // Will handle odds parsing below
|
||||
officials: [],
|
||||
isLiveSource: true, // Flag to indicate source
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!match) return null;
|
||||
|
||||
// Structure odds
|
||||
const odds: Record<
|
||||
string,
|
||||
Record<string, { odd: string; sov?: number }>
|
||||
> = {};
|
||||
|
||||
if (
|
||||
match.isLiveSource &&
|
||||
match.odds &&
|
||||
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') {
|
||||
for (const [selName, selOdd] of Object.entries(selections)) {
|
||||
odds[marketName][selName] = { odd: String(selOdd) };
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (match.oddCategories) {
|
||||
// Parse relation odds from Match
|
||||
for (const cat of match.oddCategories) {
|
||||
if (!cat.name) continue;
|
||||
odds[cat.name] = {};
|
||||
for (const sel of cat.selections) {
|
||||
if (sel.name) {
|
||||
odds[cat.name][sel.name] = {
|
||||
odd: sel.oddValue || '',
|
||||
sov: sel.sov ?? undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sportStats =
|
||||
match.sport === 'basketball'
|
||||
? match.basketballTeamStats || []
|
||||
: match.footballTeamStats || [];
|
||||
const normalizedTeamStats = sportStats.map((s: any) =>
|
||||
this.normalizeTeamStat(s, match.sport),
|
||||
);
|
||||
const homeStat = sportStats.find((s: any) => s.teamId === match.homeTeamId);
|
||||
const awayStat = sportStats.find((s: any) => s.teamId === match.awayTeamId);
|
||||
|
||||
return {
|
||||
...match,
|
||||
teamStats: normalizedTeamStats,
|
||||
mstUtc: Number(match.mstUtc),
|
||||
date: match.date || new Date(Number(match.mstUtc)),
|
||||
// Ensure score is in expected format (nested object for frontend if needed, but frontend seems to use match.score.home in some places and match.scoreHome in others.
|
||||
// The match-detail-content uses match.score.home. Match entity has scoreHome/scoreAway fields.
|
||||
// Let's ensure compatibility.
|
||||
score: match.score || { home: match.scoreHome, away: match.scoreAway },
|
||||
stats: {
|
||||
home: this.normalizeTeamStat(homeStat, match.sport),
|
||||
away: this.normalizeTeamStat(awayStat, match.sport),
|
||||
},
|
||||
lineups: {
|
||||
home: match.playerParticipations.filter(
|
||||
(p: any) => p.teamId === match.homeTeamId,
|
||||
),
|
||||
away: match.playerParticipations.filter(
|
||||
(p: any) => p.teamId === match.awayTeamId,
|
||||
),
|
||||
},
|
||||
events: match.playerEvents,
|
||||
odds,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get team ID by name (for legacy compatibility)
|
||||
*/
|
||||
async getTeamIdByName(
|
||||
teamName: string,
|
||||
sport: Sport,
|
||||
): Promise<string | null> {
|
||||
const trimmedName = teamName.trim();
|
||||
|
||||
// Exact match first
|
||||
let team = await this.prisma.team.findFirst({
|
||||
where: { name: trimmedName, sport: sport as any },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (team) return team.id;
|
||||
|
||||
// Fuzzy search
|
||||
team = await this.prisma.team.findFirst({
|
||||
where: {
|
||||
name: { contains: trimmedName, mode: 'insensitive' },
|
||||
sport: sport as any,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
return team?.id || null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user