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, ); } /** * Downgrade cancelled subscriptions that have passed their cancel effective date */ @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(); // Find subscriptions with passed cancel effective date const expiredSubs = await this.prisma.subscription.findMany({ where: { plan: "cancelled", cancelEffectiveDate: { lt: now }, }, select: { id: true, userId: true }, }); for (const sub of expiredSubs) { // Downgrade to free await this.prisma.user.update({ where: { id: sub.userId }, data: { subscriptionStatus: "free" }, }); // Sync limits to free tier await this.prisma.usageLimit.upsert({ where: { userId: sub.userId }, update: { maxAnalyses: 3, maxCoupons: 1 }, create: { userId: sub.userId, analysisCount: 0, couponCount: 0, maxAnalyses: 3, maxCoupons: 1, lastResetDate: new Date(), }, }); // Reset subscription to free await this.prisma.subscription.update({ where: { id: sub.id }, data: { plan: "free", cancelledAt: null, cancelEffectiveDate: null, }, }); } if (expiredSubs.length > 0) { this.logger.log( `${expiredSubs.length} cancelled subscriptions downgraded to free`, ); } } catch (error: unknown) { const err = error as Error; this.logger.error(`Subscription check failed: ${err.message}`); } }, this.logger, ); } }