feat: add access number routing and admin list improvements

This commit is contained in:
hectorzhao
2026-07-15 15:45:27 +08:00
parent 27c464246b
commit 3e114ee729
19 changed files with 547 additions and 508 deletions
+67 -4
View File
@@ -282,9 +282,12 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
const phones = [...new Set(data.phones ?? [])];
const schedule = parseSchedule(data);
await this.validateSendResources(data.tenantId, data.applicationId, data.templateId);
const messageClassification = await this.resolveTemplateMessageClassification(data.templateId, data.content);
const unitPrice = await this.resolveUnitPrice(data.tenantId, data.applicationId);
const queuePriority = await this.resolveQueuePriority(data.tenantId, data.applicationId);
const [messageClassification, unitPrice, queuePriority, accessNumber] = await Promise.all([
this.resolveTemplateMessageClassification(data.templateId, data.content),
this.resolveUnitPrice(data.tenantId, data.applicationId),
this.resolveQueuePriority(data.tenantId, data.applicationId),
this.resolveApplicationAccessNumber(data.tenantId, data.applicationId),
]);
const risk = await this.riskReview.evaluateTask({
tenantId: data.tenantId,
applicationId: data.applicationId,
@@ -376,6 +379,8 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
unitPrice: billing.unitPrice,
amountCents: billing.billingUnitsPerMessage * billing.unitPrice,
queuePriority,
clientSrcId: accessNumber.clientSrcId,
applicationExtension: accessNumber.applicationExtension,
status: batchStatus === 'ready' ? 'queued' : batchStatus === 'scheduled' ? 'scheduled' : batchStatus,
errorMessage: risk.status === 'rejected' ? risk.reason ?? undefined : undefined,
})),
@@ -1686,6 +1691,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
if (!/^1[3-9]\d{9}$/.test(data.phoneNumber)) {
throw new BadRequestException('CMPP submit phone number is invalid');
}
const clientSrcId = validateInboundApplicationSrcId(data.srcId, application);
const template = await this.resolveInboundTemplateCandidate(application.id, data.content);
const templateVariables = template ? matchTemplateContent(template.content, data.content) ?? {} : {};
const unitPrice = application.customerUnitPrice ?? 0;
@@ -1735,6 +1741,8 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
amountCents: billing.amountCents,
queuePriority,
cmppSubmitSequenceId: data.sequenceId == null ? null : String(data.sequenceId),
clientSrcId,
applicationExtension: application.cmppApplicationExtension,
status: 'validating',
},
});
@@ -1966,6 +1974,8 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
content: string;
billingUnits: number;
queuePriority?: string | null;
clientSrcId?: string | null;
applicationExtension?: string | null;
template?: { signature?: { id?: string | null; name?: string | null } | null } | null;
signature?: { id?: string | null; name?: string | null } | null;
},
@@ -1973,6 +1983,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
attempt: number,
) {
const channel = routed.channel;
const upstreamSrcId = composeUpstreamSrcId(channel.srcId, message.applicationExtension);
await this.ensureSignatureReportedForChannel(message, channel.id);
await this.waitForChannelRateLimit(channel.id, channel.rateLimitPerSecond);
const submitId = `SUB-${randomUUID()}`;
@@ -2039,7 +2050,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
serviceId: channel.config && typeof channel.config === 'object' && 'serviceId' in channel.config
? String(channel.config.serviceId)
: 'SMS',
srcId: channel.srcId,
srcId: upstreamSrcId,
extensionDigits: getNonNegativeConfigInteger(channel.config, 'extensionDigits', 0),
registeredDelivery: 1,
msgFmt: 8,
@@ -2073,6 +2084,8 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
content: string;
billingUnits: number;
queuedAt?: Date;
clientSrcId?: string | null;
applicationExtension?: string | null;
},
reason: string,
) {
@@ -2238,6 +2251,23 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
return normalizeQueuePriority(application.queuePriority);
}
private async resolveApplicationAccessNumber(tenantId: string, applicationId?: string) {
if (!applicationId) {
return { clientSrcId: null, applicationExtension: null };
}
const application = await this.prisma.smsApplication.findUnique({
where: { id: applicationId },
select: { tenantId: true, cmppClientSrcId: true, cmppApplicationExtension: true },
});
if (!application || application.tenantId !== tenantId) {
return { clientSrcId: null, applicationExtension: null };
}
return {
clientSrcId: application.cmppClientSrcId,
applicationExtension: application.cmppApplicationExtension,
};
}
private findInboundApplication(account: string) {
return this.prisma.smsApplication.findFirst({
where: { cmppAccount: account },
@@ -3096,6 +3126,39 @@ function isProvinceChannel(item: { province?: string | null; channel: { sendRegi
return itemProvince === target || sendRegion === target;
}
function validateInboundApplicationSrcId(
srcId: string | undefined,
application: {
cmppApplicationExtension?: string | null;
cmppAccessNumberFillEnabled?: boolean | null;
cmppAccessNumberFillPrefix?: string | null;
cmppClientSrcId?: string | null;
},
) {
const submittedSrcId = srcId?.trim() ?? '';
const applicationExtension = application.cmppApplicationExtension?.trim() ?? '';
if (!applicationExtension) {
return submittedSrcId || null;
}
const fillPrefix = application.cmppAccessNumberFillEnabled
? application.cmppAccessNumberFillPrefix?.trim() ?? ''
: '';
const expectedSrcId = application.cmppClientSrcId?.trim() || `${fillPrefix}${applicationExtension}`;
if (!submittedSrcId || submittedSrcId !== expectedSrcId) {
throw new BadRequestException(`CMPP Src_Id must equal the access number assigned to this application: ${expectedSrcId}`);
}
return submittedSrcId;
}
function composeUpstreamSrcId(baseSrcId: string, applicationExtension?: string | null) {
const upstreamSrcId = `${baseSrcId.trim()}${applicationExtension?.trim() ?? ''}`;
if (upstreamSrcId.length > 21) {
throw new BadRequestException('channel base access number plus application extension must not exceed 21 digits');
}
return upstreamSrcId;
}
function positiveInteger(value: string | undefined, fallback: number) {
const parsed = Number(value);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;