feat: enforce signature-scoped drainage authorization before SMS submission
This commit is contained in:
@@ -1,22 +1,63 @@
|
||||
import { BadRequestException, forwardRef, HttpException, HttpStatus, Inject, Injectable, Logger, NotFoundException, OnModuleDestroy, OnModuleInit, Optional } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
forwardRef,
|
||||
Inject,
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
OnModuleDestroy,
|
||||
OnModuleInit,
|
||||
Optional,
|
||||
} from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { Queue, Worker } from 'bullmq';
|
||||
import IORedis from 'ioredis';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { setTimeout as sleep } from 'node:timers/promises';
|
||||
|
||||
import { BillingService } from '../billing/billing.service';
|
||||
import { isIpAllowed } from '../common/ip-allowlist';
|
||||
import { moneyToNumber } from '../common/money';
|
||||
|
||||
import { PhoneRoutingLookupService } from '../dictionaries/phone-routing-lookup.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { RiskReviewService } from '../risk-review/risk-review.service';
|
||||
import { PhoneFrequencyService } from '../risk-review/phone-frequency.service';
|
||||
import { MetricsService } from '../metrics/metrics.service';
|
||||
import { OpenApiService } from '../open-api/open-api.service';
|
||||
import type { CreateBatchTaskDto, CreateHttpBatchTaskDto, GatewayInboundAuthDto, GatewayInboundSubmitDto, GatewayInboundSingleSubmitResult, GatewaySubmitResultDto, GatewaySubmitSegmentResultDto, GatewayReceiptEventDto, GatewayUplinkEventDto, UplinkMatchCandidateInput, GatewayPendingDeliveryQueryDto, GatewayDownstreamSentDto, GatewayDownstreamAcknowledgedDto, GatewayDownstreamFailureType, GatewayControlDeliveryResult, GatewaySubmitDeadLetterDto, RequeueGatewaySubmitExceptionDto, GatewayDownstreamRecoveryStatusDto, TimeoutUnknownDto, ImportPreviewDto, ConfirmImportDto, SendJob, QueuePriority, RoutedChannel } from './send-chain.contracts';
|
||||
import { SEND_QUEUE, GATEWAY_SUBMIT_QUEUE, GATEWAY_SUBMIT_STREAM, DEFAULT_DOWNSTREAM_RETRY_DELAY_MS, DEFAULT_DOWNSTREAM_RETRY_MAX_DELAY_MS, DEFAULT_DOWNSTREAM_MAX_RETRIES, DEFAULT_DOWNSTREAM_PENDING_TIMEOUT_HOURS, DEFAULT_RECEIPT_TIMEOUT_HOURS, DEFAULT_RECEIPT_TIMEOUT_SCAN_INTERVAL_MS, RECEIPT_TIMEOUT_INITIAL_DELAY_MS, DEFAULT_SCHEDULED_DISPATCH_SCAN_INTERVAL_MS, DEFAULT_SCHEDULED_DISPATCH_STALE_MS, SCHEDULED_DISPATCH_INITIAL_DELAY_MS, DEFAULT_GATEWAY_SUBMIT_REQUEUE_STALE_MS, DEFAULT_DOWNSTREAM_MANUAL_REQUEUE_STALE_MS, DEFAULT_INBOUND_LONG_MESSAGE_SCAN_INTERVAL_MS, INBOUND_LONG_MESSAGE_SCAN_INITIAL_DELAY_MS, DEFAULT_INBOUND_LONG_MESSAGE_PROCESSING_STALE_SECONDS, DEFAULT_UPSTREAM_RECEIPT_INBOX_SCAN_INTERVAL_MS, UPSTREAM_RECEIPT_INBOX_INITIAL_DELAY_MS, DEFAULT_UPSTREAM_RECEIPT_INBOX_PROCESSING_STALE_MS, DEFAULT_UPSTREAM_RECEIPT_INBOX_MAX_ATTEMPTS, DEFAULT_UPSTREAM_RECEIPT_INBOX_MAX_AGE_HOURS, GATEWAY_SUBMIT_REQUEUE_IDEMPOTENCY_TTL_SECONDS, BULLMQ_PRIORITY, gatewaySubmitRequeueKey, drainageRejectionReason, statusFromRisk, parseSchedule, isObjectRecord, asDateOrNull, downstreamRetryDelayMs, downstreamAckTimeoutMs, downstreamRetryBaseDelayMs, downstreamRetryMaxDelayMs, downstreamMaxRetries, downstreamPendingTimeoutHours, downstreamControlFailureMessage, parseImportRows, splitImportLine, cellByHeader, normalizeCarrier, normalizeQueuePriority, getPositiveConfigInteger, getNonNegativeConfigInteger, isCarrierCompatible, normalizeRegion, matchTemplateContent, escapeRegularExpression, isNationalChannel, isProvinceChannel, validateInboundApplicationSrcId, composeUpstreamSrcId, positiveInteger, parseOptionalSequenceId, normalizeSubmitStatus, normalizeReceiptStatus, downstreamDeliveryAttemptKey, shanghaiDateKey, bullmqConnection, matchesApplicationSecret, octetString, hasRecoveryAuditStateChanged, normalizeRecoveryFailureCategory } from './send-chain.helpers';
|
||||
import { aggregateReceiptSegmentState, isSameUpstreamEndpointIdentity, receiptEventKey } from './send-chain.helpers';
|
||||
import type {
|
||||
CreateBatchTaskDto,
|
||||
CreateHttpBatchTaskDto,
|
||||
GatewayInboundAuthDto,
|
||||
GatewayInboundSubmitDto,
|
||||
GatewaySubmitResultDto,
|
||||
GatewaySubmitSegmentResultDto,
|
||||
GatewayReceiptEventDto,
|
||||
GatewayUplinkEventDto,
|
||||
UplinkMatchCandidateInput,
|
||||
GatewayPendingDeliveryQueryDto,
|
||||
GatewayDownstreamSentDto,
|
||||
GatewayDownstreamAcknowledgedDto,
|
||||
GatewayDownstreamFailureType,
|
||||
GatewaySubmitDeadLetterDto,
|
||||
RequeueGatewaySubmitExceptionDto,
|
||||
GatewayDownstreamRecoveryStatusDto,
|
||||
TimeoutUnknownDto,
|
||||
ImportPreviewDto,
|
||||
ConfirmImportDto,
|
||||
SendJob,
|
||||
QueuePriority,
|
||||
RoutedChannel,
|
||||
} from './send-chain.contracts';
|
||||
import {
|
||||
DEFAULT_RECEIPT_TIMEOUT_SCAN_INTERVAL_MS,
|
||||
RECEIPT_TIMEOUT_INITIAL_DELAY_MS,
|
||||
DEFAULT_SCHEDULED_DISPATCH_SCAN_INTERVAL_MS,
|
||||
SCHEDULED_DISPATCH_INITIAL_DELAY_MS,
|
||||
DEFAULT_INBOUND_LONG_MESSAGE_SCAN_INTERVAL_MS,
|
||||
INBOUND_LONG_MESSAGE_SCAN_INITIAL_DELAY_MS,
|
||||
DEFAULT_UPSTREAM_RECEIPT_INBOX_SCAN_INTERVAL_MS,
|
||||
UPSTREAM_RECEIPT_INBOX_INITIAL_DELAY_MS,
|
||||
downstreamPendingTimeoutHours,
|
||||
positiveInteger,
|
||||
} from './send-chain.helpers';
|
||||
|
||||
import type { DownstreamDeliveryQueueRequest } from './downstream-receipt-targets';
|
||||
import { SendSubmissionService, type SendResourceValidationOptions } from './send-submission.service';
|
||||
import { SendCompletionService, type SendCompletionFacade } from './send-completion.service';
|
||||
@@ -68,12 +109,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
},
|
||||
metrics,
|
||||
);
|
||||
this.completion = new SendCompletionService(
|
||||
prisma,
|
||||
billing,
|
||||
openApi,
|
||||
this as unknown as SendCompletionFacade,
|
||||
);
|
||||
this.completion = new SendCompletionService(prisma, billing, openApi, this as unknown as SendCompletionFacade);
|
||||
this.downstreamRequeueTasks = new SendDownstreamRequeueTaskService(prisma, this);
|
||||
}
|
||||
|
||||
@@ -91,7 +127,10 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
this.submission.startInboundWorkflowWorker();
|
||||
}
|
||||
if (process.env.SMS_RECEIPT_TIMEOUT_SCAN_ENABLED !== 'false') {
|
||||
this.receiptTimeoutInitialTimer = setTimeout(() => void this.runReceiptTimeoutScan(), RECEIPT_TIMEOUT_INITIAL_DELAY_MS);
|
||||
this.receiptTimeoutInitialTimer = setTimeout(
|
||||
() => void this.runReceiptTimeoutScan(),
|
||||
RECEIPT_TIMEOUT_INITIAL_DELAY_MS,
|
||||
);
|
||||
this.receiptTimeoutInitialTimer.unref?.();
|
||||
this.receiptTimeoutIntervalTimer = setInterval(
|
||||
() => void this.runReceiptTimeoutScan(),
|
||||
@@ -107,22 +146,27 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
this.scheduledDispatchInitialTimer.unref?.();
|
||||
this.scheduledDispatchIntervalTimer = setInterval(
|
||||
() => void this.runScheduledDispatchScan(),
|
||||
positiveInteger(process.env.SMS_SCHEDULED_DISPATCH_SCAN_INTERVAL_MS, DEFAULT_SCHEDULED_DISPATCH_SCAN_INTERVAL_MS),
|
||||
positiveInteger(
|
||||
process.env.SMS_SCHEDULED_DISPATCH_SCAN_INTERVAL_MS,
|
||||
DEFAULT_SCHEDULED_DISPATCH_SCAN_INTERVAL_MS,
|
||||
),
|
||||
);
|
||||
this.scheduledDispatchIntervalTimer.unref?.();
|
||||
}
|
||||
if (process.env.CMPP_INBOUND_LONG_MESSAGE_SCAN_ENABLED !== 'false') {
|
||||
this.inboundLongMessageInitialTimer = setTimeout(
|
||||
() => void this.expireInboundLongMessages().catch((error) => {
|
||||
this.logger.error(`Failed to expire inbound CMPP long messages: ${String(error)}`);
|
||||
}),
|
||||
() =>
|
||||
void this.expireInboundLongMessages().catch((error) => {
|
||||
this.logger.error(`Failed to expire inbound CMPP long messages: ${String(error)}`);
|
||||
}),
|
||||
INBOUND_LONG_MESSAGE_SCAN_INITIAL_DELAY_MS,
|
||||
);
|
||||
this.inboundLongMessageInitialTimer.unref?.();
|
||||
this.inboundLongMessageIntervalTimer = setInterval(
|
||||
() => void this.expireInboundLongMessages().catch((error) => {
|
||||
this.logger.error(`Failed to expire inbound CMPP long messages: ${String(error)}`);
|
||||
}),
|
||||
() =>
|
||||
void this.expireInboundLongMessages().catch((error) => {
|
||||
this.logger.error(`Failed to expire inbound CMPP long messages: ${String(error)}`);
|
||||
}),
|
||||
positiveInteger(
|
||||
process.env.CMPP_INBOUND_LONG_MESSAGE_SCAN_INTERVAL_MS,
|
||||
DEFAULT_INBOUND_LONG_MESSAGE_SCAN_INTERVAL_MS,
|
||||
@@ -147,7 +191,10 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
if (process.env.CMPP_DOWNSTREAM_REQUEUE_TASK_SCAN_ENABLED !== 'false') {
|
||||
this.downstreamRequeueTaskIntervalTimer = setInterval(
|
||||
() => void this.downstreamRequeueTasks.runScan().catch((error) => this.logger.error(`Downstream requeue task scan failed: ${String(error)}`)),
|
||||
() =>
|
||||
void this.downstreamRequeueTasks
|
||||
.runScan()
|
||||
.catch((error) => this.logger.error(`Downstream requeue task scan failed: ${String(error)}`)),
|
||||
positiveInteger(process.env.CMPP_DOWNSTREAM_REQUEUE_TASK_SCAN_INTERVAL_MS, 1_000),
|
||||
);
|
||||
this.downstreamRequeueTaskIntervalTimer.unref?.();
|
||||
@@ -191,12 +238,15 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
const taskIds = tasks.map((task) => task.id);
|
||||
const messageStats = taskIds.length > 0 ? await this.prisma.smsMessageRecord.groupBy({
|
||||
by: ['batchTaskId', 'carrier', 'province', 'status'],
|
||||
where: { batchTaskId: { in: taskIds } },
|
||||
_count: { _all: true },
|
||||
_sum: { billingUnits: true },
|
||||
}) : [];
|
||||
const messageStats =
|
||||
taskIds.length > 0
|
||||
? await this.prisma.smsMessageRecord.groupBy({
|
||||
by: ['batchTaskId', 'carrier', 'province', 'status'],
|
||||
where: { batchTaskId: { in: taskIds } },
|
||||
_count: { _all: true },
|
||||
_sum: { billingUnits: true },
|
||||
})
|
||||
: [];
|
||||
return tasks.map((task) => ({
|
||||
...task,
|
||||
messageStats: messageStats.filter((item) => item.batchTaskId === task.id),
|
||||
@@ -232,11 +282,16 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
sourceType: query.sourceType ?? 'client',
|
||||
taskNo: query.keyword?.trim() ? { contains: query.keyword.trim() } : undefined,
|
||||
tenant: query.enterpriseKeyword?.trim() ? { name: { contains: query.enterpriseKeyword.trim() } } : undefined,
|
||||
application: query.applicationKeyword?.trim() ? { name: { contains: query.applicationKeyword.trim() } } : undefined,
|
||||
createdAt: query.createdAtFrom || query.createdAtTo ? {
|
||||
gte: query.createdAtFrom ? new Date(`${query.createdAtFrom}T00:00:00+08:00`) : undefined,
|
||||
lte: query.createdAtTo ? new Date(`${query.createdAtTo}T23:59:59.999+08:00`) : undefined,
|
||||
} : undefined,
|
||||
application: query.applicationKeyword?.trim()
|
||||
? { name: { contains: query.applicationKeyword.trim() } }
|
||||
: undefined,
|
||||
createdAt:
|
||||
query.createdAtFrom || query.createdAtTo
|
||||
? {
|
||||
gte: query.createdAtFrom ? new Date(`${query.createdAtFrom}T00:00:00+08:00`) : undefined,
|
||||
lte: query.createdAtTo ? new Date(`${query.createdAtTo}T23:59:59.999+08:00`) : undefined,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
const [tasks, total] = await Promise.all([
|
||||
this.prisma.smsBatchTask.findMany({
|
||||
@@ -254,12 +309,15 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
this.prisma.smsBatchTask.count({ where }),
|
||||
]);
|
||||
const taskIds = tasks.map((task) => task.id);
|
||||
const messageStats = taskIds.length > 0 ? await this.prisma.smsMessageRecord.groupBy({
|
||||
by: ['batchTaskId', 'carrier', 'province', 'status'],
|
||||
where: { batchTaskId: { in: taskIds } },
|
||||
_count: { _all: true },
|
||||
_sum: { billingUnits: true },
|
||||
}) : [];
|
||||
const messageStats =
|
||||
taskIds.length > 0
|
||||
? await this.prisma.smsMessageRecord.groupBy({
|
||||
by: ['batchTaskId', 'carrier', 'province', 'status'],
|
||||
where: { batchTaskId: { in: taskIds } },
|
||||
_count: { _all: true },
|
||||
_sum: { billingUnits: true },
|
||||
})
|
||||
: [];
|
||||
return {
|
||||
items: tasks.map((task) => ({
|
||||
...task,
|
||||
@@ -304,14 +362,16 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
return { items, total, page: normalizedPage, pageSize: normalizedPageSize };
|
||||
}
|
||||
|
||||
listMessages(query: {
|
||||
tenantId?: string;
|
||||
applicationId?: string;
|
||||
channelId?: string;
|
||||
taskId?: string;
|
||||
phoneNumber?: string;
|
||||
status?: string;
|
||||
} = {}) {
|
||||
listMessages(
|
||||
query: {
|
||||
tenantId?: string;
|
||||
applicationId?: string;
|
||||
channelId?: string;
|
||||
taskId?: string;
|
||||
phoneNumber?: string;
|
||||
status?: string;
|
||||
} = {},
|
||||
) {
|
||||
return this.prisma.smsMessageRecord.findMany({
|
||||
where: {
|
||||
tenantId: query.tenantId,
|
||||
@@ -460,7 +520,13 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
async handleReceipt(
|
||||
data: GatewayReceiptEventDto,
|
||||
incomingIdentity?: { account: string; gatewayHost: string; gatewayPort: number; protocol: string; cmppVersion: string },
|
||||
incomingIdentity?: {
|
||||
account: string;
|
||||
gatewayHost: string;
|
||||
gatewayPort: number;
|
||||
protocol: string;
|
||||
cmppVersion: string;
|
||||
},
|
||||
) {
|
||||
return this.completion.handleReceipt(data, incomingIdentity);
|
||||
}
|
||||
@@ -530,7 +596,10 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
return this.downstreamRequeueTasks.preview(filter, operatorId);
|
||||
}
|
||||
|
||||
createDownstreamRequeueTask(data: { previewToken: string; reason: string; ratePerSecond?: number; consecutiveFailureLimit?: number }, operatorId?: string) {
|
||||
createDownstreamRequeueTask(
|
||||
data: { previewToken: string; reason: string; ratePerSecond?: number; consecutiveFailureLimit?: number },
|
||||
operatorId?: string,
|
||||
) {
|
||||
return this.downstreamRequeueTasks.create(data, operatorId);
|
||||
}
|
||||
|
||||
@@ -542,7 +611,10 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
return this.downstreamRequeueTasks.get(id);
|
||||
}
|
||||
|
||||
listDownstreamRequeueTaskItems(id: string, query: { status?: string; keyword?: string; page?: number; pageSize?: number }) {
|
||||
listDownstreamRequeueTaskItems(
|
||||
id: string,
|
||||
query: { status?: string; keyword?: string; page?: number; pageSize?: number },
|
||||
) {
|
||||
return this.downstreamRequeueTasks.listItems(id, query);
|
||||
}
|
||||
|
||||
@@ -592,7 +664,14 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
requestedMessageIds?: string[],
|
||||
workflowKey?: string,
|
||||
) {
|
||||
return this.submission.submitCompleteInboundMessage(data, phoneNumbers, application, requestedGroupMessageId, requestedMessageIds, workflowKey);
|
||||
return this.submission.submitCompleteInboundMessage(
|
||||
data,
|
||||
phoneNumbers,
|
||||
application,
|
||||
requestedGroupMessageId,
|
||||
requestedMessageIds,
|
||||
workflowKey,
|
||||
);
|
||||
}
|
||||
|
||||
private async collectInboundLongMessageFragment(
|
||||
@@ -616,22 +695,33 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
receiptRejection?: { code: string; reason: string },
|
||||
workflowItemKey?: string,
|
||||
) {
|
||||
return this.submission.submitInboundSingleMessage(data, messageId, submitGroupMessageId, application, synchronousRejection, receiptRejection, workflowItemKey);
|
||||
return this.submission.submitInboundSingleMessage(
|
||||
data,
|
||||
messageId,
|
||||
submitGroupMessageId,
|
||||
application,
|
||||
synchronousRejection,
|
||||
receiptRejection,
|
||||
workflowItemKey,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* CMPP 单号码提交必须在生成最终入队决定前原子占用号码频次。
|
||||
* 频次规则是直接拒绝,因此优先级高于其他规则产生的待人工审核结果。
|
||||
*/
|
||||
private async evaluateRiskWithPhoneFrequency(input: {
|
||||
tenantId: string;
|
||||
applicationId: string;
|
||||
templateId?: string;
|
||||
content: string;
|
||||
variables?: Record<string, unknown>;
|
||||
phoneNumber: string;
|
||||
sourceType: 'cmpp';
|
||||
}, reservationKey?: string) {
|
||||
private async evaluateRiskWithPhoneFrequency(
|
||||
input: {
|
||||
tenantId: string;
|
||||
applicationId: string;
|
||||
templateId?: string;
|
||||
content: string;
|
||||
variables?: Record<string, unknown>;
|
||||
phoneNumber: string;
|
||||
sourceType: 'cmpp';
|
||||
},
|
||||
reservationKey?: string,
|
||||
) {
|
||||
return this.submission.evaluateRiskWithPhoneFrequency(input, reservationKey);
|
||||
}
|
||||
|
||||
@@ -699,13 +789,29 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
|
||||
private async selectChannelForMessage(
|
||||
message: { id: string; tenantId: string; applicationId?: string | null; templateId?: string | null; signatureId?: string | null; phoneNumber: string; carrier?: string | null; province?: string | null; template?: { signature?: { id?: string | null } | null } | null; signature?: { id?: string | null } | null },
|
||||
message: {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
applicationId?: string | null;
|
||||
templateId?: string | null;
|
||||
signatureId?: string | null;
|
||||
phoneNumber: string;
|
||||
carrier?: string | null;
|
||||
province?: string | null;
|
||||
template?: { signature?: { id?: string | null } | null } | null;
|
||||
signature?: { id?: string | null } | null;
|
||||
},
|
||||
options: { forceNational?: boolean; excludeChannelIds?: string[] } = {},
|
||||
): Promise<RoutedChannel> {
|
||||
return this.submission.selectChannelForMessage(message, options);
|
||||
}
|
||||
|
||||
private async findApplicationRoute(tenantId: string, applicationId: string | undefined, carrier: string, signatureId?: string) {
|
||||
private async findApplicationRoute(
|
||||
tenantId: string,
|
||||
applicationId: string | undefined,
|
||||
carrier: string,
|
||||
signatureId?: string,
|
||||
) {
|
||||
return this.submission.findApplicationRoute(tenantId, applicationId, carrier, signatureId);
|
||||
}
|
||||
|
||||
@@ -754,10 +860,40 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
return this.submission.resolveDrainageInfoMatch(signatureId, content);
|
||||
}
|
||||
|
||||
private async attachMessageToReviewTask(reviewTaskId: string, messageRecordId: string, signatureId: string, drainageInfoId?: string) {
|
||||
private async attachMessageToReviewTask(
|
||||
reviewTaskId: string,
|
||||
messageRecordId: string,
|
||||
signatureId: string,
|
||||
drainageInfoId?: string,
|
||||
) {
|
||||
return this.submission.attachMessageToReviewTask(reviewTaskId, messageRecordId, signatureId, drainageInfoId);
|
||||
}
|
||||
|
||||
async recoverDrainageFailureReceipts() {
|
||||
const messages = await this.prisma.smsMessageRecord.findMany({
|
||||
where: {
|
||||
drainageReceiptPending: true,
|
||||
status: { in: ['failed', 'submit_failed'] },
|
||||
batchTask: { sourceType: 'cmpp' },
|
||||
},
|
||||
orderBy: { updatedAt: 'asc' },
|
||||
take: 50,
|
||||
});
|
||||
for (const message of messages) {
|
||||
if (!message.tenantId || !message.batchTaskId) continue;
|
||||
const reason = message.errorMessage ?? '引流发送资格校验未通过';
|
||||
await this.releaseMessageReservation(
|
||||
{ ...message, tenantId: message.tenantId, batchTaskId: message.batchTaskId },
|
||||
reason,
|
||||
);
|
||||
await this.recordCmppFailureReceipt(
|
||||
message,
|
||||
message.errorCode?.startsWith('DRN') ? message.errorCode : 'DRN',
|
||||
reason,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async recordCmppFailureReceipt(
|
||||
message: {
|
||||
id: string;
|
||||
@@ -779,7 +915,12 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
return this.submission.classifyRejectedPhones(tenantId, applicationId, phones);
|
||||
}
|
||||
|
||||
private async validateSendResources(tenantId: string, applicationId?: string, templateId?: string, options?: SendResourceValidationOptions) {
|
||||
private async validateSendResources(
|
||||
tenantId: string,
|
||||
applicationId?: string,
|
||||
templateId?: string,
|
||||
options?: SendResourceValidationOptions,
|
||||
) {
|
||||
return this.submission.validateSendResources(tenantId, applicationId, templateId, options);
|
||||
}
|
||||
|
||||
@@ -806,7 +947,13 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
|
||||
private async releaseMessageReservation(
|
||||
message: { tenantId: string; batchTaskId: string; messageId: string; amountCents: number | bigint; billingUnits: number },
|
||||
message: {
|
||||
tenantId: string;
|
||||
batchTaskId: string;
|
||||
messageId: string;
|
||||
amountCents: number | bigint;
|
||||
billingUnits: number;
|
||||
},
|
||||
remark: string,
|
||||
) {
|
||||
return this.completion.releaseMessageReservation(message, remark);
|
||||
@@ -832,7 +979,12 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
return this.submission.ensureSignatureReportedForChannel(message, channelId, carrier);
|
||||
}
|
||||
|
||||
private async resolveMessageSignatureId(message: { templateId?: string | null; signatureId?: string | null; template?: { signature?: { id?: string | null } | null } | null; signature?: { id?: string | null } | null }) {
|
||||
private async resolveMessageSignatureId(message: {
|
||||
templateId?: string | null;
|
||||
signatureId?: string | null;
|
||||
template?: { signature?: { id?: string | null } | null } | null;
|
||||
signature?: { id?: string | null } | null;
|
||||
}) {
|
||||
return this.submission.resolveMessageSignatureId(message);
|
||||
}
|
||||
|
||||
@@ -902,7 +1054,13 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
private async resolveReceiptMessage(
|
||||
data: GatewayReceiptEventDto,
|
||||
incomingIdentity?: { account: string; gatewayHost: string; gatewayPort: number; protocol: string; cmppVersion: string },
|
||||
incomingIdentity?: {
|
||||
account: string;
|
||||
gatewayHost: string;
|
||||
gatewayPort: number;
|
||||
protocol: string;
|
||||
cmppVersion: string;
|
||||
},
|
||||
) {
|
||||
return this.completion.resolveReceiptMessage(data, incomingIdentity);
|
||||
}
|
||||
@@ -926,5 +1084,4 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
private async publishGatewaySubmitCommand(command: unknown, idempotencyKey?: string) {
|
||||
return this.submission.publishGatewaySubmitCommand(command, idempotencyKey);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user