213 lines
6.3 KiB
TypeScript
Executable File
213 lines
6.3 KiB
TypeScript
Executable File
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,
|
|
);
|
|
}
|
|
}
|