This commit is contained in:
@@ -856,19 +856,46 @@ export class FeederPersistenceService {
|
||||
const matches = await this.prisma.match.findMany({
|
||||
where: {
|
||||
id: { in: matchIds },
|
||||
AND: [
|
||||
{ oddCategories: { some: {} } },
|
||||
oddCategories: { some: {} },
|
||||
OR: [
|
||||
{
|
||||
OR: [
|
||||
{ footballTeamStats: { some: {} } },
|
||||
{ basketballTeamStats: { some: {} } },
|
||||
],
|
||||
sport: "football",
|
||||
footballTeamStats: { some: {} },
|
||||
playerParticipations: { some: { isStarting: true } },
|
||||
},
|
||||
{
|
||||
sport: "basketball",
|
||||
basketballTeamStats: { some: {} },
|
||||
basketballPlayerStats: { some: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
select: { id: true },
|
||||
select: { id: true, sport: true },
|
||||
});
|
||||
return matches.map((m) => m.id);
|
||||
|
||||
const footballIds = matches
|
||||
.filter((m) => m.sport === "football")
|
||||
.map((m) => m.id);
|
||||
const completeFootballIds = new Set<string>();
|
||||
|
||||
if (footballIds.length > 0) {
|
||||
const starterCounts = await this.prisma.matchPlayerParticipation.groupBy({
|
||||
by: ["matchId"],
|
||||
where: {
|
||||
matchId: { in: footballIds },
|
||||
isStarting: true,
|
||||
},
|
||||
_count: { _all: true },
|
||||
});
|
||||
|
||||
for (const row of starterCounts) {
|
||||
if (row._count._all >= 18) completeFootballIds.add(row.matchId);
|
||||
}
|
||||
}
|
||||
|
||||
return matches
|
||||
.filter((m) => m.sport !== "football" || completeFootballIds.has(m.id))
|
||||
.map((m) => m.id);
|
||||
}
|
||||
|
||||
async hasOdds(matchId: string): Promise<boolean> {
|
||||
|
||||
@@ -168,7 +168,7 @@ export class FeederService {
|
||||
// writing to live_matches. Historical scan should only fill matches table.
|
||||
endDate.setDate(endDate.getDate() - 2);
|
||||
|
||||
const stateKey = `historical_scan_state_${sports.join("_")}${targetLeagueIds.length > 0 ? "_filtered" : ""}_desc`;
|
||||
const stateKey = `historical_full_data_v2_state_${sports.join("_")}${targetLeagueIds.length > 0 ? "_filtered" : ""}_desc`;
|
||||
let currentDate: Date | null = null;
|
||||
|
||||
// Resume from saved state
|
||||
@@ -753,10 +753,7 @@ export class FeederService {
|
||||
}
|
||||
|
||||
// Starting Formation & Substitutes (Always for lineups or all)
|
||||
// V20 OPTIMIZATION: Disabled to speed up feeder and reduce 502 errors.
|
||||
// We only use Team Stats for V20 model.
|
||||
/*
|
||||
if (scope === 'all' || scope === 'lineups') {
|
||||
if (scope === "all" || scope === "lineups") {
|
||||
// Starting Formation
|
||||
try {
|
||||
const formationData =
|
||||
@@ -780,7 +777,7 @@ export class FeederService {
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.message?.includes('502')) hasCriticalError = true;
|
||||
if (e.message?.includes("502")) hasCriticalError = true;
|
||||
this.logger.warn(`[${matchId}] Formation failed: ${e.message}`);
|
||||
}
|
||||
|
||||
@@ -807,11 +804,10 @@ export class FeederService {
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.message?.includes('502')) hasCriticalError = true;
|
||||
if (e.message?.includes("502")) hasCriticalError = true;
|
||||
this.logger.warn(`[${matchId}] Subs failed: ${e.message}`);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// Game Stats & Officials
|
||||
if (scope === "all") {
|
||||
@@ -935,6 +931,8 @@ export class FeederService {
|
||||
const missingParts: string[] = [];
|
||||
if (scope === "all" && completedMatch) {
|
||||
if (sport === "football" && !stats) missingParts.push("Stats");
|
||||
if (sport === "football" && participationData.length < 18)
|
||||
missingParts.push("Lineups");
|
||||
if (sport === "basketball" && !basketballTeamStats)
|
||||
missingParts.push("BoxScore");
|
||||
if (oddsArray.length === 0) missingParts.push("Odds");
|
||||
|
||||
@@ -588,6 +588,10 @@ export class MatchesService {
|
||||
teamStats: [],
|
||||
playerParticipations: (() => {
|
||||
const parsed: Array<{ teamId: string; isStarting: boolean; shirtNumber: string | number | null; position: string | null; player: { id: string; name: string } }> = [];
|
||||
const canTrustFeedLineups = displayStatus === "LIVE" || displayStatus === "Finished";
|
||||
if (!canTrustFeedLineups) {
|
||||
return parsed;
|
||||
}
|
||||
if (liveMatch.lineups && typeof liveMatch.lineups === 'object') {
|
||||
const lu = liveMatch.lineups as Record<string, any>;
|
||||
const addPlayers = (teamLu: any, teamId: string | null) => {
|
||||
@@ -630,6 +634,64 @@ export class MatchesService {
|
||||
|
||||
if (!match) return null;
|
||||
|
||||
const detailDisplayStatus = getDisplayMatchStatus({
|
||||
state: match.state,
|
||||
status: match.status,
|
||||
substate: match.substate,
|
||||
scoreHome: match.scoreHome,
|
||||
scoreAway: match.scoreAway,
|
||||
});
|
||||
const canTrustStoredLineups = this.canTrustStoredLineups(detailDisplayStatus);
|
||||
|
||||
if (Array.isArray(match.playerParticipations)) {
|
||||
if (!canTrustStoredLineups) {
|
||||
match.playerParticipations = [];
|
||||
}
|
||||
|
||||
const hasHomeLineup = match.playerParticipations.some(
|
||||
(p: any) => p.teamId === match.homeTeamId && p.isStarting,
|
||||
);
|
||||
const hasAwayLineup = match.playerParticipations.some(
|
||||
(p: any) => p.teamId === match.awayTeamId && p.isStarting,
|
||||
);
|
||||
|
||||
if (!hasHomeLineup || !hasAwayLineup) {
|
||||
const sidelined =
|
||||
match.sidelined && typeof match.sidelined === "object"
|
||||
? (match.sidelined as Record<string, any>)
|
||||
: {};
|
||||
const matchDateMs = Number(match.mstUtc || Date.now());
|
||||
const probableLineups: any[] = [];
|
||||
|
||||
if (!hasHomeLineup && match.homeTeamId) {
|
||||
probableLineups.push(
|
||||
...(await this.buildProbableLineupForTeam({
|
||||
teamId: match.homeTeamId,
|
||||
beforeDateMs: matchDateMs,
|
||||
sidelinedTeamData: sidelined.homeTeam,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasAwayLineup && match.awayTeamId) {
|
||||
probableLineups.push(
|
||||
...(await this.buildProbableLineupForTeam({
|
||||
teamId: match.awayTeamId,
|
||||
beforeDateMs: matchDateMs,
|
||||
sidelinedTeamData: sidelined.awayTeam,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
if (probableLineups.length > 0) {
|
||||
match.playerParticipations = canTrustStoredLineups
|
||||
? [...match.playerParticipations, ...probableLineups]
|
||||
: probableLineups;
|
||||
match.lineupSource = "probable_xi";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Structure odds
|
||||
const odds: Record<
|
||||
string,
|
||||
@@ -732,4 +794,211 @@ export class MatchesService {
|
||||
|
||||
return team?.id || null;
|
||||
}
|
||||
|
||||
private async buildProbableLineupForTeam(params: {
|
||||
teamId: string;
|
||||
beforeDateMs: number;
|
||||
sidelinedTeamData?: any;
|
||||
matchLimit?: number;
|
||||
lookbackDays?: number;
|
||||
maxStalenessDays?: number;
|
||||
}) {
|
||||
const matchLimit = params.matchLimit ?? 5;
|
||||
const lookbackDays = params.lookbackDays ?? 370;
|
||||
const maxStalenessDays = params.maxStalenessDays ?? 120;
|
||||
const beforeDateMs = params.beforeDateMs || Date.now();
|
||||
const minDateMs = Math.max(
|
||||
0,
|
||||
beforeDateMs - lookbackDays * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
const excluded = this.extractSidelinedPlayerIds(params.sidelinedTeamData);
|
||||
|
||||
const rows = await this.prisma.$queryRaw<any[]>`
|
||||
SELECT
|
||||
mpp.player_id AS "playerId",
|
||||
p.name AS "playerName",
|
||||
mpp.position AS "position",
|
||||
mpp.shirt_number AS "shirtNumber",
|
||||
m.id AS "matchId",
|
||||
m.mst_utc AS "mstUtc"
|
||||
FROM match_player_participation mpp
|
||||
JOIN matches m ON m.id = mpp.match_id
|
||||
JOIN players p ON p.id = mpp.player_id
|
||||
WHERE mpp.team_id = ${params.teamId}
|
||||
AND mpp.is_starting = true
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM match_player_participation later_mpp
|
||||
JOIN matches later_m ON later_m.id = later_mpp.match_id
|
||||
WHERE later_mpp.player_id = mpp.player_id
|
||||
AND later_mpp.team_id <> ${params.teamId}
|
||||
AND later_m.mst_utc > m.mst_utc
|
||||
AND later_m.mst_utc < ${BigInt(beforeDateMs)}
|
||||
AND (
|
||||
later_m.status = 'FT'
|
||||
OR later_m.state = 'postGame'
|
||||
OR (later_m.score_home IS NOT NULL AND later_m.score_away IS NOT NULL)
|
||||
)
|
||||
)
|
||||
AND m.id IN (
|
||||
SELECT m2.id
|
||||
FROM matches m2
|
||||
JOIN match_player_participation recent_mpp
|
||||
ON recent_mpp.match_id = m2.id
|
||||
AND recent_mpp.team_id = ${params.teamId}
|
||||
AND recent_mpp.is_starting = true
|
||||
WHERE (m2.home_team_id = ${params.teamId} OR m2.away_team_id = ${params.teamId})
|
||||
AND (
|
||||
m2.status = 'FT'
|
||||
OR m2.state = 'postGame'
|
||||
OR (m2.score_home IS NOT NULL AND m2.score_away IS NOT NULL)
|
||||
)
|
||||
AND m2.mst_utc < ${BigInt(beforeDateMs)}
|
||||
AND m2.mst_utc >= ${BigInt(minDateMs)}
|
||||
GROUP BY m2.id
|
||||
HAVING COUNT(recent_mpp.*) >= 9
|
||||
ORDER BY MAX(m2.mst_utc) DESC
|
||||
LIMIT ${matchLimit}
|
||||
)
|
||||
ORDER BY m.mst_utc DESC
|
||||
`;
|
||||
|
||||
if (!rows.length) return [];
|
||||
|
||||
const latestMst = Math.max(
|
||||
...rows.map((row) => Number(row.mstUtc || 0)),
|
||||
);
|
||||
const ageDays =
|
||||
latestMst > 0
|
||||
? (beforeDateMs - latestMst) / (24 * 60 * 60 * 1000)
|
||||
: Number.POSITIVE_INFINITY;
|
||||
const staleProjection = ageDays > maxStalenessDays;
|
||||
|
||||
const matchOrder = new Map<string, number>();
|
||||
for (const row of rows) {
|
||||
const matchId = String(row.matchId);
|
||||
if (!matchOrder.has(matchId)) {
|
||||
matchOrder.set(matchId, matchOrder.size);
|
||||
}
|
||||
}
|
||||
|
||||
const playerMap = new Map<
|
||||
string,
|
||||
{
|
||||
playerId: string;
|
||||
playerName: string;
|
||||
position: string | null;
|
||||
shirtNumber: number | null;
|
||||
score: number;
|
||||
starts: number;
|
||||
lastSeenRank: number;
|
||||
}
|
||||
>();
|
||||
|
||||
for (const row of rows) {
|
||||
const playerId = String(row.playerId);
|
||||
if (excluded.has(playerId)) continue;
|
||||
|
||||
const rank = matchOrder.get(String(row.matchId)) ?? matchLimit;
|
||||
const recencyWeight = Math.max(1, matchLimit - rank);
|
||||
const score =
|
||||
recencyWeight + (rank === 0 ? 3 : rank === 1 ? 1.5 : 0);
|
||||
const existing = playerMap.get(playerId);
|
||||
|
||||
if (!existing) {
|
||||
playerMap.set(playerId, {
|
||||
playerId,
|
||||
playerName: row.playerName || "Bilinmiyor",
|
||||
position: row.position ?? null,
|
||||
shirtNumber:
|
||||
row.shirtNumber === null || row.shirtNumber === undefined
|
||||
? null
|
||||
: Number(row.shirtNumber),
|
||||
score,
|
||||
starts: 1,
|
||||
lastSeenRank: rank,
|
||||
});
|
||||
} else {
|
||||
existing.score += score;
|
||||
existing.starts += 1;
|
||||
existing.lastSeenRank = Math.min(existing.lastSeenRank, rank);
|
||||
existing.position = existing.position || row.position || null;
|
||||
existing.shirtNumber =
|
||||
existing.shirtNumber ??
|
||||
(row.shirtNumber === null || row.shirtNumber === undefined
|
||||
? null
|
||||
: Number(row.shirtNumber));
|
||||
}
|
||||
}
|
||||
|
||||
const ranked = [...playerMap.values()]
|
||||
.sort((a, b) => {
|
||||
if (b.score !== a.score) return b.score - a.score;
|
||||
if (b.starts !== a.starts) return b.starts - a.starts;
|
||||
return a.lastSeenRank - b.lastSeenRank;
|
||||
})
|
||||
.slice(0, 11);
|
||||
|
||||
const coverage = Math.min(1, ranked.length / 11);
|
||||
const historyScore = Math.min(1, matchOrder.size / matchLimit);
|
||||
const stableCore = ranked.filter((p) => p.starts >= 2).length / 11;
|
||||
const stalenessFactor = Math.max(
|
||||
0.35,
|
||||
Math.min(1, maxStalenessDays / Math.max(ageDays, 1)),
|
||||
);
|
||||
const confidence = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
staleProjection ? 0.58 : 0.88,
|
||||
(coverage * 0.45 + historyScore * 0.25 + stableCore * 0.3) *
|
||||
stalenessFactor,
|
||||
),
|
||||
);
|
||||
|
||||
return ranked.map((p) => ({
|
||||
teamId: params.teamId,
|
||||
isStarting: true,
|
||||
shirtNumber: p.shirtNumber,
|
||||
position: p.position,
|
||||
isProbable: true,
|
||||
lineupSource: "probable_xi",
|
||||
projectionConfidence: Number(confidence.toFixed(3)),
|
||||
projectionAgeDays: Number(ageDays.toFixed(1)),
|
||||
projectionStale: staleProjection,
|
||||
projectionMatchLimit: matchLimit,
|
||||
projectionLookbackDays: lookbackDays,
|
||||
projectionMaxStalenessDays: maxStalenessDays,
|
||||
player: {
|
||||
id: p.playerId,
|
||||
name: p.playerName,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
private extractSidelinedPlayerIds(teamData: any): Set<string> {
|
||||
if (!teamData || typeof teamData !== "object") return new Set();
|
||||
const players = Array.isArray(teamData.players) ? teamData.players : [];
|
||||
return new Set(
|
||||
players
|
||||
.map((player: any) =>
|
||||
String(
|
||||
player?.playerId ??
|
||||
player?.player_id ??
|
||||
player?.id ??
|
||||
player?.personId ??
|
||||
"",
|
||||
),
|
||||
)
|
||||
.filter(Boolean),
|
||||
);
|
||||
}
|
||||
|
||||
private canTrustStoredLineups(displayStatus?: string): boolean {
|
||||
const normalized = String(displayStatus || "").toLowerCase();
|
||||
return (
|
||||
normalized === "live" ||
|
||||
normalized === "finished" ||
|
||||
normalized === "ft"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,11 +96,10 @@ export class PredictionsController {
|
||||
async getPrediction(
|
||||
@Param("matchId") matchId: string,
|
||||
): Promise<MatchPredictionDto> {
|
||||
// Check cache first - DISABLED per user request to always fetch from scratch
|
||||
// const cached = await this.predictionsService.getCachedPrediction(matchId);
|
||||
// if (cached) {
|
||||
// return cached;
|
||||
// }
|
||||
const cached = await this.predictionsService.getCachedPrediction(matchId);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Get from AI Engine
|
||||
const prediction = await this.predictionsService.getPredictionById(matchId);
|
||||
|
||||
@@ -223,11 +223,13 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
`/v20plus/analyze/${matchId}`,
|
||||
{ simulate: true, is_simulation: true, pre_match_only: true },
|
||||
);
|
||||
await this.recordPredictionRun(matchId, response.data);
|
||||
return this.enrichPredictionResponse(
|
||||
response.data as MatchPredictionDto,
|
||||
const prediction = this.enrichPredictionResponse(
|
||||
response.data,
|
||||
matchContext,
|
||||
);
|
||||
await this.recordPredictionRun(matchId, response.data);
|
||||
await this.cachePrediction(matchId, prediction);
|
||||
return prediction;
|
||||
} catch (e: unknown) {
|
||||
const requestError =
|
||||
e instanceof AiEngineRequestError
|
||||
@@ -235,6 +237,20 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
: new AiEngineRequestError("AI Engine request failed");
|
||||
const status = requestError.status;
|
||||
const detail = requestError.detail || requestError.message;
|
||||
|
||||
if (
|
||||
status === HttpStatus.SERVICE_UNAVAILABLE &&
|
||||
this.hasCooldown(detail)
|
||||
) {
|
||||
const storedPrediction = await this.getStoredPrediction(matchId);
|
||||
if (storedPrediction) {
|
||||
this.logger.warn(
|
||||
`AI Engine cooldown for ${matchId}; returning stored prediction`,
|
||||
);
|
||||
return this.enrichPredictionResponse(storedPrediction, matchContext);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.error(
|
||||
`Direct AI Engine call failed for ${matchId}: status=${status}, detail=${JSON.stringify(detail)}`,
|
||||
);
|
||||
@@ -674,6 +690,11 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
odds: this.normalizeDisplayOdds(odds, impliedProb),
|
||||
implied_prob: impliedProb,
|
||||
ev_edge: evEdge,
|
||||
playable: Boolean(record.playable) && interval.threshold_met,
|
||||
stake_units:
|
||||
Boolean(record.playable) && interval.threshold_met
|
||||
? this.asNumber(record.stake_units)
|
||||
: 0,
|
||||
reasons: Array.isArray(record.reasons)
|
||||
? record.reasons.map((reason) => this.translateReason(String(reason)))
|
||||
: [],
|
||||
@@ -919,15 +940,39 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const normalizedPick = pickName.toUpperCase();
|
||||
const normalizedPick = this.normalizePickKey(pickName);
|
||||
for (const [key, value] of Object.entries(probabilities)) {
|
||||
if (key.toUpperCase() === normalizedPick) {
|
||||
if (this.normalizePickKey(key) === normalizedPick) {
|
||||
return this.asNumber(value);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private normalizePickKey(value: string): string {
|
||||
const normalized = value.trim().toUpperCase();
|
||||
const aliases: Record<string, string> = {
|
||||
ÜST: "OVER",
|
||||
UST: "OVER",
|
||||
OVER: "OVER",
|
||||
ALT: "UNDER",
|
||||
UNDER: "UNDER",
|
||||
"KG VAR": "YES",
|
||||
VAR: "YES",
|
||||
YES: "YES",
|
||||
"KG YOK": "NO",
|
||||
YOK: "NO",
|
||||
NO: "NO",
|
||||
TEK: "ODD",
|
||||
ODD: "ODD",
|
||||
ÇİFT: "EVEN",
|
||||
CIFT: "EVEN",
|
||||
EVEN: "EVEN",
|
||||
};
|
||||
|
||||
return aliases[normalized] ?? normalized;
|
||||
}
|
||||
|
||||
private impliedProbabilityFromOdds(odds: number): number {
|
||||
if (odds <= 1) {
|
||||
return 0;
|
||||
@@ -1132,6 +1177,30 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
return prediction.predictionJson as unknown as MatchPredictionDto;
|
||||
}
|
||||
|
||||
private async getStoredPrediction(
|
||||
matchId: string,
|
||||
): Promise<MatchPredictionDto | null> {
|
||||
const prediction = await this.prisma.prediction.findUnique({
|
||||
where: { matchId },
|
||||
});
|
||||
|
||||
return prediction
|
||||
? (prediction.predictionJson as unknown as MatchPredictionDto)
|
||||
: null;
|
||||
}
|
||||
|
||||
private hasCooldown(detail: unknown): boolean {
|
||||
if (typeof detail === "string") {
|
||||
return detail.includes("cooldownRemainingMs");
|
||||
}
|
||||
|
||||
if (detail && typeof detail === "object") {
|
||||
return "cooldownRemainingMs" in detail;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async ensureSmartCouponDataReady(matchIds: string[]): Promise<void> {
|
||||
const uniqueMatchIds = [...new Set(matchIds.filter((id) => !!id))];
|
||||
if (uniqueMatchIds.length === 0) {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Run Previous-Day Completed Match Sync
|
||||
* Usage: npm run feeder:previous-day
|
||||
*/
|
||||
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { FeederService } from "../modules/feeder/feeder.service";
|
||||
import { Logger } from "@nestjs/common";
|
||||
|
||||
async function bootstrap() {
|
||||
process.env.FEEDER_MODE = "historical";
|
||||
|
||||
const logger = new Logger("FeederPreviousDayScript");
|
||||
|
||||
logger.log("🚀 Starting previous-day completed match sync...");
|
||||
|
||||
// Load AppModule after FEEDER_MODE is set so cron imports can be disabled.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { AppModule } = require("../app.module");
|
||||
const app = await NestFactory.createApplicationContext(AppModule, {
|
||||
logger: ["log", "error", "warn"],
|
||||
});
|
||||
|
||||
try {
|
||||
const feederService = app.get(FeederService);
|
||||
await feederService.runPreviousDayCompletedMatchesScan();
|
||||
logger.log("✅ Previous-day completed match sync completed successfully!");
|
||||
} catch (error: any) {
|
||||
logger.error(`❌ Feeder failed: ${error.message}`);
|
||||
logger.error(error.stack);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
void bootstrap();
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Run Previous-Day Completed Match Sync
|
||||
* Run Full Historical Feeder
|
||||
* Usage: npm run feeder:historical
|
||||
*/
|
||||
|
||||
@@ -12,7 +12,7 @@ async function bootstrap() {
|
||||
|
||||
const logger = new Logger("FeederScript");
|
||||
|
||||
logger.log("🚀 Starting previous-day completed match sync...");
|
||||
logger.log("🚀 Starting full historical feeder...");
|
||||
|
||||
// Load AppModule after FEEDER_MODE is set so cron imports can be disabled.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
@@ -23,8 +23,14 @@ async function bootstrap() {
|
||||
|
||||
try {
|
||||
const feederService = app.get(FeederService);
|
||||
await feederService.runPreviousDayCompletedMatchesScan();
|
||||
logger.log("✅ Previous-day completed match sync completed successfully!");
|
||||
const startDate = process.env.FEEDER_START_DATE || "2023-06-01";
|
||||
const sports = (process.env.FEEDER_SPORTS || "football,basketball")
|
||||
.split(",")
|
||||
.map((sport) => sport.trim())
|
||||
.filter(Boolean) as Array<"football" | "basketball">;
|
||||
|
||||
await feederService.runHistoricalScan(sports, startDate);
|
||||
logger.log("✅ Full historical feeder completed successfully!");
|
||||
} catch (error: any) {
|
||||
logger.error(`❌ Feeder failed: ${error.message}`);
|
||||
logger.error(error.stack);
|
||||
|
||||
Reference in New Issue
Block a user