import { Injectable, Logger } from "@nestjs/common"; import { Cron } from "@nestjs/schedule"; import { PrismaService } from "../database/prisma.service"; import { FINISHED_STATE_VALUES_FOR_DB, FINISHED_STATUS_VALUES_FOR_DB, LIVE_STATE_VALUES_FOR_DB, LIVE_STATUS_VALUES_FOR_DB, } from "../common/utils/match-status.util"; import { getDateOnlyValueForTimeZone, getShiftedDateStringInTimeZone, getDayBoundsForTimeZone, } from "../common/utils/timezone.util"; import { TaskLockService } from "./task-lock.service"; @Injectable() export class LimitResetterTask { private readonly logger = new Logger(LimitResetterTask.name); private readonly timeZone = "Europe/Istanbul"; constructor( private readonly prisma: PrismaService, private readonly taskLock: TaskLockService, ) {} 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; await this.taskLock.runWithLease( "resetUsageLimits", 30 * 60 * 1000, async () => { this.logger.log("Starting daily usage limit reset job..."); try { const today = getDateOnlyValueForTimeZone(this.timeZone); 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}`); } }, this.logger, ); } /** * Clean up old predictions (older than 30 days) */ @Cron("0 4 * * *", { timeZone: "Europe/Istanbul" }) async cleanupOldData() { if (this.shouldSkipInHistoricalMode("cleanupOldData")) return; await this.taskLock.runWithLease( "cleanupOldData", 60 * 60 * 1000, async () => { this.logger.log("Starting data cleanup job..."); try { const thirtyDaysAgo = new Date(); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); const deletedLogs = await this.prisma.aiPredictionsLog.deleteMany({ where: { createdAt: { lt: thirtyDaysAgo }, }, }); const yesterdayDate = getShiftedDateStringInTimeZone( -1, this.timeZone, ); const { startMs: yesterdayStartMs } = getDayBoundsForTimeZone( yesterdayDate, this.timeZone, ); const liveMatchCutoff = new Date(yesterdayStartMs); const deletedLiveMatches = await this.prisma.liveMatch.deleteMany({ where: { updatedAt: { lt: liveMatchCutoff }, OR: [ { status: { in: FINISHED_STATUS_VALUES_FOR_DB } }, { state: { in: FINISHED_STATE_VALUES_FOR_DB } }, { AND: [ { scoreHome: { not: null } }, { scoreAway: { not: null } }, { NOT: { OR: [ { status: { in: LIVE_STATUS_VALUES_FOR_DB } }, { state: { in: LIVE_STATE_VALUES_FOR_DB } }, ], }, }, ], }, ], }, }); 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}`); } }, this.logger, ); } /** * Reset subscription status for expired users */ @Cron("0 0 * * *", { timeZone: "Europe/Istanbul" }) async checkSubscriptions() { if (this.shouldSkipInHistoricalMode("checkSubscriptions")) return; await this.taskLock.runWithLease( "checkSubscriptions", 30 * 60 * 1000, async () => { 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}`); } }, this.logger, ); } }