first (part 3: src directory)
Deploy Iddaai Backend / build-and-deploy (push) Successful in 33s

This commit is contained in:
2026-04-16 15:12:27 +03:00
parent 2f0b85a0c7
commit 182f4aae16
125 changed files with 22552 additions and 0 deletions
+1461
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,34 @@
import { FeederService } from '../modules/feeder/feeder.service';
import { HistoricalResultsSyncTask } from './historical-results-sync.task';
describe('HistoricalResultsSyncTask', () => {
const runPreviousDayCompletedMatchesScan = jest.fn();
let task: HistoricalResultsSyncTask;
beforeEach(() => {
jest.clearAllMocks();
delete process.env.FEEDER_MODE;
task = new HistoricalResultsSyncTask({
runPreviousDayCompletedMatchesScan,
} as unknown as FeederService);
});
afterEach(() => {
delete process.env.FEEDER_MODE;
});
it('calls feeder service in normal mode', async () => {
await task.syncPreviousDayCompletedMatches();
expect(runPreviousDayCompletedMatchesScan).toHaveBeenCalledTimes(1);
});
it('skips execution in historical feeder mode', async () => {
process.env.FEEDER_MODE = 'historical';
await task.syncPreviousDayCompletedMatches();
expect(runPreviousDayCompletedMatchesScan).not.toHaveBeenCalled();
});
});
+41
View File
@@ -0,0 +1,41 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { FeederService } from '../modules/feeder/feeder.service';
@Injectable()
export class HistoricalResultsSyncTask {
private readonly logger = new Logger(HistoricalResultsSyncTask.name);
constructor(private readonly feederService: FeederService) {}
private shouldSkipInHistoricalMode(jobName: string): boolean {
if (process.env.FEEDER_MODE === 'historical') {
this.logger.debug(`Skipping ${jobName} in historical feeder mode`);
return true;
}
return false;
}
/**
* Pull yesterday's completed matches into the permanent matches table.
*/
@Cron('0 8 * * *', { timeZone: 'Europe/Istanbul' })
async syncPreviousDayCompletedMatches() {
if (this.shouldSkipInHistoricalMode('syncPreviousDayCompletedMatches')) {
return;
}
this.logger.log(
'Starting previous-day completed match sync for football and basketball...',
);
try {
await this.feederService.runPreviousDayCompletedMatchesScan();
this.logger.log('Previous-day completed match sync finished');
} catch (error: any) {
this.logger.error(
`Previous-day completed match sync failed: ${error.message}`,
);
}
}
}
+122
View File
@@ -0,0 +1,122 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { PrismaService } from '../database/prisma.service';
@Injectable()
export class LimitResetterTask {
private readonly logger = new Logger(LimitResetterTask.name);
constructor(private readonly prisma: PrismaService) {}
private shouldSkipInHistoricalMode(jobName: string): boolean {
if (process.env.FEEDER_MODE === 'historical') {
this.logger.debug(`Skipping ${jobName} in historical feeder mode`);
return true;
}
return false;
}
/**
* Reset usage limits daily at 03:00 (Europe/Istanbul)
*/
@Cron('0 3 * * *', { timeZone: 'Europe/Istanbul' })
async resetUsageLimits() {
if (this.shouldSkipInHistoricalMode('resetUsageLimits')) return;
this.logger.log('Starting daily usage limit reset job...');
try {
const today = new Date();
today.setHours(0, 0, 0, 0);
// Reset all limits that were last reset before today
const result = await this.prisma.usageLimit.updateMany({
where: {
lastResetDate: { lt: today },
},
data: {
analysisCount: 0,
couponCount: 0,
lastResetDate: today,
},
});
if (result.count > 0) {
this.logger.log(
`Usage limits for ${result.count} users have been reset`,
);
} else {
this.logger.log('No user limits needed resetting');
}
} catch (error: any) {
this.logger.error(`Limit reset job failed: ${error.message}`);
}
}
/**
* Clean up old predictions (older than 30 days)
*/
@Cron('0 4 * * *', { timeZone: 'Europe/Istanbul' })
async cleanupOldData() {
if (this.shouldSkipInHistoricalMode('cleanupOldData')) return;
this.logger.log('Starting data cleanup job...');
try {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
// Delete old AI prediction logs
const deletedLogs = await this.prisma.aiPredictionsLog.deleteMany({
where: {
createdAt: { lt: thirtyDaysAgo },
},
});
// Delete old live matches (finished more than 1 day ago)
// Historical data is already persisted in the 'matches' table
const oneDayAgo = new Date();
oneDayAgo.setDate(oneDayAgo.getDate() - 1);
const deletedLiveMatches = await this.prisma.liveMatch.deleteMany({
where: {
state: 'Finished',
updatedAt: { lt: oneDayAgo },
},
});
this.logger.log(
`Cleanup complete: ${deletedLogs.count} old logs, ${deletedLiveMatches.count} old live matches`,
);
} catch (error: any) {
this.logger.error(`Cleanup job failed: ${error.message}`);
}
}
/**
* Reset subscription status for expired users
*/
@Cron('0 0 * * *', { timeZone: 'Europe/Istanbul' })
async checkSubscriptions() {
if (this.shouldSkipInHistoricalMode('checkSubscriptions')) return;
this.logger.log('Checking expired subscriptions...');
try {
const now = new Date();
const result = await this.prisma.user.updateMany({
where: {
subscriptionStatus: 'active',
subscriptionExpiresAt: { lt: now },
},
data: {
subscriptionStatus: 'expired',
},
});
if (result.count > 0) {
this.logger.log(`${result.count} subscriptions marked as expired`);
}
} catch (error: any) {
this.logger.error(`Subscription check failed: ${error.message}`);
}
}
}
+177
View File
@@ -0,0 +1,177 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { HttpService } from '@nestjs/axios';
import { PrismaService } from '../database/prisma.service';
import { firstValueFrom } from 'rxjs';
@Injectable()
export class LiveUpdaterTask {
private readonly logger = new Logger(LiveUpdaterTask.name);
constructor(
private readonly httpService: HttpService,
private readonly prisma: PrismaService,
) {}
private shouldSkipInHistoricalMode(jobName: string): boolean {
if (process.env.FEEDER_MODE === 'historical') {
this.logger.debug(`Skipping ${jobName} in historical feeder mode`);
return true;
}
return false;
}
/**
* Update live match scores every 5 minutes
*/
@Cron('*/15 * * * *') // Every 15 minutes
async updateLiveScores() {
if (this.shouldSkipInHistoricalMode('updateLiveScores')) return;
this.logger.debug('Updating live scores...');
try {
// Get all live matches
const liveMatches = await this.prisma.liveMatch.findMany({
where: {
state: {
in: ['live', 'firsthalf', 'secondhalf', '1H', '2H', 'HT', 'LIVE'],
},
},
select: { id: true, matchSlug: true },
});
if (liveMatches.length === 0) {
this.logger.debug('No live matches to update');
return;
}
this.logger.log(`Updating ${liveMatches.length} live matches`);
// Fetch scores for each live match
for (const match of liveMatches) {
try {
const url = `https://www.mackolik.com/ajax/football/match-info?matchId=${match.id}`;
const response = await firstValueFrom(
this.httpService.get(url, { timeout: 5000 }),
);
if (response.data?.data) {
const matchData = response.data.data;
await this.prisma.liveMatch.update({
where: { id: match.id },
data: {
scoreHome: matchData.homeScore ?? null,
scoreAway: matchData.awayScore ?? null,
state: matchData.state || matchData.status,
status: matchData.status,
updatedAt: new Date(),
},
});
}
} catch {
// Individual match update failed, continue with others
}
}
this.logger.log('Live score update complete');
} catch (error: any) {
this.logger.error(`Live update failed: ${error.message}`);
}
}
/**
* Update finished match results every 30 minutes
*/
@Cron('*/30 * * * *')
async finalizeFinishedMatches() {
if (this.shouldSkipInHistoricalMode('finalizeFinishedMatches')) return;
this.logger.log('Finalizing finished matches...');
try {
// Find recently finished matches that need final data
const finishedMatches = await this.prisma.liveMatch.findMany({
where: {
state: 'Finished',
updatedAt: {
gte: new Date(Date.now() - 60 * 60 * 1000), // Last hour
},
},
select: {
id: true,
matchSlug: true,
homeTeamId: true,
awayTeamId: true,
},
});
if (finishedMatches.length === 0) {
return;
}
this.logger.log(`Finalizing ${finishedMatches.length} matches`);
for (const liveMatch of finishedMatches) {
try {
// Check if permanent match record exists
const existingMatch = await this.prisma.match.findUnique({
where: { id: liveMatch.id },
});
if (!existingMatch) {
// Create permanent match record from live match
const liveData = await this.prisma.liveMatch.findUnique({
where: { id: liveMatch.id },
});
if (liveData) {
await this.prisma.match.create({
data: {
id: liveData.id,
matchName: liveData.matchName,
matchSlug: liveData.matchSlug,
sport: (liveData.sport || 'football') as any,
leagueId: liveData.leagueId,
homeTeamId: liveData.homeTeamId,
awayTeamId: liveData.awayTeamId,
mstUtc: liveData.mstUtc ?? BigInt(Date.now()),
scoreHome: liveData.scoreHome,
scoreAway: liveData.scoreAway,
state: 'Finished',
status: 'Finished',
},
});
this.logger.log(
`Migrated match ${liveData.id} to permanent storage`,
);
}
} else {
// Update existing match with final score
const liveData = await this.prisma.liveMatch.findUnique({
where: { id: liveMatch.id },
});
if (liveData) {
await this.prisma.match.update({
where: { id: liveMatch.id },
data: {
scoreHome: liveData.scoreHome,
scoreAway: liveData.scoreAway,
state: 'Finished',
status: 'Finished',
},
});
}
}
} catch (err: any) {
this.logger.warn(
`Failed to finalize match ${liveMatch.id}: ${err.message}`,
);
}
}
} catch (error: any) {
this.logger.error(`Finalize job failed: ${error.message}`);
}
}
}
+37
View File
@@ -0,0 +1,37 @@
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { HttpModule } from '@nestjs/axios';
import { DataFetcherTask } from './data-fetcher.task';
import { HistoricalResultsSyncTask } from './historical-results-sync.task';
import { LimitResetterTask } from './limit-resetter.task';
import { LiveUpdaterTask } from './live-updater.task';
import { DatabaseModule } from '../database/database.module';
import { FeederModule } from '../modules/feeder/feeder.module';
@Module({
imports: [
ScheduleModule.forRoot(),
HttpModule.register({
timeout: 30000,
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
},
}),
DatabaseModule,
FeederModule,
],
providers: [
DataFetcherTask,
HistoricalResultsSyncTask,
LimitResetterTask,
LiveUpdaterTask,
],
exports: [
DataFetcherTask,
HistoricalResultsSyncTask,
LimitResetterTask,
LiveUpdaterTask,
],
})
export class TasksModule {}