fix: reject CMPP daily limit synchronously
This commit is contained in:
@@ -850,7 +850,7 @@ describe('SendChainService', () => {
|
||||
expect(prisma.cmppDownstreamDelivery.create).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('rejects every destination in one CMPP Submit with auditable receipts when the daily limit is exceeded', async () => {
|
||||
it('rejects the whole CMPP Submit synchronously while keeping per-destination audit records when the daily limit is exceeded', async () => {
|
||||
const { service, prisma, billing } = createService();
|
||||
prisma.$queryRaw.mockResolvedValueOnce([{ dailyLimit: 1, usedCount: null }]);
|
||||
let taskIndex = 0;
|
||||
@@ -866,12 +866,13 @@ describe('SendChainService', () => {
|
||||
remoteIp: '127.0.0.1',
|
||||
});
|
||||
|
||||
expect(result).toEqual(expect.objectContaining({ accepted: true, phoneCount: 2 }));
|
||||
expect(result).toEqual(expect.objectContaining({ accepted: false, result: 8, phoneCount: 2 }));
|
||||
expect(prisma.smsMessageRecord.create).toHaveBeenCalledTimes(2);
|
||||
expect(prisma.smsReceiptRecord.create).toHaveBeenCalledTimes(2);
|
||||
expect(prisma.smsReceiptRecord.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({ errorCode: 'DAILY_LIMIT', receiptStatus: 'undelivered' }),
|
||||
expect(prisma.smsMessageRecord.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({ status: 'rejected', errorCode: 'DAILY_LIMIT' }),
|
||||
});
|
||||
expect(prisma.smsReceiptRecord.create).not.toHaveBeenCalled();
|
||||
expect(prisma.cmppDownstreamDelivery.create).not.toHaveBeenCalled();
|
||||
expect(billing.freeze).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -2028,9 +2028,12 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
throw new BadRequestException('CMPP account is invalid');
|
||||
}
|
||||
const dailyQuota = await this.tryReserveDailySendQuota(application.id, phoneNumbers.length);
|
||||
const dailyLimitFailure = dailyQuota.reserved
|
||||
const dailyLimitRejection = dailyQuota.reserved
|
||||
? undefined
|
||||
: `应用当日发送上限${dailyQuota.dailyLimit}条,本次${phoneNumbers.length}条超出剩余配额`;
|
||||
: {
|
||||
code: 'DAILY_LIMIT',
|
||||
reason: `应用当日发送上限${dailyQuota.dailyLimit}条,本次${phoneNumbers.length}条超出剩余配额`,
|
||||
};
|
||||
|
||||
const submitGroupMessageId = `MSG-${randomUUID()}`;
|
||||
const submissions = phoneNumbers.map((phoneNumber, index) => ({
|
||||
@@ -2045,11 +2048,12 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
...data,
|
||||
phoneNumber: submission.phoneNumber,
|
||||
phoneNumbers: undefined,
|
||||
}, submission.messageId, submitGroupMessageId, dailyLimitFailure))));
|
||||
}, submission.messageId, submitGroupMessageId, dailyLimitRejection))));
|
||||
}
|
||||
const first = results[0];
|
||||
return {
|
||||
...first,
|
||||
result: dailyLimitRejection ? 8 : undefined,
|
||||
phoneCount: results.length,
|
||||
messages: results.map((result, index) => ({
|
||||
phoneNumber: phoneNumbers[index],
|
||||
@@ -2065,7 +2069,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
data: GatewayInboundSubmitDto & { phoneNumber: string },
|
||||
messageId: string,
|
||||
submitGroupMessageId: string,
|
||||
dailyLimitFailure?: string,
|
||||
synchronousRejection?: { code: string; reason: string },
|
||||
) {
|
||||
const application = await this.findInboundApplication(data.account);
|
||||
if (!application) {
|
||||
@@ -2098,7 +2102,9 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
sourceType: 'cmpp',
|
||||
content: data.content,
|
||||
phoneTotal: 1,
|
||||
status: 'validating',
|
||||
status: synchronousRejection ? 'rejected' : 'validating',
|
||||
auditStatus: synchronousRejection ? 'rejected' : undefined,
|
||||
rejectReason: synchronousRejection?.reason,
|
||||
progressTotal: 1,
|
||||
},
|
||||
});
|
||||
@@ -2110,7 +2116,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
sourceIp: data.remoteIp,
|
||||
userAgent: 'cmpp-gateway',
|
||||
payloadSummary: { phoneTotal: 1, contentLength: [...data.content].length, account: data.account },
|
||||
status: 'accepted',
|
||||
status: synchronousRejection ? 'rejected' : 'accepted',
|
||||
},
|
||||
});
|
||||
const message = await this.prisma.smsMessageRecord.create({
|
||||
@@ -2130,10 +2136,24 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
cmppSubmitGroupMessageId: submitGroupMessageId,
|
||||
clientSrcId,
|
||||
applicationExtension: application.cmppApplicationExtension,
|
||||
status: 'validating',
|
||||
status: synchronousRejection ? 'rejected' : 'validating',
|
||||
errorCode: synchronousRejection?.code,
|
||||
errorMessage: synchronousRejection?.reason,
|
||||
},
|
||||
});
|
||||
|
||||
if (synchronousRejection) {
|
||||
return {
|
||||
accepted: false,
|
||||
tenantId: application.tenantId,
|
||||
applicationId: application.id,
|
||||
taskId: task.id,
|
||||
messageId: message.messageId,
|
||||
messageRecordId: message.id,
|
||||
status: 'rejected',
|
||||
};
|
||||
}
|
||||
|
||||
const reject = async (code: string, reason: string) => {
|
||||
await this.prisma.smsBatchTask.update({
|
||||
where: { id: task.id },
|
||||
@@ -2203,9 +2223,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
});
|
||||
await this.enqueueBatchTask(task.id);
|
||||
};
|
||||
if (dailyLimitFailure) {
|
||||
await reject('DAILY_LIMIT', dailyLimitFailure);
|
||||
} else if (application.status !== 'active' || application.tenant.status !== 'active') {
|
||||
if (application.status !== 'active' || application.tenant.status !== 'active') {
|
||||
await reject('ACCOUNT', '企业或短信应用已停用');
|
||||
} else if (!application.interfaceEnabled) {
|
||||
await reject('INTERFACE', '短信应用 CMPP 接口已停用');
|
||||
|
||||
Reference in New Issue
Block a user