import { Injectable, Logger } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { PrismaService } from '../prisma/prisma.service'; import { jalaliMonthKeyOf, previousPeriod } from './gamification-engine'; import { GamificationService } from './gamification.service'; @Injectable() export class GamificationCronService { private readonly logger = new Logger(GamificationCronService.name); constructor( private readonly prisma: PrismaService, private readonly game: GamificationService, ) {} /** پاداش «نفر اول ماه» برای ماه شمسی گذشته؛ روزانه اجرا می‌شود و تکراری ثبت نمی‌کند */ @Cron('20 0 * * *', { timeZone: 'Asia/Tehran' }) async awardTopOfMonth() { const range = previousPeriod('month'); if (!range) return; const monthKey = jalaliMonthKeyOf(range.from); const bonuses = await this.prisma.gamificationBonus.findMany({ where: { kind: 'top_of_month', isActive: true }, }); for (const bonus of bonuses) { try { const settings = await this.game.settingsFor(bonus.organizationId); if (!settings?.enabled) continue; const memberIds = new Set((await this.game.members(bonus.organizationId)).map((m) => m.userId)); const rows = await this.prisma.gamificationTransaction.groupBy({ by: ['userId'], where: { organizationId: bonus.organizationId, status: 'approved', createdAt: { gte: range.from, lt: range.to }, }, _sum: { score: true }, }); const winner = rows .filter((r) => memberIds.has(r.userId) && (r._sum.score ?? 0) > 0) .sort((a, b) => (b._sum.score ?? 0) - (a._sum.score ?? 0))[0]; if (!winner) continue; await this.game.record({ organizationId: bonus.organizationId, userId: winner.userId, kind: 'bonus', bonusId: bonus.id, score: bonus.score, coins: bonus.coins, title: `پاداش ویژه: ${bonus.name}`, idempotencyKey: `b:${bonus.id}:m:${monthKey}`, }); } catch (err) { this.logger.warn(`top-of-month ${bonus.id}: ${err instanceof Error ? err.message : String(err)}`); } } } }