514 lines
24 KiB
TypeScript
514 lines
24 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 { SmsApplicationLifecycleService } from './application-lifecycle.service';
|
|
|
|
/** R3 domain service. Kept framework-agnostic and composed behind SmsConfigService. */
|
|
export class SmsApplicationConfigService {
|
|
constructor(private readonly prisma: PrismaService, private readonly lifecycle: SmsApplicationLifecycleService) {}
|
|
async listApplications(queryOrTenantId?: string | ApplicationListQuery) {
|
|
const query = typeof queryOrTenantId === 'string' ? { tenantId: queryOrTenantId } : queryOrTenantId ?? {};
|
|
if (query.includeConnections) {
|
|
await this.lifecycle.markTimedOutDownstreamConnections();
|
|
}
|
|
const applications = await this.prisma.smsApplication.findMany({
|
|
where: {
|
|
tenantId: query.tenantId,
|
|
status: query.status && query.status !== 'all' ? query.status : { not: 'deleted' },
|
|
tenant: query.enterpriseKeyword ? { name: { contains: query.enterpriseKeyword } } : undefined,
|
|
name: query.applicationKeyword ? { contains: query.applicationKeyword } : undefined,
|
|
OR: query.keyword ? [
|
|
{ name: { contains: query.keyword } },
|
|
{ tenant: { name: { contains: query.keyword } } },
|
|
] : undefined,
|
|
},
|
|
include: {
|
|
tenant: true,
|
|
ipAllowlist: true,
|
|
httpConfig: true,
|
|
},
|
|
omit: {
|
|
secretHash: true,
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
...(query.page && query.pageSize ? {
|
|
skip: (query.page - 1) * query.pageSize,
|
|
take: query.pageSize,
|
|
} : {}),
|
|
});
|
|
if (!query.includeConnections) {
|
|
return applications;
|
|
}
|
|
const applicationIds = applications.map((application) => application.id);
|
|
const [connections, messageStats] = await Promise.all([
|
|
this.prisma.cmppDownstreamConnection.findMany({
|
|
where: { applicationId: { in: applicationIds }, status: 'connected' },
|
|
orderBy: { updatedAt: 'desc' },
|
|
}),
|
|
this.prisma.smsMessageRecord.groupBy({
|
|
by: ['applicationId', 'status'],
|
|
where: { applicationId: { in: applicationIds }, queuedAt: { gte: startOfToday() } },
|
|
_count: { _all: true },
|
|
}),
|
|
]);
|
|
const disablingDetails = new Map((await Promise.all(applications
|
|
.filter((application) => application.status === 'disabling')
|
|
.map(async (application) => [application.id, await this.lifecycle.getApplicationDeactivationPreview(application.id)] as const))));
|
|
return applications.map((application) => {
|
|
const appConnections = connections.filter((connection) => connection.applicationId === application.id);
|
|
const appStats = messageStats.filter((item) => item.applicationId === application.id);
|
|
const todayTotal = appStats.reduce((sum, item) => sum + item._count._all, 0);
|
|
const delivered = appStats.find((item) => item.status === 'delivered')?._count._all ?? 0;
|
|
return {
|
|
...application,
|
|
cmppConnections: appConnections,
|
|
cmppStatus: normalizeApplicationCmppStatus(appConnections, application.status),
|
|
sentToday: todayTotal,
|
|
deliveryRate: todayTotal > 0 ? Number(((delivered / todayTotal) * 100).toFixed(1)) : 0,
|
|
deactivation: disablingDetails.get(application.id) ?? null,
|
|
};
|
|
}).sort((left, right) => right.sentToday - left.sentToday
|
|
|| left.name.localeCompare(right.name, 'zh-CN')
|
|
|| left.id.localeCompare(right.id));
|
|
}
|
|
|
|
async listApplicationsPage(query: ApplicationListQuery) {
|
|
const page = Math.max(1, Math.floor(Number(query.page) || 1));
|
|
const pageSize = Math.min(100, Math.max(1, Math.floor(Number(query.pageSize) || 10)));
|
|
const where: Prisma.SmsApplicationWhereInput = {
|
|
tenantId: query.tenantId,
|
|
status: query.status && query.status !== 'all' ? query.status : { not: 'deleted' },
|
|
tenant: query.enterpriseKeyword ? { name: { contains: query.enterpriseKeyword } } : undefined,
|
|
name: query.applicationKeyword ? { contains: query.applicationKeyword } : undefined,
|
|
OR: query.keyword ? [
|
|
{ name: { contains: query.keyword } },
|
|
{ tenant: { name: { contains: query.keyword } } },
|
|
] : undefined,
|
|
};
|
|
const [items, total] = await Promise.all([
|
|
this.listApplications({ ...query, page, pageSize }),
|
|
this.prisma.smsApplication.count({ where }),
|
|
]);
|
|
return { items, total, page, pageSize };
|
|
}
|
|
|
|
listApplicationOptions(tenantId?: string) {
|
|
return this.prisma.smsApplication.findMany({
|
|
where: { tenantId, status: { not: 'deleted' } },
|
|
select: { id: true, tenantId: true, name: true, status: true },
|
|
orderBy: [{ name: 'asc' }, { id: 'asc' }],
|
|
});
|
|
}
|
|
|
|
async getApplication(applicationId: string, tenantId?: string) {
|
|
const application = await this.prisma.smsApplication.findUnique({
|
|
where: { id: applicationId },
|
|
include: {
|
|
tenant: true,
|
|
ipAllowlist: true,
|
|
httpConfig: true,
|
|
},
|
|
});
|
|
if (!application || (tenantId && application.tenantId !== tenantId)) {
|
|
throw new NotFoundException('Application not found');
|
|
}
|
|
return application;
|
|
}
|
|
|
|
async getApplicationReportFields(applicationId?: string, reportType?: 'signature' | 'drainage') {
|
|
if (applicationId) await this.getApplication(applicationId);
|
|
const [commonFields, routes] = await Promise.all([
|
|
this.prisma.commonReportField.findMany({
|
|
where: {
|
|
status: 'active',
|
|
reportType,
|
|
drainageField: { status: 'active' },
|
|
},
|
|
include: { drainageField: true },
|
|
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'asc' }],
|
|
}),
|
|
applicationId ? this.prisma.channelRouteRule.findMany({
|
|
where: { applicationId, status: 'active' },
|
|
include: {
|
|
group: {
|
|
include: {
|
|
items: {
|
|
include: {
|
|
channel: {
|
|
include: {
|
|
reportFields: { include: { drainageField: true }, orderBy: { sortOrder: 'asc' } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
orderBy: { priority: 'asc' },
|
|
}) : Promise.resolve([]),
|
|
]);
|
|
type MergedReportField = {
|
|
id: string;
|
|
code: string;
|
|
name: string;
|
|
fieldType: string;
|
|
required: boolean;
|
|
description?: string | null;
|
|
reportTypes: string[];
|
|
commonReportTypes: string[];
|
|
channels: Array<{ id: string; code: string; name: string; groupId: string; groupName: string; required: boolean; reportType: string; source: 'common' | 'channel' | 'both' }>;
|
|
};
|
|
const merged = new Map<string, MergedReportField>();
|
|
const routeChannels = new Map<string, { id: string; code: string; name: string; groupId: string; groupName: string }>();
|
|
for (const route of routes) {
|
|
if (!route.group) continue;
|
|
for (const item of route.group.items) {
|
|
if (!routeChannels.has(item.channel.id)) {
|
|
routeChannels.set(item.channel.id, {
|
|
id: item.channel.id,
|
|
code: item.channel.code,
|
|
name: item.channel.name,
|
|
groupId: route.group.id,
|
|
groupName: route.group.name,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
for (const configured of commonFields) {
|
|
merged.set(configured.drainageField.id, {
|
|
id: configured.drainageField.id,
|
|
code: configured.drainageField.code,
|
|
name: configured.drainageField.name,
|
|
fieldType: configured.drainageField.fieldType,
|
|
required: configured.required,
|
|
description: configured.drainageField.description,
|
|
reportTypes: [configured.reportType],
|
|
commonReportTypes: [configured.reportType],
|
|
channels: Array.from(routeChannels.values()).map((channel) => ({
|
|
...channel,
|
|
required: configured.required,
|
|
reportType: configured.reportType,
|
|
source: 'common' as const,
|
|
})),
|
|
});
|
|
}
|
|
for (const route of routes) {
|
|
if (!route.group) continue;
|
|
for (const item of route.group.items) {
|
|
for (const configured of item.channel.reportFields) {
|
|
if (configured.status !== 'active' || !configured.drainageField || configured.drainageField.status !== 'active') continue;
|
|
if (reportType && configured.reportType !== 'both' && configured.reportType !== reportType) continue;
|
|
const key = configured.drainageField.id;
|
|
const current: MergedReportField = merged.get(key) ?? {
|
|
id: configured.drainageField.id,
|
|
code: configured.drainageField.code,
|
|
name: configured.drainageField.name,
|
|
fieldType: configured.drainageField.fieldType,
|
|
required: false,
|
|
description: configured.drainageField.description,
|
|
reportTypes: [],
|
|
commonReportTypes: [],
|
|
channels: [],
|
|
};
|
|
current.required = current.required || configured.required;
|
|
if (!current.reportTypes.includes(configured.reportType)) current.reportTypes.push(configured.reportType);
|
|
const existingChannel = current.channels.find((channel) => channel.id === item.channel.id);
|
|
if (existingChannel) {
|
|
existingChannel.required = existingChannel.required || configured.required;
|
|
existingChannel.reportType = configured.reportType;
|
|
existingChannel.source = existingChannel.source === 'common' ? 'both' : existingChannel.source;
|
|
} else {
|
|
current.channels.push({
|
|
id: item.channel.id,
|
|
code: item.channel.code,
|
|
name: item.channel.name,
|
|
groupId: route.group.id,
|
|
groupName: route.group.name,
|
|
required: configured.required,
|
|
reportType: configured.reportType,
|
|
source: 'channel',
|
|
});
|
|
}
|
|
merged.set(key, current);
|
|
}
|
|
}
|
|
}
|
|
return Array.from(merged.values());
|
|
}
|
|
|
|
async getClientApplicationReportFields(applicationId?: string, reportType?: 'signature' | 'drainage') {
|
|
const fields = await this.getApplicationReportFields(applicationId, reportType);
|
|
return fields.map(({ channels: _channels, commonReportTypes: _commonReportTypes, ...field }) => field);
|
|
}
|
|
|
|
async createApplication(data: CreateSmsApplicationDto) {
|
|
assertMoneyUnits(data.customerUnitPrice ?? 0, '客户单价');
|
|
const secret = normalizeApplicationPassword(data.passwordCipher);
|
|
const queuePriority = normalizeApplicationQueuePriority(data.queuePriority);
|
|
const interfaceType = normalizeApplicationInterfaceType(data.interfaceType);
|
|
const cmppAccount = data.cmppAccount ? await this.validateAndReserveCmppAccount(data.cmppAccount) : await this.generateCmppAccount();
|
|
const cmppEnterpriseCode = cmppAccount;
|
|
const accessNumber = normalizeCmppAccessNumberConfig(data);
|
|
await this.validateClientSrcIdAvailable(accessNumber.clientSrcId);
|
|
return this.prisma.smsApplication.create({
|
|
data: {
|
|
tenantId: data.tenantId,
|
|
name: data.name,
|
|
scene: data.scene,
|
|
callbackUrl: data.callbackUrl,
|
|
cmppAccount,
|
|
cmppEnterpriseCode,
|
|
cmppApplicationExtension: accessNumber.applicationExtension,
|
|
cmppAccessNumberFillEnabled: accessNumber.fillEnabled,
|
|
cmppAccessNumberFillPrefix: accessNumber.fillPrefix,
|
|
cmppClientSrcId: accessNumber.clientSrcId,
|
|
secretHash: secret,
|
|
interfaceEnabled: data.interfaceEnabled ?? true,
|
|
interfaceType,
|
|
cmppMaxConnections: getPositiveInteger(data.cmppMaxConnections, 1, 'cmppMaxConnections'),
|
|
cmppWindowSize: getPositiveInteger(data.cmppWindowSize, 16, 'cmppWindowSize'),
|
|
dailyLimit: getPositiveInteger(data.dailyLimit, 100000, 'dailyLimit'),
|
|
customerUnitPrice: data.customerUnitPrice ?? 0,
|
|
queuePriority,
|
|
templateMismatchMode: data.templateMismatchMode ?? 'reject',
|
|
downstreamReceiptRetryEnabled: data.downstreamReceiptRetryEnabled ?? true,
|
|
downstreamUplinkRetryEnabled: data.downstreamUplinkRetryEnabled ?? true,
|
|
ipAllowlist: {
|
|
create: (data.ipAllowlist ?? []).map((ipCidr) => ({ ipCidr })),
|
|
},
|
|
},
|
|
include: { ipAllowlist: true },
|
|
});
|
|
}
|
|
|
|
async updateApplication(applicationId: string, data: UpdateSmsApplicationDto) {
|
|
const application = await this.prisma.smsApplication.findUnique({
|
|
where: { id: applicationId },
|
|
include: { httpConfig: true },
|
|
});
|
|
if (!application) {
|
|
throw new NotFoundException('Application not found');
|
|
}
|
|
if (data.customerUnitPrice !== undefined) {
|
|
assertMoneyUnits(data.customerUnitPrice, '客户单价');
|
|
}
|
|
const queuePriority = data.queuePriority === undefined
|
|
? undefined
|
|
: normalizeApplicationQueuePriority(data.queuePriority);
|
|
const cmppAccount = data.cmppAccount === undefined
|
|
? undefined
|
|
: await this.validateAndReserveCmppAccount(data.cmppAccount, applicationId);
|
|
const cmppEnterpriseCode = cmppAccount ?? application.cmppAccount;
|
|
const interfaceType = data.interfaceType === undefined
|
|
? undefined
|
|
: normalizeApplicationInterfaceType(data.interfaceType);
|
|
const secretHash = data.passwordCipher === undefined
|
|
? undefined
|
|
: normalizeApplicationPassword(data.passwordCipher);
|
|
const accessNumberChanged = data.cmppApplicationExtension !== undefined
|
|
|| data.cmppAccessNumberFillEnabled !== undefined
|
|
|| data.cmppAccessNumberFillPrefix !== undefined;
|
|
const accessNumber = accessNumberChanged
|
|
? normalizeCmppAccessNumberConfig(data, application)
|
|
: undefined;
|
|
if (accessNumber?.clientSrcId && accessNumber.clientSrcId !== application.cmppClientSrcId) {
|
|
await this.validateClientSrcIdAvailable(accessNumber.clientSrcId, applicationId);
|
|
}
|
|
|
|
return this.prisma.$transaction(async (tx) => {
|
|
if (data.ipAllowlist) {
|
|
await tx.smsApplicationIpAllowlist.deleteMany({ where: { applicationId } });
|
|
}
|
|
const updated = await tx.smsApplication.update({
|
|
where: { id: applicationId },
|
|
data: {
|
|
name: data.name,
|
|
scene: data.scene,
|
|
callbackUrl: data.callbackUrl,
|
|
cmppAccount,
|
|
cmppEnterpriseCode,
|
|
cmppApplicationExtension: accessNumber?.applicationExtension,
|
|
cmppAccessNumberFillEnabled: accessNumber?.fillEnabled,
|
|
cmppAccessNumberFillPrefix: accessNumber?.fillPrefix,
|
|
cmppClientSrcId: accessNumber?.clientSrcId,
|
|
secretHash,
|
|
interfaceEnabled: data.interfaceEnabled,
|
|
interfaceType,
|
|
cmppMaxConnections: data.cmppMaxConnections === undefined ? undefined : getPositiveInteger(data.cmppMaxConnections, 1, 'cmppMaxConnections'),
|
|
cmppWindowSize: data.cmppWindowSize === undefined ? undefined : getPositiveInteger(data.cmppWindowSize, 16, 'cmppWindowSize'),
|
|
dailyLimit: data.dailyLimit === undefined ? undefined : getPositiveInteger(data.dailyLimit, 100000, 'dailyLimit'),
|
|
customerUnitPrice: data.customerUnitPrice,
|
|
queuePriority,
|
|
templateMismatchMode: data.templateMismatchMode,
|
|
downstreamReceiptRetryEnabled: data.downstreamReceiptRetryEnabled,
|
|
downstreamUplinkRetryEnabled: data.downstreamUplinkRetryEnabled,
|
|
status: data.status,
|
|
ipAllowlist: data.ipAllowlist ? {
|
|
create: data.ipAllowlist.map((ipCidr) => ({ ipCidr })),
|
|
} : undefined,
|
|
},
|
|
include: { tenant: true, ipAllowlist: true },
|
|
});
|
|
if (data.interfaceEnabled !== undefined && application.httpConfig) {
|
|
const deliveryMode = automaticDeliveryMode(data.interfaceEnabled, application.httpConfig.enabled);
|
|
await tx.smsApplicationHttpConfig.update({
|
|
where: { applicationId },
|
|
data: {
|
|
receiptDeliveryMode: deliveryMode,
|
|
uplinkDeliveryMode: deliveryMode,
|
|
},
|
|
});
|
|
}
|
|
return updated;
|
|
});
|
|
}
|
|
|
|
async replaceApplicationRouteRules(applicationId: string, data: ReplaceApplicationRouteRulesDto) {
|
|
const application = await this.prisma.smsApplication.findUnique({ where: { id: applicationId } });
|
|
if (!application) {
|
|
throw new NotFoundException('Application not found');
|
|
}
|
|
const routes = data.routes ?? [];
|
|
if (routes.length === 0) {
|
|
throw new BadRequestException('At least one carrier channel group is required');
|
|
}
|
|
|
|
const carriers = new Set<string>();
|
|
routes.forEach((route) => {
|
|
if (!['mobile', 'unicom', 'telecom'].includes(route.carrier)) {
|
|
throw new BadRequestException('carrier must be mobile, unicom or telecom');
|
|
}
|
|
if (carriers.has(route.carrier)) {
|
|
throw new BadRequestException('Duplicate carrier route is not allowed');
|
|
}
|
|
carriers.add(route.carrier);
|
|
});
|
|
|
|
const groups = await this.prisma.smsChannelGroup.findMany({
|
|
where: { id: { in: routes.map((route) => route.groupId) }, status: { not: 'deleted' } },
|
|
select: { id: true, carrier: true },
|
|
});
|
|
const groupMap = new Map(groups.map((group) => [group.id, group]));
|
|
routes.forEach((route) => {
|
|
const group = groupMap.get(route.groupId);
|
|
if (!group) {
|
|
throw new BadRequestException(`channel group ${route.groupId} does not exist`);
|
|
}
|
|
if (group.carrier !== route.carrier) {
|
|
throw new BadRequestException('channel group carrier must match route carrier');
|
|
}
|
|
});
|
|
|
|
return this.prisma.$transaction(async (tx) => {
|
|
await tx.channelRouteRule.deleteMany({
|
|
where: {
|
|
applicationId,
|
|
channelId: null,
|
|
province: null,
|
|
},
|
|
});
|
|
await tx.channelRouteRule.createMany({
|
|
data: routes.map((route, index) => ({
|
|
tenantId: application.tenantId,
|
|
applicationId,
|
|
groupId: route.groupId,
|
|
carrier: route.carrier,
|
|
priority: route.priority ?? (index + 1) * 10,
|
|
status: route.status ?? 'active',
|
|
})),
|
|
});
|
|
return tx.channelRouteRule.findMany({
|
|
where: { applicationId, channelId: null, province: null, status: { not: 'deleted' } },
|
|
orderBy: [{ priority: 'asc' }, { createdAt: 'asc' }],
|
|
});
|
|
});
|
|
}
|
|
|
|
async resetApplicationSecret(applicationId: string, data: StatusChangeDto = {}) {
|
|
const application = await this.prisma.smsApplication.findUnique({ where: { id: applicationId } });
|
|
if (!application) {
|
|
throw new NotFoundException('Application not found');
|
|
}
|
|
const secret = generateApplicationPassword();
|
|
const updated = await this.prisma.smsApplication.update({
|
|
where: { id: applicationId },
|
|
data: { secretHash: secret },
|
|
});
|
|
await this.lifecycle.writeOperationLog(application.tenantId, data.operatorId, 'sms_application.secret_reset', 'sms_application', applicationId, {
|
|
reason: data.reason,
|
|
});
|
|
return { ...updated, secret };
|
|
}
|
|
|
|
async getApplicationCmppParams(applicationId: string, tenantId?: string) {
|
|
const application = await this.prisma.smsApplication.findUnique({
|
|
where: { id: applicationId },
|
|
include: { tenant: true },
|
|
});
|
|
if (!application || (tenantId && application.tenantId !== tenantId)) {
|
|
throw new NotFoundException('Application not found');
|
|
}
|
|
if (tenantId && !application.interfaceEnabled) {
|
|
throw new ForbiddenException('该企业应用未开通 CMPP 接口');
|
|
}
|
|
return {
|
|
applicationId: application.id,
|
|
applicationName: application.name,
|
|
tenantId: application.tenantId,
|
|
tenantName: application.tenant.name,
|
|
appCode: application.id,
|
|
gatewayHost: process.env.CMPP_PUBLIC_HOST?.trim() || '127.0.0.1',
|
|
gatewayPort: getPositiveIntegerEnv('CMPP_PUBLIC_PORT', 17890),
|
|
enterpriseCode: application.cmppEnterpriseCode,
|
|
account: application.cmppAccount,
|
|
passwordCipher: application.secretHash,
|
|
srcId: application.cmppClientSrcId ?? '',
|
|
applicationExtension: application.cmppApplicationExtension,
|
|
accessNumberFillEnabled: application.cmppAccessNumberFillEnabled,
|
|
accessNumberFillPrefix: application.cmppAccessNumberFillPrefix,
|
|
interfaceEnabled: application.interfaceEnabled,
|
|
interfaceType: application.interfaceType,
|
|
maxConnections: application.cmppMaxConnections,
|
|
heartbeatSeconds: 30,
|
|
windowSize: application.cmppWindowSize,
|
|
protocolVersion: application.interfaceType === 'cmpp20' ? 'CMPP2.0' : application.interfaceType,
|
|
};
|
|
}
|
|
|
|
async validateAndReserveCmppAccount(cmppAccount: string, currentApplicationId?: string) {
|
|
if (!/^\d{6}$/.test(cmppAccount)) {
|
|
throw new BadRequestException('cmppAccount must be a 6-digit number');
|
|
}
|
|
const exists = await this.prisma.smsApplication.findUnique({ where: { cmppAccount } });
|
|
if (exists && exists.id !== currentApplicationId) {
|
|
throw new BadRequestException('cmppAccount already exists');
|
|
}
|
|
return cmppAccount;
|
|
}
|
|
|
|
async validateClientSrcIdAvailable(clientSrcId: string | null, currentApplicationId?: string) {
|
|
if (!clientSrcId) return;
|
|
const exists = await this.prisma.smsApplication.findUnique({ where: { cmppClientSrcId: clientSrcId } });
|
|
if (exists && exists.id !== currentApplicationId) {
|
|
throw new BadRequestException('client CMPP Src_Id already exists');
|
|
}
|
|
}
|
|
|
|
async generateCmppAccount() {
|
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
const cmppAccount = String(randomInt(100000, 1000000));
|
|
const exists = await this.prisma.smsApplication.findUnique({ where: { cmppAccount } });
|
|
if (!exists) {
|
|
return cmppAccount;
|
|
}
|
|
}
|
|
throw new BadRequestException('Unable to generate unique CMPP account');
|
|
}
|
|
}
|