fix: route through reported signature channels
This commit is contained in:
@@ -84,7 +84,7 @@ function createPrismaMock() {
|
||||
tenantId: 'tenant-1',
|
||||
applicationId: 'app-1',
|
||||
auditStatus: 'approved',
|
||||
signature: { auditStatus: 'approved', reportStatus: 'approved' },
|
||||
signature: { auditStatus: 'approved', reportStatus: 'reporting' },
|
||||
}),
|
||||
findFirst: jest.fn().mockResolvedValue({
|
||||
id: 'tpl-1',
|
||||
@@ -92,11 +92,11 @@ function createPrismaMock() {
|
||||
applicationId: 'app-1',
|
||||
content: 'hello',
|
||||
auditStatus: 'approved',
|
||||
signature: { auditStatus: 'approved', reportStatus: 'approved' },
|
||||
signature: { auditStatus: 'approved', reportStatus: 'reporting' },
|
||||
}),
|
||||
},
|
||||
smsSignature: {
|
||||
findFirst: jest.fn().mockResolvedValue({ id: 'sig-1', name: '签名', auditStatus: 'approved', reportStatus: 'approved' }),
|
||||
findFirst: jest.fn().mockResolvedValue({ id: 'sig-1', name: '签名', auditStatus: 'approved', reportStatus: 'reporting' }),
|
||||
},
|
||||
smsSendTask: {
|
||||
findUnique: jest.fn().mockResolvedValue(null),
|
||||
@@ -154,6 +154,7 @@ function createPrismaMock() {
|
||||
},
|
||||
channelSignatureReportTask: {
|
||||
findFirst: jest.fn().mockResolvedValue({ id: 'report-task-1' }),
|
||||
findMany: jest.fn().mockImplementation(({ where }) => Promise.resolve((where.channelId?.in ?? []).map((channelId: string) => ({ channelId })))),
|
||||
},
|
||||
smsReceiptRecord: {
|
||||
create: jest.fn().mockResolvedValue({ id: 'receipt-1' }),
|
||||
@@ -720,6 +721,27 @@ describe('SendChainService', () => {
|
||||
expect(service['postGatewayControl']).not.toHaveBeenCalledWith('/upstream/submit', expect.anything());
|
||||
});
|
||||
|
||||
it('routes a partially reported signature only through its approved backup channel', async () => {
|
||||
const { service, prisma } = createService();
|
||||
const baseRoute = await prisma.channelRouteRule.findFirst();
|
||||
const primary = baseRoute.group.items[0].channel;
|
||||
const backup = { ...primary, id: 'channel-backup', code: 'CMPP-B' };
|
||||
prisma.channelRouteRule.findFirst.mockResolvedValue({
|
||||
...baseRoute,
|
||||
group: { ...baseRoute.group, items: [
|
||||
{ ...baseRoute.group.items[0], channelId: primary.id, priority: 1, channel: primary },
|
||||
{ ...baseRoute.group.items[0], id: 'item-2', channelId: backup.id, priority: 2, channel: backup },
|
||||
] },
|
||||
});
|
||||
prisma.channelSignatureReportTask.findMany.mockResolvedValue([{ channelId: backup.id }]);
|
||||
const gatewayAdd = jest.fn().mockResolvedValue(undefined);
|
||||
service['waitForChannelRateLimit'] = jest.fn().mockResolvedValue(undefined);
|
||||
service['getGatewayQueue'] = jest.fn().mockReturnValue({ add: gatewayAdd });
|
||||
|
||||
await expect(service.processSendJob({ messageRecordId: 'record-1' })).resolves.toEqual(expect.objectContaining({ submitted: true, channelId: backup.id }));
|
||||
expect(prisma.smsMessageRecord.update).toHaveBeenCalledWith(expect.objectContaining({ data: expect.objectContaining({ channelId: backup.id }) }));
|
||||
});
|
||||
|
||||
it('persists identified carrier and province before a route lookup fails', async () => {
|
||||
const { service, prisma } = createService();
|
||||
prisma.channelRouteRule.findFirst.mockResolvedValueOnce(null);
|
||||
@@ -1047,7 +1069,7 @@ describe('SendChainService', () => {
|
||||
const gatewayAdd = jest.fn().mockResolvedValue(undefined);
|
||||
service['waitForChannelRateLimit'] = jest.fn().mockResolvedValue(undefined);
|
||||
service['getGatewayQueue'] = jest.fn().mockReturnValue({ add: gatewayAdd });
|
||||
prisma.channelSignatureReportTask.findFirst.mockResolvedValue(null);
|
||||
prisma.channelSignatureReportTask.findMany.mockResolvedValue([]);
|
||||
prisma.smsMessageRecord.findUnique.mockResolvedValue({
|
||||
id: 'record-1',
|
||||
tenantId: 'tenant-1',
|
||||
@@ -1067,7 +1089,7 @@ describe('SendChainService', () => {
|
||||
});
|
||||
|
||||
await expect(service.processSendJob({ messageRecordId: 'record-1' })).resolves.toEqual(
|
||||
expect.objectContaining({ submitted: false, status: 'failed', reason: '短信签名未在最终通道报备通过' }),
|
||||
expect.objectContaining({ submitted: false, status: 'failed', reason: '无已报备通过且在线的可用通道' }),
|
||||
);
|
||||
expect(gatewayAdd).not.toHaveBeenCalled();
|
||||
expect(prisma.smsReceiptRecord.create).toHaveBeenCalledWith({
|
||||
|
||||
@@ -1626,8 +1626,6 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
await reject('TEMPLATE', '短信模板尚未审核通过');
|
||||
} else if (!template.signature || template.signature.auditStatus !== 'approved') {
|
||||
await reject('SIGNATURE', '短信签名尚未审核通过');
|
||||
} else if (template.signature.reportStatus !== 'approved') {
|
||||
await reject('REPORT', '短信签名尚未报备通过');
|
||||
} else {
|
||||
const risk = await this.riskReview.evaluateTask({
|
||||
tenantId: application.tenantId,
|
||||
@@ -1861,7 +1859,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
|
||||
private async selectChannelForMessage(
|
||||
message: { id: string; tenantId: string; applicationId?: string | null; phoneNumber: string },
|
||||
message: { id: string; tenantId: string; applicationId?: string | null; templateId?: string | null; phoneNumber: string; template?: { signature?: { id?: string | null } | null } | null; signature?: { id?: string | null } | null },
|
||||
options: { forceNational?: boolean; excludeChannelIds?: string[] } = {},
|
||||
): Promise<RoutedChannel> {
|
||||
if (!message.applicationId) {
|
||||
@@ -1875,8 +1873,16 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
});
|
||||
const route = await this.findApplicationRoute(message.tenantId, message.applicationId, carrier);
|
||||
const excluded = new Set(options.excludeChannelIds ?? []);
|
||||
const signatureId = await this.resolveMessageSignatureId(message);
|
||||
if (!signatureId) throw new BadRequestException('短信签名未配置,无法选择已报备通道');
|
||||
const approvedTasks = await this.prisma.channelSignatureReportTask.findMany({
|
||||
where: { signatureId, status: 'approved', channelId: { in: route.group.items.map((item) => item.channelId) } },
|
||||
select: { channelId: true },
|
||||
});
|
||||
const approvedChannelIds = new Set(approvedTasks.map((task) => task.channelId));
|
||||
const items = route.group.items.filter((item) =>
|
||||
!excluded.has(item.channelId)
|
||||
&& approvedChannelIds.has(item.channelId)
|
||||
&& normalizeCarrier(item.carrier) === carrier
|
||||
&& isCarrierCompatible(item.channel.carrier, carrier),
|
||||
);
|
||||
@@ -1884,7 +1890,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
const nationalCandidates = items.filter((item) => isNationalChannel(item));
|
||||
const selected = [...provinceCandidates, ...nationalCandidates].find((item) => this.isChannelSendAvailable(item.channel));
|
||||
if (!selected) {
|
||||
throw new NotFoundException('无可用在线通道');
|
||||
throw new NotFoundException('无已报备通过且在线的可用通道');
|
||||
}
|
||||
return {
|
||||
channel: selected.channel,
|
||||
@@ -2111,8 +2117,8 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
if (!template || template.tenantId !== tenantId || template.applicationId !== applicationId || template.auditStatus !== 'approved') {
|
||||
throw new BadRequestException('短信模板不存在、未通过审核或不属于当前应用');
|
||||
}
|
||||
if (!template.signature || template.signature.auditStatus !== 'approved' || template.signature.reportStatus !== 'approved') {
|
||||
throw new BadRequestException('短信签名未审核通过或通道报备未通过');
|
||||
if (!template.signature || template.signature.auditStatus !== 'approved') {
|
||||
throw new BadRequestException('短信签名未审核通过');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2236,14 +2242,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
},
|
||||
channelId: string,
|
||||
) {
|
||||
let signatureId = message.template?.signature?.id ?? message.signature?.id ?? null;
|
||||
if (!signatureId && message.templateId) {
|
||||
const template = await this.prisma.smsTemplate.findUnique({
|
||||
where: { id: message.templateId },
|
||||
include: { signature: true },
|
||||
});
|
||||
signatureId = template?.signature?.id ?? null;
|
||||
}
|
||||
const signatureId = await this.resolveMessageSignatureId(message);
|
||||
if (!signatureId) {
|
||||
throw new BadRequestException('短信签名未配置,不能提交到通道');
|
||||
}
|
||||
@@ -2256,6 +2255,13 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveMessageSignatureId(message: { templateId?: string | null; template?: { signature?: { id?: string | null } | null } | null; signature?: { id?: string | null } | null }) {
|
||||
const direct = message.template?.signature?.id ?? message.signature?.id ?? null;
|
||||
if (direct || !message.templateId) return direct;
|
||||
const template = await this.prisma.smsTemplate.findUnique({ where: { id: message.templateId }, include: { signature: true } });
|
||||
return template?.signature?.id ?? null;
|
||||
}
|
||||
|
||||
private async waitForChannelRateLimit(channelId: string, tps: number) {
|
||||
const redis = this.getRedis();
|
||||
for (;;) {
|
||||
|
||||
Reference in New Issue
Block a user