152 lines
5.2 KiB
TypeScript
152 lines
5.2 KiB
TypeScript
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
|
import { Prisma } from '@prisma/client';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { shanghaiDateRange } from '../common/shanghai-date-range';
|
|
|
|
export interface SubmitCertificationDto {
|
|
tenantId: string;
|
|
companyName: string;
|
|
licenseNo?: string;
|
|
contactName?: string;
|
|
contactPhone?: string;
|
|
materials?: Record<string, unknown>;
|
|
}
|
|
|
|
export interface ReviewCertificationDto {
|
|
reviewerId?: string;
|
|
reason?: string;
|
|
}
|
|
|
|
@Injectable()
|
|
export class CertificationService {
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
async list(tenantId?: string, status?: string, keyword?: string, submittedAtFrom?: string, submittedAtTo?: string) {
|
|
const records = await this.prisma.enterpriseCertification.findMany({
|
|
where: {
|
|
tenantId,
|
|
status: status && status !== 'all' ? status : undefined,
|
|
submittedAt: shanghaiDateRange(submittedAtFrom, submittedAtTo),
|
|
OR: keyword ? [
|
|
{ companyName: { contains: keyword } },
|
|
{ licenseNo: { contains: keyword } },
|
|
{ contactName: { contains: keyword } },
|
|
{ contactPhone: { contains: keyword } },
|
|
{ tenant: { name: { contains: keyword } } },
|
|
] : undefined,
|
|
},
|
|
include: { tenant: true },
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
return this.attachReviewers(records);
|
|
}
|
|
|
|
async get(id: string) {
|
|
const certification = await this.prisma.enterpriseCertification.findUnique({
|
|
where: { id },
|
|
include: { tenant: true },
|
|
});
|
|
if (!certification) {
|
|
throw new NotFoundException('Enterprise certification not found');
|
|
}
|
|
return (await this.attachReviewers([certification]))[0];
|
|
}
|
|
|
|
async submit(data: SubmitCertificationDto) {
|
|
const tenant = await this.prisma.tenant.findUnique({ where: { id: data.tenantId } });
|
|
if (!tenant) {
|
|
throw new NotFoundException('Tenant not found');
|
|
}
|
|
const certification = await this.prisma.enterpriseCertification.create({
|
|
data: {
|
|
tenantId: data.tenantId,
|
|
companyName: data.companyName,
|
|
licenseNo: data.licenseNo,
|
|
contactName: data.contactName,
|
|
contactPhone: data.contactPhone,
|
|
materials: data.materials as Prisma.InputJsonValue | undefined,
|
|
status: 'pending',
|
|
},
|
|
});
|
|
await this.prisma.tenant.update({
|
|
where: { id: data.tenantId },
|
|
data: { certificationStatus: 'pending' },
|
|
});
|
|
await this.prisma.operationLog.create({
|
|
data: {
|
|
tenantId: data.tenantId,
|
|
action: 'enterprise_certification.submit',
|
|
resource: 'enterprise_certification',
|
|
resourceId: certification.id,
|
|
detail: { companyName: data.companyName } as Prisma.InputJsonValue,
|
|
},
|
|
});
|
|
return certification;
|
|
}
|
|
|
|
approve(id: string, data: ReviewCertificationDto) {
|
|
return this.review(id, 'approved', data);
|
|
}
|
|
|
|
reject(id: string, data: ReviewCertificationDto) {
|
|
if (!data.reason) {
|
|
throw new BadRequestException('Reject reason is required');
|
|
}
|
|
return this.review(id, 'rejected', data);
|
|
}
|
|
|
|
private async review(id: string, status: 'approved' | 'rejected', data: ReviewCertificationDto) {
|
|
const certification = await this.prisma.enterpriseCertification.findUnique({ where: { id } });
|
|
if (!certification) {
|
|
throw new NotFoundException('Enterprise certification not found');
|
|
}
|
|
let reviewer: { id: string; username: string; displayName: string } | null = null;
|
|
if (data.reviewerId) {
|
|
reviewer = await this.prisma.user.findUnique({
|
|
where: { id: data.reviewerId },
|
|
select: { id: true, username: true, displayName: true },
|
|
});
|
|
if (!reviewer) {
|
|
throw new BadRequestException('reviewerId does not reference an existing user');
|
|
}
|
|
}
|
|
const updated = await this.prisma.enterpriseCertification.update({
|
|
where: { id },
|
|
data: {
|
|
status,
|
|
rejectReason: status === 'rejected' ? data.reason : null,
|
|
reviewerId: data.reviewerId,
|
|
reviewedAt: new Date(),
|
|
},
|
|
});
|
|
await this.prisma.tenant.update({
|
|
where: { id: certification.tenantId },
|
|
data: { certificationStatus: status },
|
|
});
|
|
await this.prisma.operationLog.create({
|
|
data: {
|
|
tenantId: certification.tenantId,
|
|
userId: data.reviewerId,
|
|
action: `enterprise_certification.${status}`,
|
|
resource: 'enterprise_certification',
|
|
resourceId: id,
|
|
detail: { reason: data.reason } as Prisma.InputJsonValue,
|
|
},
|
|
});
|
|
return { ...updated, reviewer };
|
|
}
|
|
|
|
private async attachReviewers<T extends { reviewerId: string | null }>(records: T[]) {
|
|
const reviewerIds = [...new Set(records.map((record) => record.reviewerId).filter((id): id is string => Boolean(id)))];
|
|
const reviewers = reviewerIds.length ? await this.prisma.user.findMany({
|
|
where: { id: { in: reviewerIds } },
|
|
select: { id: true, username: true, displayName: true },
|
|
}) : [];
|
|
const reviewerById = new Map(reviewers.map((reviewer) => [reviewer.id, reviewer]));
|
|
return records.map((record) => ({
|
|
...record,
|
|
reviewer: record.reviewerId ? reviewerById.get(record.reviewerId) ?? null : null,
|
|
}));
|
|
}
|
|
}
|