fix: enforce drainage uniqueness and carrier-specific reporting
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
import { BadRequestException, ForbiddenException, Injectable, Logger, NotFoundException, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||
import { BadRequestException, NotFoundException } 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 type {
|
||||
CreateSmsDrainageInfoDto,
|
||||
CreateSmsSignatureOptions,
|
||||
DrainageInfoListQuery,
|
||||
ReviewDto,
|
||||
StatusChangeDto,
|
||||
UpdateSmsDrainageInfoDto,
|
||||
} from './sms-config.contracts';
|
||||
import { isRecord } from './sms-config.helpers';
|
||||
import { SmsReportValidationService } from './report-validation.service';
|
||||
import { SmsAuditService } from './audit.service';
|
||||
import { shanghaiDateRange } from '../common/shanghai-date-range';
|
||||
@@ -20,67 +23,98 @@ function normalizeDrainageTarget(value?: string) {
|
||||
|
||||
/** R3 domain service. Kept framework-agnostic and composed behind SmsConfigService. */
|
||||
export class SmsDrainageService {
|
||||
constructor(private readonly prisma: PrismaService, private readonly reportValidation: SmsReportValidationService, private readonly audit: SmsAuditService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly reportValidation: SmsReportValidationService,
|
||||
private readonly audit: SmsAuditService,
|
||||
) {}
|
||||
private async assertUniqueTarget(
|
||||
tx: Prisma.TransactionClient,
|
||||
signatureId: string,
|
||||
target: string,
|
||||
excludeId?: string,
|
||||
) {
|
||||
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${signatureId}, 910))`;
|
||||
const duplicate = await tx.smsDrainageInfo.findFirst({
|
||||
where: {
|
||||
signatureId,
|
||||
url: target,
|
||||
auditStatus: { not: 'deleted' },
|
||||
...(excludeId ? { id: { not: excludeId } } : {}),
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (duplicate) throw new BadRequestException('同一签名下已存在相同的引流信息');
|
||||
}
|
||||
async listClientDrainageInfos(tenantId?: string, itemId?: string) {
|
||||
return this.prisma.smsDrainageInfo.findMany({
|
||||
where: { id: itemId, tenantId, auditStatus: { not: 'deleted' } },
|
||||
select: {
|
||||
id: true,
|
||||
tenantId: true,
|
||||
signatureId: true,
|
||||
applicationId: true,
|
||||
siteName: true,
|
||||
url: true,
|
||||
remark: true,
|
||||
reportValues: true,
|
||||
auditStatus: true,
|
||||
rejectReason: true,
|
||||
submittedAt: true,
|
||||
reviewedAt: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
signature: { select: { id: true, name: true, auditStatus: true } },
|
||||
application: { select: { id: true, name: true, status: true } },
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
});
|
||||
}
|
||||
return this.prisma.smsDrainageInfo.findMany({
|
||||
where: { id: itemId, tenantId, auditStatus: { not: 'deleted' } },
|
||||
select: {
|
||||
id: true,
|
||||
tenantId: true,
|
||||
signatureId: true,
|
||||
applicationId: true,
|
||||
siteName: true,
|
||||
url: true,
|
||||
remark: true,
|
||||
reportValues: true,
|
||||
auditStatus: true,
|
||||
rejectReason: true,
|
||||
submittedAt: true,
|
||||
reviewedAt: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
signature: { select: { id: true, name: true, auditStatus: true } },
|
||||
application: { select: { id: true, name: true, status: true } },
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async getClientDrainageInfoView(itemId: string, tenantId?: string) {
|
||||
const [item] = await this.listClientDrainageInfos(tenantId, itemId);
|
||||
if (!item) throw new NotFoundException('Drainage info not found');
|
||||
return item;
|
||||
}
|
||||
const [item] = await this.listClientDrainageInfos(tenantId, itemId);
|
||||
if (!item) throw new NotFoundException('Drainage info not found');
|
||||
return item;
|
||||
}
|
||||
|
||||
listDrainageInfos(query: DrainageInfoListQuery = {}) {
|
||||
return this.prisma.smsDrainageInfo.findMany({
|
||||
where: {
|
||||
tenantId: query.tenantId,
|
||||
signatureId: query.signatureId,
|
||||
auditStatus: query.status && query.status !== 'all' ? query.status : { not: 'deleted' },
|
||||
submittedAt: shanghaiDateRange(query.submittedAtFrom, query.submittedAtTo),
|
||||
OR: query.keyword ? [
|
||||
{ siteName: { contains: query.keyword } },
|
||||
{ url: { contains: query.keyword } },
|
||||
{ signature: { name: { contains: query.keyword } } },
|
||||
{ tenant: { name: { contains: query.keyword } } },
|
||||
{ application: { name: { contains: query.keyword } } },
|
||||
] : undefined,
|
||||
},
|
||||
include: { tenant: true, signature: true, application: true, reportTasks: { include: { channel: true } } },
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
});
|
||||
}
|
||||
return this.prisma.smsDrainageInfo.findMany({
|
||||
where: {
|
||||
tenantId: query.tenantId,
|
||||
signatureId: query.signatureId,
|
||||
auditStatus: query.status && query.status !== 'all' ? query.status : { not: 'deleted' },
|
||||
submittedAt: shanghaiDateRange(query.submittedAtFrom, query.submittedAtTo),
|
||||
OR: query.keyword
|
||||
? [
|
||||
{ siteName: { contains: query.keyword } },
|
||||
{ url: { contains: query.keyword } },
|
||||
{ signature: { name: { contains: query.keyword } } },
|
||||
{ tenant: { name: { contains: query.keyword } } },
|
||||
{ application: { name: { contains: query.keyword } } },
|
||||
]
|
||||
: undefined,
|
||||
},
|
||||
include: { tenant: true, signature: true, application: true, reportTasks: { include: { channel: true } } },
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async createDrainageInfo(signatureId: string, data: CreateSmsDrainageInfoDto, options: CreateSmsSignatureOptions = {}, tenantId?: string) {
|
||||
const signature = await this.prisma.smsSignature.findUnique({ where: { id: signatureId } });
|
||||
if (!signature) throw new NotFoundException('Signature not found');
|
||||
if (tenantId && signature.tenantId !== tenantId) throw new NotFoundException('Signature not found');
|
||||
if (signature.auditStatus !== 'approved') throw new BadRequestException('签名审核通过后才能新增引流信息');
|
||||
const target = normalizeDrainageTarget(data.url);
|
||||
await this.reportValidation.validateDrainageReportValues(signature.applicationId ?? undefined, data.reportValues);
|
||||
const auditStatus = options.initialAuditStatus ?? 'pending';
|
||||
const item = await this.prisma.smsDrainageInfo.create({
|
||||
async createDrainageInfo(
|
||||
signatureId: string,
|
||||
data: CreateSmsDrainageInfoDto,
|
||||
options: CreateSmsSignatureOptions = {},
|
||||
tenantId?: string,
|
||||
) {
|
||||
const signature = await this.prisma.smsSignature.findUnique({ where: { id: signatureId } });
|
||||
if (!signature) throw new NotFoundException('Signature not found');
|
||||
if (tenantId && signature.tenantId !== tenantId) throw new NotFoundException('Signature not found');
|
||||
if (signature.auditStatus !== 'approved') throw new BadRequestException('签名审核通过后才能新增引流信息');
|
||||
const target = normalizeDrainageTarget(data.url);
|
||||
await this.reportValidation.validateDrainageReportValues(signature.applicationId ?? undefined, data.reportValues);
|
||||
const auditStatus = options.initialAuditStatus ?? 'pending';
|
||||
const item = await this.prisma.$transaction(async (tx) => {
|
||||
await this.assertUniqueTarget(tx, signatureId, target);
|
||||
return tx.smsDrainageInfo.create({
|
||||
data: {
|
||||
tenantId: signature.tenantId,
|
||||
signatureId,
|
||||
@@ -94,28 +128,65 @@ export class SmsDrainageService {
|
||||
},
|
||||
include: { tenant: true, signature: true, application: true },
|
||||
});
|
||||
await this.audit.createAuditRecord({
|
||||
tenantId: item.tenantId,
|
||||
targetType: 'sms_drainage_info',
|
||||
targetId: item.id,
|
||||
action: auditStatus === 'approved' ? 'admin_create_approved' : 'submit',
|
||||
statusAfter: auditStatus,
|
||||
reason: auditStatus === 'approved' ? '运营端新建引流信息自动审核通过' : undefined,
|
||||
});
|
||||
if (auditStatus === 'approved') await this.reportValidation.activateDrainageReporting(item.id);
|
||||
return item;
|
||||
}
|
||||
});
|
||||
await this.audit.createAuditRecord({
|
||||
tenantId: item.tenantId,
|
||||
targetType: 'sms_drainage_info',
|
||||
targetId: item.id,
|
||||
action: auditStatus === 'approved' ? 'admin_create_approved' : 'submit',
|
||||
statusAfter: auditStatus,
|
||||
reason: auditStatus === 'approved' ? '运营端新建引流信息自动审核通过' : undefined,
|
||||
});
|
||||
if (auditStatus === 'approved') await this.reportValidation.activateDrainageReporting(item.id);
|
||||
return item;
|
||||
}
|
||||
|
||||
async updateDrainageInfo(itemId: string, data: UpdateSmsDrainageInfoDto, options: CreateSmsSignatureOptions = {}, tenantId?: string) {
|
||||
const current = await this.prisma.smsDrainageInfo.findUnique({ where: { id: itemId }, include: { signature: true } });
|
||||
if (!current) throw new NotFoundException('Drainage info not found');
|
||||
if (tenantId && current.tenantId !== tenantId) throw new NotFoundException('Drainage info not found');
|
||||
if (current.auditStatus === 'deleted') throw new BadRequestException('已删除的引流信息不能修改');
|
||||
const target = data.url === undefined ? undefined : normalizeDrainageTarget(data.url);
|
||||
const applicationId = current.signature.applicationId ?? current.applicationId ?? undefined;
|
||||
await this.reportValidation.validateDrainageReportValues(applicationId, data.reportValues ?? (isRecord(current.reportValues) ? current.reportValues : {}));
|
||||
const auditStatus = options.initialAuditStatus ?? 'pending';
|
||||
const updated = await this.prisma.smsDrainageInfo.update({
|
||||
async updateDrainageInfo(
|
||||
itemId: string,
|
||||
data: UpdateSmsDrainageInfoDto,
|
||||
options: CreateSmsSignatureOptions = {},
|
||||
tenantId?: string,
|
||||
) {
|
||||
const current = await this.prisma.smsDrainageInfo.findUnique({
|
||||
where: { id: itemId },
|
||||
include: { signature: true },
|
||||
});
|
||||
if (!current) throw new NotFoundException('Drainage info not found');
|
||||
if (tenantId && current.tenantId !== tenantId) throw new NotFoundException('Drainage info not found');
|
||||
if (current.auditStatus === 'deleted') throw new BadRequestException('已删除的引流信息不能修改');
|
||||
const target = data.url === undefined ? undefined : normalizeDrainageTarget(data.url);
|
||||
const applicationId = current.signature.applicationId ?? current.applicationId ?? undefined;
|
||||
await this.reportValidation.validateDrainageReportValues(
|
||||
applicationId,
|
||||
data.reportValues ?? (isRecord(current.reportValues) ? current.reportValues : {}),
|
||||
);
|
||||
const auditStatus = options.initialAuditStatus ?? 'pending';
|
||||
const updated = await this.prisma.$transaction(async (tx) => {
|
||||
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${current.signatureId}, 910))`;
|
||||
const latest = await tx.smsDrainageInfo.findUnique({ where: { id: itemId } });
|
||||
if (!latest || latest.auditStatus === 'deleted') throw new BadRequestException('已删除的引流信息不能修改');
|
||||
if (target !== undefined && target !== latest.url)
|
||||
await this.assertUniqueTarget(tx, current.signatureId, target, itemId);
|
||||
const priorTasks = await tx.channelSignatureReportTask.findMany({
|
||||
where: { drainageItemId: itemId, reportType: 'drainage' },
|
||||
});
|
||||
for (const task of priorTasks) {
|
||||
await tx.channelSignatureReportTask.update({
|
||||
where: { id: task.id },
|
||||
data: { status: 'waiting_review', approvedAt: null, reason: '引流资料修改,原报备失效' },
|
||||
});
|
||||
await tx.channelSignatureReportRecord.create({
|
||||
data: {
|
||||
taskId: task.id,
|
||||
channelId: task.channelId,
|
||||
action: 'material_changed',
|
||||
statusBefore: task.status,
|
||||
statusAfter: 'waiting_review',
|
||||
reason: '引流资料修改,原报备失效',
|
||||
},
|
||||
});
|
||||
}
|
||||
return tx.smsDrainageInfo.update({
|
||||
where: { id: itemId },
|
||||
data: {
|
||||
applicationId,
|
||||
@@ -133,37 +204,51 @@ export class SmsDrainageService {
|
||||
},
|
||||
include: { tenant: true, signature: true, application: true },
|
||||
});
|
||||
await this.audit.createAuditRecord({
|
||||
tenantId: current.tenantId,
|
||||
targetType: 'sms_drainage_info',
|
||||
targetId: itemId,
|
||||
action: auditStatus === 'approved' ? 'admin_update_approved' : 'update_submit',
|
||||
statusBefore: current.auditStatus,
|
||||
statusAfter: auditStatus,
|
||||
reason: auditStatus === 'approved' ? '运营端修改引流信息并自动审核通过' : undefined,
|
||||
});
|
||||
if (auditStatus === 'approved') await this.reportValidation.activateDrainageReporting(itemId);
|
||||
else await this.reportValidation.suspendDrainageReporting(itemId, '引流信息修改后等待运营审核');
|
||||
return updated;
|
||||
}
|
||||
});
|
||||
await this.audit.createAuditRecord({
|
||||
tenantId: current.tenantId,
|
||||
targetType: 'sms_drainage_info',
|
||||
targetId: itemId,
|
||||
action: auditStatus === 'approved' ? 'admin_update_approved' : 'update_submit',
|
||||
statusBefore: current.auditStatus,
|
||||
statusAfter: auditStatus,
|
||||
reason: auditStatus === 'approved' ? '运营端修改引流信息并自动审核通过' : undefined,
|
||||
});
|
||||
if (auditStatus === 'approved') await this.reportValidation.activateDrainageReporting(itemId);
|
||||
else await this.reportValidation.suspendDrainageReporting(itemId, '引流信息修改后等待运营审核');
|
||||
return updated;
|
||||
}
|
||||
|
||||
approveDrainageInfo(itemId: string, data: ReviewDto) {
|
||||
return this.audit.reviewDrainageInfo(itemId, 'approved', 'approve', data);
|
||||
}
|
||||
return this.audit.reviewDrainageInfo(itemId, 'approved', 'approve', data);
|
||||
}
|
||||
|
||||
rejectDrainageInfo(itemId: string, data: ReviewDto) {
|
||||
return this.audit.reviewDrainageInfo(itemId, 'rejected', 'reject', data);
|
||||
}
|
||||
return this.audit.reviewDrainageInfo(itemId, 'rejected', 'reject', data);
|
||||
}
|
||||
|
||||
async changeDrainageInfoStatus(itemId: string, data: StatusChangeDto, tenantId?: string) {
|
||||
const current = await this.prisma.smsDrainageInfo.findUnique({ where: { id: itemId } });
|
||||
if (!current) throw new NotFoundException('Drainage info not found');
|
||||
if (tenantId && current.tenantId !== tenantId) throw new NotFoundException('Drainage info not found');
|
||||
const status = data.status ?? 'deleted';
|
||||
if (tenantId && status !== 'deleted') throw new BadRequestException('客户端只能删除引流信息,不能直接修改审核状态');
|
||||
const updated = await this.prisma.smsDrainageInfo.update({ where: { id: itemId }, data: { auditStatus: status } });
|
||||
if (status === 'deleted') await this.reportValidation.suspendDrainageReporting(itemId, data.reason ?? '引流信息已删除', 'abandoned');
|
||||
await this.audit.createAuditRecord({ tenantId: current.tenantId, targetType: 'sms_drainage_info', targetId: itemId, action: status, statusBefore: current.auditStatus, statusAfter: status, reason: data.reason });
|
||||
return updated;
|
||||
}
|
||||
const current = await this.prisma.smsDrainageInfo.findUnique({ where: { id: itemId } });
|
||||
if (!current) throw new NotFoundException('Drainage info not found');
|
||||
if (tenantId && current.tenantId !== tenantId) throw new NotFoundException('Drainage info not found');
|
||||
const status = data.status ?? 'deleted';
|
||||
if (tenantId && status !== 'deleted') throw new BadRequestException('客户端只能删除引流信息,不能直接修改审核状态');
|
||||
const updated = await this.prisma.$transaction(async (tx) => {
|
||||
// Restoration must obey the same uniqueness lock as create and edit.
|
||||
if (status !== 'deleted') await this.assertUniqueTarget(tx, current.signatureId, current.url, itemId);
|
||||
return tx.smsDrainageInfo.update({ where: { id: itemId }, data: { auditStatus: status } });
|
||||
});
|
||||
if (status === 'deleted')
|
||||
await this.reportValidation.suspendDrainageReporting(itemId, data.reason ?? '引流信息已删除', 'abandoned');
|
||||
await this.audit.createAuditRecord({
|
||||
tenantId: current.tenantId,
|
||||
targetType: 'sms_drainage_info',
|
||||
targetId: itemId,
|
||||
action: status,
|
||||
statusBefore: current.auditStatus,
|
||||
statusAfter: status,
|
||||
reason: data.reason,
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,122 +1,186 @@
|
||||
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 { normalizeChannelCarriers } from '../channels/channels.helpers';
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
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 { hasReportValue, isRecord, reportValueParts } 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) {}
|
||||
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,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
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 },
|
||||
});
|
||||
}
|
||||
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('、')}`);
|
||||
}
|
||||
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('、')}`);
|
||||
}
|
||||
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: '引流信息运营审核通过' },
|
||||
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.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${item.signatureId}, 910))`;
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
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: '应用当前路由已不包含此通道' } });
|
||||
}
|
||||
}
|
||||
const existingTasks = await tx.channelSignatureReportTask.findMany({
|
||||
where: { drainageItemId: item.id, reportType: 'drainage' },
|
||||
});
|
||||
}
|
||||
const configuredChannels = await tx.smsChannel.findMany({
|
||||
where: { id: { in: [...channels.keys()] }, status: { not: 'deleted' } },
|
||||
});
|
||||
const activeKeys = new Set<string>();
|
||||
for (const channel of configuredChannels) {
|
||||
for (const carrier of normalizeChannelCarriers(channel.carriers, channel.carrier)) {
|
||||
const key = `${channel.id}:${carrier}`;
|
||||
activeKeys.add(key);
|
||||
const existing = existingTasks.find((task) => task.channelId === channel.id && task.carrier === carrier);
|
||||
const task = existing
|
||||
? await tx.channelSignatureReportTask.update({
|
||||
where: { id: existing.id },
|
||||
data: { status: 'pending', reason: null, approvedAt: null },
|
||||
})
|
||||
: await tx.channelSignatureReportTask.create({
|
||||
data: {
|
||||
tenantId: item.tenantId,
|
||||
signatureId: item.signatureId,
|
||||
channelId: channel.id,
|
||||
carrier,
|
||||
approvalScope: 'carrier_specific',
|
||||
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) => !activeKeys.has(`${current.channelId}:${current.carrier}`) && current.status !== 'abandoned',
|
||||
)) {
|
||||
const reason = '资料版本更新或应用路由已不包含此通道运营商';
|
||||
await tx.channelSignatureReportTask.update({
|
||||
where: { id: task.id },
|
||||
data: { status: 'abandoned', reason, approvedAt: null },
|
||||
});
|
||||
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 } });
|
||||
}
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,15 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
OnModuleDestroy,
|
||||
OnModuleInit,
|
||||
} from '@nestjs/common';
|
||||
import { selectDrainageReportTask } from '../common/drainage-report-task';
|
||||
import { BadRequestException, NotFoundException } 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 { isRecord, normalizeSmsSignature, validateCompleteSmsSignature } from './sms-config.helpers';
|
||||
import { SmsReportValidationService } from './report-validation.service';
|
||||
import { SmsAuditService } from './audit.service';
|
||||
import { shanghaiDateRange } from '../common/shanghai-date-range';
|
||||
@@ -202,6 +153,7 @@ export class SmsSignatureService {
|
||||
.then((count) => count > 0);
|
||||
return signatures.map((signature) => {
|
||||
const { reportBatchItems: _reportBatchItems, ...signatureView } = signature;
|
||||
void _reportBatchItems;
|
||||
const legacyPayload = isRecord(signature.drainageInfo) ? signature.drainageInfo : {};
|
||||
const applicationChannels = [
|
||||
...new Map(
|
||||
@@ -305,64 +257,70 @@ export class SmsSignatureService {
|
||||
pendingReportBlockedReason,
|
||||
drainageReportTargets: Object.fromEntries(
|
||||
signature.drainageItems.map((drainageItem) => {
|
||||
const drainageItemId = drainageItem.id;
|
||||
const channels = routes
|
||||
.filter((route) => route.applicationId === signature.applicationId && route.group)
|
||||
.flatMap((route) => route.group!.items.map((item) => item.channel))
|
||||
const tasks = (signature.reportTasks ?? []).filter(
|
||||
(task) => task.reportType === 'drainage' && task.drainageItemId === drainageItem.id,
|
||||
);
|
||||
const targets = applicationChannels
|
||||
.filter(
|
||||
(channel) =>
|
||||
channel.status !== 'deleted' &&
|
||||
(hasCommonDrainageFields ||
|
||||
channel.reportFields.some(
|
||||
(field) => field.status === 'active' && ['drainage', 'both'].includes(field.reportType),
|
||||
)),
|
||||
hasCommonDrainageFields ||
|
||||
channel.reportFields.some(
|
||||
(field) => field.status === 'active' && ['drainage', 'both'].includes(field.reportType),
|
||||
),
|
||||
)
|
||||
.flatMap((channel) =>
|
||||
normalizeChannelCarriers(channel.carriers, channel.carrier).map((carrier) => {
|
||||
const task = selectDrainageReportTask(tasks, channel.id, carrier);
|
||||
return {
|
||||
channel,
|
||||
channelId: channel.id,
|
||||
carrier,
|
||||
status: task?.status ?? 'pending',
|
||||
taskId: task?.id,
|
||||
approvedAt: task?.approvedAt,
|
||||
approvalScope: task?.carrier ? 'carrier_specific' : task ? 'legacy_channel' : 'carrier_specific',
|
||||
};
|
||||
}),
|
||||
);
|
||||
const taskByChannel = new Map(
|
||||
(signature.reportTasks ?? [])
|
||||
.filter((task) => task.reportType === 'drainage' && task.drainageItemId === drainageItemId)
|
||||
.map((task) => [task.channelId, task]),
|
||||
);
|
||||
return [
|
||||
drainageItemId,
|
||||
[...new Map(channels.map((channel) => [channel.id, channel])).values()].flatMap((channel) => {
|
||||
const task = taskByChannel.get(channel.id);
|
||||
return task ? [{ channel, channelId: channel.id, status: task.status, taskId: task.id }] : [];
|
||||
}),
|
||||
];
|
||||
return [drainageItem.id, targets];
|
||||
}),
|
||||
),
|
||||
drainageCarrierReportSummary: Object.fromEntries(
|
||||
signature.drainageItems.map((drainageItem) => {
|
||||
const drainageItemId = drainageItem.id;
|
||||
const channels = routes
|
||||
.filter((route) => route.applicationId === signature.applicationId && route.group)
|
||||
.flatMap((route) => route.group!.items.map((item) => item.channel))
|
||||
const tasks = (signature.reportTasks ?? []).filter(
|
||||
(task) => task.reportType === 'drainage' && task.drainageItemId === drainageItem.id,
|
||||
);
|
||||
const targets = applicationChannels
|
||||
.filter(
|
||||
(channel) =>
|
||||
channel.status !== 'deleted' &&
|
||||
(hasCommonDrainageFields ||
|
||||
channel.reportFields.some(
|
||||
(field) => field.status === 'active' && ['drainage', 'both'].includes(field.reportType),
|
||||
)),
|
||||
);
|
||||
const targets = [...new Map(channels.map((channel) => [channel.id, channel])).values()];
|
||||
const taskByChannel = new Map(
|
||||
(signature.reportTasks ?? [])
|
||||
.filter((task) => task.reportType === 'drainage' && task.drainageItemId === drainageItemId)
|
||||
.map((task) => [task.channelId, task]),
|
||||
);
|
||||
return [
|
||||
drainageItemId,
|
||||
Object.fromEntries(
|
||||
['mobile', 'unicom', 'telecom'].map((carrier) => {
|
||||
const carrierTargets = targets.filter((channel) =>
|
||||
normalizeChannelCarriers(channel.carriers, channel.carrier).includes(carrier),
|
||||
);
|
||||
const statuses = carrierTargets.flatMap((channel) =>
|
||||
taskByChannel.get(channel.id)?.status ? [taskByChannel.get(channel.id)!.status] : [],
|
||||
);
|
||||
return [carrier, summarizeReportStatuses(statuses)];
|
||||
hasCommonDrainageFields ||
|
||||
channel.reportFields.some(
|
||||
(field) => field.status === 'active' && ['drainage', 'both'].includes(field.reportType),
|
||||
),
|
||||
)
|
||||
.flatMap((channel) =>
|
||||
normalizeChannelCarriers(channel.carriers, channel.carrier).map((carrier) => {
|
||||
const task = selectDrainageReportTask(tasks, channel.id, carrier);
|
||||
return {
|
||||
channel,
|
||||
channelId: channel.id,
|
||||
carrier,
|
||||
status: task?.status ?? 'pending',
|
||||
taskId: task?.id,
|
||||
approvedAt: task?.approvedAt,
|
||||
approvalScope: task?.carrier ? 'carrier_specific' : task ? 'legacy_channel' : 'carrier_specific',
|
||||
};
|
||||
}),
|
||||
);
|
||||
return [
|
||||
drainageItem.id,
|
||||
Object.fromEntries(
|
||||
['mobile', 'unicom', 'telecom'].map((carrier) => [
|
||||
carrier,
|
||||
summarizeReportStatuses(
|
||||
targets.filter((target) => target.carrier === carrier).map((target) => target.status),
|
||||
),
|
||||
]),
|
||||
),
|
||||
];
|
||||
}),
|
||||
@@ -400,10 +358,14 @@ export class SmsSignatureService {
|
||||
drainageReportTargets: _drainageReportTargets,
|
||||
...summary
|
||||
} = view;
|
||||
void [_materials, _reportTasks, _reportTargets, _drainageReportTargets];
|
||||
return {
|
||||
...summary,
|
||||
drainageInfo: {
|
||||
links: drainageLinks.map(({ reportValues: _reportValues, ...link }) => link),
|
||||
links: drainageLinks.map(({ reportValues: _reportValues, ...link }) => {
|
||||
void _reportValues;
|
||||
return link;
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -508,7 +470,10 @@ export class SmsSignatureService {
|
||||
for (const value of businessKeys) {
|
||||
const match = typeof value === 'string' ? value.match(/:channel:([^:]+):carrier:([^:]+)$/) : null;
|
||||
if (!match) continue;
|
||||
for (const carrier of match[2].split(',').map((entry) => entry.trim()).filter(Boolean))
|
||||
for (const carrier of match[2]
|
||||
.split(',')
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean))
|
||||
generatedTargets.add(`${match[1]}:${carrier}`);
|
||||
}
|
||||
}
|
||||
@@ -534,7 +499,8 @@ export class SmsSignatureService {
|
||||
candidate.approvalScope === 'legacy_channel',
|
||||
);
|
||||
if (task?.status === 'abandoned') continue;
|
||||
if (generatedTargets.has(`${channel.id}:${carrier}`) || generatedTargets.has(`${channel.id}:legacy`)) continue;
|
||||
if (generatedTargets.has(`${channel.id}:${carrier}`) || generatedTargets.has(`${channel.id}:legacy`))
|
||||
continue;
|
||||
detailTotal += 1;
|
||||
hasPendingTarget = true;
|
||||
}
|
||||
@@ -552,7 +518,7 @@ export class SmsSignatureService {
|
||||
|
||||
async getSignatureReportTargets(id: string) {
|
||||
const item = await this.getSignature(id);
|
||||
return 'reportTargets' in item ? item.reportTargets ?? [] : [];
|
||||
return 'reportTargets' in item ? (item.reportTargets ?? []) : [];
|
||||
}
|
||||
|
||||
async getDrainageReportTargets(id: string) {
|
||||
@@ -562,7 +528,7 @@ export class SmsSignatureService {
|
||||
});
|
||||
if (!drainage || drainage.auditStatus === 'deleted') throw new NotFoundException('Drainage info not found');
|
||||
const signature = await this.getSignature(drainage.signatureId);
|
||||
return 'drainageReportTargets' in signature ? signature.drainageReportTargets?.[id] ?? [] : [];
|
||||
return 'drainageReportTargets' in signature ? (signature.drainageReportTargets?.[id] ?? []) : [];
|
||||
}
|
||||
|
||||
listSignatureOptions(tenantId?: string) {
|
||||
@@ -876,10 +842,7 @@ export class SmsSignatureService {
|
||||
updated.applicationId ?? undefined,
|
||||
drainageInfo,
|
||||
);
|
||||
if (
|
||||
options.initialAuditStatus === 'approved' &&
|
||||
(materialChanged || signature.auditStatus !== 'approved')
|
||||
) {
|
||||
if (options.initialAuditStatus === 'approved' && (materialChanged || signature.auditStatus !== 'approved')) {
|
||||
await this.audit.createAuditRecord({
|
||||
tenantId: signature.tenantId,
|
||||
targetType: 'sms_signature',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user