Files
CRM_Montzar/apps/api/src/gamification/gamification-cron.service.ts
T

59 lines
2.3 KiB
TypeScript
Raw Normal View History

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)}`);
}
}
}
}