import { BadRequestException, Injectable, NotFoundException, } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import type { DeletionRetention } from '@mento/shared'; import { PrismaService } from '../prisma/prisma.service'; import { modelLabel, restoreArchive } from './deletion-archive'; const RETENTIONS = new Set([ 'month', 'two_months', 'three_months', 'six_months', 'year', 'never', ]); const RETENTION_DAYS: Record = { month: 30, two_months: 60, three_months: 90, six_months: 180, year: 365, never: null, }; @Injectable() export class DeletionsService { constructor(private readonly prisma: PrismaService) {} async list( organizationId: string, page = 1, limit = 30, q?: string, deletedById?: string, ) { const take = Math.min(100, Math.max(1, limit)); const skip = (Math.max(1, page) - 1) * take; const query = q?.trim(); const actorId = deletedById?.trim(); const where = { organizationId, ...(actorId ? { deletedById: actorId } : {}), ...(query ? { OR: [ { label: { contains: query, mode: 'insensitive' as const } }, { model: { contains: query, mode: 'insensitive' as const } }, ], } : {}), }; const [rows, total, org] = await Promise.all([ this.prisma.deletionArchive.findMany({ where, include: { deletedBy: { select: { id: true, fullName: true } } }, orderBy: { deletedAt: 'desc' }, skip, take, }), this.prisma.deletionArchive.count({ where }), this.prisma.organization.findUnique({ where: { id: organizationId }, select: { deletionRetention: true }, }), ]); return { items: rows.map((row) => ({ id: row.id, model: row.model, modelLabel: modelLabel(row.model), entityId: row.entityId, label: row.label, deletedById: row.deletedById, deletedByName: row.deletedBy?.fullName ?? null, deletedAt: row.deletedAt.toISOString(), })), total, page: Math.max(1, page), limit: take, retention: this.retentionOf(org?.deletionRetention), }; } async updateRetention(organizationId: string, retention: string) { if (!RETENTIONS.has(retention as DeletionRetention)) { throw new BadRequestException('مدت نگهداری معتبر نیست.'); } await this.prisma.organization.update({ where: { id: organizationId }, data: { deletionRetention: retention }, }); return { retention: retention as DeletionRetention }; } async restore(organizationId: string, id: string) { const row = await this.owned(organizationId, id); try { await restoreArchive( this.prisma, row.snapshot as never, row.model, ); } catch (error) { const message = error instanceof Error ? error.message : ''; if (message === 'این مورد هنوز در سامانه وجود دارد.') { await this.prisma.deletionArchive.delete({ where: { id: row.id } }); } throw new BadRequestException(message || 'بازگردانی ناموفق بود.'); } await this.prisma.deletionArchive.delete({ where: { id: row.id } }); return { ok: true }; } async purge(organizationId: string, id: string) { const row = await this.owned(organizationId, id); await this.prisma.deletionArchive.delete({ where: { id: row.id } }); return { ok: true }; } @Cron('15 3 * * *', { timeZone: 'Asia/Tehran' }) async purgeExpired() { const orgs = await this.prisma.organization.findMany({ select: { id: true, deletionRetention: true }, }); const now = Date.now(); for (const org of orgs) { const days = RETENTION_DAYS[this.retentionOf(org.deletionRetention)]; if (!days) continue; const cutoff = new Date(now - days * 24 * 60 * 60 * 1000); await this.prisma.deletionArchive.deleteMany({ where: { organizationId: org.id, deletedAt: { lt: cutoff } }, }); } } private retentionOf(value: string | null | undefined): DeletionRetention { if (value && RETENTIONS.has(value as DeletionRetention)) { return value as DeletionRetention; } return 'never'; } private async owned(organizationId: string, id: string) { const row = await this.prisma.deletionArchive.findFirst({ where: { id, organizationId }, }); if (!row) throw new NotFoundException('این حذف در فهرست پیدا نشد.'); return row; } }