Files
CRM_Montzar/apps/api/src/deletions/deletions.service.ts
T

155 lines
4.6 KiB
TypeScript

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<DeletionRetention>([
'month',
'two_months',
'three_months',
'six_months',
'year',
'never',
]);
const RETENTION_DAYS: Record<DeletionRetention, number | null> = {
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;
}
}