123 lines
8.8 KiB
TypeScript
123 lines
8.8 KiB
TypeScript
import { BadRequestException, ForbiddenException, Injectable, Logger, NotFoundException, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
|
import { Prisma } from '@prisma/client';
|
|
import { randomInt, randomUUID } from 'node:crypto';
|
|
import { isIpAllowed } from '../common/ip-allowlist';
|
|
import { assertMoneyUnits } from '../common/money';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { automaticDeliveryMode } from '../open-api/delivery-mode';
|
|
import type { ApplicationListQuery, CreateSignatureMaterialDto, CreateSmsApplicationDto, CreateSmsDrainageInfoDto, CreateSmsSignatureDto, CreateSmsSignatureOptions, CreateSmsTemplateDto, CreateSmsTemplateOptions, DrainageInfoListQuery, GatewayDownstreamConnectionEventDto, ReplaceApplicationRouteRulesDto, ReviewDto, SignatureListQuery, StatusChangeDto, TemplateListQuery, UpdateSmsApplicationDto, UpdateSmsDrainageInfoDto, UpdateSmsSignatureDto, UpdateSmsTemplateDto } from './sms-config.contracts';
|
|
import { APPLICATION_DISABLE_GRACE_MS, DEFAULT_APPLICATION_DISABLE_SCAN_INTERVAL_MS, DEFAULT_DOWNSTREAM_HEARTBEAT_TIMEOUT_MS, UNRESOLVED_DOWNSTREAM_STATUSES, type TemplateVariableInput, estimateBillingUnits, generateApplicationPassword, getPositiveInteger, getPositiveIntegerEnv, hasReportValue, inferTemplateVariables, isRecord, normalizeApplicationCmppStatus, normalizeApplicationInterfaceType, normalizeApplicationPassword, normalizeApplicationQueuePriority, normalizeCmppAccessNumberConfig, normalizeSmsSignature, parseGatewayDate, reportValueParts, startOfToday, validateAndNormalizeTemplateVariables, validateCompleteSmsSignature } from './sms-config.helpers';
|
|
import { SmsApplicationConfigService } from './application-config.service';
|
|
|
|
/** R3 domain service. Kept framework-agnostic and composed behind SmsConfigService. */
|
|
export class SmsReportValidationService {
|
|
constructor(private readonly prisma: PrismaService, private readonly applications: SmsApplicationConfigService) {}
|
|
async withReportRequirementSnapshot(applicationId?: string, drainageInfo?: Record<string, unknown>) {
|
|
if (!drainageInfo) return drainageInfo;
|
|
const fields = await this.applications.getApplicationReportFields(applicationId);
|
|
return {
|
|
...drainageInfo,
|
|
reportRequirementSnapshot: {
|
|
capturedAt: new Date().toISOString(),
|
|
applicationId,
|
|
fields: fields.map((field) => ({
|
|
id: field.id,
|
|
code: field.code,
|
|
name: field.name,
|
|
fieldType: field.fieldType,
|
|
required: field.required,
|
|
reportTypes: field.reportTypes,
|
|
commonReportTypes: field.commonReportTypes,
|
|
channels: field.channels,
|
|
})),
|
|
},
|
|
};
|
|
}
|
|
|
|
async syncSignatureReportValues(signatureId: string, applicationId?: string, drainageInfo?: Record<string, unknown>) {
|
|
if (!drainageInfo) return;
|
|
const fields = await this.applications.getApplicationReportFields(applicationId);
|
|
const signatureValues = isRecord(drainageInfo.signatureReportValues) ? drainageInfo.signatureReportValues : {};
|
|
for (const field of fields.filter((item) => item.reportTypes.some((type) => type === 'signature' || type === 'both'))) {
|
|
const value = reportValueParts(signatureValues[field.code]);
|
|
for (const channel of field.channels) {
|
|
await this.prisma.signatureReportMaterial.upsert({
|
|
where: { signatureId_channelId_fieldCode: { signatureId, channelId: channel.id, fieldCode: field.code } },
|
|
update: value,
|
|
create: { signatureId, channelId: channel.id, fieldCode: field.code, ...value },
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
async validateSignatureReportValues(applicationId?: string, drainageInfo?: Record<string, unknown>) {
|
|
const fields = await this.applications.getApplicationReportFields(applicationId, 'signature');
|
|
const signatureValues = isRecord(drainageInfo?.signatureReportValues) ? drainageInfo.signatureReportValues : {};
|
|
const missingSignature = fields
|
|
.filter((field) => field.required && field.reportTypes.some((type) => type === 'signature' || type === 'both'))
|
|
.filter((field) => !hasReportValue(signatureValues[field.code]));
|
|
if (missingSignature.length > 0) {
|
|
throw new BadRequestException(`缺少必填签名报备资料:${missingSignature.map((field) => field.name).join('、')}`);
|
|
}
|
|
}
|
|
|
|
async validateDrainageReportValues(applicationId?: string, reportValues: Record<string, unknown> = {}) {
|
|
const fields = await this.applications.getApplicationReportFields(applicationId, 'drainage');
|
|
const missing = fields.filter((field) => field.required && !hasReportValue(reportValues[field.code]));
|
|
if (missing.length > 0) {
|
|
throw new BadRequestException(`引流信息缺少必填报备资料:${missing.map((field) => field.name).join('、')}`);
|
|
}
|
|
}
|
|
|
|
async activateDrainageReporting(itemId: string) {
|
|
const item = await this.prisma.smsDrainageInfo.findUnique({ where: { id: itemId }, include: { signature: true } });
|
|
if (!item) throw new NotFoundException('Drainage info not found');
|
|
if (item.auditStatus !== 'approved') throw new BadRequestException('引流信息审核通过后才能进入通道报备');
|
|
const applicationId = item.signature.applicationId ?? item.applicationId ?? undefined;
|
|
if (!applicationId) return;
|
|
const fields = (await this.applications.getApplicationReportFields(applicationId, 'drainage'))
|
|
.filter((field) => field.reportTypes.some((type) => type === 'drainage' || type === 'both'));
|
|
const channels = new Map(fields.flatMap((field) => field.channels).map((channel) => [channel.id, channel]));
|
|
const values = isRecord(item.reportValues) ? item.reportValues : {};
|
|
await this.prisma.$transaction(async (tx) => {
|
|
await tx.drainageReportMaterial.deleteMany({ where: { signatureId: item.signatureId, drainageItemId: item.id } });
|
|
for (const field of fields) {
|
|
const value = reportValueParts(values[field.code]);
|
|
for (const channel of field.channels) {
|
|
await tx.drainageReportMaterial.create({
|
|
data: { signatureId: item.signatureId, drainageItemId: item.id, channelId: channel.id, fieldCode: field.code, ...value },
|
|
});
|
|
}
|
|
}
|
|
const existingTasks = await tx.channelSignatureReportTask.findMany({ where: { drainageItemId: item.id, reportType: 'drainage' } });
|
|
const existingByChannel = new Map(existingTasks.map((task) => [task.channelId, task]));
|
|
for (const channel of channels.values()) {
|
|
const existing = existingByChannel.get(channel.id);
|
|
const task = existing
|
|
? await tx.channelSignatureReportTask.update({ where: { id: existing.id }, data: { status: 'pending', reason: null } })
|
|
: await tx.channelSignatureReportTask.create({ data: { tenantId: item.tenantId, signatureId: item.signatureId, channelId: channel.id, reportType: 'drainage', drainageItemId: item.id, status: 'pending' } });
|
|
await tx.channelSignatureReportRecord.create({
|
|
data: { taskId: task.id, channelId: channel.id, action: existing ? 'audit_approved_reset' : 'audit_approved_create', statusBefore: existing?.status, statusAfter: 'pending', reason: '引流信息运营审核通过' },
|
|
});
|
|
}
|
|
for (const task of existingTasks.filter((current) => !channels.has(current.channelId) && current.status !== 'abandoned')) {
|
|
await tx.channelSignatureReportTask.update({ where: { id: task.id }, data: { status: 'abandoned', reason: '应用当前路由已不包含此通道' } });
|
|
await tx.channelSignatureReportRecord.create({ data: { taskId: task.id, channelId: task.channelId, action: 'route_removed', statusBefore: task.status, statusAfter: 'abandoned', reason: '应用当前路由已不包含此通道' } });
|
|
}
|
|
});
|
|
}
|
|
|
|
async suspendDrainageReporting(itemId: string, reason: string, statusAfter = 'waiting_review') {
|
|
await this.prisma.$transaction(async (tx) => {
|
|
const item = await tx.smsDrainageInfo.findUnique({ where: { id: itemId } });
|
|
if (!item) throw new NotFoundException('Drainage info not found');
|
|
await tx.drainageReportMaterial.deleteMany({ where: { signatureId: item.signatureId, drainageItemId: item.id } });
|
|
const tasks = await tx.channelSignatureReportTask.findMany({ where: { drainageItemId: item.id, reportType: 'drainage' } });
|
|
for (const task of tasks.filter((current) => current.status !== statusAfter)) {
|
|
await tx.channelSignatureReportTask.update({ where: { id: task.id }, data: { status: statusAfter, reason } });
|
|
await tx.channelSignatureReportRecord.create({ data: { taskId: task.id, channelId: task.channelId, action: 'audit_suspended', statusBefore: task.status, statusAfter, reason } });
|
|
}
|
|
});
|
|
}
|
|
}
|