gg
Deploy Iddaai Backend / build-and-deploy (push) Successful in 2m45s

This commit is contained in:
2026-05-05 14:06:20 +03:00
parent 7a1cf14e2f
commit 9bb8f39bca
8 changed files with 404 additions and 157 deletions
+1
View File
@@ -77,6 +77,7 @@ const historicalFeederMode = process.env.FEEDER_MODE === "historical";
// Configuration
ConfigModule.forRoot({
isGlobal: true,
envFilePath: [".env.local", ".env"],
validate: validateEnv,
load: [
appConfig,
+18 -1
View File
@@ -134,7 +134,13 @@ export class AiEngineClient {
const shouldRetry = attempt < retries && this.isRetriableError(error);
if (!shouldRetry) {
this.registerFailure(error);
// Only register circuit breaker failure for server/network errors, not client errors (4xx)
if (this.isServerError(error)) {
this.registerFailure(error);
} else {
// It's a successful contact with the engine (e.g. 404, 422), so reset failures
this.resetFailures();
}
throw this.toRequestError(error);
}
@@ -220,6 +226,17 @@ export class AiEngineClient {
return status >= 500 || status === 429 || error.code === "ECONNABORTED";
}
private isServerError(error: unknown): boolean {
if (!axios.isAxiosError(error)) {
return true; // Not an axios error, assume internal/network error
}
if (!error.response) {
return true; // Network error, timeout, etc.
}
const status = error.response.status;
return status >= 500 || status === 429;
}
private toRequestError(error: unknown): AiEngineRequestError {
if (error instanceof AiEngineRequestError) {
return error;
@@ -167,7 +167,7 @@ export class FeederPersistenceService {
const leagueId = this.safeString(league.id);
if (leagueId) {
const logoUrl = `https://file.mackolikfeeds.com/areas/${leagueId}`;
const logoUrl = `https://file.mackolikfeeds.com/competitions/${leagueId}`;
const localPath = `public/uploads/competitions/${leagueId}.png`;
imageDownloads.push(
ImageUtils.downloadImage(logoUrl, localPath)
+4 -1
View File
@@ -188,7 +188,10 @@ export class LeaguesService {
{ homeTeamId: teamId1, awayTeamId: teamId2 },
{ homeTeamId: teamId2, awayTeamId: teamId1 },
],
state: "postGame", // Finished matches are stored as "postGame"
AND: [
{ scoreHome: { not: null } },
{ scoreAway: { not: null } },
],
},
include: {
homeTeam: true,
+57 -111
View File
@@ -20,100 +20,53 @@ import {
@Injectable()
export class MatchesService {
private readonly logger = new Logger(MatchesService.name);
private qualifiedLeagueIds: string[] = [];
private topLeagueIds: string[] = [];
constructor(private readonly prisma: PrismaService) {
this.loadQualifiedLeagues();
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.`,
);
const filePath = path.join(process.cwd(), "top_leagues.json");
if (fs.existsSync(filePath)) {
this.topLeagueIds = JSON.parse(fs.readFileSync(filePath, "utf8"));
}
} catch (e) {
this.logger.warn(`Failed to load top_leagues.json: ${e.message}`);
}
}
/**
* Generate flag URL from country code or name using flagcdn.com
* Falls back to a name-to-code mapping for Turkish country names
*/
private getCountryFlagUrl(
countryCode?: string | null,
countryName?: string | null,
): string | undefined {
// If we have a 2-letter ISO code, use it directly
if (countryCode && countryCode.length === 2) {
return `https://flagcdn.com/w40/${countryCode.toLowerCase()}.png`;
}
// Fallback: map common Turkish country names to ISO codes
const COUNTRY_NAME_TO_CODE: Record<string, string> = {
"Türkiye": "tr",
"İngiltere": "gb-eng",
"İspanya": "es",
"İtalya": "it",
"Almanya": "de",
"Fransa": "fr",
"Portekiz": "pt",
"Hollanda": "nl",
"Belçika": "be",
"İskoçya": "gb-sct",
"Galler": "gb-wls",
"İrlanda": "ie",
"Avusturya": "at",
"İsviçre": "ch",
"Yunanistan": "gr",
"Polonya": "pl",
"Çekya": "cz",
"Hırvatistan": "hr",
"Sırbistan": "rs",
"Danimarka": "dk",
"Norveç": "no",
"İsveç": "se",
"Finlandiya": "fi",
"Rusya": "ru",
"Ukrayna": "ua",
"Romanya": "ro",
"Macaristan": "hu",
"Bulgaristan": "bg",
"Arjantin": "ar",
"Brezilya": "br",
"Meksika": "mx",
"ABD": "us",
"Japonya": "jp",
"Güney Kore": "kr",
"Çin": "cn",
"Avustralya": "au",
"Suudi Arabistan": "sa",
"BAE": "ae",
"Katar": "qa",
"Mısır": "eg",
"Güney Afrika": "za",
"Kolombiya": "co",
"Şili": "cl",
"Peru": "pe",
"Ekvador": "ec",
"Paraguay": "py",
"Uruguay": "uy",
"Avrupa": "eu",
"Dünya": "un",
};
if (countryName) {
const code = COUNTRY_NAME_TO_CODE[countryName];
if (code) {
return `https://flagcdn.com/w40/${code}.png`;
private loadQualifiedLeagues() {
try {
const filePath = path.join(process.cwd(), "qualified_leagues.json");
if (fs.existsSync(filePath)) {
this.qualifiedLeagueIds = JSON.parse(fs.readFileSync(filePath, "utf8"));
this.logger.log(
`Loaded ${this.qualifiedLeagueIds.length} qualified leagues for filtering.`,
);
}
} catch (e) {
this.logger.warn(`Failed to load qualified_leagues.json: ${e.message}`);
}
}
return undefined;
/**
* Generate URL for the country flag served from Mackolik
*/
private getCountryFlagUrl(countryId?: string | null): string | undefined {
if (!countryId) return undefined;
return `https://file.mackolikfeeds.com/areas/${countryId}`;
}
/**
* Generate URL for the team logo served from local uploads
*/
private getTeamLogoUrl(teamId?: string | null): string | undefined {
if (!teamId) return undefined;
return `https://file.mackolikfeeds.com/teams/${teamId}`;
}
private getLiveFilter(): Prisma.LiveMatchWhereInput {
@@ -215,10 +168,9 @@ export class MatchesService {
if (leagueId) {
where.leagueId = leagueId;
} else if (this.topLeagueIds.length > 0) {
// Always filter by top leagues when no specific leagueId is requested
// This ensures match list is consistent with the active leagues sidebar
where.leagueId = { in: this.topLeagueIds };
} else if (this.qualifiedLeagueIds.length > 0) {
// Only show matches from qualified leagues (leagues with historical data for AI analysis)
where.leagueId = { in: this.qualifiedLeagueIds };
}
if (status === "LIVE") {
@@ -375,7 +327,9 @@ export class MatchesService {
country: {
id: match.league?.country?.id || "",
name: match.league?.country?.name || "",
flagUrl: match.league?.country?.flagUrl || this.getCountryFlagUrl(null, match.league?.country?.name),
flagUrl:
match.league?.country?.flagUrl ||
this.getCountryFlagUrl(match.league?.country?.id),
},
sport: sport,
matches: [],
@@ -430,11 +384,11 @@ export class MatchesService {
htScoreAway: undefined,
homeTeamName: match.homeTeam?.name || "Unknown",
homeTeamLogo: match.homeTeamId
? `https://file.mackolikfeeds.com/teams/${match.homeTeamId}`
? this.getTeamLogoUrl(match.homeTeamId)
: undefined,
awayTeamName: match.awayTeam?.name || "Unknown",
awayTeamLogo: match.awayTeamId
? `https://file.mackolikfeeds.com/teams/${match.awayTeamId}`
? this.getTeamLogoUrl(match.awayTeamId)
: undefined,
leagueName: match.league?.name,
countryName: match.league?.country?.name,
@@ -442,7 +396,15 @@ export class MatchesService {
});
}
return Array.from(leaguesMap.values());
return Array.from(leaguesMap.values()).sort((a, b) => {
const aIdx = this.topLeagueIds.indexOf(a.id);
const bIdx = this.topLeagueIds.indexOf(b.id);
const aPriority = aIdx === -1 ? 999 : aIdx;
const bPriority = bIdx === -1 ? 999 : bIdx;
if (aPriority !== bPriority) return aPriority - bPriority;
return (a.name || "").localeCompare(b.name || "");
});
}
/**
@@ -465,6 +427,7 @@ export class MatchesService {
const leagues = await this.prisma.$queryRaw<any[]>`
SELECT
l.id, l.name, l.code,
c.id as country_id,
c.name as country_name,
c.flag_url as country_flag,
COUNT(lm.id)::int as match_count,
@@ -474,34 +437,21 @@ export class MatchesService {
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}
${this.qualifiedLeagueIds.length > 0 ? Prisma.sql`AND l.id IN (${Prisma.join(this.qualifiedLeagueIds)})` : Prisma.empty}
AND (
(lm.mst_utc >= ${todayMs} AND lm.status NOT IN (${Prisma.join(finishedStatuses)}) AND COALESCE(lm.state, '') NOT IN (${Prisma.join(finishedStates)}))
OR lm.status IN (${Prisma.join(liveStatuses)})
OR lm.state IN (${Prisma.join(liveStates)})
)
GROUP BY l.id, l.name, l.code, c.name, c.flag_url
GROUP BY l.id, l.name, l.code, c.id, 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
.filter((l) => l.match_count > 0)
.sort((a, b) => {
const aIdx = PRIORITY.findIndex((p) => a.name?.includes(p));
const bIdx = PRIORITY.findIndex((p) => b.name?.includes(p));
const aIdx = this.topLeagueIds.indexOf(a.id);
const bIdx = this.topLeagueIds.indexOf(b.id);
const aPriority = aIdx === -1 ? 999 : aIdx;
const bPriority = bIdx === -1 ? 999 : bIdx;
@@ -514,7 +464,7 @@ export class MatchesService {
name: l.name,
code: l.code,
countryName: l.country_name,
countryFlag: l.country_flag || this.getCountryFlagUrl(null, l.country_name),
countryFlag: l.country_flag || this.getCountryFlagUrl(l.country_id),
matchCount: l.match_count,
liveCount: l.live_count,
}));
@@ -553,13 +503,9 @@ export class MatchesService {
scoreAway: m.scoreAway,
status: m.status,
homeTeamName: m.homeTeam?.name,
homeTeamLogo: m.homeTeamId
? `https://file.mackolikfeeds.com/teams/${m.homeTeamId}`
: null,
homeTeamLogo: m.homeTeamId ? this.getTeamLogoUrl(m.homeTeamId) : null,
awayTeamName: m.awayTeam?.name,
awayTeamLogo: m.awayTeamId
? `https://file.mackolikfeeds.com/teams/${m.awayTeamId}`
: null,
awayTeamLogo: m.awayTeamId ? this.getTeamLogoUrl(m.awayTeamId) : null,
leagueName: m.league?.name,
countryName: m.league?.country?.name,
})),
@@ -860,13 +806,13 @@ export class MatchesService {
homeTeam: {
...match.homeTeam,
logo: match.homeTeamId
? `https://file.mackolikfeeds.com/teams/${match.homeTeamId}`
? this.getTeamLogoUrl(match.homeTeamId)
: match.homeTeam?.logoUrl || null,
},
awayTeam: {
...match.awayTeam,
logo: match.awayTeamId
? `https://file.mackolikfeeds.com/teams/${match.awayTeamId}`
? this.getTeamLogoUrl(match.awayTeamId)
: match.awayTeam?.logoUrl || null,
},
stats: {